mirror of
https://github.com/apache/superset.git
synced 2026-09-10 01:04:25 +00:00
Compare commits
74
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
482bef1507 | ||
|
|
20c04a4663 | ||
|
|
782f5eab16 | ||
|
|
1234533c67 | ||
|
|
7f0c0aea94 | ||
|
|
d9dcbb68b7 | ||
|
|
98fba1eefe | ||
|
|
bad03b1e76 | ||
|
|
fcfafebb29 | ||
|
|
47e82b02ed | ||
|
|
a463d66c80 | ||
|
|
337da13ba7 | ||
|
|
4a3453999a | ||
|
|
58758de93d | ||
|
|
b4a8acc584 | ||
|
|
08f89690e9 | ||
|
|
f02899d38d | ||
|
|
86583f1121 | ||
|
|
26cbd71099 | ||
|
|
500ce7a02a | ||
|
|
6d8ceed10e | ||
|
|
68d65f727f | ||
|
|
f165785003 | ||
|
|
8e31c93119 | ||
|
|
4974c08f7d | ||
|
|
fa90ba976c | ||
|
|
35c3d8dfbc | ||
|
|
ee23815aff | ||
|
|
7c946ae3db | ||
|
|
3926f5c55c | ||
|
|
fdc03d4bf3 | ||
|
|
24f0aed8a7 | ||
|
|
00d2f577df | ||
|
|
c35fc71bc5 | ||
|
|
1b6d57c3f3 | ||
|
|
d089a96163 | ||
|
|
0b3fe3d60c | ||
|
|
0eeb184b6a | ||
|
|
8e7edce616 | ||
|
|
754201b3d0 | ||
|
|
925401b4e1 | ||
|
|
8368ea4094 | ||
|
|
e8a6fb24ae | ||
|
|
311b7a72dc | ||
|
|
aa496def53 | ||
|
|
aea4375255 | ||
|
|
9ab0a0179d | ||
|
|
3db613dab5 | ||
|
|
de5ca79805 | ||
|
|
aede3bb5ba | ||
|
|
408f84aea6 | ||
|
|
92c07aaf54 | ||
|
|
f405174fcf | ||
|
|
8c125d2553 | ||
|
|
fb8fca4c64 | ||
|
|
dc0c055518 | ||
|
|
09349cb1e7 | ||
|
|
ca29adb0cb | ||
|
|
1617bbbe71 | ||
|
|
de1dd53186 | ||
|
|
58672dfab6 | ||
|
|
4b5629d1c8 | ||
|
|
4ddc3f14ed | ||
|
|
400a8aec89 | ||
|
|
51489a75ce | ||
|
|
09772eeda0 | ||
|
|
78907d08cd | ||
|
|
d0a0d280a1 | ||
|
|
5d77ed3677 | ||
|
|
f68ee6ba67 | ||
|
|
a01560cfa1 | ||
|
|
7e06ce8eeb | ||
|
|
ccc0e3dbb2 | ||
|
|
bd48e87eeb |
+7
-7
@@ -25,14 +25,14 @@ little bit helps, and credit will always be given.
|
||||
|
||||
All developer and contribution documentation has moved to the Apache Superset Developer Portal:
|
||||
|
||||
**[📚 View the Developer Portal →](https://superset.apache.org/docs/developer-portal/)**
|
||||
**[📚 View the Developer Portal →](https://superset.apache.org/developer_portal/)**
|
||||
|
||||
The Developer Portal includes comprehensive guides for:
|
||||
- [Contributing Overview](https://superset.apache.org/docs/developer-portal/contributing/overview)
|
||||
- [Development Setup](https://superset.apache.org/docs/developer-portal/contributing/development-setup)
|
||||
- [Submitting Pull Requests](https://superset.apache.org/docs/developer-portal/contributing/submitting-pr)
|
||||
- [Contribution Guidelines](https://superset.apache.org/docs/developer-portal/contributing/guidelines)
|
||||
- [Code Review Process](https://superset.apache.org/docs/developer-portal/contributing/code-review)
|
||||
- [Development How-tos](https://superset.apache.org/docs/developer-portal/contributing/howtos)
|
||||
- [Contributing Overview](https://superset.apache.org/developer_portal/contributing/overview)
|
||||
- [Development Setup](https://superset.apache.org/developer_portal/contributing/development-setup)
|
||||
- [Submitting Pull Requests](https://superset.apache.org/developer_portal/contributing/submitting-pr)
|
||||
- [Contribution Guidelines](https://superset.apache.org/developer_portal/contributing/guidelines)
|
||||
- [Code Review Process](https://superset.apache.org/developer_portal/contributing/code-review)
|
||||
- [Development How-tos](https://superset.apache.org/developer_portal/contributing/howtos)
|
||||
|
||||
Source for the Developer Portal documentation is [located here](https://github.com/apache/superset/tree/master/docs/developer_portal).
|
||||
|
||||
@@ -12,11 +12,13 @@ version: 1
|
||||
SQL Lab and Explore supports [Jinja templating](https://jinja.palletsprojects.com/en/2.11.x/) in queries.
|
||||
To enable templating, the `ENABLE_TEMPLATE_PROCESSING` [feature flag](/docs/configuration/configuring-superset#feature-flags) needs to be enabled in `superset_config.py`.
|
||||
|
||||
> #### ⚠️ Security Warning
|
||||
>
|
||||
> While powerful, this feature executes template code on the server. Within the Superset security model, this is **intended functionality**, as users with permissions to edit charts and virtual datasets are considered **trusted users**.
|
||||
>
|
||||
> If you grant these permissions to untrusted users, this feature can be exploited as a **Server-Side Template Injection (SSTI)** vulnerability. Do not enable `ENABLE_TEMPLATE_PROCESSING` unless you fully understand and accept the associated security risks.
|
||||
:::warning[Security Warning]
|
||||
|
||||
While powerful, this feature executes template code on the server. Within the Superset security model, this is **intended functionality**, as users with permissions to edit charts and virtual datasets are considered **trusted users**.
|
||||
|
||||
If you grant these permissions to untrusted users, this feature can be exploited as a **Server-Side Template Injection (SSTI)** vulnerability. Do not enable `ENABLE_TEMPLATE_PROCESSING` unless you fully understand and accept the associated security risks.
|
||||
|
||||
:::
|
||||
|
||||
When templating is enabled, python code can be embedded in virtual datasets and
|
||||
in Custom SQL in the filter and metric controls in Explore. By default, the following variables are
|
||||
@@ -182,7 +184,7 @@ The available validators and names can be found in
|
||||
|
||||
In this section, we'll walkthrough the pre-defined Jinja macros in Superset.
|
||||
|
||||
**Current Username**
|
||||
### Current Username
|
||||
|
||||
The `{{ current_username() }}` macro returns the `username` of the currently logged in user.
|
||||
|
||||
@@ -197,7 +199,7 @@ cache key by adding the following parameter to your Jinja code:
|
||||
{{ current_username(add_to_cache_keys=False) }}
|
||||
```
|
||||
|
||||
**Current User ID**
|
||||
### Current User ID
|
||||
|
||||
The `{{ current_user_id() }}` macro returns the account ID of the currently logged in user.
|
||||
|
||||
@@ -212,7 +214,7 @@ cache key by adding the following parameter to your Jinja code:
|
||||
{{ current_user_id(add_to_cache_keys=False) }}
|
||||
```
|
||||
|
||||
**Current User Email**
|
||||
### Current User Email
|
||||
|
||||
The `{{ current_user_email() }}` macro returns the email address of the currently logged in user.
|
||||
|
||||
@@ -227,7 +229,7 @@ cache key by adding the following parameter to your Jinja code:
|
||||
{{ current_user_email(add_to_cache_keys=False) }}
|
||||
```
|
||||
|
||||
**Current User Roles**
|
||||
### Current User Roles
|
||||
|
||||
The `{{ current_user_roles() }}` macro returns an array of roles for the logged in user.
|
||||
|
||||
@@ -257,7 +259,7 @@ Will be rendered as:
|
||||
SELECT * FROM users WHERE role IN ('admin', 'viewer')
|
||||
```
|
||||
|
||||
**Current User RLS Rules**
|
||||
### Current User RLS Rules
|
||||
|
||||
The `{{ current_user_rls_rules() }}` macro returns an array of RLS rules applied to the current dataset for the logged in user.
|
||||
|
||||
@@ -265,7 +267,7 @@ If you have caching enabled in your Superset configuration, then the list of RLS
|
||||
by Superset when calculating the cache key. A cache key is a unique identifier that determines if there's a
|
||||
cache hit in the future and Superset can retrieve cached data.
|
||||
|
||||
**Custom URL Parameters**
|
||||
### Custom URL Parameters
|
||||
|
||||
The `{{ url_param('custom_variable') }}` macro lets you define arbitrary URL
|
||||
parameters and reference them in your SQL code.
|
||||
@@ -299,7 +301,7 @@ Here's a concrete example:
|
||||
WHERE country_code = 'US'
|
||||
```
|
||||
|
||||
**Explicitly Including Values in Cache Key**
|
||||
### Explicitly Including Values in Cache Key
|
||||
|
||||
The `{{ cache_key_wrapper() }}` function explicitly instructs Superset to add a value to the
|
||||
accumulated list of values used in the calculation of the cache key.
|
||||
@@ -311,7 +313,7 @@ in the cache key. You can gain more context
|
||||
Note that this function powers the caching of the `user_id` and `username` values
|
||||
in the `current_user_id()` and `current_username()` function calls (if you have caching enabled).
|
||||
|
||||
**Filter Values**
|
||||
### Filter Values
|
||||
|
||||
You can retrieve the value for a specific filter as a list using `{{ filter_values() }}`.
|
||||
|
||||
@@ -332,7 +334,7 @@ GROUP BY action
|
||||
|
||||
There `where_in` filter converts the list of values from `filter_values('action_type')` into a string suitable for an `IN` expression.
|
||||
|
||||
**Filters for a Specific Column**
|
||||
### Filters for a Specific Column
|
||||
|
||||
The `{{ get_filters() }}` macro returns the filters applied to a given column. In addition to
|
||||
returning the values (similar to how `filter_values()` does), the `get_filters()` macro
|
||||
@@ -394,7 +396,7 @@ Here's a concrete example:
|
||||
order by lineage, level
|
||||
```
|
||||
|
||||
**Time Filter**
|
||||
### Time Filter
|
||||
|
||||
The `{{ get_time_filter() }}` macro returns the time filter applied to a specific column. This is useful if you want
|
||||
to handle time filters inside the virtual dataset, as by default the time filter is placed on the outer query. This can
|
||||
@@ -469,7 +471,7 @@ WHERE
|
||||
AND dttm < {{ time_filter.to_expr }}
|
||||
```
|
||||
|
||||
**Datasets**
|
||||
### Datasets
|
||||
|
||||
It's possible to query physical and virtual datasets using the `dataset` macro. This is useful if you've defined computed columns and metrics on your datasets, and want to reuse the definition in adhoc SQL Lab queries.
|
||||
|
||||
@@ -493,7 +495,7 @@ Since metrics are aggregations, the resulting SQL expression will be grouped by
|
||||
SELECT * FROM {{ dataset(42, include_metrics=True, columns=["ds", "category"]) }} LIMIT 10
|
||||
```
|
||||
|
||||
**Metrics**
|
||||
### Metrics
|
||||
|
||||
The `{{ metric('metric_key', dataset_id) }}` macro can be used to retrieve the metric SQL syntax from a dataset. This can be useful for different purposes:
|
||||
|
||||
@@ -511,7 +513,7 @@ The parameter can be used in SQL Lab, or when fetching a metric from another dat
|
||||
|
||||
Superset supports [builtin filters from the Jinja2 templating package](https://jinja.palletsprojects.com/en/stable/templates/#builtin-filters). Custom filters have also been implemented:
|
||||
|
||||
**Where In**
|
||||
### Where In
|
||||
Parses a list into a SQL-compatible statement. This is useful with macros that return an array (for example the `filter_values` macro):
|
||||
|
||||
```
|
||||
@@ -528,7 +530,7 @@ Dashboard filter without any value applied
|
||||
{{ filter_values('column')|where_in(default_to_none=True) }} => None
|
||||
```
|
||||
|
||||
**To Datetime**
|
||||
### To Datetime
|
||||
|
||||
Loads a string as a `datetime` object. This is useful when performing date operations. For example:
|
||||
```
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
---
|
||||
title: Securing Your Superset Installation for Production
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
> *This guide applies to Apache Superset version 4.0 and later and is an evolving set of best practices that administrators should adapt to their specific deployment architecture.*
|
||||
|
||||
The default Apache Superset configuration is optimized for ease of use and development, not for security. For any production deployment, it is **critical** that you review and apply the following security configurations to harden your instance, protect user data, and prevent unauthorized access.
|
||||
|
||||
This guide provides a comprehensive checklist of essential security configurations and best practices.
|
||||
|
||||
### **Critical Prerequisites: HTTPS/TLS Configuration**
|
||||
|
||||
Running Superset without HTTPS (TLS) is not secure. Without it, all network traffic—including user credentials, session tokens, and sensitive data—is sent in cleartext and can be easily intercepted.
|
||||
|
||||
* **Use a Reverse Proxy:** Your Superset instance should always be deployed behind a reverse proxy (e.g., Nginx, Traefik) or a load balancer (e.g., AWS ALB, Google Cloud Load Balancer) that is configured to handle HTTPS termination.
|
||||
* **Enforce Modern TLS:** Configure your proxy to enforce TLS 1.2 or higher with strong, industry-standard cipher suites.
|
||||
* **Implement HSTS:** Use the HTTP Strict Transport Security (HSTS) header to ensure browsers only connect to your Superset instance over HTTPS. This can be configured in your reverse proxy or within Superset's Talisman settings.
|
||||
|
||||
### **`SUPERSET_SECRET_KEY` Management (CRITICAL)**
|
||||
|
||||
This is the most critical security setting for your Superset instance. It is used to sign all session cookies and encrypt sensitive information in the metadata database, such as database connection credentials.
|
||||
|
||||
* **Generate a Unique, Strong Key:** A unique key must be generated for every Superset instance. Use a cryptographically secure method to create it.
|
||||
```bash
|
||||
# Example using openssl to generate a strong key
|
||||
openssl rand -base64 42
|
||||
```
|
||||
* **Store the Key Securely:** The key must be kept confidential. The recommended approach is to store it as an environment variable or in a secrets management system (e.g., AWS Secrets Manager, HashiCorp Vault). **Do not hardcode the key in `superset_config.py` or commit it to version control.**
|
||||
```python
|
||||
# In superset_config.py
|
||||
import os
|
||||
SECRET_KEY = os.environ.get('SUPERSET_SECRET_KEY')
|
||||
```
|
||||
|
||||
> #### ⚠️ Warning: Your `SUPERSET_SECRET_KEY` Must Be Unique
|
||||
>
|
||||
> **NEVER** reuse the same `SUPERSET_SECRET_KEY` across different environments (e.g., development, staging, production) or different Superset instances. Reusing a key allows cryptographically signed session cookies to be used across those instances, which can lead to a full authentication bypass if a cookie is compromised. Treat this key like a master password.
|
||||
|
||||
### **Session Management Security (CRITICAL)**
|
||||
|
||||
Properly configuring user sessions is essential to prevent session hijacking and ensure that sessions are terminated correctly.
|
||||
|
||||
#### **Use a Server-Side Session Backend (Strongly Recommended for Production)**
|
||||
|
||||
The default stateless cookie-based session handling presents challenges for immediate session invalidation upon logout. For all production deployments, we strongly recommend configuring an optional server-side session backend like Redis, Memcached, or a database. This ensures that session data is stored securely on the server and can be instantly destroyed upon logout, rendering any copied session cookies immediately useless.
|
||||
|
||||
**Example `superset_config.py` for Redis:**
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
from redis import Redis
|
||||
import os
|
||||
|
||||
# 1. Enable server-side sessions
|
||||
SESSION_SERVER_SIDE = True
|
||||
|
||||
# 2. Choose your backend (e.g., 'redis', 'memcached', 'filesystem', 'sqlalchemy')
|
||||
SESSION_TYPE = 'redis'
|
||||
|
||||
# 3. Configure your Redis connection
|
||||
# Use environment variables for sensitive details
|
||||
SESSION_REDIS = Redis(
|
||||
host=os.environ.get('REDIS_HOST', 'localhost'),
|
||||
port=int(os.environ.get('REDIS_PORT', 6379)),
|
||||
password=os.environ.get('REDIS_PASSWORD'),
|
||||
db=int(os.environ.get('REDIS_DB', 0)),
|
||||
ssl=os.environ.get('REDIS_SSL_ENABLED', 'True').lower() == 'true',
|
||||
ssl_cert_reqs='required' # Or another appropriate SSL setting
|
||||
)
|
||||
|
||||
# 4. Ensure the session cookie is signed for integrity
|
||||
SESSION_USE_SIGNER = True
|
||||
```
|
||||
|
||||
#### **Configure Session Lifetime and Cookie Security Flags**
|
||||
|
||||
This is mandatory for *all* deployments, whether stateless or server-side.
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
from datetime import timedelta
|
||||
|
||||
# Set a short absolute session timeout
|
||||
# The default is 31 days, which is NOT recommended for production.
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(hours=8)
|
||||
|
||||
# Enforce secure cookie flags to prevent browser-based attacks
|
||||
SESSION_COOKIE_SECURE = True # Transmit cookie only over HTTPS
|
||||
SESSION_COOKIE_HTTPONLY = True # Prevent client-side JS from accessing the cookie
|
||||
SESSION_COOKIE_SAMESITE = 'Lax' # Provide protection against CSRF attacks
|
||||
```
|
||||
|
||||
> ##### Note on iFrame Embedding and `SESSION_COOKIE_SAMESITE`
|
||||
>The recommended default setting `'Lax'` provides good CSRF protection for most use cases. However, if you need to embed Superset dashboards into other applications using an iFrame, you will need to change this setting to `'None'`.
|
||||
|
||||
SESSION_COOKIE_SAMESITE = 'None'
|
||||
|
||||
Setting SameSite to 'None' requires that SESSION_COOKIE_SECURE is also set to True. Be aware that this configuration disables some of the browser's built-in CSRF protections to allow for cross-domain functionality, so it should only be used when iFrame embedding is necessary.
|
||||
|
||||
### **Authentication and Authorization**
|
||||
|
||||
While Superset's built-in database authentication is convenient, for production it's highly recommended to integrate with an enterprise-grade identity provider (IdP).
|
||||
|
||||
* **Use an Enterprise IdP:** Configure authentication via OAuth or LDAP to leverage your organization's existing identity management system. This provides benefits like Single Sign-On (SSO), Multi-Factor Authentication (MFA), and centralized user provisioning/deprovisioning.
|
||||
* **Principle of Least Privilege:** Assign users to the most restrictive roles necessary for their jobs. Avoid over-provisioning users with Admin or Alpha roles, and ensure row-level security is applied where appropriate.
|
||||
* **Admin Accounts:** Delete or disable the default admin user after a new administrative account has been configured.
|
||||
|
||||
### **Content Security Policy (CSP) and Other Headers**
|
||||
|
||||
Superset can use Flask-Talisman to set security headers. However, it must be explicitly enabled.
|
||||
|
||||
> #### ⚠️ Important: Talisman is Disabled by Default
|
||||
>
|
||||
> In Superset 4.0 and later, Talisman is disabled by default (`TALISMAN_ENABLED = False`). You **must** explicitly enable it in your `superset_config.py` for the security headers defined in `TALISMAN_CONFIG` to take effect.
|
||||
|
||||
Here's the documentation section how how to set up Talisman: https://superset.apache.org/docs/security/#content-security-policy-csp
|
||||
|
||||
### **Database Security**
|
||||
|
||||
> #### ❗ Superset is Not a Database Firewall
|
||||
>
|
||||
> It is essential to understand that **Apache Superset is a data visualization and exploration platform, not a database firewall or a comprehensive security solution for your data warehouse.** While Superset provides features to help manage data access, the ultimate responsibility for securing your underlying databases lies with your database administrators (DBAs) and security teams. This includes managing network access, user privileges, and fine-grained permissions directly within the database. The configurations below are an important secondary layer of security but should not be your only line of defense.
|
||||
|
||||
* **Use a Dedicated Database User:** The database connection configured in Superset should use a dedicated, limited-privilege database user. This user should only have the minimum required permissions (e.g., `SELECT` on specific schemas) for the data sources it needs to query. It should **not** have `INSERT`, `UPDATE`, `DELETE`, or administrative privileges.
|
||||
* **Restrict Dangerous SQL Functions:** To mitigate potential SQL injection risks, configure the `DISALLOWED_SQL_FUNCTIONS` list in your `superset_config.py`. Be aware that this is a defense-in-depth measure, not a substitute for proper database permissions.
|
||||
|
||||
### **Additional Security Layers**
|
||||
|
||||
* **Web Application Firewall (WAF):** Deploying Superset behind a WAF (e.g., Cloudflare, AWS WAF) is strongly recommended. A WAF with a standard ruleset (like the OWASP Core Rule Set) provides a critical layer of defense against common attacks like SQL Injection, XSS, and remote code execution.
|
||||
|
||||
### **Monitoring and Logging**
|
||||
|
||||
* **Configure Structured Logging:** Set up a robust logging configuration to capture important security events.
|
||||
* **Centralize Logs:** Ship logs from all Superset components (frontend, worker, etc.) to a centralized SIEM (Security Information and Event Management) system for analysis and alerting.
|
||||
* **Monitor Key Events:** Create alerts for suspicious activities, including:
|
||||
* Multiple failed login attempts for a single user or from a single IP address.
|
||||
* Changes to user roles or permissions.
|
||||
* Creation or deletion of high-privilege users.
|
||||
* Attempts to use disallowed SQL functions.
|
||||
|
||||
-----
|
||||
|
||||
### **Appendix A: Production Deployment Checklist**
|
||||
|
||||
#### **Initial Setup:**
|
||||
|
||||
- [ ] HTTPS/TLS is configured and enforced via a reverse proxy.
|
||||
- [ ] A unique, strong `SUPERSET_SECRET_KEY` is generated and secured in an environment variable or secrets vault.
|
||||
- [ ] Server-side session management is configured (e.g., Redis).
|
||||
- [ ] `PERMANENT_SESSION_LIFETIME` is set to a short duration (e.g., 8 hours).
|
||||
- [ ] All session cookie security flags (`Secure`, `HttpOnly`, `SameSite`) are enabled.
|
||||
- [ ] `DEBUG` mode is set to `False`.
|
||||
- [ ] Talisman is explicitly enabled and configured with a strict Content Security Policy.
|
||||
- [ ] Database connections use dedicated, limited-privilege accounts.
|
||||
- [ ] Authentication is integrated with an enterprise identity provider (OAuth/LDAP).
|
||||
- [ ] A Web Application Firewall (WAF) is deployed in front of Superset.
|
||||
- [ ] Logging is configured and logs are shipped to a central monitoring system.
|
||||
|
||||
#### **Ongoing Maintenance:**
|
||||
|
||||
- [ ] Regularly update to the latest major or minor versions of Superset. Those versions receive up-to-date security patches.
|
||||
- [ ] Rotate the `SUPERSET_SECRET_KEY` periodically (e.g., quarterly) and after any potential security incident.
|
||||
- [ ] Conduct quarterly access reviews for all users.
|
||||
- [ ] Assuming logging and monitoring is in place, review security monitoring alerts weekly.
|
||||
|
||||
### **Appendix B: `SECRET_KEY` Rotation and Compromise Response**
|
||||
|
||||
**Why and When to Rotate the `SECRET_KEY`**
|
||||
Rotating the `SUPERSET_SECRET_KEY` is a critical security procedure. It is mandatory after a known or suspected compromise and is a best practice when an employee with access to the key departs. While periodic rotation can limit the window of exposure for an unknown leak, it is a high-impact operation that will invalidate all user sessions and requires careful execution to avoid breaking your instance. The principles behind managing this key align with general best practices for cryptographic storage, which are further detailed in the OWASP Cryptographic Storage Cheat Sheet here: https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html
|
||||
|
||||
**Procedure for Rotating the Key**
|
||||
The procedure for safely rotating the SECRET_KEY must be followed precisely to avoid locking yourself out of your instance. The official Apache Superset documentation maintains the correct, up-to-date procedure. Please follow the official guide here:
|
||||
https://superset.apache.org/docs/configuration/configuring-superset/#rotating-to-a-newer-secret_key
|
||||
+11
-11
@@ -28,10 +28,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@docusaurus/core": "3.9.1",
|
||||
"@docusaurus/plugin-client-redirects": "3.9.1",
|
||||
"@docusaurus/preset-classic": "3.9.1",
|
||||
"@docusaurus/theme-mermaid": "^3.9.1",
|
||||
"@docusaurus/core": "3.9.2",
|
||||
"@docusaurus/plugin-client-redirects": "3.9.2",
|
||||
"@docusaurus/preset-classic": "3.9.2",
|
||||
"@docusaurus/theme-mermaid": "^3.9.2",
|
||||
"@emotion/core": "^10.0.27",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^10.0.27",
|
||||
@@ -49,8 +49,8 @@
|
||||
"@storybook/preview-api": "^8.6.11",
|
||||
"@storybook/theming": "^8.6.11",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"antd": "^5.27.4",
|
||||
"caniuse-lite": "^1.0.30001749",
|
||||
"antd": "^5.27.6",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
"docusaurus-plugin-less": "^2.0.2",
|
||||
"json-bigint": "^1.0.0",
|
||||
"less": "^4.4.2",
|
||||
@@ -63,25 +63,25 @@
|
||||
"remark-import-partial": "^0.0.2",
|
||||
"reselect": "^5.1.1",
|
||||
"storybook": "^8.6.11",
|
||||
"swagger-ui-react": "^5.29.3",
|
||||
"swagger-ui-react": "^5.29.5",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"ts-loader": "^9.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.9.1",
|
||||
"@docusaurus/tsconfig": "^3.9.1",
|
||||
"@eslint/js": "^9.37.0",
|
||||
"@docusaurus/tsconfig": "^3.9.2",
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.3",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.4.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.0",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"webpack": "^5.102.1"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
+355
-357
@@ -1593,10 +1593,10 @@
|
||||
marked "^16.3.0"
|
||||
zod "^4.1.8"
|
||||
|
||||
"@docusaurus/babel@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.9.1.tgz#5297195ab34df9e184e3e2fe20de1a2e1b2a22e8"
|
||||
integrity sha512-/uoi3oG+wvbVWNBRfPrzrEslOSeLxrQEyWMywK51TLDFTANqIRivzkMusudh5bdDty8fXzCYUT+tg5t697jYqg==
|
||||
"@docusaurus/babel@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.9.2.tgz#f956c638baeccf2040e482c71a742bc7e35fdb22"
|
||||
integrity sha512-GEANdi/SgER+L7Japs25YiGil/AUDnFFHaCGPBbundxoWtCkA2lmy7/tFmgED4y1htAy6Oi4wkJEQdGssnw9MA==
|
||||
dependencies:
|
||||
"@babel/core" "^7.25.9"
|
||||
"@babel/generator" "^7.25.9"
|
||||
@@ -1608,23 +1608,23 @@
|
||||
"@babel/runtime" "^7.25.9"
|
||||
"@babel/runtime-corejs3" "^7.25.9"
|
||||
"@babel/traverse" "^7.25.9"
|
||||
"@docusaurus/logger" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/logger" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
babel-plugin-dynamic-import-node "^2.3.3"
|
||||
fs-extra "^11.1.1"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/bundler@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.9.1.tgz#6b78c152cf364d706249f6978f8e3fedf576b118"
|
||||
integrity sha512-E1c9DgNmAz4NqbNtiJVp4UgjLtr8O01IgtXD/NDQ4PZaK8895cMiTOgb3k7mN0qX8A3lb8vqyrPJ842+yMpuUg==
|
||||
"@docusaurus/bundler@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.9.2.tgz#0ca82cda4acf13a493e3f66061aea351e9d356cf"
|
||||
integrity sha512-ZOVi6GYgTcsZcUzjblpzk3wH1Fya2VNpd5jtHoCCFcJlMQ1EYXZetfAnRHLcyiFeBABaI1ltTYbOBtH/gahGVA==
|
||||
dependencies:
|
||||
"@babel/core" "^7.25.9"
|
||||
"@docusaurus/babel" "3.9.1"
|
||||
"@docusaurus/cssnano-preset" "3.9.1"
|
||||
"@docusaurus/logger" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/babel" "3.9.2"
|
||||
"@docusaurus/cssnano-preset" "3.9.2"
|
||||
"@docusaurus/logger" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
babel-loader "^9.2.1"
|
||||
clean-css "^5.3.3"
|
||||
copy-webpack-plugin "^11.0.0"
|
||||
@@ -1644,18 +1644,18 @@
|
||||
webpack "^5.95.0"
|
||||
webpackbar "^6.0.1"
|
||||
|
||||
"@docusaurus/core@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.9.1.tgz#be4d859464fee8889794d8527f884e931d591f2e"
|
||||
integrity sha512-FWDk1LIGD5UR5Zmm9rCrXRoxZUgbwuP6FBA7rc50DVfzqDOMkeMe3NyJhOsA2dF0zBE3VbHEIMmTjKwTZJwbaA==
|
||||
"@docusaurus/core@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.9.2.tgz#cc970f29b85a8926d63c84f8cffdcda43ed266ff"
|
||||
integrity sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw==
|
||||
dependencies:
|
||||
"@docusaurus/babel" "3.9.1"
|
||||
"@docusaurus/bundler" "3.9.1"
|
||||
"@docusaurus/logger" "3.9.1"
|
||||
"@docusaurus/mdx-loader" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-common" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/babel" "3.9.2"
|
||||
"@docusaurus/bundler" "3.9.2"
|
||||
"@docusaurus/logger" "3.9.2"
|
||||
"@docusaurus/mdx-loader" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-common" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
boxen "^6.2.1"
|
||||
chalk "^4.1.2"
|
||||
chokidar "^3.5.3"
|
||||
@@ -1692,32 +1692,32 @@
|
||||
webpack-dev-server "^5.2.2"
|
||||
webpack-merge "^6.0.1"
|
||||
|
||||
"@docusaurus/cssnano-preset@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.9.1.tgz#fa57c81a3f41e4d118115f86c85a71aed6b90f49"
|
||||
integrity sha512-2y7+s7RWQMqBg+9ejeKwvZs7Bdw/hHIVJIodwMXbs2kr+S48AhcmAfdOh6Cwm0unJb0hJUshN0ROwRoQMwl3xg==
|
||||
"@docusaurus/cssnano-preset@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.9.2.tgz#523aab65349db3c51a77f2489048d28527759428"
|
||||
integrity sha512-8gBKup94aGttRduABsj7bpPFTX7kbwu+xh3K9NMCF5K4bWBqTFYW+REKHF6iBVDHRJ4grZdIPbvkiHd/XNKRMQ==
|
||||
dependencies:
|
||||
cssnano-preset-advanced "^6.1.2"
|
||||
postcss "^8.5.4"
|
||||
postcss-sort-media-queries "^5.2.0"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/logger@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.9.1.tgz#0209c4c1044ee35d89dbf676e3cbb5dc8b59c82b"
|
||||
integrity sha512-C9iFzXwHzwvGlisE4bZx+XQE0JIqlGAYAd5LzpR7fEDgjctu7yL8bE5U4nTNywXKHURDzMt4RJK8V6+stFHVkA==
|
||||
"@docusaurus/logger@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.9.2.tgz#6ec6364b90f5a618a438cc9fd01ac7376869f92a"
|
||||
integrity sha512-/SVCc57ByARzGSU60c50rMyQlBuMIJCjcsJlkphxY6B0GV4UH3tcA1994N8fFfbJ9kX3jIBe/xg3XP5qBtGDbA==
|
||||
dependencies:
|
||||
chalk "^4.1.2"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/mdx-loader@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.9.1.tgz#0ef77bee13450c83c18338f8e5f1753ed2e9ee3f"
|
||||
integrity sha512-/1PY8lqry8jCt0qZddJSpc0U2sH6XC27kVJZfpA7o2TiQ3mdBQyH5AVbj/B2m682B1ounE+XjI0LdpOkAQLPoA==
|
||||
"@docusaurus/mdx-loader@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.9.2.tgz#78d238de6c6203fa811cc2a7e90b9b79e111408c"
|
||||
integrity sha512-wiYoGwF9gdd6rev62xDU8AAM8JuLI/hlwOtCzMmYcspEkzecKrP8J8X+KpYnTlACBUUtXNJpSoCwFWJhLRevzQ==
|
||||
dependencies:
|
||||
"@docusaurus/logger" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/logger" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
"@mdx-js/mdx" "^3.0.0"
|
||||
"@slorber/remark-comment" "^1.0.0"
|
||||
escape-html "^1.0.3"
|
||||
@@ -1740,12 +1740,12 @@
|
||||
vfile "^6.0.1"
|
||||
webpack "^5.88.1"
|
||||
|
||||
"@docusaurus/module-type-aliases@3.9.1", "@docusaurus/module-type-aliases@^3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.9.1.tgz#201959a22e7b30881cf879a21d2ae5b26415b705"
|
||||
integrity sha512-YBce3GbJGGcMbJTyHcnEOMvdXqg41pa5HsrMCGA5Rm4z0h0tHS6YtEldj0mlfQRhCG7Y0VD66t2tb87Aom+11g==
|
||||
"@docusaurus/module-type-aliases@3.9.2", "@docusaurus/module-type-aliases@^3.9.1":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.9.2.tgz#993c7cb0114363dea5ef6855e989b3ad4b843a34"
|
||||
integrity sha512-8qVe2QA9hVLzvnxP46ysuofJUIc/yYQ82tvA/rBTrnpXtCjNSFLxEZfd5U8cYZuJIVlkPxamsIgwd5tGZXfvew==
|
||||
dependencies:
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@types/history" "^4.7.11"
|
||||
"@types/react" "*"
|
||||
"@types/react-router-config" "*"
|
||||
@@ -1753,34 +1753,34 @@
|
||||
react-helmet-async "npm:@slorber/react-helmet-async@1.3.0"
|
||||
react-loadable "npm:@docusaurus/react-loadable@6.0.0"
|
||||
|
||||
"@docusaurus/plugin-client-redirects@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.9.1.tgz#06e07487f1596c62536d50afac81535d5fe60a20"
|
||||
integrity sha512-+1InCGvAnw46H+TnVqxaYlJC0qy9AY5gTMgTx2ZFryjAsImJNs3i1pEYW/iUTVbOdtWRj3E/87E4ehbBIaA1TA==
|
||||
"@docusaurus/plugin-client-redirects@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.9.2.tgz#9c27025c72aeeedeb783a94720163911567da0e8"
|
||||
integrity sha512-lUgMArI9vyOYMzLRBUILcg9vcPTCyyI2aiuXq/4npcMVqOr6GfmwtmBYWSbNMlIUM0147smm4WhpXD0KFboffw==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/logger" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-common" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/logger" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-common" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
eta "^2.2.0"
|
||||
fs-extra "^11.1.1"
|
||||
lodash "^4.17.21"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/plugin-content-blog@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.9.1.tgz#bf6619847065360d52abc5bf1da307f5ce2a19f8"
|
||||
integrity sha512-vT6kIimpJLWvW9iuWzH4u7VpTdsGlmn4yfyhq0/Kb1h4kf9uVouGsTmrD7WgtYBUG1P+TSmQzUUQa+ALBSRTig==
|
||||
"@docusaurus/plugin-content-blog@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.9.2.tgz#d5ce51eb7757bdab0515e2dd26a793ed4e119df9"
|
||||
integrity sha512-3I2HXy3L1QcjLJLGAoTvoBnpOwa6DPUa3Q0dMK19UTY9mhPkKQg/DYhAGTiBUKcTR0f08iw7kLPqOhIgdV3eVQ==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/logger" "3.9.1"
|
||||
"@docusaurus/mdx-loader" "3.9.1"
|
||||
"@docusaurus/theme-common" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-common" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/logger" "3.9.2"
|
||||
"@docusaurus/mdx-loader" "3.9.2"
|
||||
"@docusaurus/theme-common" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-common" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
cheerio "1.0.0-rc.12"
|
||||
feed "^4.2.2"
|
||||
fs-extra "^11.1.1"
|
||||
@@ -1792,20 +1792,20 @@
|
||||
utility-types "^3.10.0"
|
||||
webpack "^5.88.1"
|
||||
|
||||
"@docusaurus/plugin-content-docs@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.1.tgz#e3e75d4aa310689c262c18e10010788e53f101ec"
|
||||
integrity sha512-DyLk9BIA6I9gPIuia8XIL+XIEbNnExam6AHzRsfrEq4zJr7k/DsWW7oi4aJMepDnL7jMRhpVcdsCxdjb0/A9xg==
|
||||
"@docusaurus/plugin-content-docs@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz#cd8f2d1c06e53c3fa3d24bdfcb48d237bf2d6b2e"
|
||||
integrity sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/logger" "3.9.1"
|
||||
"@docusaurus/mdx-loader" "3.9.1"
|
||||
"@docusaurus/module-type-aliases" "3.9.1"
|
||||
"@docusaurus/theme-common" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-common" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/logger" "3.9.2"
|
||||
"@docusaurus/mdx-loader" "3.9.2"
|
||||
"@docusaurus/module-type-aliases" "3.9.2"
|
||||
"@docusaurus/theme-common" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-common" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
"@types/react-router-config" "^5.0.7"
|
||||
combine-promises "^1.1.0"
|
||||
fs-extra "^11.1.1"
|
||||
@@ -1816,142 +1816,142 @@
|
||||
utility-types "^3.10.0"
|
||||
webpack "^5.88.1"
|
||||
|
||||
"@docusaurus/plugin-content-pages@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.9.1.tgz#044b8adbd2a673ff22630a74b3e0ce482761655d"
|
||||
integrity sha512-/1wFzRnXYASI+Nv9ck9IVPIMw0O5BGQ8ZVhDzEwhkL+tl44ycvSnY6PIe6rW2HLxsw61Z3WFwAiU8+xMMtMZpg==
|
||||
"@docusaurus/plugin-content-pages@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.9.2.tgz#22db6c88ade91cec0a9e87a00b8089898051b08d"
|
||||
integrity sha512-s4849w/p4noXUrGpPUF0BPqIAfdAe76BLaRGAGKZ1gTDNiGxGcpsLcwJ9OTi1/V8A+AzvsmI9pkjie2zjIQZKA==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/mdx-loader" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/mdx-loader" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
fs-extra "^11.1.1"
|
||||
tslib "^2.6.0"
|
||||
webpack "^5.88.1"
|
||||
|
||||
"@docusaurus/plugin-css-cascade-layers@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.9.1.tgz#958a04679279e787d14fd3cc423ad35c580dc6fc"
|
||||
integrity sha512-/QyW2gRCk/XE3ttCK/ERIgle8KJ024dBNKMu6U5SmpJvuT2il1n5jR/48Pp/9wEwut8WVml4imNm6X8JsL5A0Q==
|
||||
"@docusaurus/plugin-css-cascade-layers@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.9.2.tgz#358c85f63f1c6a11f611f1b8889d9435c11b22f8"
|
||||
integrity sha512-w1s3+Ss+eOQbscGM4cfIFBlVg/QKxyYgj26k5AnakuHkKxH6004ZtuLe5awMBotIYF2bbGDoDhpgQ4r/kcj4rQ==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/plugin-debug@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.9.1.tgz#5dbe01771176697f427b89a1ff023a3967c3e674"
|
||||
integrity sha512-qPeAuk0LccC251d7jg2MRhNI+o7niyqa924oEM/AxnZJvIpMa596aAxkRImiAqNN6+gtLE1Hkrz/RHUH2HDGsA==
|
||||
"@docusaurus/plugin-debug@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.9.2.tgz#b5df4db115583f5404a252dbf66f379ff933e53c"
|
||||
integrity sha512-j7a5hWuAFxyQAkilZwhsQ/b3T7FfHZ+0dub6j/GxKNFJp2h9qk/P1Bp7vrGASnvA9KNQBBL1ZXTe7jlh4VdPdA==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
fs-extra "^11.1.1"
|
||||
react-json-view-lite "^2.3.0"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/plugin-google-analytics@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.9.1.tgz#226e39ed6d0a5eb3978dc5189bc9676235756446"
|
||||
integrity sha512-k4Qq2HphqOrIU/CevGPdEO1yJnWUI8m0zOJsYt5NfMJwNsIn/gDD6gv/DKD+hxHndQT5pacsfBd4BWHZVNVroQ==
|
||||
"@docusaurus/plugin-google-analytics@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.9.2.tgz#857fe075fdeccdf6959e62954d9efe39769fa247"
|
||||
integrity sha512-mAwwQJ1Us9jL/lVjXtErXto4p4/iaLlweC54yDUK1a97WfkC6Z2k5/769JsFgwOwOP+n5mUQGACXOEQ0XDuVUw==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/plugin-google-gtag@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.9.1.tgz#971b075898d46d1a59482b2873ccb9aa2e679910"
|
||||
integrity sha512-n9BURBiQyJKI/Ecz35IUjXYwXcgNCSq7/eA07+ZYcDiSyH2p/EjPf8q/QcZG3CyEJPZ/SzGkDHePfcVPahY4Gg==
|
||||
"@docusaurus/plugin-google-gtag@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.9.2.tgz#df75b1a90ae9266b0471909ba0265f46d5dcae62"
|
||||
integrity sha512-YJ4lDCphabBtw19ooSlc1MnxtYGpjFV9rEdzjLsUnBCeis2djUyCozZaFhCg6NGEwOn7HDDyMh0yzcdRpnuIvA==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
"@types/gtag.js" "^0.0.12"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/plugin-google-tag-manager@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.9.1.tgz#b26770d8bb07cedc1e01305cd9d66c2e4ce6d654"
|
||||
integrity sha512-rZAQZ25ZuXaThBajxzLjXieTDUCMmBzfAA6ThElQ3o7Q+LEpOjCIrwGFau0KLY9HeG6x91+FwwsAM8zeApYDrg==
|
||||
"@docusaurus/plugin-google-tag-manager@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.9.2.tgz#d1a3cf935acb7d31b84685e92d70a1d342946677"
|
||||
integrity sha512-LJtIrkZN/tuHD8NqDAW1Tnw0ekOwRTfobWPsdO15YxcicBo2ykKF0/D6n0vVBfd3srwr9Z6rzrIWYrMzBGrvNw==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/plugin-sitemap@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.9.1.tgz#e84717c1e52f3a61f9fea414ef98ebe025e7ffd2"
|
||||
integrity sha512-k/bf5cXDxAJUYTzqatgFJwmZsLUbIgl6S8AdZMKGG2Mv2wcOHt+EQNN9qPyWZ5/9cFj+Q8f8DN+KQheBMYLong==
|
||||
"@docusaurus/plugin-sitemap@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.9.2.tgz#e1d9f7012942562cc0c6543d3cb2cdc4ae713dc4"
|
||||
integrity sha512-WLh7ymgDXjG8oPoM/T4/zUP7KcSuFYRZAUTl8vR6VzYkfc18GBM4xLhcT+AKOwun6kBivYKUJf+vlqYJkm+RHw==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/logger" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-common" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/logger" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-common" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
fs-extra "^11.1.1"
|
||||
sitemap "^7.1.1"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/plugin-svgr@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-svgr/-/plugin-svgr-3.9.1.tgz#394ad2b8da3af587a0f68167252b4bf99fb72351"
|
||||
integrity sha512-TeZOXT2PSdTNR1OpDJMkYqFyX7MMhbd4t16hQByXksgZQCXNyw3Dio+KaDJ2Nj+LA4WkOvsk45bWgYG5MAaXSQ==
|
||||
"@docusaurus/plugin-svgr@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-svgr/-/plugin-svgr-3.9.2.tgz#62857ed79d97c0150d25f7e7380fdee65671163a"
|
||||
integrity sha512-n+1DE+5b3Lnf27TgVU5jM1d4x5tUh2oW5LTsBxJX4PsAPV0JGcmI6p3yLYtEY0LRVEIJh+8RsdQmRE66wSV8mw==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
"@svgr/core" "8.1.0"
|
||||
"@svgr/webpack" "^8.1.0"
|
||||
tslib "^2.6.0"
|
||||
webpack "^5.88.1"
|
||||
|
||||
"@docusaurus/preset-classic@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.9.1.tgz#58d86664b5c9779578092556a0e6ae5ccebbd6c0"
|
||||
integrity sha512-ZHga2xsxxsyd0dN1BpLj8S889Eu9eMBuj2suqxdw/vaaXu/FjJ8KEGbcaeo6nHPo8VQcBBnPEdkBtSDm2TfMNw==
|
||||
"@docusaurus/preset-classic@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.9.2.tgz#85cc4f91baf177f8146c9ce896dfa1f0fd377050"
|
||||
integrity sha512-IgyYO2Gvaigi21LuDIe+nvmN/dfGXAiMcV/murFqcpjnZc7jxFAxW+9LEjdPt61uZLxG4ByW/oUmX/DDK9t/8w==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/plugin-content-blog" "3.9.1"
|
||||
"@docusaurus/plugin-content-docs" "3.9.1"
|
||||
"@docusaurus/plugin-content-pages" "3.9.1"
|
||||
"@docusaurus/plugin-css-cascade-layers" "3.9.1"
|
||||
"@docusaurus/plugin-debug" "3.9.1"
|
||||
"@docusaurus/plugin-google-analytics" "3.9.1"
|
||||
"@docusaurus/plugin-google-gtag" "3.9.1"
|
||||
"@docusaurus/plugin-google-tag-manager" "3.9.1"
|
||||
"@docusaurus/plugin-sitemap" "3.9.1"
|
||||
"@docusaurus/plugin-svgr" "3.9.1"
|
||||
"@docusaurus/theme-classic" "3.9.1"
|
||||
"@docusaurus/theme-common" "3.9.1"
|
||||
"@docusaurus/theme-search-algolia" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/plugin-content-blog" "3.9.2"
|
||||
"@docusaurus/plugin-content-docs" "3.9.2"
|
||||
"@docusaurus/plugin-content-pages" "3.9.2"
|
||||
"@docusaurus/plugin-css-cascade-layers" "3.9.2"
|
||||
"@docusaurus/plugin-debug" "3.9.2"
|
||||
"@docusaurus/plugin-google-analytics" "3.9.2"
|
||||
"@docusaurus/plugin-google-gtag" "3.9.2"
|
||||
"@docusaurus/plugin-google-tag-manager" "3.9.2"
|
||||
"@docusaurus/plugin-sitemap" "3.9.2"
|
||||
"@docusaurus/plugin-svgr" "3.9.2"
|
||||
"@docusaurus/theme-classic" "3.9.2"
|
||||
"@docusaurus/theme-common" "3.9.2"
|
||||
"@docusaurus/theme-search-algolia" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
|
||||
"@docusaurus/theme-classic@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.9.1.tgz#790fb1b8058d0572632211023ead238c1a6450e0"
|
||||
integrity sha512-LrAIu/mQ04nG6s1cssC0TMmICD8twFIIn/hJ5Pd9uIPQvtKnyAKEn12RefopAul5KfMo9kixPaqogV5jIJr26w==
|
||||
"@docusaurus/theme-classic@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.9.2.tgz#6e514f99a0ff42b80afcf42d5e5d042618311ce0"
|
||||
integrity sha512-IGUsArG5hhekXd7RDb11v94ycpJpFdJPkLnt10fFQWOVxAtq5/D7hT6lzc2fhyQKaaCE62qVajOMKL7OiAFAIA==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/logger" "3.9.1"
|
||||
"@docusaurus/mdx-loader" "3.9.1"
|
||||
"@docusaurus/module-type-aliases" "3.9.1"
|
||||
"@docusaurus/plugin-content-blog" "3.9.1"
|
||||
"@docusaurus/plugin-content-docs" "3.9.1"
|
||||
"@docusaurus/plugin-content-pages" "3.9.1"
|
||||
"@docusaurus/theme-common" "3.9.1"
|
||||
"@docusaurus/theme-translations" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-common" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/logger" "3.9.2"
|
||||
"@docusaurus/mdx-loader" "3.9.2"
|
||||
"@docusaurus/module-type-aliases" "3.9.2"
|
||||
"@docusaurus/plugin-content-blog" "3.9.2"
|
||||
"@docusaurus/plugin-content-docs" "3.9.2"
|
||||
"@docusaurus/plugin-content-pages" "3.9.2"
|
||||
"@docusaurus/theme-common" "3.9.2"
|
||||
"@docusaurus/theme-translations" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-common" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
"@mdx-js/react" "^3.0.0"
|
||||
clsx "^2.0.0"
|
||||
infima "0.2.0-alpha.45"
|
||||
@@ -1965,15 +1965,15 @@
|
||||
tslib "^2.6.0"
|
||||
utility-types "^3.10.0"
|
||||
|
||||
"@docusaurus/theme-common@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.9.1.tgz#095cbeab489d51380143951508571888f4d2928d"
|
||||
integrity sha512-j9adi961F+6Ps9d0jcb5BokMcbjXAAJqKkV43eo8nh4YgmDj7KUNDX4EnOh/MjTQeO06oPY5cxp3yUXdW/8Ggw==
|
||||
"@docusaurus/theme-common@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.9.2.tgz#487172c6fef9815c2746ef62a71e4f5b326f9ba5"
|
||||
integrity sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==
|
||||
dependencies:
|
||||
"@docusaurus/mdx-loader" "3.9.1"
|
||||
"@docusaurus/module-type-aliases" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-common" "3.9.1"
|
||||
"@docusaurus/mdx-loader" "3.9.2"
|
||||
"@docusaurus/module-type-aliases" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-common" "3.9.2"
|
||||
"@types/history" "^4.7.11"
|
||||
"@types/react" "*"
|
||||
"@types/react-router-config" "*"
|
||||
@@ -1983,32 +1983,32 @@
|
||||
tslib "^2.6.0"
|
||||
utility-types "^3.10.0"
|
||||
|
||||
"@docusaurus/theme-mermaid@^3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.9.1.tgz#92de20580489e05b3da6716503fda17e5a337a4d"
|
||||
integrity sha512-aKMFlQfxueVBPdCdrNSshG12fOkJXSn1sb6EhI/sGn3UpiTEiazJm4QLP6NoF78mqq8O5Ar2Yll+iHWLvCsuZQ==
|
||||
"@docusaurus/theme-mermaid@^3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.9.2.tgz#f065e4b4b319560ddd8c3be65ce9dd19ce1d5cc8"
|
||||
integrity sha512-5vhShRDq/ntLzdInsQkTdoKWSzw8d1jB17sNPYhA/KvYYFXfuVEGHLM6nrf8MFbV8TruAHDG21Fn3W4lO8GaDw==
|
||||
dependencies:
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/module-type-aliases" "3.9.1"
|
||||
"@docusaurus/theme-common" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/module-type-aliases" "3.9.2"
|
||||
"@docusaurus/theme-common" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
mermaid ">=11.6.0"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/theme-search-algolia@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.1.tgz#2f2ad5212201a1bed3acf8527ae6d81a079e654e"
|
||||
integrity sha512-WjM28bzlgfT6nHlEJemkwyGVpvGsZWPireV/w+wZ1Uo64xCZ8lNOb4xwQRukDaLSed3oPBN0gSnu06l5VuCXHg==
|
||||
"@docusaurus/theme-search-algolia@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz#420fd5b27fc1673b48151fdc9fe7167ba135ed50"
|
||||
integrity sha512-GBDSFNwjnh5/LdkxCKQHkgO2pIMX1447BxYUBG2wBiajS21uj64a+gH/qlbQjDLxmGrbrllBrtJkUHxIsiwRnw==
|
||||
dependencies:
|
||||
"@docsearch/react" "^3.9.0 || ^4.1.0"
|
||||
"@docusaurus/core" "3.9.1"
|
||||
"@docusaurus/logger" "3.9.1"
|
||||
"@docusaurus/plugin-content-docs" "3.9.1"
|
||||
"@docusaurus/theme-common" "3.9.1"
|
||||
"@docusaurus/theme-translations" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-validation" "3.9.1"
|
||||
"@docusaurus/core" "3.9.2"
|
||||
"@docusaurus/logger" "3.9.2"
|
||||
"@docusaurus/plugin-content-docs" "3.9.2"
|
||||
"@docusaurus/theme-common" "3.9.2"
|
||||
"@docusaurus/theme-translations" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-validation" "3.9.2"
|
||||
algoliasearch "^5.37.0"
|
||||
algoliasearch-helper "^3.26.0"
|
||||
clsx "^2.0.0"
|
||||
@@ -2018,23 +2018,23 @@
|
||||
tslib "^2.6.0"
|
||||
utility-types "^3.10.0"
|
||||
|
||||
"@docusaurus/theme-translations@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.9.1.tgz#189f1942d0178bc0da659db88c07682c7d7191ee"
|
||||
integrity sha512-mUQd49BSGKTiM6vP9+JFgRJL28lMIN3PUvXjF3rzuOHMByUZUBNwCt26Z23GkKiSIOrRkjKoaBNTipR/MHdYSQ==
|
||||
"@docusaurus/theme-translations@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.9.2.tgz#238cd69c2da92d612be3d3b4f95944c1d0f1e041"
|
||||
integrity sha512-vIryvpP18ON9T9rjgMRFLr2xJVDpw1rtagEGf8Ccce4CkTrvM/fRB8N2nyWYOW5u3DdjkwKw5fBa+3tbn9P4PA==
|
||||
dependencies:
|
||||
fs-extra "^11.1.1"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/tsconfig@^3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.9.1.tgz#a39cb74021f16dd4794db9182de812303817528e"
|
||||
integrity sha512-stdzM1dNDgRO0OvxeznXlE3N1igUoeHPNJjiKqyffLizgpVgNXJBAWeG6fuoYiCH4udGUBqy2dyM+1+kG2/UPQ==
|
||||
"@docusaurus/tsconfig@^3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.9.2.tgz#7f440e0ae665b841e1d487749037f26a0275f9c1"
|
||||
integrity sha512-j6/Fp4Rlpxsc632cnRnl5HpOWeb6ZKssDj6/XzzAzVGXXfm9Eptx3rxCC+fDzySn9fHTS+CWJjPineCR1bB5WQ==
|
||||
|
||||
"@docusaurus/types@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.9.1.tgz#e4fdaf0b91ea014a6aae0d8b62d59f3f020117b6"
|
||||
integrity sha512-ElekJ29sk39s5LTEZMByY1c2oH9FMtw7KbWFU3BtuQ1TytfIK39HhUivDEJvm5KCLyEnnfUZlvSNDXeyk0vzAA==
|
||||
"@docusaurus/types@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.9.2.tgz#e482cf18faea0d1fa5ce0e3f1e28e0f32d2593eb"
|
||||
integrity sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==
|
||||
dependencies:
|
||||
"@mdx-js/mdx" "^3.0.0"
|
||||
"@types/history" "^4.7.11"
|
||||
@@ -2047,36 +2047,36 @@
|
||||
webpack "^5.95.0"
|
||||
webpack-merge "^5.9.0"
|
||||
|
||||
"@docusaurus/utils-common@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.9.1.tgz#202778391caed923c2527a166a3aae3a22b2dcad"
|
||||
integrity sha512-4M1u5Q8Zn2CYL2TJ864M51FV4YlxyGyfC3x+7CLuR6xsyTVNBNU4QMcPgsTHRS9J2+X6Lq7MyH6hiWXyi/sXUQ==
|
||||
"@docusaurus/utils-common@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.9.2.tgz#e89bfcf43d66359f43df45293fcdf22814847460"
|
||||
integrity sha512-I53UC1QctruA6SWLvbjbhCpAw7+X7PePoe5pYcwTOEXD/PxeP8LnECAhTHHwWCblyUX5bMi4QLRkxvyZ+IT8Aw==
|
||||
dependencies:
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/utils-validation@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.9.1.tgz#8f9816b31ffb647539881f3c153d46f54e6399f7"
|
||||
integrity sha512-5bzab5si3E1udrlZuVGR17857Lfwe8iFPoy5AvMP9PXqDfoyIKT7gDQgAmxdRDMurgHaJlyhXEHHdzDKkOxxZQ==
|
||||
"@docusaurus/utils-validation@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.9.2.tgz#04aec285604790806e2fc5aa90aa950dc7ba75ae"
|
||||
integrity sha512-l7yk3X5VnNmATbwijJkexdhulNsQaNDwoagiwujXoxFbWLcxHQqNQ+c/IAlzrfMMOfa/8xSBZ7KEKDesE/2J7A==
|
||||
dependencies:
|
||||
"@docusaurus/logger" "3.9.1"
|
||||
"@docusaurus/utils" "3.9.1"
|
||||
"@docusaurus/utils-common" "3.9.1"
|
||||
"@docusaurus/logger" "3.9.2"
|
||||
"@docusaurus/utils" "3.9.2"
|
||||
"@docusaurus/utils-common" "3.9.2"
|
||||
fs-extra "^11.2.0"
|
||||
joi "^17.9.2"
|
||||
js-yaml "^4.1.0"
|
||||
lodash "^4.17.21"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/utils@3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.9.1.tgz#9b78849a2be5e3023580b800409aae36a0da6dc8"
|
||||
integrity sha512-YAL4yhhWLl9DXuf5MVig260a6INz4MehrBGFU/CZu8yXmRiYEuQvRFWh9ZsjfAOyaG7za1MNmBVZ4VVAi/CiJA==
|
||||
"@docusaurus/utils@3.9.2":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.9.2.tgz#ffab7922631c7e0febcb54e6d499f648bf8a89eb"
|
||||
integrity sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==
|
||||
dependencies:
|
||||
"@docusaurus/logger" "3.9.1"
|
||||
"@docusaurus/types" "3.9.1"
|
||||
"@docusaurus/utils-common" "3.9.1"
|
||||
"@docusaurus/logger" "3.9.2"
|
||||
"@docusaurus/types" "3.9.2"
|
||||
"@docusaurus/utils-common" "3.9.2"
|
||||
escape-string-regexp "^4.0.0"
|
||||
execa "5.1.1"
|
||||
file-loader "^6.2.0"
|
||||
@@ -2428,19 +2428,19 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
|
||||
integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
|
||||
|
||||
"@eslint/config-array@^0.21.0":
|
||||
version "0.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.0.tgz#abdbcbd16b124c638081766392a4d6b509f72636"
|
||||
integrity sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==
|
||||
"@eslint/config-array@^0.21.1":
|
||||
version "0.21.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.1.tgz#7d1b0060fea407f8301e932492ba8c18aff29713"
|
||||
integrity sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==
|
||||
dependencies:
|
||||
"@eslint/object-schema" "^2.1.6"
|
||||
"@eslint/object-schema" "^2.1.7"
|
||||
debug "^4.3.1"
|
||||
minimatch "^3.1.2"
|
||||
|
||||
"@eslint/config-helpers@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.0.tgz#e9f94ba3b5b875e32205cb83fece18e64486e9e6"
|
||||
integrity sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==
|
||||
"@eslint/config-helpers@^0.4.1":
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.1.tgz#7d173a1a35fe256f0989a0fdd8d911ebbbf50037"
|
||||
integrity sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==
|
||||
dependencies:
|
||||
"@eslint/core" "^0.16.0"
|
||||
|
||||
@@ -2466,15 +2466,15 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@9.37.0", "@eslint/js@^9.37.0":
|
||||
version "9.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.37.0.tgz#0cfd5aa763fe5d1ee60bedf84cd14f54bcf9e21b"
|
||||
integrity sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==
|
||||
"@eslint/js@9.38.0", "@eslint/js@^9.38.0":
|
||||
version "9.38.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.38.0.tgz#f7aa9c7577577f53302c1d795643589d7709ebd1"
|
||||
integrity sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==
|
||||
|
||||
"@eslint/object-schema@^2.1.6":
|
||||
version "2.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
|
||||
integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
|
||||
"@eslint/object-schema@^2.1.7":
|
||||
version "2.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad"
|
||||
integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==
|
||||
|
||||
"@eslint/plugin-kit@^0.4.0":
|
||||
version "0.4.0"
|
||||
@@ -2796,14 +2796,13 @@
|
||||
classnames "^2.3.2"
|
||||
rc-util "^5.24.4"
|
||||
|
||||
"@rc-component/qrcode@~1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/qrcode/-/qrcode-1.0.0.tgz#48a8de5eb11d0e65926f1377c4b1ef4c888997f5"
|
||||
integrity sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==
|
||||
"@rc-component/qrcode@~1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/qrcode/-/qrcode-1.0.1.tgz#98e0a79dc95f26fe211b59d04ef3312bc70dedbe"
|
||||
integrity sha512-g8eeeaMyFXVlq8cZUeaxCDhfIYjpao0l9cvm5gFwKXy/Vm1yDWV7h2sjH5jHYzdFedlVKBpATFB1VKMrHzwaWQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.24.7"
|
||||
classnames "^2.3.2"
|
||||
rc-util "^5.38.0"
|
||||
|
||||
"@rc-component/tour@~1.15.1":
|
||||
version "1.15.1"
|
||||
@@ -4336,79 +4335,79 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.46.0", "@typescript-eslint/eslint-plugin@^8.37.0":
|
||||
version "8.46.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz#fc90b35d8025b5eaa66b2f6c3859cd5381a1e751"
|
||||
integrity sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==
|
||||
"@typescript-eslint/eslint-plugin@8.46.2", "@typescript-eslint/eslint-plugin@^8.37.0":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz#dc4ab93ee3d7e6c8e38820a0d6c7c93c7183e2dc"
|
||||
integrity sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.46.0"
|
||||
"@typescript-eslint/type-utils" "8.46.0"
|
||||
"@typescript-eslint/utils" "8.46.0"
|
||||
"@typescript-eslint/visitor-keys" "8.46.0"
|
||||
"@typescript-eslint/scope-manager" "8.46.2"
|
||||
"@typescript-eslint/type-utils" "8.46.2"
|
||||
"@typescript-eslint/utils" "8.46.2"
|
||||
"@typescript-eslint/visitor-keys" "8.46.2"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^7.0.0"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@8.46.0", "@typescript-eslint/parser@^8.46.0":
|
||||
version "8.46.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.46.0.tgz#9186f28c59f6e477ab8919312d2654f4f27d45c1"
|
||||
integrity sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==
|
||||
"@typescript-eslint/parser@8.46.2", "@typescript-eslint/parser@^8.46.0":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.46.2.tgz#dd938d45d581ac8ffa9d8a418a50282b306f7ebf"
|
||||
integrity sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.46.0"
|
||||
"@typescript-eslint/types" "8.46.0"
|
||||
"@typescript-eslint/typescript-estree" "8.46.0"
|
||||
"@typescript-eslint/visitor-keys" "8.46.0"
|
||||
"@typescript-eslint/scope-manager" "8.46.2"
|
||||
"@typescript-eslint/types" "8.46.2"
|
||||
"@typescript-eslint/typescript-estree" "8.46.2"
|
||||
"@typescript-eslint/visitor-keys" "8.46.2"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/project-service@8.46.0":
|
||||
version "8.46.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.46.0.tgz#1190dcc0d3494d46a85773e0c3a2838cbb2b45a7"
|
||||
integrity sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==
|
||||
"@typescript-eslint/project-service@8.46.2":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.46.2.tgz#ab2f02a0de4da6a7eeb885af5e059be57819d608"
|
||||
integrity sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.46.0"
|
||||
"@typescript-eslint/types" "^8.46.0"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.46.2"
|
||||
"@typescript-eslint/types" "^8.46.2"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.46.0":
|
||||
version "8.46.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz#a41833fe387044075cb2d4cfab490a7f1dd19b61"
|
||||
integrity sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==
|
||||
"@typescript-eslint/scope-manager@8.46.2":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz#7d37df2493c404450589acb3b5d0c69cc0670a88"
|
||||
integrity sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.46.0"
|
||||
"@typescript-eslint/visitor-keys" "8.46.0"
|
||||
"@typescript-eslint/types" "8.46.2"
|
||||
"@typescript-eslint/visitor-keys" "8.46.2"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.46.0", "@typescript-eslint/tsconfig-utils@^8.46.0":
|
||||
version "8.46.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz#3e33019e0b94838d37d7cc61341fbcc5bf791007"
|
||||
integrity sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==
|
||||
"@typescript-eslint/tsconfig-utils@8.46.2", "@typescript-eslint/tsconfig-utils@^8.46.2":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz#d110451cb93bbd189865206ea37ef677c196828c"
|
||||
integrity sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==
|
||||
|
||||
"@typescript-eslint/type-utils@8.46.0":
|
||||
version "8.46.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz#815efeb11b9533da68fd825628cecf283ac79829"
|
||||
integrity sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==
|
||||
"@typescript-eslint/type-utils@8.46.2":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz#802d027864e6fb752e65425ed09f3e089fb4d384"
|
||||
integrity sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.46.0"
|
||||
"@typescript-eslint/typescript-estree" "8.46.0"
|
||||
"@typescript-eslint/utils" "8.46.0"
|
||||
"@typescript-eslint/types" "8.46.2"
|
||||
"@typescript-eslint/typescript-estree" "8.46.2"
|
||||
"@typescript-eslint/utils" "8.46.2"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@8.46.0", "@typescript-eslint/types@^8.46.0":
|
||||
version "8.46.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.46.0.tgz#20af6b332f9cd55a15fcd862fdb07d47a6131bf4"
|
||||
integrity sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==
|
||||
"@typescript-eslint/types@8.46.2", "@typescript-eslint/types@^8.46.2":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.46.2.tgz#2bad7348511b31e6e42579820e62b73145635763"
|
||||
integrity sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.46.0":
|
||||
version "8.46.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz#f45a0d5f5e99b26f0280e8cff3ed3380658fd720"
|
||||
integrity sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==
|
||||
"@typescript-eslint/typescript-estree@8.46.2":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz#ab547a27e4222bb6a3281cb7e98705272e2c7d08"
|
||||
integrity sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.46.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.46.0"
|
||||
"@typescript-eslint/types" "8.46.0"
|
||||
"@typescript-eslint/visitor-keys" "8.46.0"
|
||||
"@typescript-eslint/project-service" "8.46.2"
|
||||
"@typescript-eslint/tsconfig-utils" "8.46.2"
|
||||
"@typescript-eslint/types" "8.46.2"
|
||||
"@typescript-eslint/visitor-keys" "8.46.2"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
@@ -4416,22 +4415,22 @@
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/utils@8.46.0":
|
||||
version "8.46.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.46.0.tgz#27025c5ed7cbc928440d6a30edd6ba34cc5b927a"
|
||||
integrity sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==
|
||||
"@typescript-eslint/utils@8.46.2":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.46.2.tgz#b313d33d67f9918583af205bd7bcebf20f231732"
|
||||
integrity sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.7.0"
|
||||
"@typescript-eslint/scope-manager" "8.46.0"
|
||||
"@typescript-eslint/types" "8.46.0"
|
||||
"@typescript-eslint/typescript-estree" "8.46.0"
|
||||
"@typescript-eslint/scope-manager" "8.46.2"
|
||||
"@typescript-eslint/types" "8.46.2"
|
||||
"@typescript-eslint/typescript-estree" "8.46.2"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.46.0":
|
||||
version "8.46.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz#23936809054c511f703713c56ddd2f46dc197845"
|
||||
integrity sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==
|
||||
"@typescript-eslint/visitor-keys@8.46.2":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz#803fa298948c39acf810af21bdce6f8babfa9738"
|
||||
integrity sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.46.0"
|
||||
"@typescript-eslint/types" "8.46.2"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -4746,10 +4745,10 @@ ansi-styles@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
antd@^5.27.4:
|
||||
version "5.27.4"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.27.4.tgz#13c97deb12e6aeb43adecd23f3dbe3139a62e579"
|
||||
integrity sha512-rhArohoAUCxhkPjGI/BXthOrrjaElL4Fb7d4vEHnIR3DpxFXfegd4rN21IgGdiF+Iz4EFuUZu8MdS8NuJHLSVQ==
|
||||
antd@^5.27.6:
|
||||
version "5.27.6"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.27.6.tgz#6b7c7a87b5c696395d2aab2fdbd8409a342813e1"
|
||||
integrity sha512-70HrjVbzDXvtiUQ5MP1XdNudr/wGAk9Ivaemk6f36yrAeJurJSmZ8KngOIilolLRHdGuNc6/Vk+4T1OZpSjpag==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^7.2.1"
|
||||
"@ant-design/cssinjs" "^1.23.0"
|
||||
@@ -4760,7 +4759,7 @@ antd@^5.27.4:
|
||||
"@babel/runtime" "^7.26.0"
|
||||
"@rc-component/color-picker" "~2.0.1"
|
||||
"@rc-component/mutate-observer" "^1.1.0"
|
||||
"@rc-component/qrcode" "~1.0.0"
|
||||
"@rc-component/qrcode" "~1.0.1"
|
||||
"@rc-component/tour" "~1.15.1"
|
||||
"@rc-component/trigger" "^2.3.0"
|
||||
classnames "^2.5.1"
|
||||
@@ -4790,7 +4789,7 @@ antd@^5.27.4:
|
||||
rc-slider "~11.1.9"
|
||||
rc-steps "~6.0.1"
|
||||
rc-switch "~4.1.0"
|
||||
rc-table "~7.53.0"
|
||||
rc-table "~7.54.0"
|
||||
rc-tabs "~15.7.0"
|
||||
rc-textarea "~1.10.2"
|
||||
rc-tooltip "~6.4.0"
|
||||
@@ -5305,10 +5304,10 @@ caniuse-api@^3.0.0:
|
||||
lodash.memoize "^4.1.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001746, caniuse-lite@^1.0.30001749:
|
||||
version "1.0.30001749"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz#21a43b923577932097fe32bcaabb6da7f4677632"
|
||||
integrity sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001746, caniuse-lite@^1.0.30001751:
|
||||
version "1.0.30001751"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz#dacd5d9f4baeea841641640139d2b2a4df4226ad"
|
||||
integrity sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
@@ -6997,24 +6996,23 @@ eslint-visitor-keys@^4.2.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
|
||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||
|
||||
eslint@^9.37.0:
|
||||
version "9.37.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.37.0.tgz#ac0222127f76b09c0db63036f4fe289562072d74"
|
||||
integrity sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==
|
||||
eslint@^9.38.0:
|
||||
version "9.38.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.38.0.tgz#3957d2af804e5cf6cc503c618f60acc71acb2e7e"
|
||||
integrity sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.8.0"
|
||||
"@eslint-community/regexpp" "^4.12.1"
|
||||
"@eslint/config-array" "^0.21.0"
|
||||
"@eslint/config-helpers" "^0.4.0"
|
||||
"@eslint/config-array" "^0.21.1"
|
||||
"@eslint/config-helpers" "^0.4.1"
|
||||
"@eslint/core" "^0.16.0"
|
||||
"@eslint/eslintrc" "^3.3.1"
|
||||
"@eslint/js" "9.37.0"
|
||||
"@eslint/js" "9.38.0"
|
||||
"@eslint/plugin-kit" "^0.4.0"
|
||||
"@humanfs/node" "^0.16.6"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
"@humanwhocodes/retry" "^0.4.2"
|
||||
"@types/estree" "^1.0.6"
|
||||
"@types/json-schema" "^7.0.15"
|
||||
ajv "^6.12.4"
|
||||
chalk "^4.0.0"
|
||||
cross-spawn "^7.0.6"
|
||||
@@ -11729,10 +11727,10 @@ rc-switch@~4.1.0:
|
||||
classnames "^2.2.1"
|
||||
rc-util "^5.30.0"
|
||||
|
||||
rc-table@~7.53.0:
|
||||
version "7.53.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.53.1.tgz#b891aa39e9d1d944711f018692d2c52013afc90f"
|
||||
integrity sha512-firAd7Z+liqIDS5TubJ1qqcoBd6YcANLKWQDZhFf3rfoOTt/UNPj4n3O+2vhl+z4QMqwPEUVAil661WHA8H8Aw==
|
||||
rc-table@~7.54.0:
|
||||
version "7.54.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.54.0.tgz#dedd4ea18d1189f2acdf90a80f04d8ca0111e16a"
|
||||
integrity sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
"@rc-component/context" "^1.4.0"
|
||||
@@ -13259,10 +13257,10 @@ swagger-client@^3.35.7:
|
||||
ramda "^0.30.1"
|
||||
ramda-adjunct "^5.1.0"
|
||||
|
||||
swagger-ui-react@^5.29.3:
|
||||
version "5.29.3"
|
||||
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.29.3.tgz#a132c3c3c4553c2acd0aca1f02c8484ca4c78183"
|
||||
integrity sha512-cx47SmqrxXCP86+6NHEzXUBEG/MGbNK/H8BQphzUVomxGpG9lZCUo6hIGFNe1i7fP5eaMxpLV/qoqaWVo3TSvw==
|
||||
swagger-ui-react@^5.29.5:
|
||||
version "5.29.5"
|
||||
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.29.5.tgz#8c6eafebb75972c15a9f3e24627caec10cc32cbe"
|
||||
integrity sha512-D0YbsDhi4F38HsY5p1DjzuNduU/fVQxtqm3v0o2dRTF5BbLJYRSgjMZ79jejG4q3nNw4kuouCKKiq5xqCLjWrQ==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.27.1"
|
||||
"@scarf/scarf" "=1.4.0"
|
||||
@@ -13590,15 +13588,15 @@ types-ramda@^0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.46.0:
|
||||
version "8.46.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.46.0.tgz#fb1c37a90fadf42fe1c8f8b192b974b6d9c439cc"
|
||||
integrity sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==
|
||||
typescript-eslint@^8.46.2:
|
||||
version "8.46.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.46.2.tgz#da1adec683ba93a1b6c3850a4efb0922ffbc627d"
|
||||
integrity sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.46.0"
|
||||
"@typescript-eslint/parser" "8.46.0"
|
||||
"@typescript-eslint/typescript-estree" "8.46.0"
|
||||
"@typescript-eslint/utils" "8.46.0"
|
||||
"@typescript-eslint/eslint-plugin" "8.46.2"
|
||||
"@typescript-eslint/parser" "8.46.2"
|
||||
"@typescript-eslint/typescript-estree" "8.46.2"
|
||||
"@typescript-eslint/utils" "8.46.2"
|
||||
|
||||
typescript@~5.9.3:
|
||||
version "5.9.3"
|
||||
|
||||
Generated
+192
-265
@@ -62,14 +62,14 @@
|
||||
"content-disposition": "^0.5.4",
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-scale": "^2.1.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"dom-to-image-more": "^3.6.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"geolib": "^2.0.24",
|
||||
"geostyler": "^14.1.3",
|
||||
"geostyler-data": "^1.1.0",
|
||||
@@ -112,7 +112,7 @@
|
||||
"react-loadable": "^5.5.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-reverse-portal": "^2.1.2",
|
||||
"react-reverse-portal": "^2.3.0",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-search-input": "^0.11.3",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
@@ -139,10 +139,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-storybook": "^3.60.0",
|
||||
"@babel/cli": "^7.27.2",
|
||||
"@babel/compat-data": "^7.28.0",
|
||||
"@babel/cli": "^7.28.3",
|
||||
"@babel/compat-data": "^7.28.4",
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/eslint-parser": "^7.25.9",
|
||||
"@babel/eslint-parser": "^7.28.4",
|
||||
"@babel/node": "^7.22.6",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
||||
@@ -161,7 +161,7 @@
|
||||
"@hot-loader/react-dom": "^17.0.2",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@mihkeleidast/storybook-addon-source": "^1.0.1",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@playwright/test": "^1.56.0",
|
||||
"@storybook/addon-actions": "8.1.11",
|
||||
"@storybook/addon-controls": "8.1.11",
|
||||
"@storybook/addon-essentials": "8.1.11",
|
||||
@@ -184,7 +184,7 @@
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/math-expression-evaluator": "^1.3.3",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^24.6.2",
|
||||
"@types/node": "^24.8.1",
|
||||
"@types/react": "^17.0.83",
|
||||
"@types/react-dom": "^17.0.26",
|
||||
"@types/react-json-tree": "^0.13.0",
|
||||
@@ -210,7 +210,7 @@
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"babel-plugin-typescript-to-proptypes": "^2.0.0",
|
||||
"cheerio": "1.1.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"copy-webpack-plugin": "^13.0.1",
|
||||
"cross-env": "^10.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.2",
|
||||
@@ -228,7 +228,7 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"eslint-plugin-no-only-tests": "^3.3.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
||||
@@ -239,7 +239,7 @@
|
||||
"fetch-mock": "^11.1.5",
|
||||
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
||||
"history": "^5.3.0",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"html-webpack-plugin": "^5.6.4",
|
||||
"imports-loader": "^5.0.0",
|
||||
"jest": "^30.0.2",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
@@ -251,7 +251,7 @@
|
||||
"open-cli": "^8.0.0",
|
||||
"po2json": "^0.4.5",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-packagejson": "^2.5.3",
|
||||
"prettier-plugin-packagejson": "^2.5.19",
|
||||
"process": "^0.11.10",
|
||||
"react-resizable": "^3.0.5",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
@@ -262,13 +262,13 @@
|
||||
"storybook": "8.1.11",
|
||||
"style-loader": "^4.0.0",
|
||||
"thread-loader": "^4.0.4",
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-jest": "^29.4.5",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "5.4.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.102.0",
|
||||
"webpack": "^5.102.1",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.2",
|
||||
@@ -1106,13 +1106,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/cli": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.27.2.tgz",
|
||||
"integrity": "sha512-cfd7DnGlhH6OIyuPSSj3vcfIdnbXukhAyKY8NaZrFadC7pXyL9mOL5WgjcptiEJLi5k3j8aYvLIVCzezrWTaiA==",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.28.3.tgz",
|
||||
"integrity": "sha512-n1RU5vuCX0CsaqaXm9I0KUCNKNQMy5epmzl/xdSSm70bSqhg9GWhgeosypyQLc0bK24+Xpk1WGzZlI9pJtkZdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"commander": "^6.2.0",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"fs-readdir-recursive": "^1.1.0",
|
||||
@@ -1150,9 +1150,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
|
||||
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
|
||||
"integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1201,9 +1201,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/eslint-parser": {
|
||||
"version": "7.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.26.5.tgz",
|
||||
"integrity": "sha512-Kkm8C8uxI842AwQADxl0GbcG1rupELYLShazYEZO/2DYjhyWXJIOUVOE3tBYm6JXzUCNJOZEzqc4rCW/jsEQYQ==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.4.tgz",
|
||||
"integrity": "sha512-Aa+yDiH87980jR6zvRfFuCR1+dLb00vBydhTL+zI992Rz/wQhSvuxjmOOuJOgO3XmakO6RykRGD2S1mq1AtgHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4125,6 +4125,12 @@
|
||||
"mjolnir.js": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@deck.gl/core/node_modules/@luma.gl/constants": {
|
||||
"version": "9.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-9.1.10.tgz",
|
||||
"integrity": "sha512-O4Nx8UbWmrHHZ7ihKB8WiscX1cz05l1KvKorYTgq+xeXwz2Beh3MkXBMnA46uuyEtimN945OEdYshZnbh80jyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@deck.gl/extensions": {
|
||||
"version": "9.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@deck.gl/extensions/-/extensions-9.1.13.tgz",
|
||||
@@ -9000,9 +9006,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@luma.gl/constants": {
|
||||
"version": "9.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-9.1.9.tgz",
|
||||
"integrity": "sha512-yc9fml04OeTTcwK+7gmDMxoLQ67j4ZiAFXjmYvPomYyBVzS0NZxTDuwcCBmnxjLOiroOZW8FRRrVc/yOiFug2w==",
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-9.2.2.tgz",
|
||||
"integrity": "sha512-XURMF0gSh0ImZltYa/PCe9KgmopQJiOA6y1m1PxDxJY8OCLma7ZJyvomLn7TQBvPtWTYZsibTW7blu7RwThsaQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@luma.gl/core": {
|
||||
@@ -9078,6 +9084,12 @@
|
||||
"@luma.gl/core": "^9.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@luma.gl/webgl/node_modules/@luma.gl/constants": {
|
||||
"version": "9.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-9.1.9.tgz",
|
||||
"integrity": "sha512-yc9fml04OeTTcwK+7gmDMxoLQ67j4ZiAFXjmYvPomYyBVzS0NZxTDuwcCBmnxjLOiroOZW8FRRrVc/yOiFug2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mapbox/extent": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/extent/-/extent-0.4.0.tgz",
|
||||
@@ -10577,26 +10589,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgr/core": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
|
||||
"integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
|
||||
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz",
|
||||
"integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==",
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0.tgz",
|
||||
"integrity": "sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.55.0"
|
||||
"playwright": "1.56.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -16371,12 +16383,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz",
|
||||
"integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==",
|
||||
"version": "24.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz",
|
||||
"integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.13.0"
|
||||
"undici-types": "~7.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-forge": {
|
||||
@@ -19140,9 +19152,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ace-builds": {
|
||||
"version": "1.43.1",
|
||||
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.1.tgz",
|
||||
"integrity": "sha512-n9/n+zBhbbkEJjU0FJ4wWAZBDl5G8WYzg4+uIjSER/U3wSSSSVo52W4sco4Jryg11JAJvorExxMr3GDINqtjdA==",
|
||||
"version": "1.43.4",
|
||||
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.4.tgz",
|
||||
"integrity": "sha512-8hAxVfo2ImICd69BWlZwZlxe9rxDGDjuUhh+WeWgGDvfBCE+r3lkynkQvIovDz4jcMi8O7bsEaFygaDT+h9sBA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
@@ -20893,9 +20905,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.26.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
|
||||
"integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==",
|
||||
"version": "4.26.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
|
||||
"integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -20913,9 +20925,9 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
"electron-to-chromium": "^1.5.218",
|
||||
"baseline-browser-mapping": "^2.8.9",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
"electron-to-chromium": "^1.5.227",
|
||||
"node-releases": "^2.0.21",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
},
|
||||
@@ -22723,9 +22735,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/copy-webpack-plugin": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.0.tgz",
|
||||
"integrity": "sha512-FgR/h5a6hzJqATDGd9YG41SeDViH+0bkHn6WNXCi5zKAZkeESeSxLySSsFLHqLEVCh0E+rITmCf0dusXWYukeQ==",
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz",
|
||||
"integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -23925,9 +23937,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
|
||||
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debounce": {
|
||||
@@ -24352,13 +24364,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/detect-indent": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz",
|
||||
"integrity": "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.2.tgz",
|
||||
"integrity": "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-newline": {
|
||||
@@ -26343,14 +26358,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz",
|
||||
"integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==",
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz",
|
||||
"integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.0",
|
||||
"synckit": "^0.9.1"
|
||||
"synckit": "^0.11.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
@@ -26361,7 +26376,7 @@
|
||||
"peerDependencies": {
|
||||
"@types/eslint": ">=8.0.0",
|
||||
"eslint": ">=8.0.0",
|
||||
"eslint-config-prettier": "*",
|
||||
"eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
|
||||
"prettier": ">=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -28414,9 +28429,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fuse.js": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz",
|
||||
"integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
|
||||
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -29526,9 +29541,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/git-hooks-list": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-3.1.0.tgz",
|
||||
"integrity": "sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-4.1.1.tgz",
|
||||
"integrity": "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -30871,9 +30886,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/html-webpack-plugin": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz",
|
||||
"integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==",
|
||||
"version": "5.6.4",
|
||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz",
|
||||
"integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -35729,19 +35744,6 @@
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-snapshot/node_modules/@pkgr/core": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz",
|
||||
"integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-snapshot/node_modules/@sinclair/typebox": {
|
||||
"version": "0.34.37",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz",
|
||||
@@ -35934,22 +35936,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-snapshot/node_modules/synckit": {
|
||||
"version": "0.11.8",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz",
|
||||
"integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-util": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
|
||||
@@ -46079,13 +46065,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.55.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz",
|
||||
"integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==",
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz",
|
||||
"integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.55.0"
|
||||
"playwright-core": "1.56.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -46098,9 +46084,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.55.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz",
|
||||
"integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==",
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0.tgz",
|
||||
"integrity": "sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -46863,14 +46849,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-packagejson": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.8.tgz",
|
||||
"integrity": "sha512-BaGOF63I0IJZoudxpuQe17naV93BRtK8b3byWktkJReKEMX9CC4qdGUzThPDVO/AUhPzlqDiAXbp18U6X8wLKA==",
|
||||
"version": "2.5.19",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.19.tgz",
|
||||
"integrity": "sha512-Qsqp4+jsZbKMpEGZB1UP1pxeAT8sCzne2IwnKkr+QhUe665EXUo3BAvTf1kAPCqyMv9kg3ZmO0+7eOni/C6Uag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sort-package-json": "2.14.0",
|
||||
"synckit": "0.9.2"
|
||||
"sort-package-json": "3.4.0",
|
||||
"synckit": "0.11.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prettier": ">= 1.16.0"
|
||||
@@ -48969,13 +48955,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-reverse-portal": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-reverse-portal/-/react-reverse-portal-2.1.2.tgz",
|
||||
"integrity": "sha512-li4puNtBmMMJhtI+IVxeSX0RvK1ft8qjPSbCih4OKQ/YUIcROc31Nmo22gv94hTx8EUfR7fzZY47RuZF2YRMdQ==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-reverse-portal/-/react-reverse-portal-2.3.0.tgz",
|
||||
"integrity": "sha512-kvbPfLPKg6Y3S6tVq83us2RghvDpOS4GcJxbI7cZ0V0tuzUaSzblRIhVnKLOucfqF4lN/i9oWvEmpEi6bAOYlQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
@@ -51689,9 +51675,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
|
||||
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -53001,23 +52987,25 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sort-package-json": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-2.14.0.tgz",
|
||||
"integrity": "sha512-xBRdmMjFB/KW3l51mP31dhlaiFmqkHLfWTfZAno8prb/wbDxwBPWFpxB16GZbiPbYr3wL41H8Kx22QIDWRe8WQ==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.4.0.tgz",
|
||||
"integrity": "sha512-97oFRRMM2/Js4oEA9LJhjyMlde+2ewpZQf53pgue27UkbEXfHJnDzHlUxQ/DWUkzqmp7DFwJp8D+wi/TYeQhpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-indent": "^7.0.1",
|
||||
"detect-newline": "^4.0.0",
|
||||
"get-stdin": "^9.0.0",
|
||||
"git-hooks-list": "^3.0.0",
|
||||
"detect-newline": "^4.0.1",
|
||||
"git-hooks-list": "^4.0.0",
|
||||
"is-plain-obj": "^4.1.0",
|
||||
"semver": "^7.6.0",
|
||||
"semver": "^7.7.1",
|
||||
"sort-object-keys": "^1.1.3",
|
||||
"tinyglobby": "^0.2.9"
|
||||
"tinyglobby": "^0.2.12"
|
||||
},
|
||||
"bin": {
|
||||
"sort-package-json": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/sort-package-json/node_modules/detect-newline": {
|
||||
@@ -53046,6 +53034,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/sort-package-json/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/source-list-map": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
||||
@@ -54355,26 +54356,25 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
|
||||
"integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
|
||||
"version": "0.11.11",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
|
||||
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.1.0",
|
||||
"tslib": "^2.6.2"
|
||||
"@pkgr/core": "^0.2.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
|
||||
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -55359,19 +55359,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ts-jest": {
|
||||
"version": "29.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz",
|
||||
"integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==",
|
||||
"version": "29.4.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz",
|
||||
"integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bs-logger": "^0.2.6",
|
||||
"ejs": "^3.1.10",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"json5": "^2.2.3",
|
||||
"lodash.memoize": "^4.1.2",
|
||||
"make-error": "^1.3.6",
|
||||
"semver": "^7.7.2",
|
||||
"semver": "^7.7.3",
|
||||
"type-fest": "^4.41.0",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
@@ -55412,9 +55412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ts-jest/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
@@ -56413,9 +56413,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
|
||||
"integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
|
||||
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
@@ -57554,9 +57554,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.102.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.0.tgz",
|
||||
"integrity": "sha512-hUtqAR3ZLVEYDEABdBioQCIqSoguHbFn1K7WlPPWSuXmx0031BD73PSE35jKyftdSh4YLDoQNgK4pqBt5Q82MA==",
|
||||
"version": "5.102.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
|
||||
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -57568,7 +57568,7 @@
|
||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-import-phases": "^1.0.3",
|
||||
"browserslist": "^4.24.5",
|
||||
"browserslist": "^4.26.3",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.17.3",
|
||||
"es-module-lexer": "^1.2.1",
|
||||
@@ -57580,8 +57580,8 @@
|
||||
"loader-runner": "^4.2.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^4.3.2",
|
||||
"tapable": "^2.2.3",
|
||||
"schema-utils": "^4.3.3",
|
||||
"tapable": "^2.3.0",
|
||||
"terser-webpack-plugin": "^5.3.11",
|
||||
"watchpack": "^2.4.4",
|
||||
"webpack-sources": "^3.3.3"
|
||||
@@ -59296,7 +59296,7 @@
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"chalk": "^5.4.1",
|
||||
"chalk": "^5.6.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"yeoman-generator": "^7.5.1",
|
||||
"yosay": "^3.0.0"
|
||||
@@ -59891,19 +59891,6 @@
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
}
|
||||
},
|
||||
"packages/generator-superset/node_modules/@pkgr/core": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
|
||||
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"packages/generator-superset/node_modules/@sinclair/typebox": {
|
||||
"version": "0.34.38",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz",
|
||||
@@ -59977,9 +59964,9 @@
|
||||
}
|
||||
},
|
||||
"packages/generator-superset/node_modules/chalk": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
@@ -61279,22 +61266,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/generator-superset/node_modules/synckit": {
|
||||
"version": "0.11.11",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
|
||||
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.2.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"packages/generator-superset/node_modules/universal-user-agent": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
|
||||
@@ -61343,7 +61314,7 @@
|
||||
"version": "0.0.1-rc5",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.26.4",
|
||||
"@babel/cli": "^7.28.3",
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
@@ -64067,11 +64038,11 @@
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@apache-superset/core": "*",
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@fontsource/fira-code": "^5.2.6",
|
||||
"@fontsource/fira-code": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@visx/responsive": "^3.12.0",
|
||||
"ace-builds": "^1.43.3",
|
||||
"ace-builds": "^1.43.4",
|
||||
"ag-grid-community": "34.2.0",
|
||||
"ag-grid-react": "34.2.0",
|
||||
"brace": "^0.11.1",
|
||||
@@ -64083,14 +64054,14 @@
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-time": "^3.1.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"dompurify": "^3.2.4",
|
||||
"fetch-retry": "^6.0.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"jed": "^1.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"math-expression-evaluator": "^2.0.6",
|
||||
"pretty-ms": "^9.2.0",
|
||||
"pretty-ms": "^9.3.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react-ace": "^14.0.1",
|
||||
"react-draggable": "^4.5.0",
|
||||
@@ -64107,7 +64078,7 @@
|
||||
"reselect": "^5.1.1",
|
||||
"rison": "^0.1.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"xss": "^1.0.14"
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emotion/styled": "^11.14.1",
|
||||
@@ -64119,7 +64090,7 @@
|
||||
"@types/jquery": "^3.5.33",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/math-expression-evaluator": "^1.3.3",
|
||||
"@types/node": "^24.6.2",
|
||||
"@types/node": "^24.8.1",
|
||||
"@types/prop-types": "^15.7.15",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/react-table": "^7.7.20",
|
||||
@@ -65000,6 +64971,20 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/superset-ui-core/node_modules/pretty-ms": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
|
||||
"integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==",
|
||||
"dependencies": {
|
||||
"parse-ms": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/superset-ui-core/node_modules/re-resizable": {
|
||||
"version": "6.11.2",
|
||||
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz",
|
||||
@@ -65936,7 +65921,7 @@
|
||||
"@deck.gl/geo-layers": "^9.1.13",
|
||||
"@deck.gl/layers": "^9.1.13",
|
||||
"@deck.gl/react": "^9.1.14",
|
||||
"@luma.gl/constants": "^9.1.9",
|
||||
"@luma.gl/constants": "^9.2.2",
|
||||
"@luma.gl/core": "^9.1.9",
|
||||
"@luma.gl/engine": "^9.1.9",
|
||||
"@luma.gl/shadertools": "^9.1.9",
|
||||
@@ -65950,7 +65935,7 @@
|
||||
"d3-array": "^1.2.4",
|
||||
"d3-color": "^1.4.1",
|
||||
"d3-scale": "^3.0.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"handlebars": "^4.7.8",
|
||||
"lodash": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -65994,6 +65979,12 @@
|
||||
"@luma.gl/engine": "~9.1.9"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-preset-chart-deckgl/node_modules/@deck.gl/aggregation-layers/node_modules/@luma.gl/constants": {
|
||||
"version": "9.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-9.1.10.tgz",
|
||||
"integrity": "sha512-O4Nx8UbWmrHHZ7ihKB8WiscX1cz05l1KvKorYTgq+xeXwz2Beh3MkXBMnA46uuyEtimN945OEdYshZnbh80jyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"plugins/legacy-preset-chart-deckgl/node_modules/@mapbox/tiny-sdf": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz",
|
||||
@@ -66084,7 +66075,7 @@
|
||||
"d3": "^3.5.17",
|
||||
"d3-tip": "^0.9.1",
|
||||
"dayjs": "^1.11.18",
|
||||
"dompurify": "^3.2.7",
|
||||
"dompurify": "^3.3.0",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
@@ -66097,16 +66088,10 @@
|
||||
"react": "^17.0.2"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-preset-chart-nvd3/node_modules/dayjs": {
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
|
||||
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"plugins/legacy-preset-chart-nvd3/node_modules/dompurify": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
|
||||
"integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
@@ -66178,7 +66163,7 @@
|
||||
"dependencies": {
|
||||
"@types/react-redux": "^7.1.34",
|
||||
"d3-array": "^1.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -66524,19 +66509,6 @@
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-handlebars/node_modules/@pkgr/core": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
|
||||
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-handlebars/node_modules/@sinclair/typebox": {
|
||||
"version": "0.34.37",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz",
|
||||
@@ -67433,22 +67405,6 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"plugins/plugin-chart-handlebars/node_modules/synckit": {
|
||||
"version": "0.11.11",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
|
||||
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.2.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-pivot-table": {
|
||||
"name": "@superset-ui/plugin-chart-pivot-table",
|
||||
"version": "0.20.3",
|
||||
@@ -67786,19 +67742,6 @@
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-pivot-table/node_modules/@pkgr/core": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
|
||||
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-pivot-table/node_modules/@sinclair/typebox": {
|
||||
"version": "0.34.41",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
|
||||
@@ -68658,22 +68601,6 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-pivot-table/node_modules/synckit": {
|
||||
"version": "0.11.11",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
|
||||
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.2.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-table": {
|
||||
"name": "@superset-ui/plugin-chart-table",
|
||||
"version": "0.20.3",
|
||||
|
||||
@@ -135,14 +135,14 @@
|
||||
"content-disposition": "^0.5.4",
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-scale": "^2.1.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"dom-to-image-more": "^3.6.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"geolib": "^2.0.24",
|
||||
"geostyler": "^14.1.3",
|
||||
"geostyler-data": "^1.1.0",
|
||||
@@ -185,7 +185,7 @@
|
||||
"react-loadable": "^5.5.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-reverse-portal": "^2.1.2",
|
||||
"react-reverse-portal": "^2.3.0",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-search-input": "^0.11.3",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
@@ -212,10 +212,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-storybook": "^3.60.0",
|
||||
"@babel/cli": "^7.27.2",
|
||||
"@babel/compat-data": "^7.28.0",
|
||||
"@babel/cli": "^7.28.3",
|
||||
"@babel/compat-data": "^7.28.4",
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/eslint-parser": "^7.25.9",
|
||||
"@babel/eslint-parser": "^7.28.4",
|
||||
"@babel/node": "^7.22.6",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
||||
@@ -234,7 +234,7 @@
|
||||
"@hot-loader/react-dom": "^17.0.2",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@mihkeleidast/storybook-addon-source": "^1.0.1",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@playwright/test": "^1.56.0",
|
||||
"@storybook/addon-actions": "8.1.11",
|
||||
"@storybook/addon-controls": "8.1.11",
|
||||
"@storybook/addon-essentials": "8.1.11",
|
||||
@@ -257,7 +257,7 @@
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/math-expression-evaluator": "^1.3.3",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^24.6.2",
|
||||
"@types/node": "^24.8.1",
|
||||
"@types/react": "^17.0.83",
|
||||
"@types/react-dom": "^17.0.26",
|
||||
"@types/react-json-tree": "^0.13.0",
|
||||
@@ -283,7 +283,7 @@
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"babel-plugin-typescript-to-proptypes": "^2.0.0",
|
||||
"cheerio": "1.1.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"copy-webpack-plugin": "^13.0.1",
|
||||
"cross-env": "^10.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.2",
|
||||
@@ -301,7 +301,7 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"eslint-plugin-no-only-tests": "^3.3.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
||||
@@ -312,7 +312,7 @@
|
||||
"fetch-mock": "^11.1.5",
|
||||
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
||||
"history": "^5.3.0",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"html-webpack-plugin": "^5.6.4",
|
||||
"imports-loader": "^5.0.0",
|
||||
"jest": "^30.0.2",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
@@ -324,7 +324,7 @@
|
||||
"open-cli": "^8.0.0",
|
||||
"po2json": "^0.4.5",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-packagejson": "^2.5.3",
|
||||
"prettier-plugin-packagejson": "^2.5.19",
|
||||
"process": "^0.11.10",
|
||||
"react-resizable": "^3.0.5",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
@@ -335,13 +335,13 @@
|
||||
"storybook": "8.1.11",
|
||||
"style-loader": "^4.0.0",
|
||||
"thread-loader": "^4.0.4",
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-jest": "^29.4.5",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "5.4.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.102.0",
|
||||
"webpack": "^5.102.1",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.2",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.4.1",
|
||||
"chalk": "^5.6.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"yeoman-generator": "^7.5.1",
|
||||
"yosay": "^3.0.0"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.26.4",
|
||||
"@babel/cli": "^7.28.3",
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
|
||||
@@ -27,10 +27,10 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@fontsource/fira-code": "^5.2.6",
|
||||
"@fontsource/fira-code": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"ace-builds": "^1.43.3",
|
||||
"ace-builds": "^1.43.4",
|
||||
"ag-grid-community": "34.2.0",
|
||||
"ag-grid-react": "34.2.0",
|
||||
"brace": "^0.11.1",
|
||||
@@ -38,7 +38,7 @@
|
||||
"csstype": "^3.1.3",
|
||||
"core-js": "^3.38.1",
|
||||
"d3-format": "^1.3.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-time": "^3.1.0",
|
||||
@@ -49,7 +49,7 @@
|
||||
"jed": "^1.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"math-expression-evaluator": "^2.0.6",
|
||||
"pretty-ms": "^9.2.0",
|
||||
"pretty-ms": "^9.3.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react-ace": "^14.0.1",
|
||||
"react-js-cron": "^5.2.0",
|
||||
@@ -67,7 +67,7 @@
|
||||
"rison": "^0.1.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"@visx/responsive": "^3.12.0",
|
||||
"xss": "^1.0.14"
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emotion/styled": "^11.14.1",
|
||||
@@ -81,7 +81,7 @@
|
||||
"@types/jquery": "^3.5.33",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/math-expression-evaluator": "^1.3.3",
|
||||
"@types/node": "^24.6.2",
|
||||
"@types/node": "^24.8.1",
|
||||
"@types/prop-types": "^15.7.15",
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
|
||||
@@ -28,6 +28,7 @@ export const StyledHeader = styled.span<{ headerPosition: string }>`
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: ${headerPosition === 'left' ? theme.sizeUnit * 2 : 0}px;
|
||||
font-size: ${theme.fontSizeSM}px;
|
||||
`}
|
||||
`;
|
||||
|
||||
|
||||
@@ -324,6 +324,7 @@ export type Query = {
|
||||
schema?: string;
|
||||
sql: string;
|
||||
sqlEditorId: string;
|
||||
sqlEditorImmutableId: string;
|
||||
state: QueryState;
|
||||
tab: string | null;
|
||||
tempSchema: string | null;
|
||||
@@ -373,6 +374,7 @@ export const testQuery: Query = {
|
||||
dbId: 1,
|
||||
sql: 'SELECT * FROM something',
|
||||
sqlEditorId: 'dfsadfs',
|
||||
sqlEditorImmutableId: 'immutableId2353',
|
||||
tab: 'unimportant',
|
||||
tempTable: '',
|
||||
ctas: false,
|
||||
|
||||
@@ -402,7 +402,7 @@ export interface ThemeContextType {
|
||||
setTheme: (config: AnyThemeConfig) => void;
|
||||
setThemeMode: (newMode: ThemeMode) => void;
|
||||
resetTheme: () => void;
|
||||
setTemporaryTheme: (config: AnyThemeConfig) => void;
|
||||
setTemporaryTheme: (config: AnyThemeConfig, themeId?: number | null) => void;
|
||||
clearLocalOverrides: () => void;
|
||||
getCurrentCrudThemeId: () => string | null;
|
||||
hasDevOverride: () => boolean;
|
||||
@@ -410,6 +410,7 @@ export interface ThemeContextType {
|
||||
canSetTheme: () => boolean;
|
||||
canDetectOSPreference: () => boolean;
|
||||
createDashboardThemeProvider: (themeId: string) => Promise<Theme | null>;
|
||||
getAppliedThemeId: () => number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@deck.gl/geo-layers": "^9.1.13",
|
||||
"@deck.gl/layers": "^9.1.13",
|
||||
"@deck.gl/react": "^9.1.14",
|
||||
"@luma.gl/constants": "^9.1.9",
|
||||
"@luma.gl/constants": "^9.2.2",
|
||||
"@luma.gl/core": "^9.1.9",
|
||||
"@luma.gl/engine": "^9.1.9",
|
||||
"@luma.gl/shadertools": "^9.1.9",
|
||||
@@ -43,7 +43,7 @@
|
||||
"d3-array": "^1.2.4",
|
||||
"d3-color": "^1.4.1",
|
||||
"d3-scale": "^3.0.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"handlebars": "^4.7.8",
|
||||
"lodash": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
|
||||
+11
-12
@@ -16,10 +16,9 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useState, memo } from 'react';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { useEffect, useState, memo, useMemo } from 'react';
|
||||
import { styled, t, sanitizeHtml } from '@superset-ui/core';
|
||||
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
|
||||
import { SafeMarkdown } from '@superset-ui/core/components';
|
||||
import Handlebars from 'handlebars';
|
||||
import { isPlainObject } from 'lodash';
|
||||
|
||||
@@ -45,8 +44,6 @@ export const HandlebarsRenderer: React.FC<HandlebarsRendererProps> = memo(
|
||||
appContainer?.getAttribute('data-bootstrap') || '{}',
|
||||
);
|
||||
const htmlSanitization = common?.conf?.HTML_SANITIZATION ?? true;
|
||||
const htmlSchemaOverrides =
|
||||
common?.conf?.HTML_SANITIZATION_SCHEMA_EXTENSIONS || {};
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -60,6 +57,12 @@ export const HandlebarsRenderer: React.FC<HandlebarsRendererProps> = memo(
|
||||
}
|
||||
}, [templateSource, data]);
|
||||
|
||||
const htmlContent = useMemo(
|
||||
() =>
|
||||
htmlSanitization ? sanitizeHtml(renderedTemplate) : renderedTemplate,
|
||||
[renderedTemplate, htmlSanitization],
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <ErrorContainer>{error}</ErrorContainer>;
|
||||
}
|
||||
@@ -73,13 +76,9 @@ export const HandlebarsRenderer: React.FC<HandlebarsRendererProps> = memo(
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.4',
|
||||
}}
|
||||
>
|
||||
<SafeMarkdown
|
||||
source={renderedTemplate || ''}
|
||||
htmlSanitization={htmlSanitization}
|
||||
htmlSchemaOverrides={htmlSchemaOverrides}
|
||||
/>
|
||||
</div>
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"dayjs": "^1.11.18",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
"dompurify": "^3.2.7",
|
||||
"dompurify": "^3.3.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"urijs": "^1.19.11"
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"dependencies": {
|
||||
"@types/react-redux": "^7.1.34",
|
||||
"d3-array": "^1.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -142,7 +142,7 @@ const config: ControlPanelConfig = {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
clearable: true,
|
||||
label: t('X AXIS TITLE MARGIN'),
|
||||
label: t('X axis title margin'),
|
||||
renderTrigger: true,
|
||||
default: sections.TITLE_MARGIN_OPTIONS[1],
|
||||
choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS),
|
||||
@@ -214,7 +214,7 @@ const config: ControlPanelConfig = {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
clearable: true,
|
||||
label: t('Y AXIS TITLE MARGIN'),
|
||||
label: t('Y axis title margin'),
|
||||
renderTrigger: true,
|
||||
default: sections.TITLE_MARGIN_OPTIONS[1],
|
||||
choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS),
|
||||
|
||||
+3
-3
@@ -81,7 +81,7 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
clearable: true,
|
||||
label: t('AXIS TITLE MARGIN'),
|
||||
label: t('Axis title margin'),
|
||||
renderTrigger: true,
|
||||
default: sections.TITLE_MARGIN_OPTIONS[0],
|
||||
choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS),
|
||||
@@ -114,7 +114,7 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
clearable: true,
|
||||
label: t('AXIS TITLE MARGIN'),
|
||||
label: t('Axis title margin'),
|
||||
renderTrigger: true,
|
||||
default: sections.TITLE_MARGIN_OPTIONS[1],
|
||||
choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS),
|
||||
@@ -132,7 +132,7 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
clearable: false,
|
||||
label: t('AXIS TITLE POSITION'),
|
||||
label: t('Axis title position'),
|
||||
renderTrigger: true,
|
||||
default: sections.TITLE_POSITION_OPTIONS[0][0],
|
||||
choices: sections.TITLE_POSITION_OPTIONS,
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
DragEvent,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { styled, typedMemo, usePrevious } from '@superset-ui/core';
|
||||
import { typedMemo, usePrevious } from '@superset-ui/core';
|
||||
import {
|
||||
useTable,
|
||||
usePagination,
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
} from 'react-table';
|
||||
import { matchSorter, rankings } from 'match-sorter';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Space } from '@superset-ui/core/components';
|
||||
import { Flex, Space } from '@superset-ui/core/components';
|
||||
import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter';
|
||||
import SelectPageSize, {
|
||||
SelectPageSizeProps,
|
||||
@@ -77,7 +77,7 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||
sticky?: boolean;
|
||||
rowCount: number;
|
||||
wrapperRef?: MutableRefObject<HTMLDivElement>;
|
||||
onColumnOrderChange: () => void;
|
||||
onColumnOrderChange?: () => void;
|
||||
renderGroupingHeaders?: () => JSX.Element;
|
||||
renderTimeComparisonDropdown?: () => JSX.Element;
|
||||
handleSortByChange: (sortBy: SortByItem[]) => void;
|
||||
@@ -98,24 +98,6 @@ const sortTypes = {
|
||||
alphanumeric: sortAlphanumericCaseInsensitive,
|
||||
};
|
||||
|
||||
const StyledSpace = styled(Space)`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.search-select-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-by-label {
|
||||
align-self: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRow = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
// Be sure to pass our updateMyData and the skipReset option
|
||||
export default typedMemo(function DataTable<D extends object>({
|
||||
tableClassName,
|
||||
@@ -336,8 +318,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
const colToBeMoved = currentCols.splice(columnBeingDragged, 1);
|
||||
currentCols.splice(newPosition, 0, colToBeMoved[0]);
|
||||
setColumnOrder(currentCols);
|
||||
// toggle value in TableChart to trigger column width recalc
|
||||
onColumnOrderChange();
|
||||
onColumnOrderChange?.();
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
@@ -450,30 +431,36 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
>
|
||||
{hasGlobalControl ? (
|
||||
<div ref={globalControlRef} className="form-inline dt-controls">
|
||||
<StyledRow className="row">
|
||||
<StyledSpace size="middle">
|
||||
{hasPagination ? (
|
||||
<SelectPageSize
|
||||
total={resultsSize}
|
||||
current={resultCurrentPageSize}
|
||||
options={pageSizeOptions}
|
||||
selectRenderer={
|
||||
typeof selectPageSize === 'boolean'
|
||||
? undefined
|
||||
: selectPageSize
|
||||
}
|
||||
onChange={setPageSize}
|
||||
/>
|
||||
) : null}
|
||||
<Flex
|
||||
wrap
|
||||
className="row"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
gap="middle"
|
||||
>
|
||||
{hasPagination ? (
|
||||
<SelectPageSize
|
||||
total={resultsSize}
|
||||
current={resultCurrentPageSize}
|
||||
options={pageSizeOptions}
|
||||
selectRenderer={
|
||||
typeof selectPageSize === 'boolean'
|
||||
? undefined
|
||||
: selectPageSize
|
||||
}
|
||||
onChange={setPageSize}
|
||||
/>
|
||||
) : null}
|
||||
<Flex wrap align="center" gap="middle">
|
||||
{serverPagination && (
|
||||
<div className="search-select-container">
|
||||
<span className="search-by-label">Search by: </span>
|
||||
<Space size="small" className="search-select-container">
|
||||
<span className="search-by-label">Search by:</span>
|
||||
<SearchSelectDropdown
|
||||
searchOptions={searchOptions}
|
||||
value={serverPaginationData?.searchColumn || ''}
|
||||
onChange={onSearchColChange}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
)}
|
||||
{searchInput && (
|
||||
<GlobalFilter<D>
|
||||
@@ -493,8 +480,8 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
{renderTimeComparisonDropdown
|
||||
? renderTimeComparisonDropdown()
|
||||
: null}
|
||||
</StyledSpace>
|
||||
</StyledRow>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
) : null}
|
||||
{wrapStickyTable ? wrapStickyTable(renderTable) : renderTable()}
|
||||
|
||||
@@ -195,6 +195,21 @@ function SortIcon<D extends object>({ column }: { column: ColumnInstance<D> }) {
|
||||
return sortIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Label that is visually hidden but accessible
|
||||
*/
|
||||
const VisuallyHidden = styled.label`
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
`;
|
||||
|
||||
function SearchInput({
|
||||
count,
|
||||
value,
|
||||
@@ -225,10 +240,10 @@ function SelectPageSize({
|
||||
const { Option } = Select;
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor="pageSizeSelect" className="sr-only">
|
||||
<span className="dt-select-page-size">
|
||||
<VisuallyHidden htmlFor="pageSizeSelect">
|
||||
{t('Select page size')}
|
||||
</label>
|
||||
</VisuallyHidden>
|
||||
{t('Show')}{' '}
|
||||
<Select<number>
|
||||
id="pageSizeSelect"
|
||||
@@ -252,7 +267,7 @@ function SelectPageSize({
|
||||
})}
|
||||
</Select>{' '}
|
||||
{t('entries per page')}
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -296,12 +311,17 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
serverPageLength,
|
||||
slice_id,
|
||||
} = props;
|
||||
const comparisonColumns = [
|
||||
{ key: 'all', label: t('Display all') },
|
||||
{ key: '#', label: '#' },
|
||||
{ key: '△', label: '△' },
|
||||
{ key: '%', label: '%' },
|
||||
];
|
||||
|
||||
const comparisonColumns = useMemo(
|
||||
() => [
|
||||
{ key: 'all', label: t('Display all') },
|
||||
{ key: '#', label: '#' },
|
||||
{ key: '△', label: '△' },
|
||||
{ key: '%', label: '%' },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const timestampFormatter = useCallback(
|
||||
value => getTimeFormatterForGranularity(timeGrain)(value),
|
||||
[timeGrain],
|
||||
@@ -353,71 +373,74 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
[filters],
|
||||
);
|
||||
|
||||
const getCrossFilterDataMask = (key: string, value: DataRecordValue) => {
|
||||
let updatedFilters = { ...(filters || {}) };
|
||||
if (filters && isActiveFilterValue(key, value)) {
|
||||
updatedFilters = {};
|
||||
} else {
|
||||
updatedFilters = {
|
||||
[key]: [value],
|
||||
};
|
||||
}
|
||||
if (
|
||||
Array.isArray(updatedFilters[key]) &&
|
||||
updatedFilters[key].length === 0
|
||||
) {
|
||||
delete updatedFilters[key];
|
||||
}
|
||||
|
||||
const groupBy = Object.keys(updatedFilters);
|
||||
const groupByValues = Object.values(updatedFilters);
|
||||
const labelElements: string[] = [];
|
||||
groupBy.forEach(col => {
|
||||
const isTimestamp = col === DTTM_ALIAS;
|
||||
const filterValues = ensureIsArray(updatedFilters?.[col]);
|
||||
if (filterValues.length) {
|
||||
const valueLabels = filterValues.map(value =>
|
||||
isTimestamp ? timestampFormatter(value) : value,
|
||||
);
|
||||
labelElements.push(`${valueLabels.join(', ')}`);
|
||||
const getCrossFilterDataMask = useCallback(
|
||||
(key: string, value: DataRecordValue) => {
|
||||
let updatedFilters = { ...(filters || {}) };
|
||||
if (filters && isActiveFilterValue(key, value)) {
|
||||
updatedFilters = {};
|
||||
} else {
|
||||
updatedFilters = {
|
||||
[key]: [value],
|
||||
};
|
||||
}
|
||||
if (
|
||||
Array.isArray(updatedFilters[key]) &&
|
||||
updatedFilters[key].length === 0
|
||||
) {
|
||||
delete updatedFilters[key];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters:
|
||||
groupBy.length === 0
|
||||
? []
|
||||
: groupBy.map(col => {
|
||||
const val = ensureIsArray(updatedFilters?.[col]);
|
||||
if (!val.length)
|
||||
const groupBy = Object.keys(updatedFilters);
|
||||
const groupByValues = Object.values(updatedFilters);
|
||||
const labelElements: string[] = [];
|
||||
groupBy.forEach(col => {
|
||||
const isTimestamp = col === DTTM_ALIAS;
|
||||
const filterValues = ensureIsArray(updatedFilters?.[col]);
|
||||
if (filterValues.length) {
|
||||
const valueLabels = filterValues.map(value =>
|
||||
isTimestamp ? timestampFormatter(value) : value,
|
||||
);
|
||||
labelElements.push(`${valueLabels.join(', ')}`);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters:
|
||||
groupBy.length === 0
|
||||
? []
|
||||
: groupBy.map(col => {
|
||||
const val = ensureIsArray(updatedFilters?.[col]);
|
||||
if (!val.length)
|
||||
return {
|
||||
col,
|
||||
op: 'IS NULL' as const,
|
||||
};
|
||||
return {
|
||||
col,
|
||||
op: 'IS NULL' as const,
|
||||
op: 'IN' as const,
|
||||
val: val.map(el =>
|
||||
el instanceof Date ? el.getTime() : el!,
|
||||
),
|
||||
grain: col === DTTM_ALIAS ? timeGrain : undefined,
|
||||
};
|
||||
return {
|
||||
col,
|
||||
op: 'IN' as const,
|
||||
val: val.map(el =>
|
||||
el instanceof Date ? el.getTime() : el!,
|
||||
),
|
||||
grain: col === DTTM_ALIAS ? timeGrain : undefined,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
},
|
||||
filterState: {
|
||||
label: labelElements.join(', '),
|
||||
value: groupByValues.length ? groupByValues : null,
|
||||
filters:
|
||||
updatedFilters && Object.keys(updatedFilters).length
|
||||
? updatedFilters
|
||||
: null,
|
||||
},
|
||||
},
|
||||
filterState: {
|
||||
label: labelElements.join(', '),
|
||||
value: groupByValues.length ? groupByValues : null,
|
||||
filters:
|
||||
updatedFilters && Object.keys(updatedFilters).length
|
||||
? updatedFilters
|
||||
: null,
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: isActiveFilterValue(key, value),
|
||||
};
|
||||
};
|
||||
isCurrentValueSelected: isActiveFilterValue(key, value),
|
||||
};
|
||||
},
|
||||
[filters, isActiveFilterValue, timestampFormatter, timeGrain],
|
||||
);
|
||||
|
||||
const toggleFilter = useCallback(
|
||||
function toggleFilter(key: string, val: DataRecordValue) {
|
||||
@@ -429,17 +452,21 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
[emitCrossFilters, getCrossFilterDataMask, setDataMask],
|
||||
);
|
||||
|
||||
const getSharedStyle = (column: DataColumnMeta): CSSProperties => {
|
||||
const { isNumeric, config = {} } = column;
|
||||
const textAlign =
|
||||
config.horizontalAlign ||
|
||||
(isNumeric && !isUsingTimeComparison ? 'right' : 'left');
|
||||
return {
|
||||
textAlign,
|
||||
};
|
||||
};
|
||||
const getSharedStyle = useCallback(
|
||||
(column: DataColumnMeta): CSSProperties => {
|
||||
const { isNumeric, config = {} } = column;
|
||||
const textAlign =
|
||||
config.horizontalAlign ||
|
||||
(isNumeric && !isUsingTimeComparison ? 'right' : 'left');
|
||||
return {
|
||||
textAlign,
|
||||
};
|
||||
},
|
||||
[isUsingTimeComparison],
|
||||
);
|
||||
|
||||
const comparisonLabels = useMemo(() => [t('Main'), '#', '△', '%'], []);
|
||||
|
||||
const comparisonLabels = [t('Main'), '#', '△', '%'];
|
||||
const filteredColumnsMeta = useMemo(() => {
|
||||
if (!isUsingTimeComparison) {
|
||||
return columnsMeta;
|
||||
@@ -471,79 +498,86 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
selectedComparisonColumns,
|
||||
]);
|
||||
|
||||
const handleContextMenu =
|
||||
onContextMenu && !isRawRecords
|
||||
? (
|
||||
value: D,
|
||||
cellPoint: {
|
||||
key: string;
|
||||
value: DataRecordValue;
|
||||
isMetric?: boolean;
|
||||
},
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
) => {
|
||||
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
||||
filteredColumnsMeta.forEach(col => {
|
||||
if (!col.isMetric) {
|
||||
const dataRecordValue = value[col.key];
|
||||
drillToDetailFilters.push({
|
||||
col: col.key,
|
||||
op: '==',
|
||||
val: dataRecordValue as string | number | boolean,
|
||||
formattedVal: formatColumnValue(col, dataRecordValue)[1],
|
||||
});
|
||||
}
|
||||
});
|
||||
onContextMenu(clientX, clientY, {
|
||||
drillToDetail: drillToDetailFilters,
|
||||
crossFilter: cellPoint.isMetric
|
||||
? undefined
|
||||
: getCrossFilterDataMask(cellPoint.key, cellPoint.value),
|
||||
drillBy: cellPoint.isMetric
|
||||
? undefined
|
||||
: {
|
||||
filters: [
|
||||
{
|
||||
col: cellPoint.key,
|
||||
op: '==',
|
||||
val: cellPoint.value as string | number | boolean,
|
||||
},
|
||||
],
|
||||
groupbyFieldName: 'groupby',
|
||||
},
|
||||
});
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const getHeaderColumns = (
|
||||
columnsMeta: DataColumnMeta[],
|
||||
enableTimeComparison?: boolean,
|
||||
) => {
|
||||
const resultMap: Record<string, number[]> = {};
|
||||
|
||||
if (!enableTimeComparison) {
|
||||
return resultMap;
|
||||
const handleContextMenu = useMemo(() => {
|
||||
if (onContextMenu && !isRawRecords) {
|
||||
return (
|
||||
value: D,
|
||||
cellPoint: {
|
||||
key: string;
|
||||
value: DataRecordValue;
|
||||
isMetric?: boolean;
|
||||
},
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
) => {
|
||||
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
||||
filteredColumnsMeta.forEach(col => {
|
||||
if (!col.isMetric) {
|
||||
const dataRecordValue = value[col.key];
|
||||
drillToDetailFilters.push({
|
||||
col: col.key,
|
||||
op: '==',
|
||||
val: dataRecordValue as string | number | boolean,
|
||||
formattedVal: formatColumnValue(col, dataRecordValue)[1],
|
||||
});
|
||||
}
|
||||
});
|
||||
onContextMenu(clientX, clientY, {
|
||||
drillToDetail: drillToDetailFilters,
|
||||
crossFilter: cellPoint.isMetric
|
||||
? undefined
|
||||
: getCrossFilterDataMask(cellPoint.key, cellPoint.value),
|
||||
drillBy: cellPoint.isMetric
|
||||
? undefined
|
||||
: {
|
||||
filters: [
|
||||
{
|
||||
col: cellPoint.key,
|
||||
op: '==',
|
||||
val: cellPoint.value as string | number | boolean,
|
||||
},
|
||||
],
|
||||
groupbyFieldName: 'groupby',
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, [
|
||||
onContextMenu,
|
||||
isRawRecords,
|
||||
filteredColumnsMeta,
|
||||
getCrossFilterDataMask,
|
||||
]);
|
||||
|
||||
columnsMeta.forEach((element, index) => {
|
||||
// Check if element's label is one of the comparison labels
|
||||
if (comparisonLabels.includes(element.label)) {
|
||||
// Extract the key portion after the space, assuming the format is always "label key"
|
||||
const keyPortion = element.key.substring(element.label.length);
|
||||
const getHeaderColumns = useCallback(
|
||||
(columnsMeta: DataColumnMeta[], enableTimeComparison?: boolean) => {
|
||||
const resultMap: Record<string, number[]> = {};
|
||||
|
||||
// If the key portion is not in the map, initialize it with the current index
|
||||
if (!resultMap[keyPortion]) {
|
||||
resultMap[keyPortion] = [index];
|
||||
} else {
|
||||
// Add the index to the existing array
|
||||
resultMap[keyPortion].push(index);
|
||||
}
|
||||
if (!enableTimeComparison) {
|
||||
return resultMap;
|
||||
}
|
||||
});
|
||||
|
||||
return resultMap;
|
||||
};
|
||||
columnsMeta.forEach((element, index) => {
|
||||
// Check if element's label is one of the comparison labels
|
||||
if (comparisonLabels.includes(element.label)) {
|
||||
// Extract the key portion after the space, assuming the format is always "label key"
|
||||
const keyPortion = element.key.substring(element.label.length);
|
||||
|
||||
// If the key portion is not in the map, initialize it with the current index
|
||||
if (!resultMap[keyPortion]) {
|
||||
resultMap[keyPortion] = [index];
|
||||
} else {
|
||||
// Add the index to the existing array
|
||||
resultMap[keyPortion].push(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return resultMap;
|
||||
},
|
||||
[comparisonLabels],
|
||||
);
|
||||
|
||||
const renderTimeComparisonDropdown = (): JSX.Element => {
|
||||
const allKey = comparisonColumns[0].key;
|
||||
@@ -638,6 +672,11 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
);
|
||||
};
|
||||
|
||||
const groupHeaderColumns = useMemo(
|
||||
() => getHeaderColumns(filteredColumnsMeta, isUsingTimeComparison),
|
||||
[filteredColumnsMeta, getHeaderColumns, isUsingTimeComparison],
|
||||
);
|
||||
|
||||
const renderGroupingHeaders = (): JSX.Element => {
|
||||
// TODO: Make use of ColumnGroup to render the aditional headers
|
||||
const headers: any = [];
|
||||
@@ -719,11 +758,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
);
|
||||
};
|
||||
|
||||
const groupHeaderColumns = useMemo(
|
||||
() => getHeaderColumns(filteredColumnsMeta, isUsingTimeComparison),
|
||||
[filteredColumnsMeta, isUsingTimeComparison],
|
||||
);
|
||||
|
||||
const getColumnConfigs = useCallback(
|
||||
(
|
||||
column: DataColumnMeta,
|
||||
@@ -1086,19 +1120,27 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
};
|
||||
},
|
||||
[
|
||||
getSharedStyle,
|
||||
defaultAlignPN,
|
||||
defaultColorPN,
|
||||
emitCrossFilters,
|
||||
getValueRange,
|
||||
isActiveFilterValue,
|
||||
isRawRecords,
|
||||
showCellBars,
|
||||
sortDesc,
|
||||
toggleFilter,
|
||||
totals,
|
||||
columnColorFormatters,
|
||||
columnOrderToggle,
|
||||
isUsingTimeComparison,
|
||||
basicColorFormatters,
|
||||
showCellBars,
|
||||
isRawRecords,
|
||||
getValueRange,
|
||||
emitCrossFilters,
|
||||
comparisonLabels,
|
||||
totals,
|
||||
theme,
|
||||
sortDesc,
|
||||
groupHeaderColumns,
|
||||
allowRenderHtml,
|
||||
basicColorColumnFormatters,
|
||||
isActiveFilterValue,
|
||||
toggleFilter,
|
||||
handleContextMenu,
|
||||
allowRearrangeColumns,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1131,7 +1173,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
if (!isEqual(options, searchOptions)) {
|
||||
setSearchOptions(options || []);
|
||||
}
|
||||
}, [columns]);
|
||||
}, [columns, searchOptions]);
|
||||
|
||||
const handleServerPaginationChange = useCallback(
|
||||
(pageNumber: number, pageSize: number) => {
|
||||
@@ -1142,7 +1184,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
},
|
||||
[setDataMask],
|
||||
[serverPaginationData, setDataMask],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1154,7 +1196,12 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
}
|
||||
}, []);
|
||||
}, [
|
||||
hasServerPageLengthChanged,
|
||||
serverPageLength,
|
||||
serverPaginationData,
|
||||
setDataMask,
|
||||
]);
|
||||
|
||||
const handleSizeChange = useCallback(
|
||||
({ width, height }: { width: number; height: number }) => {
|
||||
@@ -1200,7 +1247,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
},
|
||||
[setDataMask, serverPagination],
|
||||
[serverPagination, serverPaginationData, setDataMask],
|
||||
);
|
||||
|
||||
const handleSearch = (searchText: string) => {
|
||||
|
||||
+49
-30
@@ -16,6 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { DatasourceType } from '@superset-ui/core';
|
||||
|
||||
export const id = 7;
|
||||
export const datasourceId = `${id}__table`;
|
||||
|
||||
@@ -40,125 +42,135 @@ export default {
|
||||
},
|
||||
metrics: [
|
||||
{
|
||||
id: 1,
|
||||
uuid: 'metric-1-uuid',
|
||||
expression: 'SUM(birth_names.num)',
|
||||
warning_text: null,
|
||||
verbose_name: 'sum__num',
|
||||
metric_name: 'sum__num',
|
||||
description: null,
|
||||
metric_type: 'sum',
|
||||
certified_by: 'someone',
|
||||
certification_details: 'foo',
|
||||
warning_markdown: 'bar',
|
||||
extra:
|
||||
'{"certification":{"details":"foo", "certified_by":"someone"},"warning_markdown":"bar"}',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
uuid: 'metric-2-uuid',
|
||||
expression: 'AVG(birth_names.num)',
|
||||
warning_text: null,
|
||||
verbose_name: 'avg__num',
|
||||
metric_name: 'avg__num',
|
||||
description: null,
|
||||
metric_type: 'avg',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
uuid: 'metric-3-uuid',
|
||||
expression: 'SUM(birth_names.num_boys)',
|
||||
warning_text: null,
|
||||
verbose_name: 'sum__num_boys',
|
||||
metric_name: 'sum__num_boys',
|
||||
description: null,
|
||||
metric_type: 'sum',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
uuid: 'metric-4-uuid',
|
||||
expression: 'AVG(birth_names.num_boys)',
|
||||
warning_text: null,
|
||||
verbose_name: 'avg__num_boys',
|
||||
metric_name: 'avg__num_boys',
|
||||
description: null,
|
||||
metric_type: 'avg',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
uuid: 'metric-5-uuid',
|
||||
expression: 'SUM(birth_names.num_girls)',
|
||||
warning_text: null,
|
||||
verbose_name: 'sum__num_girls',
|
||||
metric_name: 'sum__num_girls',
|
||||
description: null,
|
||||
metric_type: 'sum',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
uuid: 'metric-6-uuid',
|
||||
expression: 'AVG(birth_names.num_girls)',
|
||||
warning_text: null,
|
||||
verbose_name: 'avg__num_girls',
|
||||
metric_name: 'avg__num_girls',
|
||||
description: null,
|
||||
metric_type: 'avg',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
uuid: 'metric-7-uuid',
|
||||
expression: 'COUNT(*)',
|
||||
warning_text: null,
|
||||
verbose_name: 'COUNT(*)',
|
||||
metric_name: 'count',
|
||||
description: null,
|
||||
metric_type: 'count',
|
||||
},
|
||||
],
|
||||
column_formats: {},
|
||||
columns: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'DATETIME',
|
||||
description: null,
|
||||
filterable: false,
|
||||
verbose_name: null,
|
||||
is_dttm: true,
|
||||
is_active: true,
|
||||
expression: '',
|
||||
groupby: false,
|
||||
column_name: 'ds',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'VARCHAR(16)',
|
||||
description: null,
|
||||
filterable: true,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
is_active: true,
|
||||
expression: '',
|
||||
groupby: true,
|
||||
column_name: 'gender',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'VARCHAR(255)',
|
||||
description: null,
|
||||
filterable: true,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
is_active: true,
|
||||
expression: '',
|
||||
groupby: true,
|
||||
column_name: 'name',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'BIGINT',
|
||||
description: null,
|
||||
filterable: false,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
is_active: true,
|
||||
expression: '',
|
||||
groupby: false,
|
||||
column_name: 'num',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 'VARCHAR(10)',
|
||||
description: null,
|
||||
filterable: true,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
is_active: true,
|
||||
expression: '',
|
||||
groupby: true,
|
||||
column_name: 'state',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
type: 'BIGINT',
|
||||
description: null,
|
||||
filterable: false,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
is_active: true,
|
||||
expression: '',
|
||||
groupby: false,
|
||||
column_name: 'num_boys',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
type: 'BIGINT',
|
||||
description: null,
|
||||
filterable: false,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
is_active: true,
|
||||
expression: '',
|
||||
groupby: false,
|
||||
column_name: 'num_girls',
|
||||
@@ -169,7 +181,9 @@ export default {
|
||||
granularity_sqla: [['ds', 'ds']],
|
||||
main_dttm_col: 'ds',
|
||||
name: 'birth_names',
|
||||
owners: [{ first_name: 'joe', last_name: 'man', id: 1 }],
|
||||
owners: [
|
||||
{ first_name: 'joe', last_name: 'man', id: 1, username: 'joeman' },
|
||||
],
|
||||
database: {
|
||||
name: 'main',
|
||||
backend: 'sqlite',
|
||||
@@ -198,6 +212,11 @@ export default {
|
||||
['["num_girls", true]', 'num_girls [asc]'],
|
||||
['["num_girls", false]', 'num_girls [desc]'],
|
||||
],
|
||||
type: 'table',
|
||||
type: DatasourceType.Table,
|
||||
description: null,
|
||||
is_managed_externally: false,
|
||||
normalize_columns: false,
|
||||
always_filter_main_dttm: false,
|
||||
datasource_name: null,
|
||||
},
|
||||
};
|
||||
@@ -394,7 +394,7 @@ export function runQueryFromSqlEditor(
|
||||
dbId: qe.dbId,
|
||||
sql: qe.selectedText || qe.sql,
|
||||
sqlEditorId: qe.tabViewId ?? qe.id,
|
||||
immutableId: qe.immutableId,
|
||||
sqlEditorImmutableId: qe.immutableId,
|
||||
tab: qe.name,
|
||||
catalog: qe.catalog,
|
||||
schema: qe.schema,
|
||||
|
||||
@@ -602,4 +602,42 @@ describe('ResultSet', () => {
|
||||
);
|
||||
expect(queryByTestId('copy-to-clipboard-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should include sqlEditorImmutableId in query object when fetching results', async () => {
|
||||
const queryWithResultsKey = {
|
||||
...queries[0],
|
||||
resultsKey: 'test-results-key',
|
||||
sqlEditorImmutableId: 'test-immutable-id-123',
|
||||
};
|
||||
|
||||
const store = mockStore({
|
||||
...initialState,
|
||||
user,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
queries: {
|
||||
[queryWithResultsKey.id]: queryWithResultsKey,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setup({ ...mockedProps, queryId: queryWithResultsKey.id }, store);
|
||||
|
||||
await waitFor(() => {
|
||||
// Check that REQUEST_QUERY_RESULTS action was dispatched
|
||||
const actions = store.getActions();
|
||||
const requestAction = actions.find(
|
||||
action => action.type === 'REQUEST_QUERY_RESULTS',
|
||||
);
|
||||
expect(requestAction).toBeDefined();
|
||||
// Verify sqlEditorImmutableId is present in the query object
|
||||
expect(requestAction?.query?.sqlEditorImmutableId).toBe(
|
||||
'test-immutable-id-123',
|
||||
);
|
||||
});
|
||||
|
||||
// Verify the API was called
|
||||
const resultsCalls = fetchMock.calls('glob:*/api/v1/sqllab/results/*');
|
||||
expect(resultsCalls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -198,6 +198,7 @@ const ResultSet = ({
|
||||
'sql',
|
||||
'executedSql',
|
||||
'sqlEditorId',
|
||||
'sqlEditorImmutableId',
|
||||
'templateParams',
|
||||
'schema',
|
||||
'rows',
|
||||
|
||||
@@ -238,6 +238,7 @@ export const queries = [
|
||||
ctas: false,
|
||||
cached: false,
|
||||
id: 'BkA1CLrJg',
|
||||
sqlEditorImmutableId: 'BkA1CLrJg_immutable',
|
||||
progress: 100,
|
||||
startDttm: 1476910566092.96,
|
||||
state: QueryState.Success,
|
||||
@@ -297,6 +298,7 @@ export const queries = [
|
||||
ctas: false,
|
||||
cached: false,
|
||||
id: 'S1zeAISkx',
|
||||
sqlEditorImmutableId: 'S1zeAISkx_immutable',
|
||||
progress: 100,
|
||||
startDttm: 1476910570802.2,
|
||||
state: QueryState.Success,
|
||||
@@ -331,6 +333,7 @@ export const queryWithNoQueryLimit = {
|
||||
ctas: false,
|
||||
cached: false,
|
||||
id: 'BkA1CLrJg',
|
||||
sqlEditorImmutableId: 'BkA1CLrJg_immutable',
|
||||
progress: 100,
|
||||
startDttm: 1476910566092.96,
|
||||
state: QueryState.Success,
|
||||
@@ -589,6 +592,7 @@ const baseQuery: QueryResponse = {
|
||||
ctas: false,
|
||||
cached: false,
|
||||
id: 'BkA1CLrJg',
|
||||
sqlEditorImmutableId: 'BkA1CLrJg_immutable',
|
||||
progress: 100,
|
||||
startDttm: 1476910566092.96,
|
||||
state: QueryState.Success,
|
||||
@@ -672,6 +676,7 @@ export const runningQuery: QueryResponse = {
|
||||
cached: false,
|
||||
ctas: false,
|
||||
id: 'ryhMUZCGb',
|
||||
sqlEditorImmutableId: 'ryhMUZCGb_immutable',
|
||||
progress: 90,
|
||||
state: QueryState.Running,
|
||||
startDttm: Date.now() - 500,
|
||||
@@ -683,6 +688,7 @@ export const successfulQuery: QueryResponse = {
|
||||
cached: false,
|
||||
ctas: false,
|
||||
id: 'ryhMUZCGb',
|
||||
sqlEditorImmutableId: 'ryhMUZCGb_immutable',
|
||||
progress: 100,
|
||||
state: QueryState.Success,
|
||||
startDttm: Date.now() - 500,
|
||||
|
||||
+10
-4
@@ -1019,10 +1019,16 @@ class DatasourceEditor extends PureComponent {
|
||||
<Field
|
||||
fieldKey="default_endpoint"
|
||||
label={t('Default URL')}
|
||||
description={t(
|
||||
`Default URL to redirect to when accessing from the dataset list page.
|
||||
Accepts relative URLs such as <span style=„white-space: nowrap;”>/superset/dashboard/{id}/</span>`,
|
||||
)}
|
||||
description={
|
||||
<>
|
||||
{t(
|
||||
'Default URL to redirect to when accessing from the dataset list page. Accepts relative URLs such as',
|
||||
)}{' '}
|
||||
<Typography.Text code>
|
||||
/superset/dashboard/{'{id}'}/
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
control={<TextControl controlId="default_endpoint" />}
|
||||
/>
|
||||
<Field
|
||||
|
||||
+55
-25
@@ -25,7 +25,8 @@ import {
|
||||
cleanup,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import mockDatasource from 'spec/fixtures/mockDatasource';
|
||||
import { isFeatureEnabled } from '@superset-ui/core';
|
||||
import { DatasourceType, isFeatureEnabled } from '@superset-ui/core';
|
||||
import type { DatasetObject } from 'src/features/datasets/types';
|
||||
import DatasourceEditor from '..';
|
||||
|
||||
/* eslint-disable jest/no-export */
|
||||
@@ -34,8 +35,17 @@ jest.mock('@superset-ui/core', () => ({
|
||||
isFeatureEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
interface DatasourceEditorProps {
|
||||
datasource: DatasetObject;
|
||||
addSuccessToast: () => void;
|
||||
addDangerToast: () => void;
|
||||
onChange: jest.Mock;
|
||||
columnLabels?: Record<string, string>;
|
||||
columnLabelTooltips?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Common setup for tests
|
||||
export const props = {
|
||||
export const props: DatasourceEditorProps = {
|
||||
datasource: mockDatasource['7__table'],
|
||||
addSuccessToast: () => {},
|
||||
addDangerToast: () => {},
|
||||
@@ -47,16 +57,19 @@ export const props = {
|
||||
state: 'This is a tooltip for state',
|
||||
},
|
||||
};
|
||||
|
||||
export const DATASOURCE_ENDPOINT =
|
||||
'glob:*/datasource/external_metadata_by_name/*';
|
||||
|
||||
const routeProps = {
|
||||
history: {},
|
||||
location: {},
|
||||
match: {},
|
||||
};
|
||||
export const asyncRender = props =>
|
||||
|
||||
export const asyncRender = (renderProps: DatasourceEditorProps) =>
|
||||
waitFor(() =>
|
||||
render(<DatasourceEditor {...props} {...routeProps} />, {
|
||||
render(<DatasourceEditor {...renderProps} {...routeProps} />, {
|
||||
useRedux: true,
|
||||
initialState: { common: { currencies: ['USD', 'GBP', 'EUR'] } },
|
||||
useRouter: true,
|
||||
@@ -87,16 +100,16 @@ describe('DatasourceEditor', () => {
|
||||
|
||||
test('can sync columns from source', async () => {
|
||||
const columnsTab = screen.getByTestId('collection-tab-Columns');
|
||||
userEvent.click(columnsTab);
|
||||
await userEvent.click(columnsTab);
|
||||
|
||||
const syncButton = screen.getByText(/sync columns from source/i);
|
||||
expect(syncButton).toBeInTheDocument();
|
||||
|
||||
// Use a Promise to track when fetchMock is called
|
||||
const fetchPromise = new Promise(resolve => {
|
||||
const fetchPromise = new Promise<string>(resolve => {
|
||||
fetchMock.get(
|
||||
DATASOURCE_ENDPOINT,
|
||||
url => {
|
||||
(url: string) => {
|
||||
resolve(url);
|
||||
return [];
|
||||
},
|
||||
@@ -104,7 +117,7 @@ describe('DatasourceEditor', () => {
|
||||
);
|
||||
});
|
||||
|
||||
userEvent.click(syncButton);
|
||||
await userEvent.click(syncButton);
|
||||
|
||||
// Wait for the fetch to be called
|
||||
const url = await fetchPromise;
|
||||
@@ -114,12 +127,12 @@ describe('DatasourceEditor', () => {
|
||||
// to add, remove and modify columns accordingly
|
||||
test('can modify columns', async () => {
|
||||
const columnsTab = screen.getByTestId('collection-tab-Columns');
|
||||
userEvent.click(columnsTab);
|
||||
await userEvent.click(columnsTab);
|
||||
|
||||
const getToggles = screen.getAllByRole('button', {
|
||||
name: /expand row/i,
|
||||
});
|
||||
userEvent.click(getToggles[0]);
|
||||
await userEvent.click(getToggles[0]);
|
||||
|
||||
const getTextboxes = await screen.findAllByRole('textbox');
|
||||
expect(getTextboxes.length).toBeGreaterThanOrEqual(5);
|
||||
@@ -132,22 +145,39 @@ describe('DatasourceEditor', () => {
|
||||
'Certification details',
|
||||
);
|
||||
|
||||
userEvent.type(inputLabel, 'test_label');
|
||||
userEvent.type(inputDescription, 'test');
|
||||
userEvent.type(inputDtmFormat, 'test');
|
||||
userEvent.type(inputCertifiedBy, 'test');
|
||||
userEvent.type(inputCertDetails, 'test');
|
||||
// Clear onChange mock to track user action callbacks
|
||||
props.onChange.mockClear();
|
||||
|
||||
await userEvent.type(inputLabel, 'test_label');
|
||||
await userEvent.type(inputDescription, 'test');
|
||||
await userEvent.type(inputDtmFormat, 'test');
|
||||
await userEvent.type(inputCertifiedBy, 'test');
|
||||
await userEvent.type(inputCertDetails, 'test');
|
||||
|
||||
// Verify the inputs were updated with the typed values
|
||||
await waitFor(() => {
|
||||
expect(inputLabel).toHaveValue('test_label');
|
||||
expect(inputDescription).toHaveValue('test');
|
||||
expect(inputDtmFormat).toHaveValue('test');
|
||||
expect(inputCertifiedBy).toHaveValue('test');
|
||||
expect(inputCertDetails).toHaveValue('test');
|
||||
});
|
||||
|
||||
// Verify that onChange was triggered by user actions
|
||||
await waitFor(() => {
|
||||
expect(props.onChange).toHaveBeenCalled();
|
||||
});
|
||||
}, 40000);
|
||||
|
||||
test('can delete columns', async () => {
|
||||
const columnsTab = screen.getByTestId('collection-tab-Columns');
|
||||
userEvent.click(columnsTab);
|
||||
await userEvent.click(columnsTab);
|
||||
|
||||
const getToggles = screen.getAllByRole('button', {
|
||||
name: /expand row/i,
|
||||
});
|
||||
|
||||
userEvent.click(getToggles[0]);
|
||||
await userEvent.click(getToggles[0]);
|
||||
|
||||
const deleteButtons = await screen.findAllByRole('button', {
|
||||
name: /delete item/i,
|
||||
@@ -155,7 +185,7 @@ describe('DatasourceEditor', () => {
|
||||
const initialCount = deleteButtons.length;
|
||||
expect(initialCount).toBeGreaterThan(0);
|
||||
|
||||
userEvent.click(deleteButtons[0]);
|
||||
await userEvent.click(deleteButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
const countRows = screen.getAllByRole('button', { name: /delete item/i });
|
||||
@@ -165,14 +195,14 @@ describe('DatasourceEditor', () => {
|
||||
|
||||
test('can add new columns', async () => {
|
||||
const calcColsTab = screen.getByTestId('collection-tab-Calculated columns');
|
||||
userEvent.click(calcColsTab);
|
||||
await userEvent.click(calcColsTab);
|
||||
|
||||
const addBtn = screen.getByRole('button', {
|
||||
name: /add item/i,
|
||||
});
|
||||
expect(addBtn).toBeInTheDocument();
|
||||
|
||||
userEvent.click(addBtn);
|
||||
await userEvent.click(addBtn);
|
||||
|
||||
// newColumn (Column name) is the first textbox in the tab
|
||||
await waitFor(() => {
|
||||
@@ -185,7 +215,7 @@ describe('DatasourceEditor', () => {
|
||||
const columnsTab = screen.getByRole('tab', {
|
||||
name: /settings/i,
|
||||
});
|
||||
userEvent.click(columnsTab);
|
||||
await userEvent.click(columnsTab);
|
||||
|
||||
const extraField = screen.getAllByText(/extra/i);
|
||||
expect(extraField.length).toBeGreaterThan(0);
|
||||
@@ -199,7 +229,7 @@ describe('DatasourceEditor', () => {
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('DatasourceEditor Source Tab', () => {
|
||||
beforeAll(() => {
|
||||
isFeatureEnabled.mockImplementation(() => false);
|
||||
(isFeatureEnabled as jest.Mock).mockImplementation(() => false);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -215,12 +245,12 @@ describe('DatasourceEditor Source Tab', () => {
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
isFeatureEnabled.mockRestore();
|
||||
(isFeatureEnabled as jest.Mock).mockRestore();
|
||||
});
|
||||
|
||||
test('Source Tab: edit mode', async () => {
|
||||
const getLockBtn = screen.getByRole('img', { name: /lock/i });
|
||||
userEvent.click(getLockBtn);
|
||||
await userEvent.click(getLockBtn);
|
||||
|
||||
const physicalRadioBtn = screen.getByRole('radio', {
|
||||
name: /physical \(table or view\)/i,
|
||||
@@ -259,7 +289,7 @@ describe('DatasourceEditor Source Tab', () => {
|
||||
datasource: {
|
||||
...props.datasource,
|
||||
table_name: 'Vehicle Sales +',
|
||||
datasourceType: 'virtual',
|
||||
type: DatasourceType.Query,
|
||||
sql: 'SELECT * FROM users',
|
||||
},
|
||||
});
|
||||
+20
-14
@@ -19,13 +19,16 @@
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { DatasetObject } from 'src/features/datasets/types';
|
||||
import DatasourceEditor from '..';
|
||||
import { props, DATASOURCE_ENDPOINT } from './DatasourceEditor.test';
|
||||
|
||||
type MetricType = DatasetObject['metrics'][number];
|
||||
|
||||
// Optimized render function that doesn't use waitFor initially
|
||||
// This helps prevent one source of the timeout
|
||||
const fastRender = props =>
|
||||
render(<DatasourceEditor {...props} />, {
|
||||
const fastRender = (renderProps: typeof props) =>
|
||||
render(<DatasourceEditor {...renderProps} />, {
|
||||
useRedux: true,
|
||||
initialState: { common: { currencies: ['USD', 'GBP', 'EUR'] } },
|
||||
});
|
||||
@@ -66,13 +69,15 @@ describe('DatasourceEditor Currency Tests', () => {
|
||||
const metricButton = screen.getByTestId('collection-tab-Metrics');
|
||||
await userEvent.click(metricButton);
|
||||
|
||||
// Find and expand the first metric row
|
||||
// Find and expand the metric row with currency
|
||||
// Metrics are sorted by ID descending, so metric with id=1 (which has currency)
|
||||
// is at position 6 (last). Expand that one.
|
||||
const expandToggles = await screen.findAllByLabelText(
|
||||
/expand row/i,
|
||||
{},
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
await userEvent.click(expandToggles[0]);
|
||||
await userEvent.click(expandToggles[6]);
|
||||
|
||||
// Check for currency section header
|
||||
const currencyHeader = await screen.findByText(
|
||||
@@ -91,7 +96,7 @@ describe('DatasourceEditor Currency Tests', () => {
|
||||
expect(positionSelector).toBeInTheDocument();
|
||||
|
||||
// Open the dropdown
|
||||
userEvent.click(positionSelector);
|
||||
await userEvent.click(positionSelector);
|
||||
|
||||
// Wait for dropdown to open and find the suffix option
|
||||
const suffixOption = await waitFor(
|
||||
@@ -99,7 +104,7 @@ describe('DatasourceEditor Currency Tests', () => {
|
||||
// Look for 'suffix' option in the dropdown
|
||||
const options = document.querySelectorAll('.ant-select-item-option');
|
||||
const suffixOpt = Array.from(options).find(opt =>
|
||||
opt.textContent.toLowerCase().includes('suffix'),
|
||||
opt.textContent?.toLowerCase().includes('suffix'),
|
||||
);
|
||||
|
||||
if (!suffixOpt) throw new Error('Suffix option not found');
|
||||
@@ -112,7 +117,7 @@ describe('DatasourceEditor Currency Tests', () => {
|
||||
propsWithCurrency.onChange.mockClear();
|
||||
|
||||
// Click the suffix option
|
||||
userEvent.click(suffixOption);
|
||||
await userEvent.click(suffixOption);
|
||||
|
||||
// Check if onChange was called with the expected parameters
|
||||
await waitFor(
|
||||
@@ -123,11 +128,12 @@ describe('DatasourceEditor Currency Tests', () => {
|
||||
// More robust check for the metrics array
|
||||
const metrics = callArg.metrics || [];
|
||||
const updatedMetric = metrics.find(
|
||||
m => m.currency && m.currency.symbolPosition === 'suffix',
|
||||
(m: MetricType) =>
|
||||
m.currency && m.currency.symbolPosition === 'suffix',
|
||||
);
|
||||
|
||||
expect(updatedMetric).toBeDefined();
|
||||
expect(updatedMetric.currency.symbol).toBe('USD');
|
||||
expect(updatedMetric?.currency?.symbol).toBe('USD');
|
||||
},
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
@@ -142,7 +148,7 @@ describe('DatasourceEditor Currency Tests', () => {
|
||||
);
|
||||
|
||||
// Open the currency dropdown
|
||||
userEvent.click(currencySymbol);
|
||||
await userEvent.click(currencySymbol);
|
||||
|
||||
// Wait for dropdown to open and find the GBP option
|
||||
const gbpOption = await waitFor(
|
||||
@@ -150,7 +156,7 @@ describe('DatasourceEditor Currency Tests', () => {
|
||||
// Look for 'GBP' option in the dropdown
|
||||
const options = document.querySelectorAll('.ant-select-item-option');
|
||||
const gbpOpt = Array.from(options).find(opt =>
|
||||
opt.textContent.includes('GBP'),
|
||||
opt.textContent?.includes('GBP'),
|
||||
);
|
||||
|
||||
if (!gbpOpt) throw new Error('GBP option not found');
|
||||
@@ -163,7 +169,7 @@ describe('DatasourceEditor Currency Tests', () => {
|
||||
propsWithCurrency.onChange.mockClear();
|
||||
|
||||
// Click the GBP option
|
||||
userEvent.click(gbpOption);
|
||||
await userEvent.click(gbpOption);
|
||||
|
||||
// Verify the onChange with GBP was called
|
||||
await waitFor(
|
||||
@@ -174,11 +180,11 @@ describe('DatasourceEditor Currency Tests', () => {
|
||||
// More robust check
|
||||
const metrics = callArg.metrics || [];
|
||||
const updatedMetric = metrics.find(
|
||||
m => m.currency && m.currency.symbol === 'GBP',
|
||||
(m: MetricType) => m.currency && m.currency.symbol === 'GBP',
|
||||
);
|
||||
|
||||
expect(updatedMetric).toBeDefined();
|
||||
expect(updatedMetric.currency.symbolPosition).toBe('suffix');
|
||||
expect(updatedMetric?.currency?.symbolPosition).toBe('suffix');
|
||||
},
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
+9
-6
@@ -42,15 +42,18 @@ describe('DatasourceEditor RTL Metrics Tests', () => {
|
||||
await userEvent.click(metricButton);
|
||||
|
||||
const expandToggle = await screen.findAllByLabelText(/expand row/i);
|
||||
await userEvent.click(expandToggle[0]);
|
||||
// Metrics are sorted by ID descending, so metric with id=1 (which has certification)
|
||||
// is at position 6 (last). Expand that one.
|
||||
await userEvent.click(expandToggle[6]);
|
||||
|
||||
// Wait for fields to appear
|
||||
const certificationDetails = await screen.findByPlaceholderText(
|
||||
/certification details/i,
|
||||
);
|
||||
expect(certificationDetails.value).toEqual('foo');
|
||||
const certifiedBy = await screen.findByPlaceholderText(/certified by/i);
|
||||
|
||||
const warningMarkdown = await screen.findByPlaceholderText(/certified by/i);
|
||||
expect(warningMarkdown.value).toEqual('someone');
|
||||
expect(certificationDetails).toHaveValue('foo');
|
||||
expect(certifiedBy).toHaveValue('someone');
|
||||
});
|
||||
|
||||
test('properly updates the metric information', async () => {
|
||||
@@ -71,14 +74,14 @@ describe('DatasourceEditor RTL Metrics Tests', () => {
|
||||
/certification details/i,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(certifiedBy.value).toEqual('I am typing a new name');
|
||||
expect(certifiedBy).toHaveValue('I am typing a new name');
|
||||
});
|
||||
|
||||
await userEvent.clear(certificationDetails);
|
||||
await userEvent.type(certificationDetails, 'I am typing something new');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(certificationDetails.value).toEqual('I am typing something new');
|
||||
expect(certificationDetails).toHaveValue('I am typing something new');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
} from 'src/SqlLab/actions/sqlLab';
|
||||
import { RootState, store } from 'src/views/store';
|
||||
import { AnyListenerPredicate } from '@reduxjs/toolkit';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import type { SqlLabRootState } from 'src/SqlLab/types';
|
||||
import { Disposable } from '../models';
|
||||
import { createActionListener } from '../utils';
|
||||
@@ -198,13 +197,10 @@ const getActiveEditorImmutableId = () => {
|
||||
return activeEditor?.immutableId;
|
||||
};
|
||||
|
||||
// Memoized version to avoid repeated store lookups when active editor hasn't changed
|
||||
const getActiveEditorId = memoizeOne(getActiveEditorImmutableId);
|
||||
|
||||
const predicate = (actionType: string): AnyListenerPredicate<RootState> => {
|
||||
// Capture the immutable ID of the active editor at the time the listener is created
|
||||
// This ID never changes for a tab, ensuring stable event routing
|
||||
const registrationImmutableId = getActiveEditorId();
|
||||
const registrationImmutableId = getActiveEditorImmutableId();
|
||||
|
||||
return action => {
|
||||
if (action.type !== actionType) return false;
|
||||
@@ -212,14 +208,15 @@ const predicate = (actionType: string): AnyListenerPredicate<RootState> => {
|
||||
// If we don't have a registration ID, don't filter events
|
||||
if (!registrationImmutableId) return true;
|
||||
|
||||
// For query events, use the immutableId directly from the action payload
|
||||
if (action.query?.immutableId) {
|
||||
return action.query.immutableId === registrationImmutableId;
|
||||
// For query events, use the sqlEditorImmutableId directly from the action payload
|
||||
if (action.query?.sqlEditorImmutableId) {
|
||||
return action.query.sqlEditorImmutableId === registrationImmutableId;
|
||||
}
|
||||
|
||||
// For tab events, we need to find the immutable ID of the affected tab
|
||||
if (action.queryEditor?.id) {
|
||||
const queryEditor = findQueryEditor(action.queryEditor.id);
|
||||
const queryEditorId = action.queryEditor?.id || action.query?.sqlEditorId;
|
||||
if (queryEditorId) {
|
||||
const queryEditor = findQueryEditor(queryEditorId);
|
||||
return queryEditor?.immutableId === registrationImmutableId;
|
||||
}
|
||||
|
||||
|
||||
@@ -208,8 +208,7 @@ class TextAreaControl extends Component {
|
||||
buttonSize="small"
|
||||
style={{ marginTop: this.props.theme.sizeUnit }}
|
||||
>
|
||||
{t('Edit')} <strong>{this.props.language}</strong>{' '}
|
||||
{t('in modal')}
|
||||
{t('Edit %s in modal', this.props.language)}
|
||||
</Button>
|
||||
}
|
||||
modalBody={this.renderModalBody(true)}
|
||||
|
||||
@@ -16,12 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
FeatureFlag,
|
||||
SupersetClient,
|
||||
isFeatureEnabled,
|
||||
logging,
|
||||
} from '@superset-ui/core';
|
||||
import { SupersetClient, logging } from '@superset-ui/core';
|
||||
import type { contributions, core } from '@apache-superset/core';
|
||||
import { ExtensionContext } from '../core/models';
|
||||
|
||||
@@ -62,9 +57,6 @@ class ExtensionsManager {
|
||||
* @throws Error if initialization fails.
|
||||
*/
|
||||
public async initializeExtensions(): Promise<void> {
|
||||
if (!isFeatureEnabled(FeatureFlag.EnableExtensions)) {
|
||||
return;
|
||||
}
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: '/api/v1/extensions/',
|
||||
});
|
||||
|
||||
@@ -17,10 +17,21 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { render, waitFor } from 'spec/helpers/testing-library';
|
||||
import { logging } from '@superset-ui/core';
|
||||
import { logging, FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import ExtensionsStartup from './ExtensionsStartup';
|
||||
import ExtensionsManager from './ExtensionsManager';
|
||||
|
||||
// Mock the isFeatureEnabled function
|
||||
jest.mock('@superset-ui/core', () => ({
|
||||
...jest.requireActual('@superset-ui/core'),
|
||||
isFeatureEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockIsFeatureEnabled = isFeatureEnabled as jest.MockedFunction<
|
||||
typeof isFeatureEnabled
|
||||
>;
|
||||
|
||||
const mockInitialState = {
|
||||
user: { userId: 1 },
|
||||
};
|
||||
@@ -36,12 +47,26 @@ beforeEach(() => {
|
||||
|
||||
// Clear any existing ExtensionsManager instance
|
||||
(ExtensionsManager as any).instance = undefined;
|
||||
|
||||
// Reset feature flag mock to enabled by default
|
||||
mockIsFeatureEnabled.mockReset();
|
||||
mockIsFeatureEnabled.mockReturnValue(true);
|
||||
|
||||
// Setup fetch mocks for API calls
|
||||
fetchMock.restore();
|
||||
fetchMock.get('glob:*/api/v1/extensions/', {
|
||||
result: [],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up after each test
|
||||
delete (window as any).superset;
|
||||
(ExtensionsManager as any).instance = undefined;
|
||||
|
||||
// Reset mocks
|
||||
mockIsFeatureEnabled.mockReset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
test('renders without crashing', () => {
|
||||
@@ -55,6 +80,12 @@ test('renders without crashing', () => {
|
||||
});
|
||||
|
||||
test('sets up global superset object when user is logged in', async () => {
|
||||
// Mock initializeExtensions to avoid API calls in this test
|
||||
const manager = ExtensionsManager.getInstance();
|
||||
const initializeSpy = jest
|
||||
.spyOn(manager, 'initializeExtensions')
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
initialState: mockInitialState,
|
||||
@@ -70,6 +101,8 @@ test('sets up global superset object when user is logged in', async () => {
|
||||
expect((window as any).superset.extensions).toBeDefined();
|
||||
expect((window as any).superset.sqlLab).toBeDefined();
|
||||
});
|
||||
|
||||
initializeSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('does not set up global superset object when user is not logged in', async () => {
|
||||
@@ -85,18 +118,26 @@ test('does not set up global superset object when user is not logged in', async
|
||||
});
|
||||
|
||||
test('initializes ExtensionsManager when user is logged in', async () => {
|
||||
// Mock initializeExtensions to avoid API calls, but track that it was called
|
||||
const manager = ExtensionsManager.getInstance();
|
||||
const initializeSpy = jest
|
||||
.spyOn(manager, 'initializeExtensions')
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Verify ExtensionsManager has been initialized by checking if it has extensions loaded
|
||||
const manager = ExtensionsManager.getInstance();
|
||||
// Verify ExtensionsManager initialization was called
|
||||
expect(initializeSpy).toHaveBeenCalledTimes(1);
|
||||
// The manager should exist and be ready to use
|
||||
expect(manager).toBeDefined();
|
||||
expect(manager.getExtensions).toBeDefined();
|
||||
});
|
||||
|
||||
initializeSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('does not initialize ExtensionsManager when user is not logged in', async () => {
|
||||
@@ -114,61 +155,6 @@ test('does not initialize ExtensionsManager when user is not logged in', async (
|
||||
});
|
||||
});
|
||||
|
||||
test('handles ExtensionsManager initialization errors gracefully', async () => {
|
||||
const errorSpy = jest.spyOn(logging, 'error').mockImplementation();
|
||||
|
||||
// Mock the initializeExtensions method to throw an error
|
||||
const originalInitialize = ExtensionsManager.prototype.initializeExtensions;
|
||||
ExtensionsManager.prototype.initializeExtensions = jest
|
||||
.fn()
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Test initialization error');
|
||||
});
|
||||
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Verify error was logged
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Error setting up extensions:',
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
|
||||
// Restore original method
|
||||
ExtensionsManager.prototype.initializeExtensions = originalInitialize;
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('logs success message when ExtensionsManager initializes successfully', async () => {
|
||||
const infoSpy = jest.spyOn(logging, 'info').mockImplementation();
|
||||
|
||||
// Mock the initializeExtensions method to succeed
|
||||
const originalInitialize = ExtensionsManager.prototype.initializeExtensions;
|
||||
ExtensionsManager.prototype.initializeExtensions = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Verify success message was logged
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
'Extensions initialized successfully.',
|
||||
);
|
||||
});
|
||||
|
||||
// Restore original method
|
||||
ExtensionsManager.prototype.initializeExtensions = originalInitialize;
|
||||
infoSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('only initializes once even with multiple renders', async () => {
|
||||
// Track calls to the manager's public API
|
||||
const manager = ExtensionsManager.getInstance();
|
||||
@@ -203,3 +189,106 @@ test('only initializes once even with multiple renders', async () => {
|
||||
// Restore original method
|
||||
manager.initializeExtensions = originalInitialize;
|
||||
});
|
||||
|
||||
test('initializes ExtensionsManager and logs success when EnableExtensions feature flag is enabled', async () => {
|
||||
// Ensure feature flag is enabled
|
||||
mockIsFeatureEnabled.mockImplementation(
|
||||
(flag: FeatureFlag) => flag === FeatureFlag.EnableExtensions,
|
||||
);
|
||||
|
||||
const infoSpy = jest.spyOn(logging, 'info').mockImplementation();
|
||||
|
||||
// Mock the initializeExtensions method to succeed
|
||||
const originalInitialize = ExtensionsManager.prototype.initializeExtensions;
|
||||
ExtensionsManager.prototype.initializeExtensions = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Verify feature flag was checked
|
||||
expect(mockIsFeatureEnabled).toHaveBeenCalledWith(
|
||||
FeatureFlag.EnableExtensions,
|
||||
);
|
||||
// Verify initialization was called
|
||||
expect(
|
||||
ExtensionsManager.prototype.initializeExtensions,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
// Verify success message was logged
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
'Extensions initialized successfully.',
|
||||
);
|
||||
});
|
||||
|
||||
// Restore original method
|
||||
ExtensionsManager.prototype.initializeExtensions = originalInitialize;
|
||||
infoSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('does not initialize ExtensionsManager when EnableExtensions feature flag is disabled', async () => {
|
||||
// Disable the feature flag
|
||||
mockIsFeatureEnabled.mockReturnValue(false);
|
||||
|
||||
const manager = ExtensionsManager.getInstance();
|
||||
const initializeSpy = jest
|
||||
.spyOn(manager, 'initializeExtensions')
|
||||
.mockImplementation();
|
||||
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Verify feature flag was checked
|
||||
expect(mockIsFeatureEnabled).toHaveBeenCalledWith(
|
||||
FeatureFlag.EnableExtensions,
|
||||
);
|
||||
// Verify the global superset object is still set up
|
||||
expect((window as any).superset).toBeDefined();
|
||||
// But extensions should not be initialized
|
||||
expect(initializeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
initializeSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('logs error when ExtensionsManager initialization fails', async () => {
|
||||
// Ensure feature flag is enabled
|
||||
mockIsFeatureEnabled.mockReturnValue(true);
|
||||
|
||||
const errorSpy = jest.spyOn(logging, 'error').mockImplementation();
|
||||
|
||||
// Mock the initializeExtensions method to throw an error
|
||||
const originalInitialize = ExtensionsManager.prototype.initializeExtensions;
|
||||
ExtensionsManager.prototype.initializeExtensions = jest
|
||||
.fn()
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Test initialization error');
|
||||
});
|
||||
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Verify feature flag was checked
|
||||
expect(mockIsFeatureEnabled).toHaveBeenCalledWith(
|
||||
FeatureFlag.EnableExtensions,
|
||||
);
|
||||
// Verify error was logged
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Error setting up extensions:',
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
|
||||
// Restore original method
|
||||
ExtensionsManager.prototype.initializeExtensions = originalInitialize;
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
import * as supersetCore from '@apache-superset/core';
|
||||
import { logging } from '@superset-ui/core';
|
||||
import { FeatureFlag, isFeatureEnabled, logging } from '@superset-ui/core';
|
||||
import {
|
||||
authentication,
|
||||
core,
|
||||
@@ -75,14 +75,15 @@ const ExtensionsStartup = () => {
|
||||
};
|
||||
|
||||
// Initialize extensions
|
||||
try {
|
||||
ExtensionsManager.getInstance().initializeExtensions();
|
||||
logging.info('Extensions initialized successfully.');
|
||||
} catch (error) {
|
||||
logging.error('Error setting up extensions:', error);
|
||||
} finally {
|
||||
setInitialized(true);
|
||||
if (isFeatureEnabled(FeatureFlag.EnableExtensions)) {
|
||||
try {
|
||||
ExtensionsManager.getInstance().initializeExtensions();
|
||||
logging.info('Extensions initialized successfully.');
|
||||
} catch (error) {
|
||||
logging.error('Error setting up extensions:', error);
|
||||
}
|
||||
}
|
||||
setInitialized(true);
|
||||
}, [initialized, userId]);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -66,6 +66,7 @@ export const mapQueryResponse = (
|
||||
): Omit<
|
||||
Query,
|
||||
| 'tempSchema'
|
||||
| 'sqlEditorImmutableId'
|
||||
| 'started'
|
||||
| 'time'
|
||||
| 'duration'
|
||||
|
||||
@@ -104,22 +104,14 @@ export const useThemeMenuItems = ({
|
||||
: []),
|
||||
];
|
||||
|
||||
const children: MenuItem[] = [
|
||||
{
|
||||
type: 'group' as const,
|
||||
label: t('Theme'),
|
||||
key: 'theme-group',
|
||||
children: themeOptions,
|
||||
},
|
||||
];
|
||||
|
||||
// Add clear settings option only when there's a local theme active
|
||||
// Add clear settings option to theme options if there's a local theme active
|
||||
const themeGroupOptions = [...themeOptions];
|
||||
if (onClearLocalSettings && hasLocalOverride) {
|
||||
children.push({
|
||||
themeGroupOptions.push({
|
||||
type: 'divider' as const,
|
||||
key: 'theme-divider',
|
||||
});
|
||||
children.push({
|
||||
themeGroupOptions.push({
|
||||
key: 'clear-local',
|
||||
label: (
|
||||
<>
|
||||
@@ -130,6 +122,15 @@ export const useThemeMenuItems = ({
|
||||
});
|
||||
}
|
||||
|
||||
const children: MenuItem[] = [
|
||||
{
|
||||
type: 'group' as const,
|
||||
label: t('Theme'),
|
||||
key: 'theme-group',
|
||||
children: themeGroupOptions,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
key: 'theme-sub-menu',
|
||||
label: selectedThemeModeIcon,
|
||||
|
||||
@@ -103,6 +103,9 @@ const Actions = styled.div`
|
||||
}
|
||||
}
|
||||
color: ${theme.colorTextDisabled};
|
||||
&:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ant-menu-item:hover {
|
||||
cursor: default;
|
||||
}
|
||||
@@ -479,7 +482,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={allowEdit ? 'action-button' : 'disabled'}
|
||||
className={`action-button ${allowEdit ? '' : 'disabled'}`}
|
||||
onClick={allowEdit ? handleEdit : undefined}
|
||||
>
|
||||
<Icons.EditOutlined iconSize="l" />
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
Typography,
|
||||
Icons,
|
||||
} from '@superset-ui/core/components';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { capitalize } from 'lodash/fp';
|
||||
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -82,6 +82,26 @@ export default function Login() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const bootstrapData = getBootstrapData();
|
||||
const nextUrl = useMemo(() => {
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get('next') || '';
|
||||
} catch (_error) {
|
||||
return '';
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loginEndpoint = useMemo(
|
||||
() => (nextUrl ? `/login/?next=${encodeURIComponent(nextUrl)}` : '/login/'),
|
||||
[nextUrl],
|
||||
);
|
||||
|
||||
const buildProviderLoginUrl = (providerName: string) => {
|
||||
const base = `/login/${providerName}`;
|
||||
return nextUrl
|
||||
? `${base}${base.includes('?') ? '&' : '?'}next=${encodeURIComponent(nextUrl)}`
|
||||
: base;
|
||||
};
|
||||
|
||||
const authType: AuthType = bootstrapData.common.conf.AUTH_TYPE;
|
||||
const providers: Provider[] = bootstrapData.common.conf.AUTH_PROVIDERS;
|
||||
@@ -109,7 +129,7 @@ export default function Login() {
|
||||
sessionStorage.setItem('login_attempted', 'true');
|
||||
|
||||
// Use standard form submission for Flask-AppBuilder compatibility
|
||||
SupersetClient.postForm('/login/', values, '');
|
||||
SupersetClient.postForm(loginEndpoint, values, '');
|
||||
};
|
||||
|
||||
const getAuthIconElement = (
|
||||
@@ -146,7 +166,7 @@ export default function Login() {
|
||||
{providers.map((provider: OIDProvider) => (
|
||||
<Form.Item<LoginForm>>
|
||||
<Button
|
||||
href={`/login/${provider.name}`}
|
||||
href={buildProviderLoginUrl(provider.name)}
|
||||
block
|
||||
iconPosition="start"
|
||||
icon={getAuthIconElement(provider.name)}
|
||||
@@ -164,7 +184,7 @@ export default function Login() {
|
||||
{providers.map((provider: OAuthProvider) => (
|
||||
<Form.Item<LoginForm>>
|
||||
<Button
|
||||
href={`/login/${provider.name}`}
|
||||
href={buildProviderLoginUrl(provider.name)}
|
||||
block
|
||||
iconPosition="start"
|
||||
icon={getAuthIconElement(provider.name)}
|
||||
|
||||
@@ -62,6 +62,7 @@ jest.mock('src/views/CRUD/hooks', () => ({
|
||||
|
||||
// Mock the useThemeContext hook
|
||||
const mockSetTemporaryTheme = jest.fn();
|
||||
const mockGetAppliedThemeId = jest.fn();
|
||||
jest.mock('src/theme/ThemeProvider', () => ({
|
||||
...jest.requireActual('src/theme/ThemeProvider'),
|
||||
useThemeContext: jest.fn(),
|
||||
@@ -141,10 +142,13 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
// Mock useThemeContext
|
||||
mockGetAppliedThemeId.mockReturnValue(null);
|
||||
(useThemeContext as jest.Mock).mockReturnValue({
|
||||
getCurrentCrudThemeId: jest.fn().mockReturnValue('1'),
|
||||
appliedTheme: { theme_name: 'Light Theme', id: 1 },
|
||||
setTemporaryTheme: mockSetTemporaryTheme,
|
||||
hasDevOverride: jest.fn().mockReturnValue(false),
|
||||
getAppliedThemeId: mockGetAppliedThemeId,
|
||||
});
|
||||
|
||||
fetchMock.reset();
|
||||
@@ -460,7 +464,7 @@ test('shows create theme button when user has permissions', async () => {
|
||||
expect(addButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('clicking apply button calls setTemporaryTheme with parsed theme data', async () => {
|
||||
test('clicking apply button calls setTemporaryTheme with parsed theme data and ID', async () => {
|
||||
render(
|
||||
<ThemesList
|
||||
user={mockUser}
|
||||
@@ -483,8 +487,106 @@ test('clicking apply button calls setTemporaryTheme with parsed theme data', asy
|
||||
await userEvent.click(applyButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetTemporaryTheme).toHaveBeenCalledWith({
|
||||
colors: { primary: '#ffffff' },
|
||||
});
|
||||
expect(mockSetTemporaryTheme).toHaveBeenCalledWith(
|
||||
{
|
||||
colors: { primary: '#ffffff' },
|
||||
},
|
||||
1, // theme ID
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('applying a local theme calls setTemporaryTheme with theme ID', async () => {
|
||||
render(
|
||||
<ThemesList
|
||||
user={mockUser}
|
||||
addDangerToast={jest.fn()}
|
||||
addSuccessToast={jest.fn()}
|
||||
/>,
|
||||
{
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
useQueryParams: true,
|
||||
useTheme: true,
|
||||
},
|
||||
);
|
||||
|
||||
await screen.findByText('Custom Theme');
|
||||
|
||||
// Find and click the apply button for the first theme
|
||||
const applyButtons = await screen.findAllByTestId('apply-action');
|
||||
await userEvent.click(applyButtons[0]);
|
||||
|
||||
// Check that setTemporaryTheme was called with both theme config and ID
|
||||
await waitFor(() => {
|
||||
expect(mockSetTemporaryTheme).toHaveBeenCalledWith(
|
||||
{ colors: { primary: '#ffffff' } },
|
||||
1, // theme ID
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('component loads successfully with applied theme ID set', async () => {
|
||||
// This test verifies that having a stored theme ID doesn't break the component
|
||||
// Mock hasDevOverride to return true since we have a dev override set
|
||||
mockGetAppliedThemeId.mockReturnValue(1);
|
||||
(useThemeContext as jest.Mock).mockReturnValue({
|
||||
getCurrentCrudThemeId: jest.fn().mockReturnValue('1'),
|
||||
appliedTheme: { theme_name: 'Light Theme', id: 1 },
|
||||
setTemporaryTheme: mockSetTemporaryTheme,
|
||||
hasDevOverride: jest.fn().mockReturnValue(true),
|
||||
getAppliedThemeId: mockGetAppliedThemeId,
|
||||
});
|
||||
|
||||
render(
|
||||
<ThemesList
|
||||
user={mockUser}
|
||||
addDangerToast={jest.fn()}
|
||||
addSuccessToast={jest.fn()}
|
||||
/>,
|
||||
{
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
useQueryParams: true,
|
||||
useTheme: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Wait for list to load and verify it renders successfully
|
||||
await screen.findByText('Custom Theme');
|
||||
|
||||
// Verify the component called getAppliedThemeId
|
||||
expect(mockGetAppliedThemeId).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('component loads successfully and preserves applied theme state', async () => {
|
||||
// Mock hasDevOverride to return true and getAppliedThemeId to return a theme
|
||||
mockGetAppliedThemeId.mockReturnValue(1);
|
||||
(useThemeContext as jest.Mock).mockReturnValue({
|
||||
getCurrentCrudThemeId: jest.fn().mockReturnValue('1'),
|
||||
appliedTheme: { theme_name: 'Light Theme', id: 1 },
|
||||
setTemporaryTheme: mockSetTemporaryTheme,
|
||||
hasDevOverride: jest.fn().mockReturnValue(true),
|
||||
getAppliedThemeId: mockGetAppliedThemeId,
|
||||
});
|
||||
|
||||
render(
|
||||
<ThemesList
|
||||
user={mockUser}
|
||||
addDangerToast={jest.fn()}
|
||||
addSuccessToast={jest.fn()}
|
||||
/>,
|
||||
{
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
useQueryParams: true,
|
||||
useTheme: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Wait for list to load
|
||||
await screen.findByText('Custom Theme');
|
||||
|
||||
// Verify getAppliedThemeId is called during component mount
|
||||
expect(mockGetAppliedThemeId).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { t, SupersetClient, styled } from '@superset-ui/core';
|
||||
import {
|
||||
Tag,
|
||||
@@ -110,15 +110,27 @@ function ThemesList({
|
||||
refreshData,
|
||||
toggleBulkSelect,
|
||||
} = useListViewResource<ThemeObject>('theme', t('Themes'), addDangerToast);
|
||||
const { setTemporaryTheme, getCurrentCrudThemeId } = useThemeContext();
|
||||
const { setTemporaryTheme, hasDevOverride, getAppliedThemeId } =
|
||||
useThemeContext();
|
||||
const [themeModalOpen, setThemeModalOpen] = useState<boolean>(false);
|
||||
const [currentTheme, setCurrentTheme] = useState<ThemeObject | null>(null);
|
||||
const [preparingExport, setPreparingExport] = useState<boolean>(false);
|
||||
const [importingTheme, showImportModal] = useState<boolean>(false);
|
||||
const [appliedThemeId, setAppliedThemeId] = useState<number | null>(null);
|
||||
const [appliedThemeId, setLocalAppliedThemeId] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const { showConfirm, ConfirmModal } = useConfirmModal();
|
||||
|
||||
useEffect(() => {
|
||||
if (hasDevOverride()) {
|
||||
const storedThemeId = getAppliedThemeId();
|
||||
setLocalAppliedThemeId(storedThemeId);
|
||||
} else {
|
||||
setLocalAppliedThemeId(null);
|
||||
}
|
||||
}, [hasDevOverride, getAppliedThemeId]);
|
||||
|
||||
const canCreate = hasPerm('can_write');
|
||||
const canEdit = hasPerm('can_write');
|
||||
const canDelete = hasPerm('can_write');
|
||||
@@ -201,8 +213,11 @@ function ThemesList({
|
||||
if (themeObj.json_data) {
|
||||
try {
|
||||
const themeConfig = JSON.parse(themeObj.json_data);
|
||||
setTemporaryTheme(themeConfig);
|
||||
setAppliedThemeId(themeObj.id || null);
|
||||
const themeId = themeObj.id || null;
|
||||
|
||||
setTemporaryTheme(themeConfig, themeId);
|
||||
setLocalAppliedThemeId(themeId);
|
||||
|
||||
addSuccessToast(t('Local theme set to "%s"', themeObj.theme_name));
|
||||
} catch (error) {
|
||||
addDangerToast(
|
||||
@@ -217,23 +232,26 @@ function ThemesList({
|
||||
function handleThemeModalApply() {
|
||||
// Clear any previously applied theme ID when applying from modal
|
||||
// since the modal theme might not have an ID yet (unsaved theme)
|
||||
setAppliedThemeId(null);
|
||||
setLocalAppliedThemeId(null);
|
||||
}
|
||||
|
||||
const handleBulkThemeExport = async (themesToExport: ThemeObject[]) => {
|
||||
const ids = themesToExport
|
||||
.map(({ id }) => id)
|
||||
.filter((id): id is number => id !== undefined);
|
||||
setPreparingExport(true);
|
||||
try {
|
||||
await handleResourceExport('theme', ids, () => {
|
||||
const handleBulkThemeExport = useCallback(
|
||||
async (themesToExport: ThemeObject[]) => {
|
||||
const ids = themesToExport
|
||||
.map(({ id }) => id)
|
||||
.filter((id): id is number => id !== undefined);
|
||||
setPreparingExport(true);
|
||||
try {
|
||||
await handleResourceExport('theme', ids, () => {
|
||||
setPreparingExport(false);
|
||||
});
|
||||
} catch (error) {
|
||||
setPreparingExport(false);
|
||||
});
|
||||
} catch (error) {
|
||||
setPreparingExport(false);
|
||||
addDangerToast(t('There was an issue exporting the selected themes'));
|
||||
}
|
||||
};
|
||||
addDangerToast(t('There was an issue exporting the selected themes'));
|
||||
}
|
||||
},
|
||||
[addDangerToast],
|
||||
);
|
||||
|
||||
const openThemeImportModal = () => {
|
||||
showImportModal(true);
|
||||
@@ -346,11 +364,10 @@ function ThemesList({
|
||||
() => [
|
||||
{
|
||||
Cell: ({ row: { original } }: any) => {
|
||||
const currentCrudThemeId = getCurrentCrudThemeId();
|
||||
const isCurrentTheme =
|
||||
(currentCrudThemeId &&
|
||||
original.id?.toString() === currentCrudThemeId) ||
|
||||
(appliedThemeId && original.id === appliedThemeId);
|
||||
hasDevOverride() &&
|
||||
appliedThemeId &&
|
||||
original.id === appliedThemeId;
|
||||
|
||||
return (
|
||||
<FlexRowContainer>
|
||||
@@ -520,11 +537,12 @@ function ThemesList({
|
||||
canDelete,
|
||||
canApply,
|
||||
canExport,
|
||||
getCurrentCrudThemeId,
|
||||
hasDevOverride,
|
||||
appliedThemeId,
|
||||
canSetSystemThemes,
|
||||
addDangerToast,
|
||||
handleThemeApply,
|
||||
handleBulkThemeExport,
|
||||
handleSetSystemDefault,
|
||||
handleUnsetSystemDefault,
|
||||
handleSetSystemDark,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type ThemeControllerOptions,
|
||||
type ThemeStorage,
|
||||
isThemeConfigDark,
|
||||
makeApi,
|
||||
Theme,
|
||||
ThemeMode,
|
||||
themeObject as supersetThemeObject,
|
||||
@@ -37,6 +38,7 @@ const STORAGE_KEYS = {
|
||||
THEME_MODE: 'superset-theme-mode',
|
||||
CRUD_THEME_ID: 'superset-crud-theme-id',
|
||||
DEV_THEME_OVERRIDE: 'superset-dev-theme-override',
|
||||
APPLIED_THEME_ID: 'superset-applied-theme-id',
|
||||
} as const;
|
||||
|
||||
const MEDIA_QUERY_DARK_SCHEME = '(prefers-color-scheme: dark)';
|
||||
@@ -224,14 +226,14 @@ export class ThemeController {
|
||||
return this.dashboardThemes.get(themeId)!;
|
||||
}
|
||||
|
||||
// Fetch theme config from API
|
||||
const response = await fetch(`/api/v1/theme/${themeId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
// Fetch theme config from API using SupersetClient for proper auth
|
||||
const getTheme = makeApi<void, { result: { json_data: string } }>({
|
||||
method: 'GET',
|
||||
endpoint: `/api/v1/theme/${themeId}`,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const themeConfig = JSON.parse(data.result.json_data);
|
||||
const { result } = await getTheme();
|
||||
const themeConfig = JSON.parse(result.json_data);
|
||||
|
||||
if (themeConfig) {
|
||||
// Controller creates and owns the dashboard theme
|
||||
@@ -303,7 +305,12 @@ export class ThemeController {
|
||||
public setThemeMode(mode: ThemeMode): void {
|
||||
this.validateModeUpdatePermission(mode);
|
||||
|
||||
if (this.currentMode === mode) return;
|
||||
if (
|
||||
this.currentMode === mode &&
|
||||
!this.devThemeOverride &&
|
||||
!this.crudThemeId
|
||||
)
|
||||
return;
|
||||
|
||||
// Clear any local overrides when explicitly selecting a theme mode
|
||||
// This ensures the selected mode takes effect and provides clear UX
|
||||
@@ -367,8 +374,12 @@ export class ThemeController {
|
||||
* Sets a temporary theme override for development purposes.
|
||||
* This does not persist the theme but allows live preview.
|
||||
* @param theme - The theme configuration to apply temporarily
|
||||
* @param themeId - Optional theme ID to track which theme was applied (for UI display)
|
||||
*/
|
||||
public setTemporaryTheme(theme: AnyThemeConfig): void {
|
||||
public setTemporaryTheme(
|
||||
theme: AnyThemeConfig,
|
||||
themeId?: number | null,
|
||||
): void {
|
||||
this.validateThemeUpdatePermission();
|
||||
|
||||
this.devThemeOverride = theme;
|
||||
@@ -377,6 +388,11 @@ export class ThemeController {
|
||||
JSON.stringify(theme),
|
||||
);
|
||||
|
||||
// Store the theme ID if provided
|
||||
if (themeId !== undefined) {
|
||||
this.setAppliedThemeId(themeId);
|
||||
}
|
||||
|
||||
const mergedTheme = this.getThemeForMode(this.currentMode);
|
||||
if (mergedTheme) this.updateTheme(mergedTheme);
|
||||
}
|
||||
@@ -392,6 +408,7 @@ export class ThemeController {
|
||||
|
||||
this.storage.removeItem(STORAGE_KEYS.DEV_THEME_OVERRIDE);
|
||||
this.storage.removeItem(STORAGE_KEYS.CRUD_THEME_ID);
|
||||
this.storage.removeItem(STORAGE_KEYS.APPLIED_THEME_ID);
|
||||
|
||||
// Clear dashboard themes cache
|
||||
this.dashboardThemes.clear();
|
||||
@@ -413,6 +430,34 @@ export class ThemeController {
|
||||
return this.devThemeOverride !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the applied theme ID (for UI display purposes).
|
||||
*/
|
||||
public getAppliedThemeId(): number | null {
|
||||
try {
|
||||
const storedId = this.storage.getItem(STORAGE_KEYS.APPLIED_THEME_ID);
|
||||
return storedId ? parseInt(storedId, 10) : null;
|
||||
} catch (error) {
|
||||
console.warn('Failed to get applied theme ID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the applied theme ID (for UI display purposes).
|
||||
*/
|
||||
public setAppliedThemeId(themeId: number | null): void {
|
||||
try {
|
||||
if (themeId !== null) {
|
||||
this.storage.setItem(STORAGE_KEYS.APPLIED_THEME_ID, themeId.toString());
|
||||
} else {
|
||||
this.storage.removeItem(STORAGE_KEYS.APPLIED_THEME_ID);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to set applied theme ID:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if OS preference detection is allowed.
|
||||
* Allowed when dark theme is available (including base dark theme)
|
||||
@@ -817,13 +862,14 @@ export class ThemeController {
|
||||
themeId: string,
|
||||
): Promise<AnyThemeConfig | null> {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/theme/${themeId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
// Use SupersetClient for proper authentication handling
|
||||
const getTheme = makeApi<void, { result: { json_data: string } }>({
|
||||
method: 'GET',
|
||||
endpoint: `/api/v1/theme/${themeId}`,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const themeConfig = JSON.parse(data.result.json_data);
|
||||
const { result } = await getTheme();
|
||||
const themeConfig = JSON.parse(result.json_data);
|
||||
|
||||
return themeConfig;
|
||||
} catch (error) {
|
||||
|
||||
@@ -117,6 +117,11 @@ export function SupersetThemeProvider({
|
||||
[themeController],
|
||||
);
|
||||
|
||||
const getAppliedThemeId = useCallback(
|
||||
() => themeController.getAppliedThemeId(),
|
||||
[themeController],
|
||||
);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
theme: currentTheme,
|
||||
@@ -132,6 +137,7 @@ export function SupersetThemeProvider({
|
||||
canSetTheme,
|
||||
canDetectOSPreference,
|
||||
createDashboardThemeProvider,
|
||||
getAppliedThemeId,
|
||||
}),
|
||||
[
|
||||
currentTheme,
|
||||
@@ -147,6 +153,7 @@ export function SupersetThemeProvider({
|
||||
canSetTheme,
|
||||
canDetectOSPreference,
|
||||
createDashboardThemeProvider,
|
||||
getAppliedThemeId,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1137,4 +1137,236 @@ describe('ThemeController', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('setThemeMode clears dev override and crud theme from storage', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: DEFAULT_THEME,
|
||||
dark: DARK_THEME,
|
||||
}),
|
||||
);
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
// Simulate having overrides after initialization using Reflect to access private properties
|
||||
Reflect.set(controller, 'devThemeOverride', {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
});
|
||||
Reflect.set(controller, 'crudThemeId', '123');
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Change theme mode - should clear the overrides
|
||||
controller.setThemeMode(ThemeMode.DARK);
|
||||
|
||||
// Verify both storage keys were removed
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-dev-theme-override',
|
||||
);
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-crud-theme-id',
|
||||
);
|
||||
});
|
||||
|
||||
test('setThemeMode can be called with same mode when overrides exist', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: DEFAULT_THEME,
|
||||
dark: DARK_THEME,
|
||||
}),
|
||||
);
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Simulate having dev override after initialization using Reflect
|
||||
Reflect.set(controller, 'devThemeOverride', {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
});
|
||||
|
||||
// Call setThemeMode with DEFAULT mode - should clear override
|
||||
controller.setThemeMode(ThemeMode.DEFAULT);
|
||||
|
||||
// Verify override was removed even though mode is the same
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-dev-theme-override',
|
||||
);
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-crud-theme-id',
|
||||
);
|
||||
|
||||
// Theme should still be updated to clear the override
|
||||
expect(mockSetConfig).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('setThemeMode with no override and same mode does not trigger update', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: DEFAULT_THEME,
|
||||
dark: DARK_THEME,
|
||||
}),
|
||||
);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
// Set mode to DEFAULT
|
||||
controller.setThemeMode(ThemeMode.DEFAULT);
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Call again with same mode and no override - should skip
|
||||
controller.setThemeMode(ThemeMode.DEFAULT);
|
||||
|
||||
// Should not trigger any updates
|
||||
expect(mockSetConfig).not.toHaveBeenCalled();
|
||||
expect(mockLocalStorage.removeItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('hasDevOverride returns true when dev override is set', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: DEFAULT_THEME,
|
||||
dark: DARK_THEME,
|
||||
}),
|
||||
);
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
// Simulate dev override after initialization using Reflect
|
||||
Reflect.set(controller, 'devThemeOverride', {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
});
|
||||
|
||||
expect(controller.hasDevOverride()).toBe(true);
|
||||
});
|
||||
|
||||
test('hasDevOverride returns false when no dev override in storage', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: DEFAULT_THEME,
|
||||
dark: DARK_THEME,
|
||||
}),
|
||||
);
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
expect(controller.hasDevOverride()).toBe(false);
|
||||
});
|
||||
|
||||
test('clearLocalOverrides removes dev override, crud theme, and applied theme ID', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: DEFAULT_THEME,
|
||||
dark: DARK_THEME,
|
||||
}),
|
||||
);
|
||||
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Clear overrides
|
||||
controller.clearLocalOverrides();
|
||||
|
||||
// Verify all storage keys are removed
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-dev-theme-override',
|
||||
);
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-crud-theme-id',
|
||||
);
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-applied-theme-id',
|
||||
);
|
||||
|
||||
// Should reset to default theme
|
||||
expect(mockSetConfig).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('getAppliedThemeId returns stored theme ID', () => {
|
||||
mockLocalStorage.getItem.mockImplementation((key: string) => {
|
||||
if (key === 'superset-applied-theme-id') {
|
||||
return '42';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
expect(controller.getAppliedThemeId()).toBe(42);
|
||||
});
|
||||
|
||||
test('getAppliedThemeId returns null when no theme ID is stored', () => {
|
||||
mockLocalStorage.getItem.mockReturnValue(null);
|
||||
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
expect(controller.getAppliedThemeId()).toBeNull();
|
||||
});
|
||||
|
||||
test('setAppliedThemeId stores theme ID in storage', () => {
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
controller.setAppliedThemeId(123);
|
||||
|
||||
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
|
||||
'superset-applied-theme-id',
|
||||
'123',
|
||||
);
|
||||
});
|
||||
|
||||
test('setAppliedThemeId removes theme ID when null is passed', () => {
|
||||
const controller = new ThemeController({
|
||||
storage: mockLocalStorage,
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
controller.setAppliedThemeId(null);
|
||||
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'superset-applied-theme-id',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -276,3 +276,123 @@ test('handles various resource types', async () => {
|
||||
|
||||
expect(doneMock).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
test('handles network errors and logs them', async () => {
|
||||
const networkError = new Error('Network request failed');
|
||||
(SupersetClient.get as jest.Mock).mockRejectedValue(networkError);
|
||||
|
||||
const doneMock = jest.fn();
|
||||
|
||||
await expect(
|
||||
handleResourceExport('dashboard', [1], doneMock),
|
||||
).rejects.toThrow('Network request failed');
|
||||
|
||||
expect(logging.error).toHaveBeenCalledWith(
|
||||
'Resource export failed:',
|
||||
networkError,
|
||||
);
|
||||
expect(doneMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles 404 errors when resource not found', async () => {
|
||||
const notFoundError = new Error('Not found');
|
||||
(SupersetClient.get as jest.Mock).mockRejectedValue(notFoundError);
|
||||
|
||||
const doneMock = jest.fn();
|
||||
|
||||
await expect(
|
||||
handleResourceExport('dashboard', [999], doneMock),
|
||||
).rejects.toThrow('Not found');
|
||||
|
||||
expect(doneMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles empty response from server', async () => {
|
||||
const emptyBlob = new Blob([], { type: 'application/zip' });
|
||||
mockResponse = {
|
||||
headers: new Headers({
|
||||
'Content-Disposition': 'attachment; filename="empty.zip"',
|
||||
}),
|
||||
blob: jest.fn().mockResolvedValue(emptyBlob),
|
||||
} as unknown as Response;
|
||||
(SupersetClient.get as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const doneMock = jest.fn();
|
||||
await handleResourceExport('dashboard', [1], doneMock);
|
||||
|
||||
expect(window.URL.createObjectURL).toHaveBeenCalledWith(emptyBlob);
|
||||
expect(doneMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('cleans up blob URL even when download fails', async () => {
|
||||
const mockAnchor = document.createElement('a');
|
||||
mockAnchor.click = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Click failed');
|
||||
});
|
||||
|
||||
createElementSpy.mockRestore();
|
||||
createElementSpy = jest
|
||||
.spyOn(document, 'createElement')
|
||||
.mockReturnValue(mockAnchor);
|
||||
|
||||
const doneMock = jest.fn();
|
||||
|
||||
await expect(
|
||||
handleResourceExport('dashboard', [1], doneMock),
|
||||
).rejects.toThrow('Click failed');
|
||||
|
||||
// Verify cleanup still happens
|
||||
expect(window.URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url');
|
||||
expect(doneMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles malformed Content-Disposition header', async () => {
|
||||
mockResponse = {
|
||||
headers: new Headers({
|
||||
'Content-Disposition': 'not-a-valid-header',
|
||||
}),
|
||||
blob: jest.fn().mockResolvedValue(mockBlob),
|
||||
} as unknown as Response;
|
||||
(SupersetClient.get as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
(contentDisposition.parse as jest.Mock).mockImplementationOnce(() => {
|
||||
throw new Error('Parse error');
|
||||
});
|
||||
|
||||
const doneMock = jest.fn();
|
||||
await handleResourceExport('dataset', [5], doneMock);
|
||||
|
||||
// Should fall back to default filename
|
||||
const anchor = document.createElement('a');
|
||||
expect(anchor.download).toBe('dataset_export.zip');
|
||||
expect(logging.warn).toHaveBeenCalledWith(
|
||||
'Failed to parse Content-Disposition header:',
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
|
||||
test('handles missing headers object', async () => {
|
||||
mockResponse = {
|
||||
headers: new Headers(),
|
||||
blob: jest.fn().mockResolvedValue(mockBlob),
|
||||
} as unknown as Response;
|
||||
(SupersetClient.get as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const doneMock = jest.fn();
|
||||
await handleResourceExport('chart', [7], doneMock);
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
expect(anchor.download).toBe('chart_export.zip');
|
||||
expect(doneMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles export with empty IDs array', async () => {
|
||||
const doneMock = jest.fn();
|
||||
await handleResourceExport('dashboard', [], doneMock);
|
||||
|
||||
expect(SupersetClient.get).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
endpoint: '/api/v1/dashboard/export/?q=!()',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
Generated
+194
-194
@@ -25,22 +25,22 @@
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^24.7.1",
|
||||
"@types/node": "^24.9.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.0",
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"eslint": "^9.37.0",
|
||||
"@typescript-eslint/parser": "^8.46.2",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"globals": "^16.4.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-jest": "^29.4.4",
|
||||
"ts-jest": "^29.4.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.0"
|
||||
"typescript-eslint": "^8.46.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.4",
|
||||
@@ -750,12 +750,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
|
||||
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.6",
|
||||
"@eslint/object-schema": "^2.1.7",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
},
|
||||
@@ -764,9 +765,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
|
||||
"integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
|
||||
"integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -847,9 +848,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
|
||||
"integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
|
||||
"integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -860,10 +861,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
|
||||
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
||||
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
@@ -1857,13 +1859,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz",
|
||||
"integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==",
|
||||
"version": "24.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.14.0"
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
@@ -1909,17 +1911,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz",
|
||||
"integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
|
||||
"integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/type-utils": "8.46.0",
|
||||
"@typescript-eslint/utils": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/type-utils": "8.46.2",
|
||||
"@typescript-eslint/utils": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1933,7 +1935,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"@typescript-eslint/parser": "^8.46.2",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -1949,16 +1951,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz",
|
||||
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz",
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1974,14 +1976,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz",
|
||||
"integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
|
||||
"integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.46.0",
|
||||
"@typescript-eslint/types": "^8.46.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.46.2",
|
||||
"@typescript-eslint/types": "^8.46.2",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1996,14 +1998,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz",
|
||||
"integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz",
|
||||
"integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0"
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2014,9 +2016,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz",
|
||||
"integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz",
|
||||
"integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2031,15 +2033,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz",
|
||||
"integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz",
|
||||
"integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0",
|
||||
"@typescript-eslint/utils": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2",
|
||||
"@typescript-eslint/utils": "8.46.2",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -2056,9 +2058,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz",
|
||||
"integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz",
|
||||
"integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2070,16 +2072,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz",
|
||||
"integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz",
|
||||
"integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.46.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0",
|
||||
"@typescript-eslint/project-service": "8.46.2",
|
||||
"@typescript-eslint/tsconfig-utils": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -2125,16 +2127,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz",
|
||||
"integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz",
|
||||
"integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0"
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2149,13 +2151,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz",
|
||||
"integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz",
|
||||
"integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2839,25 +2841,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.37.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
|
||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.0",
|
||||
"@eslint/config-helpers": "^0.4.0",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-helpers": "^0.4.1",
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.37.0",
|
||||
"@eslint/js": "9.38.0",
|
||||
"@eslint/plugin-kit": "^0.4.0",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
@@ -5849,9 +5850,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -6122,9 +6123,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ts-jest": {
|
||||
"version": "29.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.4.tgz",
|
||||
"integrity": "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==",
|
||||
"version": "29.4.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz",
|
||||
"integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6134,7 +6135,7 @@
|
||||
"json5": "^2.2.3",
|
||||
"lodash.memoize": "^4.1.2",
|
||||
"make-error": "^1.3.6",
|
||||
"semver": "^7.7.2",
|
||||
"semver": "^7.7.3",
|
||||
"type-fest": "^4.41.0",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
@@ -6305,16 +6306,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.0.tgz",
|
||||
"integrity": "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz",
|
||||
"integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.46.0",
|
||||
"@typescript-eslint/parser": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0",
|
||||
"@typescript-eslint/utils": "8.46.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.46.2",
|
||||
"@typescript-eslint/parser": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2",
|
||||
"@typescript-eslint/utils": "8.46.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -6343,9 +6344,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
|
||||
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -7163,20 +7164,20 @@
|
||||
"dev": true
|
||||
},
|
||||
"@eslint/config-array": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
|
||||
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint/object-schema": "^2.1.6",
|
||||
"@eslint/object-schema": "^2.1.7",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"@eslint/config-helpers": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
|
||||
"integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
|
||||
"integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint/core": "^0.16.0"
|
||||
@@ -7232,15 +7233,15 @@
|
||||
}
|
||||
},
|
||||
"@eslint/js": {
|
||||
"version": "9.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
|
||||
"integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
|
||||
"integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
|
||||
"dev": true
|
||||
},
|
||||
"@eslint/object-schema": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
|
||||
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
||||
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
||||
"dev": true
|
||||
},
|
||||
"@eslint/plugin-kit": {
|
||||
@@ -8056,12 +8057,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "24.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz",
|
||||
"integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==",
|
||||
"version": "24.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"undici-types": "~7.14.0"
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
@@ -8106,16 +8107,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz",
|
||||
"integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
|
||||
"integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/type-utils": "8.46.0",
|
||||
"@typescript-eslint/utils": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/type-utils": "8.46.2",
|
||||
"@typescript-eslint/utils": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -8131,75 +8132,75 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz",
|
||||
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz",
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2",
|
||||
"debug": "^4.3.4"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/project-service": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz",
|
||||
"integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
|
||||
"integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.46.0",
|
||||
"@typescript-eslint/types": "^8.46.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.46.2",
|
||||
"@typescript-eslint/types": "^8.46.2",
|
||||
"debug": "^4.3.4"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz",
|
||||
"integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz",
|
||||
"integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0"
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz",
|
||||
"integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz",
|
||||
"integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@typescript-eslint/type-utils": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz",
|
||||
"integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz",
|
||||
"integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0",
|
||||
"@typescript-eslint/utils": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2",
|
||||
"@typescript-eslint/utils": "8.46.2",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz",
|
||||
"integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz",
|
||||
"integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz",
|
||||
"integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz",
|
||||
"integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/project-service": "8.46.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0",
|
||||
"@typescript-eslint/project-service": "8.46.2",
|
||||
"@typescript-eslint/tsconfig-utils": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -8229,24 +8230,24 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/utils": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz",
|
||||
"integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz",
|
||||
"integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0"
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz",
|
||||
"integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz",
|
||||
"integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -8742,24 +8743,23 @@
|
||||
"dev": true
|
||||
},
|
||||
"eslint": {
|
||||
"version": "9.37.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
|
||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.0",
|
||||
"@eslint/config-helpers": "^0.4.0",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-helpers": "^0.4.1",
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.37.0",
|
||||
"@eslint/js": "9.38.0",
|
||||
"@eslint/plugin-kit": "^0.4.0",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
@@ -10970,9 +10970,9 @@
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
@@ -11169,9 +11169,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"ts-jest": {
|
||||
"version": "29.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.4.tgz",
|
||||
"integrity": "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==",
|
||||
"version": "29.4.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz",
|
||||
"integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bs-logger": "^0.2.6",
|
||||
@@ -11180,7 +11180,7 @@
|
||||
"json5": "^2.2.3",
|
||||
"lodash.memoize": "^4.1.2",
|
||||
"make-error": "^1.3.6",
|
||||
"semver": "^7.7.2",
|
||||
"semver": "^7.7.3",
|
||||
"type-fest": "^4.41.0",
|
||||
"yargs-parser": "^21.1.1"
|
||||
}
|
||||
@@ -11259,15 +11259,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"typescript-eslint": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.0.tgz",
|
||||
"integrity": "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz",
|
||||
"integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/eslint-plugin": "8.46.0",
|
||||
"@typescript-eslint/parser": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0",
|
||||
"@typescript-eslint/utils": "8.46.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.46.2",
|
||||
"@typescript-eslint/parser": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2",
|
||||
"@typescript-eslint/utils": "8.46.2"
|
||||
}
|
||||
},
|
||||
"uglify-js": {
|
||||
@@ -11278,9 +11278,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
|
||||
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true
|
||||
},
|
||||
"unix-dgram": {
|
||||
|
||||
@@ -33,22 +33,22 @@
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^24.7.1",
|
||||
"@types/node": "^24.9.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.0",
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"eslint": "^9.37.0",
|
||||
"@typescript-eslint/parser": "^8.46.2",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"globals": "^16.4.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-jest": "^29.4.4",
|
||||
"ts-jest": "^29.4.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.0"
|
||||
"typescript-eslint": "^8.46.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.4",
|
||||
|
||||
@@ -30,6 +30,7 @@ from typing import Any, Optional, TYPE_CHECKING, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from flask import current_app
|
||||
from flask_babel import gettext as __
|
||||
|
||||
from superset.common.chart_data import ChartDataResultFormat
|
||||
@@ -340,7 +341,15 @@ def apply_client_processing( # noqa: C901
|
||||
if query["result_format"] == ChartDataResultFormat.JSON:
|
||||
df = pd.DataFrame.from_dict(data)
|
||||
elif query["result_format"] == ChartDataResultFormat.CSV:
|
||||
df = pd.read_csv(StringIO(data))
|
||||
# Use custom NA values configuration for
|
||||
# reports to avoid unwanted conversions
|
||||
# This allows users to control which values should be treated as null/NA
|
||||
na_values = current_app.config["REPORTS_CSV_NA_NAMES"]
|
||||
df = pd.read_csv(
|
||||
StringIO(data),
|
||||
keep_default_na=na_values is None,
|
||||
na_values=na_values,
|
||||
)
|
||||
|
||||
# convert all columns to verbose (label) name
|
||||
if datasource:
|
||||
|
||||
+10
-1
@@ -972,7 +972,7 @@ CORS_OPTIONS: dict[Any, Any] = {
|
||||
# Disabling this option is not recommended for security reasons. If you wish to allow
|
||||
# valid safe elements that are not included in the default sanitization schema, use the
|
||||
# HTML_SANITIZATION_SCHEMA_EXTENSIONS configuration.
|
||||
HTML_SANITIZATION = False
|
||||
HTML_SANITIZATION = True
|
||||
|
||||
# Use this configuration to extend the HTML sanitization schema.
|
||||
# By default we use the GitHub schema defined in
|
||||
@@ -1354,6 +1354,15 @@ ALLOWED_USER_CSV_SCHEMA_FUNC = allowed_schemas_for_csv_upload
|
||||
# Values that should be treated as nulls for the csv uploads.
|
||||
CSV_DEFAULT_NA_NAMES = list(STR_NA_VALUES)
|
||||
|
||||
# Values that should be treated as nulls for scheduled reports CSV processing.
|
||||
# If not set or None, defaults to standard pandas NA handling behavior.
|
||||
# Set to a custom list to control which values should be treated as null.
|
||||
# Examples:
|
||||
# REPORTS_CSV_NA_NAMES = None # Use default pandas NA handling (backwards compatible)
|
||||
# REPORTS_CSV_NA_NAMES = [] # Disable all automatic NA conversion
|
||||
# REPORTS_CSV_NA_NAMES = ["", "NULL", "null"] # Only treat these specific values as NA
|
||||
REPORTS_CSV_NA_NAMES: list[str] | None = None
|
||||
|
||||
# Chunk size for reading CSV files during uploads
|
||||
# Smaller values use less memory but may be slower for large files
|
||||
READ_CSV_CHUNK_SIZE = 1000
|
||||
|
||||
@@ -59,6 +59,7 @@ class GuestUser(AnonymousUserMixin):
|
||||
"""
|
||||
|
||||
is_guest_user = True
|
||||
active = True
|
||||
|
||||
@property
|
||||
def is_authenticated(self) -> bool:
|
||||
|
||||
@@ -22,6 +22,7 @@ from typing import Any
|
||||
|
||||
from celery import Task
|
||||
from celery.exceptions import SoftTimeLimitExceeded
|
||||
from celery.signals import task_failure
|
||||
from flask import current_app
|
||||
|
||||
from superset import is_feature_enabled
|
||||
@@ -41,8 +42,32 @@ from superset.utils.log import get_logger_from_status
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@celery_app.task(name="reports.scheduler")
|
||||
def scheduler() -> None:
|
||||
@task_failure.connect
|
||||
def log_task_failure( # pylint: disable=unused-argument
|
||||
sender: Task | None = None,
|
||||
task_id: str | None = None,
|
||||
exception: Exception | None = None,
|
||||
args: tuple[Any, ...] | None = None,
|
||||
kwargs: dict[str, Any] | None = None,
|
||||
traceback: Any = None,
|
||||
einfo: Any = None,
|
||||
**kw: Any,
|
||||
) -> None:
|
||||
task_name = sender.name if sender else "Unknown"
|
||||
logger.exception("Celery task %s failed: %s", task_name, exception, exc_info=einfo)
|
||||
|
||||
|
||||
@celery_app.task(
|
||||
name="reports.scheduler",
|
||||
bind=True,
|
||||
autoretry_for=(Exception,),
|
||||
retry_kwargs={
|
||||
"max_retries": 3,
|
||||
"countdown": 60,
|
||||
}, # Retry up to 3 times, wait 60s between
|
||||
retry_backoff=True, # exponential backoff
|
||||
)
|
||||
def scheduler(self: Task) -> None: # pylint: disable=unused-argument
|
||||
"""
|
||||
Celery beat main scheduler for reports
|
||||
"""
|
||||
|
||||
@@ -550,15 +550,9 @@ class ThemeRestApi(BaseSupersetModelRestApi):
|
||||
|
||||
overwrite = request.form.get("overwrite") == "true"
|
||||
|
||||
try:
|
||||
ImportThemesCommand(contents, overwrite=overwrite).run()
|
||||
return self.response(200, message="Theme imported successfully")
|
||||
except ValidationError as err:
|
||||
logger.exception("Import themes validation error")
|
||||
return self.response_400(message=str(err))
|
||||
except Exception as ex:
|
||||
logger.exception("Unexpected error importing themes")
|
||||
return self.response_422(message=str(ex))
|
||||
command = ImportThemesCommand(contents, overwrite=overwrite)
|
||||
command.run()
|
||||
return self.response(200, message="Theme imported successfully")
|
||||
|
||||
@expose("/<int:pk>/set_system_default", methods=("PUT",))
|
||||
@protect()
|
||||
|
||||
@@ -751,10 +751,10 @@ msgstr "AQE"
|
||||
msgid "AUG"
|
||||
msgstr "اغسطس"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "هامش عنوان المحور"
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "موضع عنوان المحور"
|
||||
|
||||
msgid "About"
|
||||
@@ -13452,7 +13452,7 @@ msgstr "اكتب وصفًا لاستعلامك"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "اكتب قالب المقاود لعرض البيانات"
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "هامش عنوان المحور X"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13501,7 +13501,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "حدود Y 2"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "هامش عنوان المحور Y"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -780,10 +780,10 @@ msgstr "AQE"
|
||||
msgid "AUG"
|
||||
msgstr "AGO"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "MARGE DEL TÍTOL DE L'EIX"
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "POSICIÓ DEL TÍTOL DE L'EIX"
|
||||
|
||||
msgid "About"
|
||||
@@ -13231,7 +13231,7 @@ msgstr "Escriu una descripció per la teva consulta"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "Escriu una plantilla handlebars per renderitzar les dades"
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "MARGE DEL TÍTOL DE L'EIX X"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13279,7 +13279,7 @@ msgstr "XYZ"
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Límits Y 2"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "MARGE DEL TÍTOL DE L'EIX Y"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -794,10 +794,10 @@ msgstr "AQE"
|
||||
msgid "AUG"
|
||||
msgstr "AUG"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "ABSTAND DES ACHSENTITELS"
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "Y-ACHSE TITEL POSITION"
|
||||
|
||||
msgid "About"
|
||||
@@ -13868,7 +13868,7 @@ msgstr "Beschreibung Ihrer Anfrage"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "Handlebars-Template zur Darstellung der Daten verfassen"
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "X AXIS TITLE MARGIN"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13917,7 +13917,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Y 2 Grenzen"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "Y-ACHSE TITEL RAND"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -689,10 +689,10 @@ msgstr ""
|
||||
msgid "AUG"
|
||||
msgstr ""
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr ""
|
||||
|
||||
msgid "About"
|
||||
@@ -12368,7 +12368,7 @@ msgstr ""
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr ""
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -12416,7 +12416,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -689,10 +689,10 @@ msgstr "AQE"
|
||||
msgid "AUG"
|
||||
msgstr "AGO"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "MARGEN DEL TÍTULO DEL EJE"
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "POSICIÓN DEL TÍTULO DEL EJE"
|
||||
|
||||
msgid "About"
|
||||
@@ -12368,7 +12368,7 @@ msgstr "Escribe una descripción para tu consulta"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "Elabora una plantilla de Handlebars para renderizar los datos"
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "MARGEN DEL TÍTULO DEL EJE X"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -12416,7 +12416,7 @@ msgstr "XYZ"
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Límites de Y 2"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "MARGEN DEL TÍTULO DEL EJE Y"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -767,10 +767,10 @@ msgstr ""
|
||||
msgid "AUG"
|
||||
msgstr "آگوست"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "حاشیه عنوان محور"
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "محل عنوان محور"
|
||||
|
||||
msgid "About"
|
||||
@@ -13425,7 +13425,7 @@ msgstr "توضیحی برای کوئری خود بنویسید."
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "یک الگوی هندلبارز برای نمایش دادهها بنویسید."
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "حاشیه عنوان محور ایکس"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13474,7 +13474,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "بازههای Y ۲"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "حاشیه عنوان محور Y"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -852,10 +852,10 @@ msgstr "AQE"
|
||||
msgid "AUG"
|
||||
msgstr "AUG"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "MARGE DU TITRE DE L'AXE"
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "POSITION DU TITRE DE L'AXE"
|
||||
|
||||
msgid "About"
|
||||
@@ -16095,7 +16095,7 @@ msgstr "Écrire une description pour votre requête"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "Écrire un modèle handlebar pour afficher les données"
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "MARGE DU TITRE DE L'AXE DES ABCISSES"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -16148,7 +16148,7 @@ msgstr "XYZ"
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Limites ordonnées 2"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "MARGE DU TITRE DE L'AXE DES ORDONNÉES"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -718,11 +718,11 @@ msgstr ""
|
||||
msgid "AUG"
|
||||
msgstr ""
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "Testa la Connessione"
|
||||
|
||||
msgid "About"
|
||||
@@ -13284,7 +13284,7 @@ msgstr ""
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr ""
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13336,7 +13336,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -725,10 +725,10 @@ msgstr "AQE"
|
||||
msgid "AUG"
|
||||
msgstr "8月"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "軸のタイトルの余白"
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "軸タイトルの位置"
|
||||
|
||||
msgid "About"
|
||||
@@ -12622,7 +12622,7 @@ msgstr "クエリの説明を書いてください"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "データをレンダリングするハンドルバー テンプレートを作成する"
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "X 軸のタイトルマージン"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -12670,7 +12670,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Y 2 バウンド"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "Y 軸のタイトルマージン"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -715,10 +715,10 @@ msgstr ""
|
||||
msgid "AUG"
|
||||
msgstr ""
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr ""
|
||||
|
||||
msgid "About"
|
||||
@@ -13159,7 +13159,7 @@ msgstr ""
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr ""
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13209,7 +13209,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -695,10 +695,10 @@ msgstr ""
|
||||
msgid "AUG"
|
||||
msgstr ""
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr ""
|
||||
|
||||
msgid "About"
|
||||
@@ -12353,7 +12353,7 @@ msgstr ""
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr ""
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -12401,7 +12401,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -757,10 +757,10 @@ msgstr "AQE"
|
||||
msgid "AUG"
|
||||
msgstr "AUG"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "AXIS TITEL MARGIN"
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "AXIS TITEL POSITIE"
|
||||
|
||||
msgid "About"
|
||||
@@ -13691,7 +13691,7 @@ msgstr "Schrijf een omschrijving voor uw query"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "Schrijf een handlebars sjabloon om de gegevens weer te geven"
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "X AXIS TITEL MARGE"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13740,7 +13740,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Y 2 grenzen"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "Y AXIS TITEL MARGE"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -796,11 +796,11 @@ msgstr "AQE"
|
||||
msgid "AUG"
|
||||
msgstr "SIE"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "MARGINES TYTUŁU OSI"
|
||||
|
||||
#, fuzzy
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "POZYCJA TYTUŁU OSI"
|
||||
|
||||
msgid "About"
|
||||
@@ -14280,7 +14280,7 @@ msgstr "Napisz opis swojego zapytania"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "Napisz szablon handlebars do renderowania danych"
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "MARGINES TYTUŁU OSI X"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -14335,7 +14335,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Granice osi Y 2"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "MARGINES TYTUŁU OSI Y"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -726,10 +726,10 @@ msgstr ""
|
||||
msgid "AUG"
|
||||
msgstr ""
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr ""
|
||||
|
||||
msgid "About"
|
||||
@@ -13486,7 +13486,7 @@ msgstr "Escreva uma descrição para sua consulta"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr ""
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13537,7 +13537,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -769,10 +769,10 @@ msgstr "AQE"
|
||||
msgid "AUG"
|
||||
msgstr "AGO"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "MARGEM DO EIXO DO TÍTULO "
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "POSIÇÃO DO EIXO DO TÍTULO"
|
||||
|
||||
msgid "About"
|
||||
@@ -13862,7 +13862,7 @@ msgstr "Escreva uma descrição para sua consulta"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "Escreva um modelo de guidão para renderizar os dados"
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "MARGEM DO TÍTULO DO EIXO X"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13912,7 +13912,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Y 2 limites"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "MARGEM DO TÍTULO DO EIXO Y"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -799,10 +799,10 @@ msgstr "Асинхронные запросы"
|
||||
msgid "AUG"
|
||||
msgstr "АВГ"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "Отступ заголовка оси"
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "Положение заголовка оси"
|
||||
|
||||
msgid "About"
|
||||
@@ -14035,7 +14035,7 @@ msgstr "Заполните описание к вашему запросу"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "Напишите шаблон Handlebars для отображения данных"
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "Отступ заголовка оси X"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -14089,7 +14089,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Границы оси Y 2"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "Отступ заголовка оси Y"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -693,10 +693,10 @@ msgstr ""
|
||||
msgid "AUG"
|
||||
msgstr ""
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr ""
|
||||
|
||||
msgid "About"
|
||||
@@ -12502,7 +12502,7 @@ msgstr ""
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr ""
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -12550,7 +12550,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -787,10 +787,10 @@ msgstr "AQE"
|
||||
msgid "AUG"
|
||||
msgstr "AVG"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "OBROBA OZNAKE OSI"
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "POLOŽAJ OZNAKE OSI"
|
||||
|
||||
msgid "About"
|
||||
@@ -13457,7 +13457,7 @@ msgstr "Dodajte opis vaše poizvedbe"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "Napišite Handlebars-predlogo za prikaz podatkov"
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "OBROBA NASLOVA X-OSI"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13506,7 +13506,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Meje Y-osi 2"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "OBROBA NASLOVA Y-OSI"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -691,10 +691,10 @@ msgstr ""
|
||||
msgid "AUG"
|
||||
msgstr "AĞU"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr ""
|
||||
|
||||
msgid "About"
|
||||
@@ -12543,7 +12543,7 @@ msgstr ""
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr ""
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -12591,7 +12591,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -759,10 +759,10 @@ msgstr "AQE"
|
||||
msgid "AUG"
|
||||
msgstr "Серпень"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "ЗАВДАННЯ ВІСІВ"
|
||||
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "Позиція заголовка вісь"
|
||||
|
||||
msgid "About"
|
||||
@@ -13662,7 +13662,7 @@ msgstr "Напишіть опис свого запиту"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr "Напишіть шаблон ручки для надання даних"
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr ""
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13712,7 +13712,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Y 2 межі"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "Y Exis title Margin"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -730,11 +730,11 @@ msgstr "异步执行查询"
|
||||
msgid "AUG"
|
||||
msgstr "八月"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "轴标题边距"
|
||||
|
||||
#, fuzzy
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "轴标题的位置"
|
||||
|
||||
msgid "About"
|
||||
@@ -13364,7 +13364,7 @@ msgstr "为您的查询写一段描述"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr ""
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "X 轴标题边距"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13416,7 +13416,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Y 界限"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "Y 轴标题边距"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -729,11 +729,11 @@ msgstr "異步執行查詢"
|
||||
msgid "AUG"
|
||||
msgstr "八月"
|
||||
|
||||
msgid "AXIS TITLE MARGIN"
|
||||
msgid "Axis title margin"
|
||||
msgstr "軸標題邊距"
|
||||
|
||||
#, fuzzy
|
||||
msgid "AXIS TITLE POSITION"
|
||||
msgid "Axis title position"
|
||||
msgstr "軸標題的位置"
|
||||
|
||||
msgid "About"
|
||||
@@ -13378,7 +13378,7 @@ msgstr "為您的查詢寫一段描述"
|
||||
msgid "Write a handlebars template to render the data"
|
||||
msgstr ""
|
||||
|
||||
msgid "X AXIS TITLE MARGIN"
|
||||
msgid "X axis title margin"
|
||||
msgstr "X 軸標題邊距"
|
||||
|
||||
msgid "X Axis"
|
||||
@@ -13430,7 +13430,7 @@ msgstr ""
|
||||
msgid "Y 2 bounds"
|
||||
msgstr "Y 界限"
|
||||
|
||||
msgid "Y AXIS TITLE MARGIN"
|
||||
msgid "Y axis title margin"
|
||||
msgstr "Y 軸標題邊距"
|
||||
|
||||
msgid "Y Axis"
|
||||
|
||||
@@ -120,15 +120,26 @@ def take_tiled_screenshot(
|
||||
dashboard_top,
|
||||
)
|
||||
|
||||
# Calculate number of tiles needed
|
||||
num_tiles = max(1, (dashboard_height + viewport_height - 1) // viewport_height)
|
||||
# Get actual viewport height to ensure we don't skip content
|
||||
actual_viewport_height = page.viewport_size["height"]
|
||||
tile_height = min(viewport_height, actual_viewport_height)
|
||||
|
||||
logger.info(
|
||||
"Viewport: configured=%s, actual=%s, using tile_height=%s",
|
||||
viewport_height,
|
||||
actual_viewport_height,
|
||||
tile_height,
|
||||
)
|
||||
|
||||
# Calculate number of tiles needed based on actual tile height
|
||||
num_tiles = max(1, (dashboard_height + tile_height - 1) // tile_height)
|
||||
logger.info("Taking %s screenshot tiles", num_tiles)
|
||||
|
||||
screenshot_tiles = []
|
||||
|
||||
for i in range(num_tiles):
|
||||
# Calculate scroll position to show this tile's content
|
||||
scroll_y = dashboard_top + (i * viewport_height)
|
||||
scroll_y = dashboard_top + (i * tile_height)
|
||||
|
||||
# Scroll the window to the desired position
|
||||
page.evaluate(f"window.scrollTo(0, {scroll_y})")
|
||||
@@ -139,29 +150,65 @@ def take_tiled_screenshot(
|
||||
# Wait for scroll to settle and content to load
|
||||
page.wait_for_timeout(2000) # 2 second wait per tile
|
||||
|
||||
# Get the current element position after scroll
|
||||
current_element_box = page.evaluate(f"""() => {{
|
||||
# Get the current element position after scroll and viewport size
|
||||
viewport_info = page.evaluate(f"""() => {{
|
||||
const el = document.querySelector(".{element_name}");
|
||||
const rect = el.getBoundingClientRect();
|
||||
return {{
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
elementX: rect.left,
|
||||
elementY: rect.top,
|
||||
elementWidth: rect.width,
|
||||
elementHeight: rect.height,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight
|
||||
}};
|
||||
}}""")
|
||||
|
||||
# Calculate what portion of the element we want to capture for this tile
|
||||
tile_start_in_element = i * viewport_height
|
||||
remaining_content = dashboard_height - tile_start_in_element
|
||||
tile_content_height = min(viewport_height, remaining_content)
|
||||
# Ensure clip coordinates are within viewport bounds
|
||||
# If element.top is negative, it's scrolled above viewport - start from y=0
|
||||
clip_y = max(0, viewport_info["elementY"])
|
||||
# If element.left is negative, start from x=0
|
||||
clip_x = max(0, viewport_info["elementX"])
|
||||
|
||||
# Calculate clip dimensions - capture what's visible of the element
|
||||
# Handle elements scrolled above viewport: if elementY is negative,
|
||||
# only the portion from (elementY + elementHeight) is visible
|
||||
if viewport_info["elementY"] < 0:
|
||||
# Element extends from above viewport - calculate visible portion
|
||||
visible_height = (
|
||||
viewport_info["elementY"] + viewport_info["elementHeight"]
|
||||
)
|
||||
clip_height = min(visible_height, viewport_info["viewportHeight"])
|
||||
else:
|
||||
# Element is within viewport
|
||||
clip_height = min(
|
||||
viewport_info["elementHeight"],
|
||||
viewport_info["viewportHeight"] - clip_y,
|
||||
)
|
||||
|
||||
clip_width = min(
|
||||
viewport_info["elementWidth"], viewport_info["viewportWidth"] - clip_x
|
||||
)
|
||||
|
||||
# Validate clip region before taking screenshot
|
||||
if clip_width <= 0 or clip_height <= 0:
|
||||
logger.warning(
|
||||
"Skipping tile %s/%s - invalid clip dimensions: %sx%s at (%s, %s)",
|
||||
i + 1,
|
||||
num_tiles,
|
||||
clip_width,
|
||||
clip_height,
|
||||
clip_x,
|
||||
clip_y,
|
||||
)
|
||||
continue
|
||||
|
||||
# Clip to capture only the current tile portion of the element
|
||||
clip = {
|
||||
"x": current_element_box["x"],
|
||||
"y": current_element_box["y"],
|
||||
"width": current_element_box["width"],
|
||||
"height": min(tile_content_height, current_element_box["height"]),
|
||||
"x": clip_x,
|
||||
"y": clip_y,
|
||||
"width": clip_width,
|
||||
"height": clip_height,
|
||||
}
|
||||
|
||||
# Take screenshot with clipping to capture only this tile's content
|
||||
|
||||
@@ -216,6 +216,13 @@ class WebDriverPlaywright(WebDriverProxy):
|
||||
|
||||
return error_messages
|
||||
|
||||
@staticmethod
|
||||
def _get_screenshot(page: Page, element: Locator, element_name: str) -> bytes:
|
||||
if element_name == "standalone":
|
||||
return page.screenshot(full_page=True)
|
||||
else:
|
||||
return element.screenshot()
|
||||
|
||||
def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # noqa: C901
|
||||
self, url: str, element_name: str, user: User
|
||||
) -> bytes | None:
|
||||
@@ -363,11 +370,18 @@ class WebDriverPlaywright(WebDriverProxy):
|
||||
"falling back to standard screenshot"
|
||||
)
|
||||
)
|
||||
img = element.screenshot()
|
||||
img = WebDriverPlaywright._get_screenshot(
|
||||
page, element, element_name
|
||||
)
|
||||
else:
|
||||
img = element.screenshot()
|
||||
img = WebDriverPlaywright._get_screenshot(
|
||||
page, element, element_name
|
||||
)
|
||||
else:
|
||||
img = element.screenshot()
|
||||
img = WebDriverPlaywright._get_screenshot(
|
||||
page, element, element_name
|
||||
)
|
||||
|
||||
except PlaywrightTimeout:
|
||||
# raise again for the finally block, but handled above
|
||||
pass
|
||||
|
||||
+13
-4
@@ -46,7 +46,6 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
from werkzeug.utils import safe_join
|
||||
|
||||
from superset import (
|
||||
appbuilder,
|
||||
db,
|
||||
event_logger,
|
||||
is_feature_enabled,
|
||||
@@ -80,6 +79,7 @@ from superset.models.slice import Slice
|
||||
from superset.models.sql_lab import Query
|
||||
from superset.models.user_attributes import UserAttribute
|
||||
from superset.superset_typing import FlaskResponse
|
||||
from superset.tasks.utils import get_current_user
|
||||
from superset.utils import core as utils, json
|
||||
from superset.utils.cache import etag_cache
|
||||
from superset.utils.core import (
|
||||
@@ -108,6 +108,7 @@ from superset.views.utils import (
|
||||
get_form_data,
|
||||
get_viz,
|
||||
loads_request_json,
|
||||
redirect_to_login,
|
||||
sanitize_datasource_data,
|
||||
)
|
||||
from superset.viz import BaseViz
|
||||
@@ -765,13 +766,21 @@ class Superset(BaseSupersetView):
|
||||
dashboard = Dashboard.get(dashboard_id_or_slug)
|
||||
|
||||
if not dashboard:
|
||||
if not get_current_user():
|
||||
return redirect_to_login()
|
||||
abort(404)
|
||||
|
||||
# Redirect anonymous users to login for unpublished dashboards,
|
||||
# in the edge case where a dataset has been shared with public
|
||||
if not get_current_user() and not dashboard.published:
|
||||
return redirect_to_login()
|
||||
|
||||
try:
|
||||
dashboard.raise_for_access()
|
||||
except SupersetSecurityException:
|
||||
# Return 404 to avoid revealing dashboard existence
|
||||
return Response(status=404)
|
||||
if not get_current_user():
|
||||
return redirect_to_login()
|
||||
abort(404)
|
||||
add_extra_log_payload(
|
||||
dashboard_id=dashboard.id,
|
||||
dashboard_version="v2",
|
||||
@@ -882,7 +891,7 @@ class Superset(BaseSupersetView):
|
||||
def welcome(self) -> FlaskResponse:
|
||||
"""Personalized welcome page"""
|
||||
if not g.user or not get_user_id():
|
||||
return redirect(appbuilder.get_url_for_login)
|
||||
return redirect_to_login()
|
||||
|
||||
if welcome_dashboard_id := (
|
||||
db.session.query(UserAttribute.welcome_dashboard_id)
|
||||
|
||||
@@ -25,7 +25,6 @@ from typing import Any, Callable, cast
|
||||
|
||||
from flask import (
|
||||
Flask,
|
||||
redirect,
|
||||
request,
|
||||
Response,
|
||||
send_file,
|
||||
@@ -34,7 +33,6 @@ from flask_wtf.csrf import CSRFError
|
||||
from sqlalchemy import exc
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from superset import appbuilder
|
||||
from superset.commands.exceptions import CommandException, CommandInvalidError
|
||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||
from superset.exceptions import (
|
||||
@@ -46,6 +44,7 @@ from superset.exceptions import (
|
||||
from superset.superset_typing import FlaskResponse
|
||||
from superset.utils import core as utils, json
|
||||
from superset.utils.log import get_logger_from_status
|
||||
from superset.views.utils import redirect_to_login
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from superset.views.base import BaseSupersetView
|
||||
@@ -153,7 +152,7 @@ def set_app_error_handlers(app: Flask) -> None: # noqa: C901
|
||||
if request.is_json:
|
||||
return show_http_exception(ex)
|
||||
|
||||
return redirect(appbuilder.get_url_for_login)
|
||||
return redirect_to_login()
|
||||
|
||||
@app.errorhandler(HTTPException)
|
||||
def show_http_exception(ex: HTTPException) -> FlaskResponse:
|
||||
@@ -165,7 +164,11 @@ def set_app_error_handlers(app: Flask) -> None: # noqa: C901
|
||||
and ex.code in {404, 500}
|
||||
):
|
||||
path = files("superset") / f"static/assets/{ex.code}.html"
|
||||
return send_file(path, max_age=0), ex.code
|
||||
# Try to serve HTML file; fall back to JSON if not built
|
||||
try:
|
||||
return send_file(path, max_age=0), ex.code
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
return json_error_response(
|
||||
[
|
||||
@@ -189,7 +192,11 @@ def set_app_error_handlers(app: Flask) -> None: # noqa: C901
|
||||
|
||||
if "text/html" in request.accept_mimetypes and not app.config["DEBUG"]:
|
||||
path = files("superset") / "static/assets/500.html"
|
||||
return send_file(path, max_age=0), 500
|
||||
# Try to serve HTML file; fall back to JSON if not built
|
||||
try:
|
||||
return send_file(path, max_age=0), 500
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
extra = ex.normalized_messages() if isinstance(ex, CommandInvalidError) else {}
|
||||
return json_error_response(
|
||||
|
||||
+31
-3
@@ -19,16 +19,17 @@ import logging
|
||||
from collections import defaultdict
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, DefaultDict, Optional, Union
|
||||
from urllib import parse
|
||||
|
||||
import msgpack
|
||||
import pyarrow as pa
|
||||
from flask import current_app as app, g, has_request_context, request
|
||||
from flask import current_app as app, g, has_request_context, redirect, request
|
||||
from flask_appbuilder.security.sqla import models as ab_models
|
||||
from flask_appbuilder.security.sqla.models import User
|
||||
from flask_babel import _
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
|
||||
from superset import dataframe, db, result_set, viz
|
||||
from superset import appbuilder, dataframe, db, result_set, viz
|
||||
from superset.common.db_query_status import QueryStatus
|
||||
from superset.daos.datasource import DatasourceDAO
|
||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||
@@ -44,7 +45,7 @@ from superset.models.core import Database
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.slice import Slice
|
||||
from superset.models.sql_lab import Query
|
||||
from superset.superset_typing import FormData
|
||||
from superset.superset_typing import FlaskResponse, FormData
|
||||
from superset.utils import json
|
||||
from superset.utils.core import DatasourceType
|
||||
from superset.utils.decorators import stats_timing
|
||||
@@ -58,6 +59,33 @@ if not feature_flag_manager.is_feature_enabled("ENABLE_JAVASCRIPT_CONTROLS"):
|
||||
REJECTED_FORM_DATA_KEYS = ["js_tooltip", "js_onclick_href", "js_data_mutator"]
|
||||
|
||||
|
||||
def redirect_to_login(next_target: str | None = None) -> FlaskResponse:
|
||||
"""Return a redirect response to the login view, preserving target URL.
|
||||
|
||||
When ``next_target`` is ``None`` the current request path (including query
|
||||
string) is used, provided a request context is available. The resulting URL
|
||||
always remains relative, mirroring Flask-AppBuilder expectations.
|
||||
"""
|
||||
|
||||
login_url = appbuilder.get_url_for_login
|
||||
parsed = parse.urlparse(login_url)
|
||||
query = parse.parse_qs(parsed.query, keep_blank_values=True)
|
||||
|
||||
target = next_target
|
||||
if target is None and has_request_context():
|
||||
if request.query_string:
|
||||
target = request.full_path.rstrip("?")
|
||||
else:
|
||||
target = request.path
|
||||
|
||||
if target:
|
||||
query["next"] = [target]
|
||||
|
||||
encoded_query = parse.urlencode(query, doseq=True)
|
||||
redirect_url = parse.urlunparse(parsed._replace(query=encoded_query))
|
||||
return redirect(redirect_url)
|
||||
|
||||
|
||||
def sanitize_datasource_data(datasource_data: dict[str, Any]) -> dict[str, Any]:
|
||||
if datasource_data:
|
||||
datasource_database = datasource_data.get("database")
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"""Unit tests for Superset"""
|
||||
|
||||
import re
|
||||
import unittest
|
||||
from random import random
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import pytest
|
||||
from flask import Response, escape, url_for
|
||||
@@ -52,7 +52,7 @@ from tests.integration_tests.fixtures.world_bank_dashboard import (
|
||||
load_world_bank_data, # noqa: F401
|
||||
)
|
||||
|
||||
from .base_tests import SupersetTestCase
|
||||
from .base_tests import DEFAULT_PASSWORD, SupersetTestCase
|
||||
|
||||
|
||||
class TestDashboard(SupersetTestCase):
|
||||
@@ -186,6 +186,72 @@ class TestDashboard(SupersetTestCase):
|
||||
# Cleanup
|
||||
self.revoke_public_access_to_table(table)
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"load_energy_table_with_slice",
|
||||
"load_dashboard",
|
||||
)
|
||||
def test_anonymous_user_redirects_to_login_with_next(self):
|
||||
self.logout()
|
||||
target_path = f"/superset/dashboard/{pytest.hidden_dash_slug}/"
|
||||
|
||||
response = self.client.get(target_path, follow_redirects=False)
|
||||
|
||||
assert response.status_code == 302
|
||||
|
||||
redirect_location = response.headers["Location"]
|
||||
parsed = urlparse(redirect_location)
|
||||
assert parsed.path.rstrip("/") == "/login"
|
||||
|
||||
next_values = parse_qs(parsed.query).get("next")
|
||||
assert next_values is not None
|
||||
assert next_values[0].endswith(target_path)
|
||||
|
||||
login_target = (
|
||||
f"{parsed.path}{'?' + parsed.query if parsed.query else ''}"
|
||||
if parsed.scheme or parsed.netloc
|
||||
else redirect_location
|
||||
)
|
||||
|
||||
login_response = self.client.post(
|
||||
login_target,
|
||||
data={"username": ADMIN_USERNAME, "password": DEFAULT_PASSWORD},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert login_response.status_code == 302
|
||||
assert login_response.headers["Location"].endswith(target_path)
|
||||
|
||||
target_response: Response = self.client.get(target_path, follow_redirects=False)
|
||||
assert target_response.status_code == 200
|
||||
|
||||
def test_anonymous_user_redirects_to_login_for_missing_dashboard(self):
|
||||
self.logout()
|
||||
target_path = "/superset/dashboard/nonexistent-dashboard/"
|
||||
|
||||
response = self.client.get(target_path, follow_redirects=False)
|
||||
|
||||
assert response.status_code == 302
|
||||
parsed = urlparse(response.headers["Location"])
|
||||
assert parsed.path.rstrip("/") == "/login"
|
||||
next_values = parse_qs(parsed.query).get("next")
|
||||
assert next_values is not None
|
||||
assert next_values[0].endswith(target_path)
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"public_role_like_gamma",
|
||||
"load_energy_table_with_slice",
|
||||
"load_dashboard",
|
||||
)
|
||||
def test_authenticated_user_without_access_gets_404(self):
|
||||
self.login(GAMMA_USERNAME)
|
||||
target_path = f"/superset/dashboard/{pytest.hidden_dash_slug}/"
|
||||
|
||||
response = self.client.get(
|
||||
target_path,
|
||||
follow_redirects=False,
|
||||
headers={"Accept": "text/html"},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"public_role_like_gamma",
|
||||
"load_energy_table_with_slice",
|
||||
@@ -248,7 +314,3 @@ class TestDashboard(SupersetTestCase):
|
||||
db.session.commit()
|
||||
|
||||
assert f"/superset/dashboard/{slug}/" not in resp
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -108,7 +108,7 @@ class TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity):
|
||||
|
||||
# act
|
||||
response = self.get_dashboard_view_response(dashboard_to_access)
|
||||
assert response.status_code == 404
|
||||
assert response.status_code == 404 # Authenticated users without access get 404
|
||||
|
||||
request_payload = get_query_context("birth_names")
|
||||
rv = self.post_assert_metric(CHART_DATA_URI, request_payload, "data")
|
||||
@@ -221,7 +221,8 @@ class TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity):
|
||||
response = self.get_dashboard_view_response(dashboard_to_access)
|
||||
|
||||
# assert
|
||||
assert response.status_code == 404
|
||||
# Anonymous users are redirected to login instead of getting 404
|
||||
assert response.status_code == 302
|
||||
|
||||
@pytest.mark.usefixtures("public_role_like_gamma")
|
||||
def test_get_dashboard_view__public_user_with_dashboard_permission_can_not_access_draft( # noqa: E501
|
||||
@@ -234,7 +235,8 @@ class TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity):
|
||||
response = self.get_dashboard_view_response(dashboard_to_access)
|
||||
|
||||
# assert
|
||||
assert response.status_code == 404
|
||||
# Anonymous users are redirected to login for unpublished dashboards
|
||||
assert response.status_code == 302
|
||||
|
||||
# post
|
||||
revoke_access_to_dashboard(dashboard_to_access, "Public") # noqa: F405
|
||||
|
||||
@@ -190,6 +190,8 @@ def delete_all_inserted_objects() -> None:
|
||||
|
||||
def delete_all_inserted_dashboards():
|
||||
try:
|
||||
# Expire all objects to ensure fresh state after potential rollbacks
|
||||
db.session.expire_all()
|
||||
dashboards_to_delete: list[Dashboard] = (
|
||||
db.session.query(Dashboard)
|
||||
.filter(Dashboard.id.in_(inserted_dashboards_ids))
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
# under the License.
|
||||
|
||||
from random import randint
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask_appbuilder.security.sqla.models import User
|
||||
@@ -25,7 +25,7 @@ from freezegun.api import FakeDatetime
|
||||
|
||||
from superset.extensions import db
|
||||
from superset.reports.models import ReportScheduleType
|
||||
from superset.tasks.scheduler import execute, scheduler
|
||||
from superset.tasks.scheduler import execute, log_task_failure, scheduler
|
||||
from tests.integration_tests.reports.utils import insert_report_schedule
|
||||
from tests.integration_tests.test_app import app
|
||||
|
||||
@@ -201,3 +201,48 @@ def test_execute_task_with_command_exception(
|
||||
|
||||
db.session.delete(report_schedule)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@patch("superset.tasks.scheduler.logger")
|
||||
def test_log_task_failure_with_sender(logger_mock):
|
||||
"""
|
||||
Test that log_task_failure logs correctly when sender is provided
|
||||
"""
|
||||
mock_task = MagicMock()
|
||||
mock_task.name = "test.task.name"
|
||||
mock_exception = Exception("Test error")
|
||||
mock_einfo = MagicMock()
|
||||
|
||||
log_task_failure(
|
||||
sender=mock_task,
|
||||
task_id="test-task-id",
|
||||
exception=mock_exception,
|
||||
einfo=mock_einfo,
|
||||
)
|
||||
|
||||
logger_mock.exception.assert_called_once_with(
|
||||
"Celery task %s failed: %s",
|
||||
"test.task.name",
|
||||
mock_exception,
|
||||
exc_info=mock_einfo,
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.tasks.scheduler.logger")
|
||||
def test_log_task_failure_without_sender(logger_mock):
|
||||
"""
|
||||
Test that log_task_failure logs correctly when sender is None
|
||||
"""
|
||||
mock_exception = Exception("Test error")
|
||||
mock_einfo = MagicMock()
|
||||
|
||||
log_task_failure(
|
||||
sender=None,
|
||||
task_id="test-task-id",
|
||||
exception=mock_exception,
|
||||
einfo=mock_einfo,
|
||||
)
|
||||
|
||||
logger_mock.exception.assert_called_once_with(
|
||||
"Celery task %s failed: %s", "Unknown", mock_exception, exc_info=mock_einfo
|
||||
)
|
||||
|
||||
@@ -19,9 +19,14 @@
|
||||
|
||||
import pytest
|
||||
import prison
|
||||
import uuid
|
||||
import yaml
|
||||
from datetime import datetime
|
||||
from freezegun import freeze_time
|
||||
from io import BytesIO
|
||||
from sqlalchemy.sql import func
|
||||
from typing import Any
|
||||
from zipfile import ZipFile
|
||||
|
||||
import tests.integration_tests.test_app # noqa: F401
|
||||
from superset import db
|
||||
@@ -399,3 +404,120 @@ class TestThemeApi(SupersetTestCase):
|
||||
uri = f"api/v1/theme/?q={prison.dumps(theme_ids)}"
|
||||
rv = self.delete_assert_metric(uri, "bulk_delete")
|
||||
assert rv.status_code == 404
|
||||
|
||||
def create_theme_import_zip(self, theme_config: dict[str, Any]) -> BytesIO:
|
||||
"""Helper method to create a theme import ZIP file"""
|
||||
buf = BytesIO()
|
||||
with ZipFile(buf, "w") as bundle:
|
||||
# Use a root folder like the export does
|
||||
root = "theme_import"
|
||||
|
||||
# Add metadata.yaml
|
||||
metadata = {
|
||||
"version": "1.0.0",
|
||||
"type": "Theme",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
with bundle.open(f"{root}/metadata.yaml", "w") as fp:
|
||||
fp.write(yaml.safe_dump(metadata).encode())
|
||||
|
||||
# Add theme YAML file
|
||||
theme_yaml = yaml.safe_dump(theme_config)
|
||||
with bundle.open(
|
||||
f"{root}/themes/{theme_config['theme_name']}.yaml", "w"
|
||||
) as fp:
|
||||
fp.write(theme_yaml.encode())
|
||||
buf.seek(0)
|
||||
return buf
|
||||
|
||||
def test_import_theme(self):
|
||||
"""
|
||||
Theme API: Test import theme
|
||||
"""
|
||||
theme_config = {
|
||||
"theme_name": "imported_theme",
|
||||
"uuid": str(uuid.uuid4()),
|
||||
"version": "1.0.0",
|
||||
"json_data": {"colors": {"primary": "#007bff"}},
|
||||
}
|
||||
|
||||
self.login(ADMIN_USERNAME)
|
||||
uri = "api/v1/theme/import/"
|
||||
|
||||
buf = self.create_theme_import_zip(theme_config)
|
||||
form_data = {
|
||||
"formData": (buf, "theme_export.zip"),
|
||||
}
|
||||
rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
|
||||
assert rv.status_code == 200
|
||||
assert response == {"message": "Theme imported successfully"}
|
||||
|
||||
theme = db.session.query(Theme).filter_by(uuid=theme_config["uuid"]).one()
|
||||
assert theme.theme_name == "imported_theme"
|
||||
|
||||
# Cleanup
|
||||
db.session.delete(theme)
|
||||
db.session.commit()
|
||||
|
||||
def test_import_theme_overwrite(self):
|
||||
"""
|
||||
Theme API: Test import existing theme without and with overwrite
|
||||
"""
|
||||
theme_config = {
|
||||
"theme_name": "overwrite_theme",
|
||||
"uuid": str(uuid.uuid4()),
|
||||
"version": "1.0.0",
|
||||
"json_data": {"colors": {"primary": "#007bff"}},
|
||||
}
|
||||
|
||||
self.login(ADMIN_USERNAME)
|
||||
uri = "api/v1/theme/import/"
|
||||
|
||||
# First import
|
||||
buf = self.create_theme_import_zip(theme_config)
|
||||
form_data = {
|
||||
"formData": (buf, "theme_export.zip"),
|
||||
}
|
||||
rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
|
||||
assert rv.status_code == 200
|
||||
assert response == {"message": "Theme imported successfully"}
|
||||
|
||||
# Import again without overwrite flag - should fail with structured error
|
||||
buf = self.create_theme_import_zip(theme_config)
|
||||
form_data = {
|
||||
"formData": (buf, "theme_export.zip"),
|
||||
}
|
||||
rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
|
||||
assert rv.status_code == 422
|
||||
assert len(response["errors"]) == 1
|
||||
error = response["errors"][0]
|
||||
assert error["message"].startswith("Error importing theme")
|
||||
assert error["error_type"] == "GENERIC_COMMAND_ERROR"
|
||||
assert error["level"] == "warning"
|
||||
assert f"themes/{theme_config['theme_name']}.yaml" in str(error["extra"])
|
||||
assert "Theme already exists and `overwrite=true` was not passed" in str(
|
||||
error["extra"]
|
||||
)
|
||||
|
||||
# Import with overwrite flag - should succeed
|
||||
buf = self.create_theme_import_zip(theme_config)
|
||||
form_data = {
|
||||
"formData": (buf, "theme_export.zip"),
|
||||
"overwrite": "true",
|
||||
}
|
||||
rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
|
||||
assert rv.status_code == 200
|
||||
assert response == {"message": "Theme imported successfully"}
|
||||
|
||||
# Cleanup
|
||||
theme = db.session.query(Theme).filter_by(uuid=theme_config["uuid"]).one()
|
||||
db.session.delete(theme)
|
||||
db.session.commit()
|
||||
|
||||
@@ -24,6 +24,7 @@ from sqlalchemy.orm.session import Session
|
||||
from superset.charts.client_processing import apply_client_processing, pivot_df, table
|
||||
from superset.common.chart_data import ChartDataResultFormat
|
||||
from superset.utils.core import GenericDataType
|
||||
from tests.conftest import with_config
|
||||
|
||||
|
||||
def test_pivot_df_no_cols_no_rows_single_metric():
|
||||
@@ -2653,3 +2654,137 @@ def test_pivot_multi_level_index():
|
||||
| ('Total (Sum)', '', '') | 210 | 105 | 0 |
|
||||
""".strip()
|
||||
)
|
||||
|
||||
|
||||
@with_config({"REPORTS_CSV_NA_NAMES": []})
|
||||
def test_apply_client_processing_csv_format_preserves_na_strings():
|
||||
"""
|
||||
Test that apply_client_processing preserves "NA" when REPORTS_CSV_NA_NAMES is [].
|
||||
|
||||
This ensures that scheduled reports can be configured to
|
||||
preserve strings like "NA" as literal values.
|
||||
"""
|
||||
|
||||
# CSV data with "NA" string that should be preserved
|
||||
csv_data = "first_name,last_name\nJeff,Smith\nAlice,NA"
|
||||
|
||||
result = {
|
||||
"queries": [
|
||||
{
|
||||
"result_format": ChartDataResultFormat.CSV,
|
||||
"data": csv_data,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
form_data = {
|
||||
"datasource": "1__table",
|
||||
"viz_type": "table",
|
||||
"slice_id": 1,
|
||||
"url_params": {},
|
||||
"metrics": [],
|
||||
"groupby": [],
|
||||
"columns": ["first_name", "last_name"],
|
||||
"extra_form_data": {},
|
||||
"force": False,
|
||||
"result_format": "csv",
|
||||
"result_type": "results",
|
||||
}
|
||||
|
||||
# Test with REPORTS_CSV_NA_NAMES set to empty list (disable NA conversion)
|
||||
|
||||
processed_result = apply_client_processing(result, form_data)
|
||||
|
||||
# Verify the CSV data still contains "NA" as string, not converted to null
|
||||
output_data = processed_result["queries"][0]["data"]
|
||||
assert "NA" in output_data
|
||||
# The "NA" should be preserved in the output CSV
|
||||
lines = output_data.strip().split("\n")
|
||||
assert "Alice,NA" in lines[2] # Second data row should preserve "NA"
|
||||
|
||||
|
||||
@with_config({"REPORTS_CSV_NA_NAMES": ["MISSING"]})
|
||||
def test_apply_client_processing_csv_format_custom_na_values():
|
||||
"""
|
||||
Test that apply_client_processing respects custom NA values configuration.
|
||||
"""
|
||||
|
||||
csv_data = "name,status\nJeff,MISSING\nAlice,OK"
|
||||
|
||||
result = {
|
||||
"queries": [
|
||||
{
|
||||
"result_format": ChartDataResultFormat.CSV,
|
||||
"data": csv_data,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
form_data = {
|
||||
"datasource": "1__table",
|
||||
"viz_type": "table",
|
||||
"slice_id": 1,
|
||||
"url_params": {},
|
||||
"metrics": [],
|
||||
"groupby": [],
|
||||
"columns": ["name", "status"],
|
||||
"extra_form_data": {},
|
||||
"force": False,
|
||||
"result_format": "csv",
|
||||
"result_type": "results",
|
||||
}
|
||||
|
||||
# Test with custom NA values - only "MISSING" should be treated as NA
|
||||
processed_result = apply_client_processing(result, form_data)
|
||||
|
||||
output_data = processed_result["queries"][0]["data"]
|
||||
lines = output_data.strip().split("\n")
|
||||
assert len(lines) >= 3 # header + 2 data rows
|
||||
assert "Jeff," in lines[1] # First data row should have empty status after "Jeff,"
|
||||
assert "Alice,OK" in lines[2] # Second data row should preserve "OK"
|
||||
|
||||
|
||||
@with_config({"REPORTS_CSV_NA_NAMES": []})
|
||||
def test_apply_client_processing_csv_format_default_na_behavior():
|
||||
"""
|
||||
Test that apply_client_processing uses default pandas NA behavior
|
||||
when REPORTS_CSV_NA_NAMES is not configured.
|
||||
This ensures backwards compatibility.
|
||||
"""
|
||||
|
||||
# CSV data with "NA" string that should be converted to null in default behavior
|
||||
csv_data = "first_name,last_name\nJeff,Smith\nAlice,NA"
|
||||
|
||||
result = {
|
||||
"queries": [
|
||||
{
|
||||
"result_format": ChartDataResultFormat.CSV,
|
||||
"data": csv_data,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
form_data = {
|
||||
"datasource": "1__table",
|
||||
"viz_type": "table",
|
||||
"slice_id": 1,
|
||||
"url_params": {},
|
||||
"metrics": [],
|
||||
"groupby": [],
|
||||
"columns": ["first_name", "last_name"],
|
||||
"extra_form_data": {},
|
||||
"force": False,
|
||||
"result_format": "csv",
|
||||
"result_type": "results",
|
||||
}
|
||||
|
||||
processed_result = apply_client_processing(result, form_data)
|
||||
|
||||
# Verify the CSV data has "NA" converted to empty (default pandas behavior)
|
||||
output_data = processed_result["queries"][0]["data"]
|
||||
lines = output_data.strip().split("\n")
|
||||
assert len(lines) >= 3 # header + 2 data rows
|
||||
# The "NA" should be converted to empty by default pandas behavior
|
||||
assert (
|
||||
"Alice," in lines[2]
|
||||
) # Second data row should have empty last_name (NA converted to null)
|
||||
|
||||
@@ -127,6 +127,21 @@ def fake_get_chart_csv_data_hierarchical(chart_url, auth_cookies=None):
|
||||
return json.dumps(fake_result).encode("utf-8")
|
||||
|
||||
|
||||
def fake_get_chart_csv_data_with_na_values(chart_url, auth_cookies=None):
|
||||
# Return JSON with data containing "NA" string value that will be treated as null
|
||||
fake_result = {
|
||||
"result": [
|
||||
{
|
||||
"data": {"first_name": ["Jeff", "Alice"], "last_name": ["Smith", "NA"]},
|
||||
"coltypes": [GenericDataType.STRING, GenericDataType.STRING],
|
||||
"colnames": ["first_name", "last_name"],
|
||||
"indexnames": ["idx1", "idx2"],
|
||||
}
|
||||
]
|
||||
}
|
||||
return json.dumps(fake_result).encode("utf-8")
|
||||
|
||||
|
||||
def test_df_to_escaped_csv():
|
||||
df = pd.DataFrame(
|
||||
data={
|
||||
@@ -263,3 +278,42 @@ def test_get_chart_dataframe_with_hierarchical_columns(monkeypatch: pytest.Monke
|
||||
| ('idx',) | 2 |
|
||||
"""
|
||||
assert markdown_str.strip() == expected_markdown_str.strip()
|
||||
|
||||
|
||||
def test_get_chart_dataframe_preserves_na_string_values(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
"""
|
||||
Test that get_chart_dataframe currently preserves rows containing "NA"
|
||||
string values.
|
||||
This test verifies the existing behavior before implementing custom NA handling.
|
||||
"""
|
||||
monkeypatch.setattr(
|
||||
csv, "get_chart_csv_data", fake_get_chart_csv_data_with_na_values
|
||||
)
|
||||
df = get_chart_dataframe("http://dummy-url")
|
||||
assert df is not None
|
||||
|
||||
# Verify the DataFrame structure
|
||||
expected_columns = pd.MultiIndex.from_tuples([("first_name",), ("last_name",)])
|
||||
pd.testing.assert_index_equal(df.columns, expected_columns)
|
||||
|
||||
expected_index = pd.MultiIndex.from_tuples([("idx1",), ("idx2",)])
|
||||
pd.testing.assert_index_equal(df.index, expected_index)
|
||||
|
||||
# Check that we have both rows initially
|
||||
assert len(df) == 2
|
||||
|
||||
# Verify the data contains the "NA" string value (not converted to NaN)
|
||||
pd.testing.assert_series_equal(
|
||||
df[("first_name",)],
|
||||
pd.Series(["Jeff", "Alice"], name=("first_name",), index=df.index),
|
||||
)
|
||||
pd.testing.assert_series_equal(
|
||||
df[("last_name",)],
|
||||
pd.Series(["Smith", "NA"], name=("last_name",), index=df.index),
|
||||
)
|
||||
|
||||
last_name_values = df[("last_name",)].values
|
||||
assert last_name_values[0] == "Smith"
|
||||
assert last_name_values[1] == "NA"
|
||||
|
||||
@@ -110,24 +110,42 @@ class TestTakeTiledScreenshot:
|
||||
"""Create a mock Playwright page object."""
|
||||
page = MagicMock()
|
||||
|
||||
# Mock viewport size
|
||||
page.viewport_size = {"width": 1024, "height": 768}
|
||||
|
||||
# Mock element locator
|
||||
element = MagicMock()
|
||||
page.locator.return_value = element
|
||||
|
||||
# Mock element info - simulating a 5000px tall dashboard
|
||||
element_info = {"height": 5000, "top": 100, "left": 50, "width": 800}
|
||||
element_box = {"x": 50, "y": 200, "width": 800, "height": 600}
|
||||
viewport_info = {
|
||||
"elementX": 50,
|
||||
"elementY": 200,
|
||||
"elementWidth": 800,
|
||||
"elementHeight": 600,
|
||||
"viewportWidth": 1024,
|
||||
"viewportHeight": 768,
|
||||
}
|
||||
|
||||
# For 3 tiles (5000px / 2000px = 2.5, rounded up to 3):
|
||||
# 1 initial call + 3 scroll + 3 element box + 1 reset scroll = 8 calls
|
||||
# For 7 tiles (5000px / 768px actual viewport = 6.5, rounded up to 7):
|
||||
# 1 initial call + 7 scroll + 7 viewport info + 1 reset scroll = 16 calls
|
||||
page.evaluate.side_effect = [
|
||||
element_info, # Initial call for dashboard dimensions
|
||||
None, # First scroll call
|
||||
element_box, # First element box call
|
||||
None, # Second scroll call
|
||||
element_box, # Second element box call
|
||||
None, # Third scroll call
|
||||
element_box, # Third element box call
|
||||
None,
|
||||
viewport_info, # Tile 1
|
||||
None,
|
||||
viewport_info, # Tile 2
|
||||
None,
|
||||
viewport_info, # Tile 3
|
||||
None,
|
||||
viewport_info, # Tile 4
|
||||
None,
|
||||
viewport_info, # Tile 5
|
||||
None,
|
||||
viewport_info, # Tile 6
|
||||
None,
|
||||
viewport_info, # Tile 7
|
||||
None, # Final reset scroll call
|
||||
]
|
||||
|
||||
@@ -150,8 +168,8 @@ class TestTakeTiledScreenshot:
|
||||
assert result == b"combined_screenshot"
|
||||
|
||||
# Should have called screenshot method multiple times
|
||||
# (3 tiles for 5000px height)
|
||||
assert mock_page.screenshot.call_count == 3
|
||||
# (7 tiles for 5000px height with 768px actual viewport)
|
||||
assert mock_page.screenshot.call_count == 7
|
||||
|
||||
# Should have called combine function
|
||||
mock_combine.assert_called_once()
|
||||
@@ -171,16 +189,23 @@ class TestTakeTiledScreenshot:
|
||||
"""Test that tiles are calculated correctly."""
|
||||
# Mock dashboard height of 3500px with viewport of 2000px
|
||||
element_info = {"height": 3500, "top": 100, "left": 50, "width": 800}
|
||||
element_box = {"x": 50, "y": 200, "width": 800, "height": 600}
|
||||
viewport_info = {
|
||||
"elementX": 50,
|
||||
"elementY": 200,
|
||||
"elementWidth": 800,
|
||||
"elementHeight": 600,
|
||||
"viewportWidth": 1024,
|
||||
"viewportHeight": 768,
|
||||
}
|
||||
|
||||
# For 2 tiles (3500px / 2000px = 1.75, rounded up to 2):
|
||||
# 1 initial call + 2 scroll + 2 element box + 1 reset scroll = 6 calls
|
||||
# 1 initial call + 2 scroll + 2 viewport info + 1 reset scroll = 6 calls
|
||||
mock_page.evaluate.side_effect = [
|
||||
element_info,
|
||||
None, # First scroll call
|
||||
element_box, # First element box call
|
||||
viewport_info, # First viewport info call
|
||||
None, # Second scroll call
|
||||
element_box, # Second element box call
|
||||
viewport_info, # Second viewport info call
|
||||
None, # Reset scroll call
|
||||
]
|
||||
|
||||
@@ -198,38 +223,57 @@ class TestTakeTiledScreenshot:
|
||||
"""Test that scroll positions are calculated correctly."""
|
||||
# Override the fixture's side_effect for this specific test
|
||||
element_info = {"height": 5000, "top": 100, "left": 50, "width": 800}
|
||||
element_box = {"x": 50, "y": 200, "width": 800, "height": 600}
|
||||
viewport_info = {
|
||||
"elementX": 50,
|
||||
"elementY": 200,
|
||||
"elementWidth": 800,
|
||||
"elementHeight": 600,
|
||||
"viewportWidth": 1024,
|
||||
"viewportHeight": 768,
|
||||
}
|
||||
|
||||
mock_page.evaluate.side_effect = [
|
||||
element_info, # Initial call for dashboard dimensions
|
||||
None, # First scroll call
|
||||
element_box, # First element box call
|
||||
None, # Second scroll call
|
||||
element_box, # Second element box call
|
||||
None, # Third scroll call
|
||||
element_box, # Third element box call
|
||||
None,
|
||||
viewport_info, # Tile 1
|
||||
None,
|
||||
viewport_info, # Tile 2
|
||||
None,
|
||||
viewport_info, # Tile 3
|
||||
None,
|
||||
viewport_info, # Tile 4
|
||||
None,
|
||||
viewport_info, # Tile 5
|
||||
None,
|
||||
viewport_info, # Tile 6
|
||||
None,
|
||||
viewport_info, # Tile 7
|
||||
None, # Reset scroll call
|
||||
]
|
||||
|
||||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||||
take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||||
|
||||
# Check scroll positions (dashboard_top = 100)
|
||||
# Check scroll positions (dashboard_top = 100, tile_height = 768)
|
||||
scroll_calls = [
|
||||
call
|
||||
for call in mock_page.evaluate.call_args_list
|
||||
if "scrollTo" in str(call)
|
||||
]
|
||||
|
||||
# Should have scrolled to positions: 100, 2100, 4100
|
||||
# Should have scrolled to positions: 100, 868, 1636, 2404, 3172, 3940, 4708
|
||||
expected_scrolls = [
|
||||
"window.scrollTo(0, 100)",
|
||||
"window.scrollTo(0, 2100)",
|
||||
"window.scrollTo(0, 4100)",
|
||||
"window.scrollTo(0, 868)",
|
||||
"window.scrollTo(0, 1636)",
|
||||
"window.scrollTo(0, 2404)",
|
||||
"window.scrollTo(0, 3172)",
|
||||
"window.scrollTo(0, 3940)",
|
||||
"window.scrollTo(0, 4708)",
|
||||
]
|
||||
actual_scrolls = [call[0][0] for call in scroll_calls]
|
||||
|
||||
assert len(actual_scrolls) == 4 # 3 tile scrolls + 1 reset
|
||||
assert len(actual_scrolls) == 8 # 7 tile scrolls + 1 reset
|
||||
for expected in expected_scrolls:
|
||||
assert expected in actual_scrolls
|
||||
|
||||
@@ -237,16 +281,31 @@ class TestTakeTiledScreenshot:
|
||||
"""Test that scroll position is reset after screenshot."""
|
||||
# Override the fixture's side_effect for this specific test
|
||||
element_info = {"height": 5000, "top": 100, "left": 50, "width": 800}
|
||||
element_box = {"x": 50, "y": 200, "width": 800, "height": 600}
|
||||
viewport_info = {
|
||||
"elementX": 50,
|
||||
"elementY": 200,
|
||||
"elementWidth": 800,
|
||||
"elementHeight": 600,
|
||||
"viewportWidth": 1024,
|
||||
"viewportHeight": 768,
|
||||
}
|
||||
|
||||
mock_page.evaluate.side_effect = [
|
||||
element_info, # Initial call for dashboard dimensions
|
||||
None, # First scroll call
|
||||
element_box, # First element box call
|
||||
None, # Second scroll call
|
||||
element_box, # Second element box call
|
||||
None, # Third scroll call
|
||||
element_box, # Third element box call
|
||||
None,
|
||||
viewport_info, # Tile 1
|
||||
None,
|
||||
viewport_info, # Tile 2
|
||||
None,
|
||||
viewport_info, # Tile 3
|
||||
None,
|
||||
viewport_info, # Tile 4
|
||||
None,
|
||||
viewport_info, # Tile 5
|
||||
None,
|
||||
viewport_info, # Tile 6
|
||||
None,
|
||||
viewport_info, # Tile 7
|
||||
None, # Reset scroll call
|
||||
]
|
||||
|
||||
@@ -268,7 +327,7 @@ class TestTakeTiledScreenshot:
|
||||
"Dashboard: %sx%spx at (%s, %s)", 800, 5000, 50, 100
|
||||
)
|
||||
# Should log number of tiles with lazy logging format
|
||||
mock_logger.info.assert_any_call("Taking %s screenshot tiles", 3)
|
||||
mock_logger.info.assert_any_call("Taking %s screenshot tiles", 7)
|
||||
|
||||
def test_exception_handling_returns_none(self):
|
||||
"""Test that exceptions are handled and None is returned."""
|
||||
@@ -289,8 +348,8 @@ class TestTakeTiledScreenshot:
|
||||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||||
take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||||
|
||||
# Should have called wait_for_timeout for each tile (3 tiles)
|
||||
assert mock_page.wait_for_timeout.call_count == 3
|
||||
# Should have called wait_for_timeout for each tile (7 tiles)
|
||||
assert mock_page.wait_for_timeout.call_count == 7
|
||||
|
||||
# Each wait should be 2000ms (2 seconds)
|
||||
for call in mock_page.wait_for_timeout.call_args_list:
|
||||
@@ -315,3 +374,367 @@ class TestTakeTiledScreenshot:
|
||||
assert clip["width"] == 800
|
||||
# Height should be min of viewport_height and remaining content
|
||||
assert clip["height"] <= 600 # Element height from mock
|
||||
|
||||
def test_negative_element_position_clipped_to_zero(self):
|
||||
"""Test that negative element positions are clipped to viewport bounds."""
|
||||
mock_page = MagicMock()
|
||||
mock_page.viewport_size = {"width": 1024, "height": 768}
|
||||
|
||||
# Mock element locator
|
||||
element = MagicMock()
|
||||
mock_page.locator.return_value = element
|
||||
|
||||
# Simulate element scrolled above viewport (negative Y position)
|
||||
element_info = {"height": 3000, "top": 100, "left": 0, "width": 800}
|
||||
viewport_info = {
|
||||
"elementX": 0,
|
||||
"elementY": -200, # Element is scrolled 200px above viewport
|
||||
"elementWidth": 800,
|
||||
"elementHeight": 600,
|
||||
"viewportWidth": 1024,
|
||||
"viewportHeight": 768,
|
||||
}
|
||||
|
||||
# For 4 tiles (3000px / 768px = 3.9, rounded up to 4):
|
||||
# 1 initial + 4 * (scroll + viewport info) + 1 reset = 10 calls
|
||||
mock_page.evaluate.side_effect = [
|
||||
element_info,
|
||||
None,
|
||||
viewport_info, # Tile 1
|
||||
None,
|
||||
viewport_info, # Tile 2
|
||||
None,
|
||||
viewport_info, # Tile 3
|
||||
None,
|
||||
viewport_info, # Tile 4
|
||||
None, # Reset scroll
|
||||
]
|
||||
|
||||
mock_page.screenshot.return_value = b"screenshot"
|
||||
|
||||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||||
result = take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||||
|
||||
# Should complete successfully
|
||||
assert result is not None
|
||||
|
||||
# Check that clip Y was adjusted to 0 (not negative)
|
||||
screenshot_calls = mock_page.screenshot.call_args_list
|
||||
for call in screenshot_calls:
|
||||
clip = call[1]["clip"]
|
||||
assert clip["y"] >= 0, "Clip Y should never be negative"
|
||||
assert clip["x"] >= 0, "Clip X should never be negative"
|
||||
|
||||
def test_element_extends_beyond_viewport(self):
|
||||
"""Test clipping when element extends beyond viewport boundaries."""
|
||||
mock_page = MagicMock()
|
||||
mock_page.viewport_size = {"width": 1024, "height": 768}
|
||||
|
||||
element = MagicMock()
|
||||
mock_page.locator.return_value = element
|
||||
|
||||
element_info = {"height": 2000, "top": 0, "left": 0, "width": 1200}
|
||||
|
||||
# Element is wider than viewport
|
||||
viewport_info = {
|
||||
"elementX": 0,
|
||||
"elementY": 100,
|
||||
"elementWidth": 1200, # Wider than viewport
|
||||
"elementHeight": 800,
|
||||
"viewportWidth": 1024, # Viewport width
|
||||
"viewportHeight": 768,
|
||||
}
|
||||
|
||||
# For 3 tiles (2000px / 768px = 2.6, rounded up to 3):
|
||||
# 1 initial + 3 * (scroll + viewport info) + 1 reset = 8 calls
|
||||
mock_page.evaluate.side_effect = [
|
||||
element_info,
|
||||
None,
|
||||
viewport_info, # Tile 1
|
||||
None,
|
||||
viewport_info, # Tile 2
|
||||
None,
|
||||
viewport_info, # Tile 3
|
||||
None, # Reset scroll
|
||||
]
|
||||
|
||||
mock_page.screenshot.return_value = b"screenshot"
|
||||
|
||||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||||
result = take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||||
|
||||
assert result is not None
|
||||
|
||||
# Check that clip width was constrained to viewport
|
||||
clip = mock_page.screenshot.call_args_list[0][1]["clip"]
|
||||
assert clip["width"] <= 1024, "Clip width should not exceed viewport"
|
||||
|
||||
def test_invalid_clip_dimensions_skipped(self):
|
||||
"""Test that tiles with invalid dimensions are skipped with a warning."""
|
||||
mock_page = MagicMock()
|
||||
mock_page.viewport_size = {"width": 1024, "height": 768}
|
||||
|
||||
element = MagicMock()
|
||||
mock_page.locator.return_value = element
|
||||
|
||||
element_info = {"height": 4000, "top": 0, "left": 0, "width": 800}
|
||||
|
||||
# First tile: valid
|
||||
valid_viewport_info = {
|
||||
"elementX": 0,
|
||||
"elementY": 100,
|
||||
"elementWidth": 800,
|
||||
"elementHeight": 600,
|
||||
"viewportWidth": 1024,
|
||||
"viewportHeight": 768,
|
||||
}
|
||||
|
||||
# Second tile: invalid (negative height after calculation)
|
||||
invalid_viewport_info = {
|
||||
"elementX": 0,
|
||||
"elementY": -1000, # Far above viewport
|
||||
"elementWidth": 800,
|
||||
"elementHeight": 100, # Not enough visible height
|
||||
"viewportWidth": 1024,
|
||||
"viewportHeight": 768,
|
||||
}
|
||||
|
||||
# For 6 tiles (4000px / 768px = 5.2, rounded up to 6):
|
||||
# 1 initial + 6 * (scroll + viewport info) + 1 reset = 14 calls
|
||||
mock_page.evaluate.side_effect = [
|
||||
element_info,
|
||||
None,
|
||||
valid_viewport_info, # Tile 1 - valid
|
||||
None,
|
||||
invalid_viewport_info, # Tile 2 - invalid, should be skipped
|
||||
None,
|
||||
valid_viewport_info, # Tile 3 - valid
|
||||
None,
|
||||
valid_viewport_info, # Tile 4 - valid
|
||||
None,
|
||||
valid_viewport_info, # Tile 5 - valid
|
||||
None,
|
||||
valid_viewport_info, # Tile 6 - valid
|
||||
None, # Reset scroll
|
||||
]
|
||||
|
||||
mock_page.screenshot.return_value = b"screenshot"
|
||||
|
||||
with patch("superset.utils.screenshot_utils.logger") as mock_logger:
|
||||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||||
result = take_tiled_screenshot(
|
||||
mock_page, "dashboard", viewport_height=2000
|
||||
)
|
||||
|
||||
# Should complete but with warning
|
||||
assert result is not None
|
||||
|
||||
# Should have logged a warning about skipping tile
|
||||
mock_logger.warning.assert_called_once()
|
||||
warning_msg = mock_logger.warning.call_args[0][0]
|
||||
assert "Skipping tile" in warning_msg
|
||||
assert "invalid clip dimensions" in warning_msg
|
||||
|
||||
# Should have taken 5 screenshots (6 tiles - 1 invalid)
|
||||
assert mock_page.screenshot.call_count == 5
|
||||
|
||||
def test_viewport_bounds_with_offset_element(self):
|
||||
"""Test proper clipping for element with positive offset from viewport edge."""
|
||||
mock_page = MagicMock()
|
||||
mock_page.viewport_size = {"width": 1024, "height": 768}
|
||||
|
||||
element = MagicMock()
|
||||
mock_page.locator.return_value = element
|
||||
|
||||
element_info = {"height": 2000, "top": 500, "left": 200, "width": 600}
|
||||
|
||||
# Element starts 200px from left edge
|
||||
viewport_info = {
|
||||
"elementX": 200, # Offset from left
|
||||
"elementY": 150,
|
||||
"elementWidth": 600,
|
||||
"elementHeight": 500,
|
||||
"viewportWidth": 1024,
|
||||
"viewportHeight": 768,
|
||||
}
|
||||
|
||||
# For 3 tiles (2000px / 768px = 2.6, rounded up to 3):
|
||||
# 1 initial + 3 * (scroll + viewport info) + 1 reset = 8 calls
|
||||
mock_page.evaluate.side_effect = [
|
||||
element_info,
|
||||
None,
|
||||
viewport_info, # Tile 1
|
||||
None,
|
||||
viewport_info, # Tile 2
|
||||
None,
|
||||
viewport_info, # Tile 3
|
||||
None, # Reset scroll
|
||||
]
|
||||
|
||||
mock_page.screenshot.return_value = b"screenshot"
|
||||
|
||||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||||
result = take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||||
|
||||
assert result is not None
|
||||
|
||||
# Check clip respects element position
|
||||
clip = mock_page.screenshot.call_args_list[0][1]["clip"]
|
||||
assert clip["x"] == 200, "Should preserve element X offset"
|
||||
assert clip["y"] == 150, "Should preserve element Y offset"
|
||||
assert clip["width"] == 600, "Should use element width"
|
||||
|
||||
def test_zero_width_element_skipped(self):
|
||||
"""Test that elements with zero or negative width are skipped."""
|
||||
mock_page = MagicMock()
|
||||
mock_page.viewport_size = {"width": 1024, "height": 768}
|
||||
|
||||
element = MagicMock()
|
||||
mock_page.locator.return_value = element
|
||||
|
||||
element_info = {"height": 2000, "top": 0, "left": 0, "width": 0}
|
||||
|
||||
viewport_info = {
|
||||
"elementX": 0,
|
||||
"elementY": 100,
|
||||
"elementWidth": 0, # Zero width
|
||||
"elementHeight": 600,
|
||||
"viewportWidth": 1024,
|
||||
"viewportHeight": 768,
|
||||
}
|
||||
|
||||
# For 3 tiles (2000px / 768px = 2.6, rounded up to 3):
|
||||
# 1 initial + 3 * (scroll + viewport info) + 1 reset = 8 calls
|
||||
# All tiles will be skipped due to zero width
|
||||
mock_page.evaluate.side_effect = [
|
||||
element_info,
|
||||
None,
|
||||
viewport_info, # Tile 1 - skipped
|
||||
None,
|
||||
viewport_info, # Tile 2 - skipped
|
||||
None,
|
||||
viewport_info, # Tile 3 - skipped
|
||||
None, # Reset scroll
|
||||
]
|
||||
|
||||
with patch("superset.utils.screenshot_utils.logger") as mock_logger:
|
||||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||||
result = take_tiled_screenshot(
|
||||
mock_page, "dashboard", viewport_height=2000
|
||||
)
|
||||
|
||||
# Should handle gracefully
|
||||
assert result is not None
|
||||
|
||||
# Should have logged warnings about invalid dimensions
|
||||
# (3 times, once per tile)
|
||||
assert mock_logger.warning.call_count == 3
|
||||
for call in mock_logger.warning.call_args_list:
|
||||
warning_msg = call[0][0]
|
||||
assert "invalid clip dimensions" in warning_msg
|
||||
|
||||
# Should not have taken any screenshots
|
||||
assert mock_page.screenshot.call_count == 0
|
||||
|
||||
def test_element_completely_above_viewport(self):
|
||||
"""Test element that is completely scrolled above the viewport."""
|
||||
mock_page = MagicMock()
|
||||
mock_page.viewport_size = {"width": 1024, "height": 768}
|
||||
|
||||
element = MagicMock()
|
||||
mock_page.locator.return_value = element
|
||||
|
||||
element_info = {"height": 2000, "top": 0, "left": 0, "width": 800}
|
||||
|
||||
# Element completely above viewport
|
||||
viewport_info = {
|
||||
"elementX": 0,
|
||||
"elementY": -800, # Completely above viewport
|
||||
"elementWidth": 800,
|
||||
"elementHeight": 600,
|
||||
"viewportWidth": 1024,
|
||||
"viewportHeight": 768,
|
||||
}
|
||||
|
||||
# For 3 tiles (2000px / 768px = 2.6, rounded up to 3):
|
||||
# 1 initial + 3 * (scroll + viewport info) + 1 reset = 8 calls
|
||||
# All tiles will be skipped because element is completely above viewport
|
||||
mock_page.evaluate.side_effect = [
|
||||
element_info,
|
||||
None,
|
||||
viewport_info, # Tile 1 - skipped
|
||||
None,
|
||||
viewport_info, # Tile 2 - skipped
|
||||
None,
|
||||
viewport_info, # Tile 3 - skipped
|
||||
None, # Reset scroll
|
||||
]
|
||||
|
||||
with patch("superset.utils.screenshot_utils.logger") as mock_logger:
|
||||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||||
result = take_tiled_screenshot(
|
||||
mock_page, "dashboard", viewport_height=2000
|
||||
)
|
||||
|
||||
# Should handle gracefully
|
||||
assert result is not None
|
||||
|
||||
# Should have skipped all 3 tiles with warnings
|
||||
assert mock_logger.warning.call_count == 3
|
||||
|
||||
# Should not have taken screenshots
|
||||
assert mock_page.screenshot.call_count == 0
|
||||
|
||||
def test_scroll_increment_respects_actual_viewport_height(self):
|
||||
"""When config viewport height > actual viewport, we still cover every tile."""
|
||||
mock_page = MagicMock()
|
||||
mock_page.viewport_size = {"width": 1600, "height": 1200}
|
||||
|
||||
element = MagicMock()
|
||||
mock_page.locator.return_value = element
|
||||
|
||||
element_info = {"height": 3600, "top": 0, "left": 0, "width": 800}
|
||||
viewport_info = {
|
||||
"elementX": 0,
|
||||
"elementY": 0,
|
||||
"elementWidth": 800,
|
||||
"elementHeight": 1200,
|
||||
"viewportWidth": 1600,
|
||||
"viewportHeight": 1200,
|
||||
}
|
||||
|
||||
mock_page.evaluate.side_effect = [
|
||||
element_info, # Initial call for dashboard dimensions
|
||||
None, # First scroll
|
||||
viewport_info, # First viewport info
|
||||
None, # Second scroll
|
||||
viewport_info, # Second viewport info
|
||||
None, # Third scroll
|
||||
viewport_info, # Third viewport info
|
||||
None, # Reset scroll
|
||||
]
|
||||
|
||||
mock_page.screenshot.return_value = b"screenshot"
|
||||
|
||||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||||
take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||||
|
||||
# We expect three tiles (0–1200, 1200–2400, 2400–3600)
|
||||
# even though config says 2000.
|
||||
assert mock_page.screenshot.call_count == 3
|
||||
|
||||
scroll_calls = [
|
||||
call
|
||||
for call in mock_page.evaluate.call_args_list
|
||||
if "scrollTo" in str(call)
|
||||
]
|
||||
actual_scrolls = [call[0][0] for call in scroll_calls]
|
||||
|
||||
# Should have scrolled to positions: 0, 1200, 2400, plus final reset to 0
|
||||
assert len(actual_scrolls) == 4 # 3 tile scrolls + 1 reset
|
||||
assert actual_scrolls == [
|
||||
"window.scrollTo(0, 0)",
|
||||
"window.scrollTo(0, 1200)",
|
||||
"window.scrollTo(0, 2400)",
|
||||
"window.scrollTo(0, 0)", # Reset scroll
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user