mirror of
https://github.com/apache/superset.git
synced 2026-06-29 03:15:34 +00:00
Compare commits
5 Commits
fix-timezo
...
semantic-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b11ac4dd90 | ||
|
|
e182520bb3 | ||
|
|
bfa4d5bd92 | ||
|
|
0e9c71e283 | ||
|
|
5c1e250b77 |
24
.github/dependabot.yml
vendored
24
.github/dependabot.yml
vendored
@@ -9,12 +9,9 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
ignore:
|
||||
# TODO: remove below entries until React >= 18.0.0
|
||||
# not until React >= 18.0.0
|
||||
- dependency-name: "storybook"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
- dependency-name: "@storybook*"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
- dependency-name: "eslint-plugin-storybook"
|
||||
# remark-gfm v4+ requires react-markdown v9+, which needs React 18
|
||||
- dependency-name: "remark-gfm"
|
||||
- dependency-name: "react-markdown"
|
||||
@@ -26,14 +23,6 @@ updates:
|
||||
# See https://github.com/apache/superset/pull/37384#issuecomment-3793991389
|
||||
# TODO: remove the plugin once Lodash usage has been migrated to a more readily tree-shakeable alternative
|
||||
- dependency-name: "@swc/plugin-transform-imports"
|
||||
groups:
|
||||
storybook:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "@storybook*"
|
||||
- "storybook"
|
||||
update-types:
|
||||
- "patch"
|
||||
directory: "/superset-frontend/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -366,17 +355,6 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-demo/"
|
||||
ignore:
|
||||
# TODO: remove below entries until React >= 18.0.0
|
||||
- dependency-name: "@storybook*"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
groups:
|
||||
storybook:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "@storybook*"
|
||||
update-types:
|
||||
- "patch"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
|
||||
70
.github/workflows/prefer-typescript.yml
vendored
Normal file
70
.github/workflows/prefer-typescript.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: Prefer TypeScript
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "[0-9].[0-9]*"
|
||||
paths:
|
||||
- "superset-frontend/src/**"
|
||||
pull_request:
|
||||
types: [synchronize, opened, reopened, ready_for_review]
|
||||
paths:
|
||||
- "superset-frontend/src/**"
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prefer_typescript:
|
||||
if: github.ref == 'ref/heads/master' && github.event_name == 'pull_request'
|
||||
name: Prefer TypeScript
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Get changed files
|
||||
id: changed
|
||||
uses: ./.github/actions/file-changes-action
|
||||
with:
|
||||
githubToken: ${{ github.token }}
|
||||
|
||||
- name: Determine if a .js or .jsx file was added
|
||||
id: check
|
||||
run: |
|
||||
js_files_added() {
|
||||
jq -r '
|
||||
map(
|
||||
select(
|
||||
endswith(".js") or endswith(".jsx")
|
||||
)
|
||||
) | join("\n")
|
||||
' ${HOME}/files_added.json
|
||||
}
|
||||
echo "js_files_added=$(js_files_added)" >> $GITHUB_OUTPUT
|
||||
|
||||
- if: steps.check.outputs.js_files_added
|
||||
name: Add Comment to PR
|
||||
uses: ./.github/actions/comment-on-pr
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
msg: |
|
||||
### WARNING: Prefer TypeScript
|
||||
|
||||
Looks like your PR contains new `.js` or `.jsx` files:
|
||||
|
||||
```
|
||||
${{steps.check.outputs.js_files_added}}
|
||||
```
|
||||
|
||||
As decided in [SIP-36](https://github.com/apache/superset/issues/9101), all new frontend code should be written in TypeScript. Please convert above files to TypeScript then re-request review.
|
||||
@@ -52,6 +52,7 @@ jobs:
|
||||
SUPERSET_SECRET_KEY: not-a-secret
|
||||
run: |
|
||||
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
|
||||
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
||||
@@ -105,7 +105,12 @@ class CeleryConfig:
|
||||
|
||||
CELERY_CONFIG = CeleryConfig
|
||||
|
||||
FEATURE_FLAGS = {"ALERT_REPORTS": True, "DATASET_FOLDERS": True}
|
||||
FEATURE_FLAGS = {
|
||||
"ALERT_REPORTS": True,
|
||||
"DATASET_FOLDERS": True,
|
||||
"ENABLE_EXTENSIONS": True,
|
||||
}
|
||||
EXTENSIONS_PATH = "/app/docker/extensions"
|
||||
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
|
||||
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
|
||||
# The base URL for the email report hyperlinks.
|
||||
|
||||
@@ -788,7 +788,7 @@ pytest ./link_to_test.py
|
||||
|
||||
### Frontend Testing
|
||||
|
||||
We use [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) to test TypeScript. Tests can be run with:
|
||||
We use [Jest](https://jestjs.io/) and [Enzyme](https://airbnb.io/enzyme/) to test TypeScript/JavaScript. Tests can be run with:
|
||||
|
||||
```bash
|
||||
cd superset-frontend
|
||||
|
||||
@@ -100,7 +100,7 @@ npm link superset-plugin-chart-hello-world
|
||||
```
|
||||
|
||||
7. **Import and register in Superset**:
|
||||
Edit `superset-frontend/src/visualizations/presets/MainPreset.ts` to include your plugin.
|
||||
Edit `superset-frontend/src/visualizations/presets/MainPreset.js` to include your plugin.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -134,9 +134,9 @@ export const onDidChangeActivePanel: Event<Panel>;
|
||||
|
||||
export const onDidChangeTabTitle: Event<string>;
|
||||
|
||||
export const onDidQueryRun: Event<QueryContext>;
|
||||
export const onDidQueryRun: Event<Editor>;
|
||||
|
||||
export const onDidQueryStop: Event<QueryContext>;
|
||||
export const onDidQueryStop: Event<Editor>;
|
||||
```
|
||||
|
||||
The following code demonstrates more examples of the existing frontend APIs:
|
||||
@@ -150,16 +150,16 @@ export function activate(context) {
|
||||
const panelDisposable = core.registerView('my_extension.panel', <MyPanel><Button/></MyPanel>);
|
||||
|
||||
// Register a custom command
|
||||
const commandDisposable = commands.registerCommand(
|
||||
'my_extension.copy_query',
|
||||
() => {
|
||||
const commandDisposable = commands.registerCommand('my_extension.copy_query', {
|
||||
title: 'Copy Query',
|
||||
execute: () => {
|
||||
// Command logic here
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Listen for query run events in SQL Lab
|
||||
const eventDisposable = sqlLab.onDidQueryRun(queryContext => {
|
||||
console.log('Query started on database:', queryContext.tab.databaseId);
|
||||
const eventDisposable = sqlLab.onDidQueryRun(editor => {
|
||||
// Handle query execution event
|
||||
});
|
||||
|
||||
// Access a CSRF token for secure API requests
|
||||
|
||||
@@ -24,7 +24,7 @@ under the License.
|
||||
|
||||
# SQL Lab Extension Points
|
||||
|
||||
SQL Lab provides 4 extension points where extensions can contribute custom UI components. Each area serves a specific purpose and supports different types of customizations. These areas will evolve over time as new features are added to SQL Lab.
|
||||
SQL Lab provides 5 extension points where extensions can contribute custom UI components. Each area serves a specific purpose and can be customized to add new functionality.
|
||||
|
||||
## Layout Overview
|
||||
|
||||
@@ -41,44 +41,42 @@ SQL Lab provides 4 extension points where extensions can contribute custom UI co
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└──────────┴─────────────────────────────────────────┴─────────────┘
|
||||
├──────────┴─────────────────────────────────────────┴─────────────┤
|
||||
│ Status Bar │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| Extension Point | ID | Views | Menus | Description |
|
||||
| ----------------- | --------------------- | ----- | ----- | ---------------------------------------------- |
|
||||
| **Left Sidebar** | `sqllab.leftSidebar` | — | ✓ | Menu actions for the database explorer |
|
||||
| **Editor** | `sqllab.editor` | ✓\* | ✓ | Custom editors + toolbar actions |
|
||||
| **Right Sidebar** | `sqllab.rightSidebar` | ✓ | — | Custom panels (AI assistants, query analysis) |
|
||||
| **Panels** | `sqllab.panels` | ✓ | ✓ | Custom tabs + toolbar actions (data profiling) |
|
||||
| Extension Point | ID | Description |
|
||||
| ----------------- | --------------------- | ---------------------------------------------------------- |
|
||||
| **Left Sidebar** | `sqllab.leftSidebar` | Navigation and browsing (database explorer, saved queries) |
|
||||
| **Editor** | `sqllab.editor` | SQL query editor workspace |
|
||||
| **Right Sidebar** | `sqllab.rightSidebar` | Contextual tools (AI assistants, query analysis) |
|
||||
| **Panels** | `sqllab.panels` | Results and related views (visualizations, data profiling) |
|
||||
| **Status Bar** | `sqllab.statusBar` | Connection status and query metrics |
|
||||
|
||||
\*Editor views are contributed via [Editor Contributions](./editors), not standard view contributions.
|
||||
## Area Customizations
|
||||
|
||||
## Customization Types
|
||||
|
||||
### Views
|
||||
|
||||
Extensions can add custom views (React components) to **Right Sidebar** and **Panels**. Views appear as new panels or tabs in their respective areas.
|
||||
|
||||
### Menus
|
||||
|
||||
Extensions can add toolbar actions to **Left Sidebar**, **Editor**, and **Panels**. Menu contributions support:
|
||||
Each extension point area supports three types of action customizations:
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ [Button] [Button] [•••] │
|
||||
│ Area Title [Button] [Button] [•••] │
|
||||
├───────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ │
|
||||
│ Area Content │
|
||||
│ │
|
||||
│ (right-click for context menu) │
|
||||
│ │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| Action Type | Location | Use Case |
|
||||
| --------------------- | ---------------- | ----------------------------------------------------- |
|
||||
| **Primary Actions** | Toolbar buttons | Frequently used actions (e.g., run, refresh, add new) |
|
||||
| **Secondary Actions** | 3-dot menu (•••) | Less common actions (e.g., export, settings) |
|
||||
|
||||
### Custom Editors
|
||||
|
||||
Extensions can replace the default SQL editor with custom implementations (Monaco, CodeMirror, etc.). See [Editor Contributions](./editors) for details.
|
||||
| Action Type | Location | Use Case |
|
||||
| --------------------- | ----------------- | ----------------------------------------------------- |
|
||||
| **Primary Actions** | Top-right buttons | Frequently used actions (e.g., run, refresh, add new) |
|
||||
| **Secondary Actions** | 3-dot menu (•••) | Less common actions (e.g., export, settings) |
|
||||
| **Context Actions** | Right-click menu | Context-sensitive actions on content |
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -173,38 +171,32 @@ import { commands, sqlLab } from '@apache-superset/core';
|
||||
|
||||
export function activate(context) {
|
||||
// Register the commands declared in extension.json
|
||||
const formatCommand = commands.registerCommand(
|
||||
'query_tools.format',
|
||||
async () => {
|
||||
const formatCommand = commands.registerCommand('query_tools.format', {
|
||||
execute: () => {
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
if (tab) {
|
||||
const editor = await tab.getEditor();
|
||||
if (tab?.editor) {
|
||||
// Format the SQL query
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const explainCommand = commands.registerCommand(
|
||||
'query_tools.explain',
|
||||
async () => {
|
||||
const explainCommand = commands.registerCommand('query_tools.explain', {
|
||||
execute: () => {
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
if (tab) {
|
||||
const editor = await tab.getEditor();
|
||||
if (tab?.editor) {
|
||||
// Show query explanation
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const copyAsCteCommand = commands.registerCommand(
|
||||
'query_tools.copy_as_cte',
|
||||
async () => {
|
||||
const copyAsCteCommand = commands.registerCommand('query_tools.copy_as_cte', {
|
||||
execute: () => {
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
if (tab) {
|
||||
const editor = await tab.getEditor();
|
||||
if (tab?.editor) {
|
||||
// Copy selected text as CTE
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
context.subscriptions.push(formatCommand, explainCommand, copyAsCteCommand);
|
||||
}
|
||||
|
||||
@@ -1,63 +1,7 @@
|
||||
---
|
||||
sidebar_position: 9
|
||||
title: Frequently Asked Questions
|
||||
description: Common questions about Apache Superset including performance, database support, visualizations, and configuration.
|
||||
keywords: [superset faq, superset questions, superset help, data visualization faq]
|
||||
---
|
||||
|
||||
import FAQSchema from '@site/src/components/FAQSchema';
|
||||
|
||||
<FAQSchema faqs={[
|
||||
{
|
||||
question: "How big of a dataset can Superset handle?",
|
||||
answer: "Superset can work with even gigantic databases. Superset acts as a thin layer above your underlying databases or data engines, which do all the processing. Superset simply visualizes the results of the query. The key to achieving acceptable performance is whether your database can execute queries and return results at acceptable speed."
|
||||
},
|
||||
{
|
||||
question: "What are the computing specifications required to run Superset?",
|
||||
answer: "The specs depend on how many users you have and their activity, not on the size of your data. Community members have reported 8GB RAM, 2vCPUs as adequate for a moderately-sized instance. Monitor your resource usage and adjust as needed."
|
||||
},
|
||||
{
|
||||
question: "Can I join or query multiple tables at one time?",
|
||||
answer: "Not in the Explore or Visualization UI directly. A Superset SQLAlchemy datasource can only be a single table or a view. You can create a view that joins tables, or use SQL Lab where you can write SQL queries to join multiple tables."
|
||||
},
|
||||
{
|
||||
question: "How do I create my own visualization?",
|
||||
answer: "Read the instructions in the Creating Visualization Plugins documentation to learn how to build custom visualizations for Superset."
|
||||
},
|
||||
{
|
||||
question: "Can I upload and visualize CSV data?",
|
||||
answer: "Yes! Superset supports CSV upload functionality. Read the Exploring Data documentation to learn how to enable and use CSV upload."
|
||||
},
|
||||
{
|
||||
question: "Why are my queries timing out?",
|
||||
answer: "There are many possible causes. For SQL Lab, Superset allows queries to run up to 6 hours by default (configurable via SQLLAB_ASYNC_TIME_LIMIT_SEC). For dashboard timeouts, check your gateway/proxy timeout settings and adjust SUPERSET_WEBSERVER_TIMEOUT in superset_config.py."
|
||||
},
|
||||
{
|
||||
question: "Why is the map not visible in the geospatial visualization?",
|
||||
answer: "You need to register a free account at Mapbox.com, obtain an API key, and add it to your .env file at the key MAPBOX_API_KEY."
|
||||
},
|
||||
{
|
||||
question: "What database engine can I use as a backend for Superset?",
|
||||
answer: "Superset is tested using MySQL, PostgreSQL, and SQLite backends for storing its internal metadata. While Superset supports many databases as data sources, only these are recommended for the metadata store in production."
|
||||
},
|
||||
{
|
||||
question: "Does Superset work with my database?",
|
||||
answer: "Superset supports any database with a Python SQLAlchemy dialect and DBAPI driver. Check the Connecting to Databases documentation for the full list of supported databases."
|
||||
},
|
||||
{
|
||||
question: "Does Superset offer a public API?",
|
||||
answer: "Yes, Superset has a public REST API documented using Swagger. Enable FAB_API_SWAGGER_UI in superset_config.py to access interactive API documentation at /swagger/v1."
|
||||
},
|
||||
{
|
||||
question: "Does Superset collect any telemetry data?",
|
||||
answer: "Superset uses Scarf by default to collect basic telemetry data to help maintainers understand version usage. Users can opt out by setting the SCARF_ANALYTICS environment variable to false."
|
||||
},
|
||||
{
|
||||
question: "Does Superset have a trash bin to recover deleted assets?",
|
||||
answer: "No, there is no built-in way to recover deleted dashboards, charts, or datasets. It is recommended to take periodic backups of the metadata database and use export functionality for recovery."
|
||||
}
|
||||
]} />
|
||||
|
||||
# FAQ
|
||||
|
||||
## How big of a dataset can Superset handle?
|
||||
|
||||
@@ -23,7 +23,6 @@ import type * as OpenApiPlugin from 'docusaurus-plugin-openapi-docs';
|
||||
import { themes } from 'prism-react-renderer';
|
||||
import remarkImportPartial from 'remark-import-partial';
|
||||
import remarkLocalizeBadges from './plugins/remark-localize-badges.mjs';
|
||||
import remarkTechArticleSchema from './plugins/remark-tech-article-schema.mjs';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -47,7 +46,7 @@ if (!versionsConfig.components.disabled) {
|
||||
sidebarPath: require.resolve('./sidebarComponents.js'),
|
||||
editUrl:
|
||||
'https://github.com/apache/superset/edit/master/docs/components',
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges],
|
||||
admonitions: {
|
||||
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
|
||||
extendDefaults: true,
|
||||
@@ -75,7 +74,7 @@ if (!versionsConfig.developer_portal.disabled) {
|
||||
sidebarPath: require.resolve('./sidebarTutorials.js'),
|
||||
editUrl:
|
||||
'https://github.com/apache/superset/edit/master/docs/developer_portal',
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges],
|
||||
admonitions: {
|
||||
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
|
||||
extendDefaults: true,
|
||||
@@ -181,83 +180,6 @@ const config: Config = {
|
||||
favicon: '/img/favicon.ico',
|
||||
organizationName: 'apache',
|
||||
projectName: 'superset',
|
||||
|
||||
// SEO: Structured data (Organization, Software, WebSite with SearchAction)
|
||||
headTags: [
|
||||
// SoftwareApplication schema
|
||||
{
|
||||
tagName: 'script',
|
||||
attributes: {
|
||||
type: 'application/ld+json',
|
||||
},
|
||||
innerHTML: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Apache Superset',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Cross-platform',
|
||||
description: 'Apache Superset is a modern, enterprise-ready business intelligence web application for data exploration and visualization.',
|
||||
url: 'https://superset.apache.org',
|
||||
license: 'https://www.apache.org/licenses/LICENSE-2.0',
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: 'Apache Software Foundation',
|
||||
url: 'https://www.apache.org/',
|
||||
logo: 'https://www.apache.org/foundation/press/kit/asf_logo.png',
|
||||
},
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
featureList: [
|
||||
'Interactive dashboards',
|
||||
'SQL IDE',
|
||||
'40+ visualization types',
|
||||
'Semantic layer',
|
||||
'Role-based access control',
|
||||
'REST API',
|
||||
],
|
||||
}),
|
||||
},
|
||||
// WebSite schema with SearchAction (enables sitelinks search box in Google)
|
||||
{
|
||||
tagName: 'script',
|
||||
attributes: {
|
||||
type: 'application/ld+json',
|
||||
},
|
||||
innerHTML: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: 'Apache Superset',
|
||||
url: 'https://superset.apache.org',
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: {
|
||||
'@type': 'EntryPoint',
|
||||
urlTemplate: 'https://superset.apache.org/search?q={search_term_string}',
|
||||
},
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
}),
|
||||
},
|
||||
// Preconnect hints for faster external resource loading
|
||||
{
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'preconnect',
|
||||
href: 'https://WR5FASX5ED-dsn.algolia.net',
|
||||
crossorigin: 'anonymous',
|
||||
},
|
||||
},
|
||||
{
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'preconnect',
|
||||
href: 'https://analytics.apache.org',
|
||||
},
|
||||
},
|
||||
],
|
||||
themes: [
|
||||
'@saucelabs/theme-github-codeblock',
|
||||
'@docusaurus/theme-mermaid',
|
||||
@@ -290,19 +212,6 @@ const config: Config = {
|
||||
},
|
||||
},
|
||||
],
|
||||
// SEO: Generate robots.txt during build
|
||||
[
|
||||
require.resolve('./plugins/robots-txt-plugin.js'),
|
||||
{
|
||||
policies: [
|
||||
{
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
disallow: ['/api/v1/', '/_next/', '/static/js/*.map'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
@@ -464,7 +373,7 @@ const config: Config = {
|
||||
}
|
||||
return `https://github.com/apache/superset/edit/master/docs/${versionDocsDirPath}/${docPath}`;
|
||||
},
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges],
|
||||
admonitions: {
|
||||
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
|
||||
extendDefaults: true,
|
||||
@@ -487,57 +396,11 @@ const config: Config = {
|
||||
theme: {
|
||||
customCss: require.resolve('./src/styles/custom.css'),
|
||||
},
|
||||
// SEO: Sitemap configuration with priorities
|
||||
sitemap: {
|
||||
lastmod: 'date',
|
||||
changefreq: 'weekly',
|
||||
priority: 0.5,
|
||||
ignorePatterns: ['/tags/**'],
|
||||
filename: 'sitemap.xml',
|
||||
createSitemapItems: async (params) => {
|
||||
const { defaultCreateSitemapItems, ...rest } = params;
|
||||
const items = await defaultCreateSitemapItems(rest);
|
||||
return items.map((item) => {
|
||||
// Boost priority for key pages
|
||||
if (item.url.includes('/docs/intro')) {
|
||||
return { ...item, priority: 1.0, changefreq: 'daily' };
|
||||
}
|
||||
if (item.url.includes('/docs/quickstart')) {
|
||||
return { ...item, priority: 0.9, changefreq: 'weekly' };
|
||||
}
|
||||
if (item.url.includes('/docs/installation/')) {
|
||||
return { ...item, priority: 0.8, changefreq: 'weekly' };
|
||||
}
|
||||
if (item.url.includes('/docs/databases')) {
|
||||
return { ...item, priority: 0.8, changefreq: 'weekly' };
|
||||
}
|
||||
if (item.url.includes('/docs/faq')) {
|
||||
return { ...item, priority: 0.7, changefreq: 'monthly' };
|
||||
}
|
||||
if (item.url === 'https://superset.apache.org/') {
|
||||
return { ...item, priority: 1.0, changefreq: 'daily' };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
},
|
||||
},
|
||||
} satisfies Options,
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
// SEO: OpenGraph and Twitter meta tags
|
||||
metadata: [
|
||||
{ name: 'keywords', content: 'data visualization, business intelligence, BI, dashboards, SQL, analytics, open source, Apache, charts, reporting' },
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:site_name', content: 'Apache Superset' },
|
||||
{ property: 'og:image', content: 'https://superset.apache.org/img/superset-og-image.png' },
|
||||
{ property: 'og:image:width', content: '1200' },
|
||||
{ property: 'og:image:height', content: '630' },
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'twitter:image', content: 'https://superset.apache.org/img/superset-og-image.png' },
|
||||
{ name: 'twitter:site', content: '@ApacheSuperset' },
|
||||
],
|
||||
colorMode: {
|
||||
defaultMode: 'dark',
|
||||
disableSwitch: false,
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// Note: visit from unist-util-visit is available if needed for tree traversal
|
||||
|
||||
/**
|
||||
* Remark plugin that automatically injects TechArticle schema import and component
|
||||
* into documentation MDX files based on frontmatter.
|
||||
*
|
||||
* This enables rich snippets for technical documentation in search results.
|
||||
*
|
||||
* Frontmatter options:
|
||||
* - title: (required) Article headline
|
||||
* - description: (required) Article description
|
||||
* - keywords: (optional) Array of keywords
|
||||
* - seo_proficiency: (optional) 'Beginner' or 'Expert', defaults to 'Beginner'
|
||||
* - seo_schema: (optional) Set to false to disable schema injection
|
||||
*/
|
||||
export default function remarkTechArticleSchema() {
|
||||
return (tree, file) => {
|
||||
const frontmatter = file.data.frontMatter || {};
|
||||
|
||||
// Skip if explicitly disabled or missing required fields
|
||||
if (frontmatter.seo_schema === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add schema if we have title and description
|
||||
if (!frontmatter.title || !frontmatter.description) {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = frontmatter.title;
|
||||
const description = frontmatter.description;
|
||||
const keywords = Array.isArray(frontmatter.keywords) ? frontmatter.keywords : [];
|
||||
const proficiencyLevel = frontmatter.seo_proficiency || 'Beginner';
|
||||
|
||||
// Create the import statement
|
||||
const importNode = {
|
||||
type: 'mdxjsEsm',
|
||||
value: `import TechArticleSchema from '@site/src/components/TechArticleSchema';`,
|
||||
data: {
|
||||
estree: {
|
||||
type: 'Program',
|
||||
sourceType: 'module',
|
||||
body: [
|
||||
{
|
||||
type: 'ImportDeclaration',
|
||||
specifiers: [
|
||||
{
|
||||
type: 'ImportDefaultSpecifier',
|
||||
local: { type: 'Identifier', name: 'TechArticleSchema' },
|
||||
},
|
||||
],
|
||||
source: {
|
||||
type: 'Literal',
|
||||
value: '@site/src/components/TechArticleSchema',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Create the component node for MDX
|
||||
const componentNode = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'TechArticleSchema',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'title',
|
||||
value: title,
|
||||
},
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'description',
|
||||
value: description,
|
||||
},
|
||||
...(keywords.length > 0
|
||||
? [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'keywords',
|
||||
value: {
|
||||
type: 'mdxJsxAttributeValueExpression',
|
||||
value: JSON.stringify(keywords),
|
||||
data: {
|
||||
estree: {
|
||||
type: 'Program',
|
||||
sourceType: 'module',
|
||||
body: [
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'ArrayExpression',
|
||||
elements: keywords.map((k) => ({
|
||||
type: 'Literal',
|
||||
value: k,
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(proficiencyLevel !== 'Beginner'
|
||||
? [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'proficiencyLevel',
|
||||
value: proficiencyLevel,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
children: [],
|
||||
};
|
||||
|
||||
// Insert import at the beginning
|
||||
tree.children.unshift(importNode);
|
||||
|
||||
// Find the first heading and insert component after it
|
||||
let insertIndex = 1; // Default: after import
|
||||
for (let i = 1; i < tree.children.length; i++) {
|
||||
if (tree.children[i].type === 'heading') {
|
||||
insertIndex = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tree.children.splice(insertIndex, 0, componentNode);
|
||||
};
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
/**
|
||||
* Docusaurus plugin to generate robots.txt during build
|
||||
* Configuration is passed via plugin options
|
||||
*/
|
||||
module.exports = function robotsTxtPlugin(context, options = {}) {
|
||||
const { siteConfig } = context;
|
||||
const {
|
||||
policies = [{ userAgent: '*', allow: '/' }],
|
||||
additionalSitemaps = [],
|
||||
} = options;
|
||||
|
||||
return {
|
||||
name: 'robots-txt-plugin',
|
||||
|
||||
async postBuild({ outDir }) {
|
||||
const sitemapUrl = `${siteConfig.url}/sitemap.xml`;
|
||||
|
||||
// Build robots.txt content
|
||||
const lines = [];
|
||||
|
||||
// Add policies
|
||||
for (const policy of policies) {
|
||||
lines.push(`User-agent: ${policy.userAgent}`);
|
||||
|
||||
if (policy.allow) {
|
||||
const allows = Array.isArray(policy.allow) ? policy.allow : [policy.allow];
|
||||
for (const allow of allows) {
|
||||
lines.push(`Allow: ${allow}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (policy.disallow) {
|
||||
const disallows = Array.isArray(policy.disallow) ? policy.disallow : [policy.disallow];
|
||||
for (const disallow of disallows) {
|
||||
lines.push(`Disallow: ${disallow}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (policy.crawlDelay) {
|
||||
lines.push(`Crawl-delay: ${policy.crawlDelay}`);
|
||||
}
|
||||
|
||||
lines.push(''); // Empty line between policies
|
||||
}
|
||||
|
||||
// Add sitemaps
|
||||
lines.push(`Sitemap: ${sitemapUrl}`);
|
||||
for (const sitemap of additionalSitemaps) {
|
||||
lines.push(`Sitemap: ${sitemap}`);
|
||||
}
|
||||
|
||||
// Write robots.txt
|
||||
const robotsPath = path.join(outDir, 'robots.txt');
|
||||
fs.writeFileSync(robotsPath, lines.join('\n'));
|
||||
|
||||
console.log('Generated robots.txt');
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { JSX } from 'react';
|
||||
import Head from '@docusaurus/Head';
|
||||
|
||||
interface FAQItem {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface FAQSchemaProps {
|
||||
faqs: FAQItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that injects FAQPage JSON-LD structured data
|
||||
* Use this on FAQ pages to enable rich snippets in search results
|
||||
*
|
||||
* @example
|
||||
* <FAQSchema faqs={[
|
||||
* { question: "What is Superset?", answer: "Apache Superset is..." },
|
||||
* { question: "How do I install it?", answer: "You can install via..." }
|
||||
* ]} />
|
||||
*/
|
||||
export default function FAQSchema({ faqs }: FAQSchemaProps): JSX.Element | null {
|
||||
// FAQPage schema requires a non-empty mainEntity array per schema.org specs
|
||||
if (!faqs || faqs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
'@type': 'Question',
|
||||
name: faq.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: faq.answer,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<script type="application/ld+json">{JSON.stringify(schema)}</script>
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { JSX } from 'react';
|
||||
import Head from '@docusaurus/Head';
|
||||
import { useLocation } from '@docusaurus/router';
|
||||
|
||||
interface TechArticleSchemaProps {
|
||||
title: string;
|
||||
description: string;
|
||||
datePublished?: string;
|
||||
dateModified?: string;
|
||||
keywords?: string[];
|
||||
proficiencyLevel?: 'Beginner' | 'Expert';
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that injects TechArticle JSON-LD structured data for documentation pages.
|
||||
* This helps search engines understand technical documentation content.
|
||||
*
|
||||
* @example
|
||||
* <TechArticleSchema
|
||||
* title="Installing Superset with Docker"
|
||||
* description="Learn how to install Apache Superset using Docker Compose"
|
||||
* keywords={['docker', 'installation', 'superset']}
|
||||
* proficiencyLevel="Beginner"
|
||||
* />
|
||||
*/
|
||||
export default function TechArticleSchema({
|
||||
title,
|
||||
description,
|
||||
datePublished,
|
||||
dateModified,
|
||||
keywords = [],
|
||||
proficiencyLevel = 'Beginner',
|
||||
}: TechArticleSchemaProps): JSX.Element {
|
||||
const location = useLocation();
|
||||
const url = `https://superset.apache.org${location.pathname}`;
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'TechArticle',
|
||||
headline: title,
|
||||
description,
|
||||
url,
|
||||
proficiencyLevel,
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: 'Apache Superset Contributors',
|
||||
url: 'https://github.com/apache/superset/graphs/contributors',
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'Apache Software Foundation',
|
||||
url: 'https://www.apache.org/',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://www.apache.org/foundation/press/kit/asf_logo.png',
|
||||
},
|
||||
},
|
||||
mainEntityOfPage: {
|
||||
'@type': 'WebPage',
|
||||
'@id': url,
|
||||
},
|
||||
...(datePublished && { datePublished }),
|
||||
...(dateModified && { dateModified }),
|
||||
...(keywords.length > 0 && { keywords: keywords.join(', ') }),
|
||||
};
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<script type="application/ld+json">{JSON.stringify(schema)}</script>
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
BIN
docs/static/img/superset-og-image.png
vendored
BIN
docs/static/img/superset-og-image.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB |
@@ -141,7 +141,7 @@ druid = ["pydruid>=0.6.5,<0.7"]
|
||||
duckdb = ["duckdb>=1.4.2,<2", "duckdb-engine>=0.17.0"]
|
||||
dynamodb = ["pydynamodb>=0.4.2"]
|
||||
solr = ["sqlalchemy-solr >= 0.2.0"]
|
||||
elasticsearch = ["elasticsearch-dbapi>=0.2.12, <0.3.0"]
|
||||
elasticsearch = ["elasticsearch-dbapi>=0.2.9, <0.3.0"]
|
||||
exasol = ["sqlalchemy-exasol >= 2.4.0, <3.0"]
|
||||
excel = ["xlrd>=1.2.0, <1.3"]
|
||||
fastmcp = ["fastmcp==2.14.3"]
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol, runtime_checkable, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
from superset_core.semantic_layers.semantic_view import SemanticView
|
||||
|
||||
ConfigT = TypeVar("ConfigT", bound=BaseModel, contravariant=True)
|
||||
SemanticViewT = TypeVar("SemanticViewT", bound="SemanticView")
|
||||
|
||||
|
||||
# TODO (betodealmeida): convert to ABC
|
||||
@runtime_checkable
|
||||
class SemanticLayer(Protocol[ConfigT, SemanticViewT]):
|
||||
"""
|
||||
A protocol for semantic layers.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_configuration(
|
||||
cls,
|
||||
configuration: dict[str, Any],
|
||||
) -> SemanticLayer[ConfigT, SemanticViewT]:
|
||||
"""
|
||||
Create a semantic layer from its configuration.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_configuration_schema(
|
||||
cls,
|
||||
configuration: ConfigT | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Get the JSON schema for the configuration needed to add the semantic layer.
|
||||
|
||||
A partial configuration `configuration` can be sent to improve the schema,
|
||||
allowing for progressive validation and better UX. For example, a semantic
|
||||
layer might require:
|
||||
|
||||
- auth information
|
||||
- a database
|
||||
|
||||
If the user provides the auth information, a client can send the partial
|
||||
configuration to this method, and the resulting JSON schema would include
|
||||
the list of databases the user has access to, allowing a dropdown to be
|
||||
populated.
|
||||
|
||||
The Snowflake semantic layer has an example implementation of this method, where
|
||||
database and schema names are populated based on the provided connection info.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_runtime_schema(
|
||||
cls,
|
||||
configuration: ConfigT,
|
||||
runtime_data: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Get the JSON schema for the runtime parameters needed to load semantic views.
|
||||
|
||||
This returns the schema needed to connect to a semantic view given the
|
||||
configuration for the semantic layer. For example, a semantic layer might
|
||||
be configured by:
|
||||
|
||||
- auth information
|
||||
- an optional database
|
||||
|
||||
If the user does not provide a database when creating the semantic layer, the
|
||||
runtime schema would require the database name to be provided before loading any
|
||||
semantic views. This allows users to create semantic layers that connect to a
|
||||
specific database (or project, account, etc.), or that allow users to select it
|
||||
at query time.
|
||||
|
||||
The Snowflake semantic layer has an example implementation of this method, where
|
||||
database and schema names are required if they were not provided in the initial
|
||||
configuration.
|
||||
"""
|
||||
|
||||
def get_semantic_views(
|
||||
self,
|
||||
runtime_configuration: dict[str, Any],
|
||||
) -> set[SemanticViewT]:
|
||||
"""
|
||||
Get the semantic views available in the semantic layer.
|
||||
|
||||
The runtime configuration can provide information like a given project or
|
||||
schema, used to restrict the semantic views returned.
|
||||
"""
|
||||
|
||||
def get_semantic_view(
|
||||
self,
|
||||
name: str,
|
||||
additional_configuration: dict[str, Any],
|
||||
) -> SemanticViewT:
|
||||
"""
|
||||
Get a specific semantic view by its name and additional configuration.
|
||||
"""
|
||||
105
superset-core/src/superset_core/semantic_layers/semantic_view.py
Normal file
105
superset-core/src/superset_core/semantic_layers/semantic_view.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from superset_core.semantic_layers.types import (
|
||||
Dimension,
|
||||
Filter,
|
||||
GroupLimit,
|
||||
Metric,
|
||||
OrderTuple,
|
||||
SemanticResult,
|
||||
)
|
||||
|
||||
|
||||
# TODO (betodealmeida): move to the extension JSON
|
||||
class SemanticViewFeature(enum.Enum):
|
||||
"""
|
||||
Custom features supported by semantic layers.
|
||||
"""
|
||||
|
||||
ADHOC_EXPRESSIONS_IN_ORDERBY = "ADHOC_EXPRESSIONS_IN_ORDERBY"
|
||||
GROUP_LIMIT = "GROUP_LIMIT"
|
||||
GROUP_OTHERS = "GROUP_OTHERS"
|
||||
|
||||
|
||||
# TODO (betodealmeida): convert to ABC
|
||||
@runtime_checkable
|
||||
class SemanticView(Protocol):
|
||||
"""
|
||||
A protocol for semantic views.
|
||||
"""
|
||||
|
||||
features: frozenset[SemanticViewFeature]
|
||||
|
||||
def uid(self) -> str:
|
||||
"""
|
||||
Returns a unique identifier for the semantic view.
|
||||
"""
|
||||
|
||||
def get_dimensions(self) -> set[Dimension]:
|
||||
"""
|
||||
Get the dimensions defined in the semantic view.
|
||||
"""
|
||||
|
||||
def get_metrics(self) -> set[Metric]:
|
||||
"""
|
||||
Get the metrics defined in the semantic view.
|
||||
"""
|
||||
|
||||
def get_values(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
filters: set[Filter] | None = None,
|
||||
) -> SemanticResult:
|
||||
"""
|
||||
Return distinct values for a dimension.
|
||||
"""
|
||||
|
||||
def get_dataframe(
|
||||
self,
|
||||
metrics: list[Metric],
|
||||
dimensions: list[Dimension],
|
||||
filters: set[Filter] | None = None,
|
||||
order: list[OrderTuple] | None = None,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
*,
|
||||
group_limit: GroupLimit | None = None,
|
||||
) -> SemanticResult:
|
||||
"""
|
||||
Execute a semantic query and return the results as a DataFrame.
|
||||
"""
|
||||
|
||||
def get_row_count(
|
||||
self,
|
||||
metrics: list[Metric],
|
||||
dimensions: list[Dimension],
|
||||
filters: set[Filter] | None = None,
|
||||
order: list[OrderTuple] | None = None,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
*,
|
||||
group_limit: GroupLimit | None = None,
|
||||
) -> SemanticResult:
|
||||
"""
|
||||
Execute a query and return the number of rows the result would have.
|
||||
"""
|
||||
328
superset-core/src/superset_core/semantic_layers/types.py
Normal file
328
superset-core/src/superset_core/semantic_layers/types.py
Normal file
@@ -0,0 +1,328 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from functools import total_ordering
|
||||
from typing import Type as TypeOf
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
__all__ = [
|
||||
"BINARY",
|
||||
"BOOLEAN",
|
||||
"DATE",
|
||||
"DATETIME",
|
||||
"DECIMAL",
|
||||
"Day",
|
||||
"Dimension",
|
||||
"Hour",
|
||||
"INTEGER",
|
||||
"INTERVAL",
|
||||
"Minute",
|
||||
"Month",
|
||||
"NUMBER",
|
||||
"OBJECT",
|
||||
"Quarter",
|
||||
"Second",
|
||||
"STRING",
|
||||
"TIME",
|
||||
"Week",
|
||||
"Year",
|
||||
]
|
||||
|
||||
|
||||
class Type:
|
||||
"""
|
||||
Base class for types.
|
||||
"""
|
||||
|
||||
|
||||
class INTEGER(Type):
|
||||
"""
|
||||
Represents an integer type.
|
||||
"""
|
||||
|
||||
|
||||
class NUMBER(Type):
|
||||
"""
|
||||
Represents a number type.
|
||||
"""
|
||||
|
||||
|
||||
class DECIMAL(Type):
|
||||
"""
|
||||
Represents a decimal type.
|
||||
"""
|
||||
|
||||
|
||||
class STRING(Type):
|
||||
"""
|
||||
Represents a string type.
|
||||
"""
|
||||
|
||||
|
||||
class BOOLEAN(Type):
|
||||
"""
|
||||
Represents a boolean type.
|
||||
"""
|
||||
|
||||
|
||||
class DATE(Type):
|
||||
"""
|
||||
Represents a date type.
|
||||
"""
|
||||
|
||||
|
||||
class TIME(Type):
|
||||
"""
|
||||
Represents a time type.
|
||||
"""
|
||||
|
||||
|
||||
class DATETIME(DATE, TIME):
|
||||
"""
|
||||
Represents a datetime type.
|
||||
"""
|
||||
|
||||
|
||||
class INTERVAL(Type):
|
||||
"""
|
||||
Represents an interval type.
|
||||
"""
|
||||
|
||||
|
||||
class OBJECT(Type):
|
||||
"""
|
||||
Represents an object type.
|
||||
"""
|
||||
|
||||
|
||||
class BINARY(Type):
|
||||
"""
|
||||
Represents a binary type.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@total_ordering
|
||||
class Grain:
|
||||
"""
|
||||
Base class for time and date grains with comparison support.
|
||||
|
||||
Attributes:
|
||||
name: Human-readable name of the grain (e.g., "Second")
|
||||
representation: ISO 8601 representation (e.g., "PT1S")
|
||||
value: Time period as a timedelta
|
||||
"""
|
||||
|
||||
name: str
|
||||
representation: str
|
||||
value: timedelta
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, Grain):
|
||||
return self.value == other.value
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other: object) -> bool:
|
||||
if isinstance(other, Grain):
|
||||
return self.value < other.value
|
||||
return NotImplemented
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.name, self.representation, self.value))
|
||||
|
||||
|
||||
class Second(Grain):
|
||||
name = "Second"
|
||||
representation = "PT1S"
|
||||
value = timedelta(seconds=1)
|
||||
|
||||
|
||||
class Minute(Grain):
|
||||
name = "Minute"
|
||||
representation = "PT1M"
|
||||
value = timedelta(minutes=1)
|
||||
|
||||
|
||||
class Hour(Grain):
|
||||
name = "Hour"
|
||||
representation = "PT1H"
|
||||
value = timedelta(hours=1)
|
||||
|
||||
|
||||
class Day(Grain):
|
||||
name = "Day"
|
||||
representation = "P1D"
|
||||
value = timedelta(days=1)
|
||||
|
||||
|
||||
class Week(Grain):
|
||||
name = "Week"
|
||||
representation = "P1W"
|
||||
value = timedelta(weeks=1)
|
||||
|
||||
|
||||
class Month(Grain):
|
||||
name = "Month"
|
||||
representation = "P1M"
|
||||
value = timedelta(days=30)
|
||||
|
||||
|
||||
class Quarter(Grain):
|
||||
name = "Quarter"
|
||||
representation = "P3M"
|
||||
value = timedelta(days=90)
|
||||
|
||||
|
||||
class Year(Grain):
|
||||
name = "Year"
|
||||
representation = "P1Y"
|
||||
value = timedelta(days=365)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Dimension:
|
||||
id: str
|
||||
name: str
|
||||
type: TypeOf[Type]
|
||||
|
||||
definition: str | None = None
|
||||
description: str | None = None
|
||||
grain: TypeOf[Grain] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Metric:
|
||||
id: str
|
||||
name: str
|
||||
type: TypeOf[Type]
|
||||
|
||||
definition: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AdhocExpression:
|
||||
id: str
|
||||
definition: str
|
||||
|
||||
|
||||
class Operator(str, enum.Enum):
|
||||
EQUALS = "="
|
||||
NOT_EQUALS = "!="
|
||||
GREATER_THAN = ">"
|
||||
LESS_THAN = "<"
|
||||
GREATER_THAN_OR_EQUAL = ">="
|
||||
LESS_THAN_OR_EQUAL = "<="
|
||||
IN = "IN"
|
||||
NOT_IN = "NOT IN"
|
||||
LIKE = "LIKE"
|
||||
NOT_LIKE = "NOT LIKE"
|
||||
IS_NULL = "IS NULL"
|
||||
IS_NOT_NULL = "IS NOT NULL"
|
||||
ADHOC = "ADHOC"
|
||||
|
||||
|
||||
FilterValues = str | int | float | bool | datetime | date | time | timedelta | None
|
||||
|
||||
|
||||
class PredicateType(enum.Enum):
|
||||
WHERE = "WHERE"
|
||||
HAVING = "HAVING"
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Filter:
|
||||
type: PredicateType
|
||||
column: Dimension | Metric | None
|
||||
operator: Operator
|
||||
value: FilterValues | frozenset[FilterValues]
|
||||
|
||||
|
||||
class OrderDirection(enum.Enum):
|
||||
ASC = "ASC"
|
||||
DESC = "DESC"
|
||||
|
||||
|
||||
OrderTuple = tuple[Metric | Dimension | AdhocExpression, OrderDirection]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroupLimit:
|
||||
"""
|
||||
Limit query to top/bottom N combinations of specified dimensions.
|
||||
|
||||
The `filters` parameter allows specifying separate filter constraints for the
|
||||
group limit subquery. This is useful when you want to determine the top N groups
|
||||
using different criteria (e.g., a different time range) than the main query.
|
||||
|
||||
For example, you might want to find the top 10 products by sales over the last
|
||||
30 days, but then show daily sales for those products over the last 7 days.
|
||||
"""
|
||||
|
||||
dimensions: list[Dimension]
|
||||
top: int
|
||||
metric: Metric | None
|
||||
direction: OrderDirection = OrderDirection.DESC
|
||||
group_others: bool = False
|
||||
filters: set[Filter] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SemanticRequest:
|
||||
"""
|
||||
Represents a request made to obtain semantic results.
|
||||
|
||||
This could be a SQL query, an HTTP request, etc.
|
||||
"""
|
||||
|
||||
type: str
|
||||
definition: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SemanticResult:
|
||||
"""
|
||||
Represents the results of a semantic query.
|
||||
|
||||
This includes any requests (SQL queries, HTTP requests) that were performed in order
|
||||
to obtain the results, in order to help troubleshooting.
|
||||
"""
|
||||
|
||||
requests: list[SemanticRequest]
|
||||
# TODO (betodealmeida): convert to PyArrow Table
|
||||
results: DataFrame
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SemanticQuery:
|
||||
"""
|
||||
Represents a semantic query.
|
||||
"""
|
||||
|
||||
metrics: list[Metric]
|
||||
dimensions: list[Dimension]
|
||||
filters: set[Filter] | None = None
|
||||
order: list[OrderTuple] | None = None
|
||||
limit: int | None = None
|
||||
offset: int | None = None
|
||||
group_limit: GroupLimit | None = None
|
||||
@@ -273,53 +273,6 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
// Ban JavaScript files in src/ - all new code must be TypeScript
|
||||
{
|
||||
files: ['src/**/*.js', 'src/**/*.jsx'],
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'Program',
|
||||
message:
|
||||
'JavaScript files are not allowed in src/. Please use TypeScript (.ts/.tsx) instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// Ban JavaScript files in plugins/ - all plugin source code must be TypeScript
|
||||
{
|
||||
files: ['plugins/**/src/**/*.js', 'plugins/**/src/**/*.jsx'],
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'Program',
|
||||
message:
|
||||
'JavaScript files are not allowed in plugins/. Please use TypeScript (.ts/.tsx) instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// Ban JavaScript files in packages/ - with exceptions for config files and generators
|
||||
{
|
||||
files: ['packages/**/src/**/*.js', 'packages/**/src/**/*.jsx'],
|
||||
excludedFiles: [
|
||||
'packages/generator-superset/**/*', // Yeoman generator templates run via Node
|
||||
'packages/superset-ui-demo/.storybook/**/*', // Storybook config files
|
||||
'packages/**/__mocks__/**/*', // Test mocks
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'Program',
|
||||
message:
|
||||
'JavaScript files are not allowed in packages/. Please use TypeScript (.ts/.tsx) instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
|
||||
64
superset-frontend/.swcrc
Normal file
64
superset-frontend/.swcrc
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"decorators": false,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic",
|
||||
"importSource": "@emotion/react",
|
||||
"throwIfNamespace": true
|
||||
},
|
||||
"optimizer": {
|
||||
"globals": {
|
||||
"vars": {
|
||||
"process.env.NODE_ENV": "production"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"target": "es2015",
|
||||
"loose": true,
|
||||
"externalHelpers": false,
|
||||
"preserveAllComments": false,
|
||||
"experimental": {
|
||||
"plugins": [
|
||||
[
|
||||
"@swc/plugin-emotion",
|
||||
{
|
||||
"sourceMap": true,
|
||||
"autoLabel": "dev-only",
|
||||
"labelFormat": "[local]"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@swc/plugin-transform-imports",
|
||||
{
|
||||
"lodash": {
|
||||
"transform": "lodash/{{member}}",
|
||||
"preventFullImport": true,
|
||||
"skipDefaultConversion": false
|
||||
},
|
||||
"lodash-es": {
|
||||
"transform": "lodash-es/{{member}}",
|
||||
"preventFullImport": true,
|
||||
"skipDefaultConversion": false
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6",
|
||||
"strict": false,
|
||||
"strictMode": false,
|
||||
"lazy": false,
|
||||
"noInterop": false
|
||||
},
|
||||
"minify": false
|
||||
}
|
||||
@@ -52,6 +52,8 @@ module.exports = {
|
||||
['@babel/plugin-transform-private-methods', { loose: true }],
|
||||
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],
|
||||
['@babel/plugin-transform-runtime', { corejs: 3 }],
|
||||
// only used in packages/superset-ui-core/src/chart/components/reactify.tsx
|
||||
['babel-plugin-typescript-to-proptypes', { loose: true }],
|
||||
[
|
||||
'@emotion/babel-plugin',
|
||||
{
|
||||
|
||||
@@ -22,40 +22,27 @@
|
||||
* @author Apache
|
||||
*/
|
||||
|
||||
import type { Rule } from 'eslint';
|
||||
import type { Node } from 'estree';
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const plugin: { rules: Record<string, Rule.RuleModule> } = {
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
rules: {
|
||||
'no-template-vars': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow variables in translation template strings',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context: Rule.RuleContext): Rule.RuleListener {
|
||||
function handler(node: Node): void {
|
||||
const callNode = node as Node & {
|
||||
arguments: Array<Node & { type: string; expressions?: Node[] }>;
|
||||
};
|
||||
// Check all arguments (e.g., tn has singular and plural templates)
|
||||
for (const arg of callNode.arguments ?? []) {
|
||||
create(context) {
|
||||
function handler(node) {
|
||||
if (node.arguments.length) {
|
||||
const firstArgs = node.arguments[0];
|
||||
if (
|
||||
arg.type === 'TemplateLiteral' &&
|
||||
(arg as Node & { expressions?: Node[] }).expressions?.length
|
||||
firstArgs.type === 'TemplateLiteral' &&
|
||||
firstArgs.expressions.length
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
"Don't use variables in translation string templates. Flask-babel is a static translation service, so it can't handle strings that include variables",
|
||||
});
|
||||
break; // Only report once per call
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,29 +53,19 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
|
||||
},
|
||||
},
|
||||
'sentence-case-buttons': {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Enforce sentence case for button text in translations',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context: Rule.RuleContext): Rule.RuleListener {
|
||||
function isTitleCase(str: string): boolean {
|
||||
create(context) {
|
||||
function isTitleCase(str) {
|
||||
// Match "Delete Dataset", "Create Chart", etc. (2+ title-cased words)
|
||||
return /^[A-Z][a-z]+(\s+[A-Z][a-z]*)+$/.test(str);
|
||||
}
|
||||
|
||||
function isButtonContext(node: Node & { parent?: Node }): boolean {
|
||||
const { parent } = node as Node & {
|
||||
parent?: Node & Record<string, unknown>;
|
||||
};
|
||||
function isButtonContext(node) {
|
||||
const { parent } = node;
|
||||
if (!parent) return false;
|
||||
|
||||
// Check for button-specific props
|
||||
if (parent.type === 'Property') {
|
||||
const key = (parent as unknown as { key: { name: string } }).key
|
||||
.name;
|
||||
const key = parent.key.name;
|
||||
return [
|
||||
'primaryButtonName',
|
||||
'secondaryButtonName',
|
||||
@@ -98,16 +75,10 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
|
||||
}
|
||||
|
||||
// Check for Button components
|
||||
// Cast to string because ESTree Node type doesn't include JSX types
|
||||
if ((parent.type as string) === 'JSXExpressionContainer') {
|
||||
const jsx = (parent as Node & { parent?: Node }).parent as
|
||||
| (Node & {
|
||||
type: string;
|
||||
openingElement?: { name: { name: string } };
|
||||
})
|
||||
| undefined;
|
||||
if ((jsx?.type as string) === 'JSXElement') {
|
||||
const elementName = jsx?.openingElement?.name.name;
|
||||
if (parent.type === 'JSXExpressionContainer') {
|
||||
const jsx = parent.parent;
|
||||
if (jsx?.type === 'JSXElement') {
|
||||
const elementName = jsx.openingElement.name.name;
|
||||
return elementName === 'Button';
|
||||
}
|
||||
}
|
||||
@@ -115,24 +86,21 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
|
||||
return false;
|
||||
}
|
||||
|
||||
function handler(node: Node): void {
|
||||
const callNode = node as Node & {
|
||||
arguments: Array<Node & { type: string; value?: unknown }>;
|
||||
};
|
||||
// Check all string literal arguments (e.g., tn has singular and plural)
|
||||
for (const arg of callNode.arguments ?? []) {
|
||||
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
||||
const text = arg.value;
|
||||
function handler(node) {
|
||||
if (node.arguments.length) {
|
||||
const firstArg = node.arguments[0];
|
||||
if (
|
||||
firstArg.type === 'Literal' &&
|
||||
typeof firstArg.value === 'string'
|
||||
) {
|
||||
const text = firstArg.value;
|
||||
|
||||
if (
|
||||
isButtonContext(node as Node & { parent?: Node }) &&
|
||||
isTitleCase(text)
|
||||
) {
|
||||
if (isButtonContext(node) && isTitleCase(text)) {
|
||||
const sentenceCase = text
|
||||
.toLowerCase()
|
||||
.replace(/^\w/, (c: string) => c.toUpperCase());
|
||||
.replace(/^\w/, c => c.toUpperCase());
|
||||
context.report({
|
||||
node: arg,
|
||||
node: firstArg,
|
||||
message: `Button text should use sentence case: "${text}" should be "${sentenceCase}"`,
|
||||
});
|
||||
}
|
||||
@@ -148,5 +116,3 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = plugin;
|
||||
@@ -22,19 +22,17 @@
|
||||
* @author Apache
|
||||
*/
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
import type { Rule } from 'eslint';
|
||||
|
||||
const { RuleTester } = require('eslint');
|
||||
const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
|
||||
const plugin = require('.');
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
|
||||
const rule: Rule.RuleModule = plugin.rules['no-template-vars'];
|
||||
const rule = plugin.rules['no-template-vars'];
|
||||
|
||||
const errors: Array<{ type: string }> = [
|
||||
const errors = [
|
||||
{
|
||||
type: 'CallExpression',
|
||||
},
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "eslint-plugin-i18n-strings",
|
||||
"version": "1.0.0",
|
||||
"description": "Warns about translation variables",
|
||||
"main": "index.ts",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
|
||||
@@ -22,29 +22,12 @@
|
||||
* @author Apache
|
||||
*/
|
||||
|
||||
import type { Rule } from 'eslint';
|
||||
import type { Node } from 'estree';
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
interface JSXAttribute {
|
||||
name?: { name: string };
|
||||
value?: { type: string; value?: string; expression?: { value: string } };
|
||||
}
|
||||
|
||||
interface JSXOpeningElement {
|
||||
name: { name: string };
|
||||
attributes: JSXAttribute[];
|
||||
}
|
||||
|
||||
interface JSXElementNode {
|
||||
type: string;
|
||||
openingElement: JSXOpeningElement;
|
||||
}
|
||||
|
||||
const plugin: { rules: Record<string, Rule.RuleModule> } = {
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
rules: {
|
||||
'no-fa-icons-usage': {
|
||||
meta: {
|
||||
@@ -56,27 +39,20 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context: Rule.RuleContext): Rule.RuleListener {
|
||||
create(context) {
|
||||
return {
|
||||
// Check for JSX elements with class names containing "fa"
|
||||
JSXElement(node: Node): void {
|
||||
const jsxNode = node as unknown as JSXElementNode;
|
||||
JSXElement(node) {
|
||||
if (
|
||||
jsxNode.openingElement &&
|
||||
jsxNode.openingElement.name.name === 'i' &&
|
||||
jsxNode.openingElement.attributes &&
|
||||
jsxNode.openingElement.attributes.some((attr: JSXAttribute) => {
|
||||
if (attr.name?.name !== 'className') return false;
|
||||
// Handle className="fa fa-home"
|
||||
if (attr.value?.type === 'Literal') {
|
||||
return /fa fa-/.test(attr.value.value ?? '');
|
||||
}
|
||||
// Handle className={'fa fa-home'}
|
||||
if (attr.value?.type === 'JSXExpressionContainer') {
|
||||
return /fa fa-/.test(attr.value.expression?.value ?? '');
|
||||
}
|
||||
return false;
|
||||
})
|
||||
node.openingElement &&
|
||||
node.openingElement.name.name === 'i' &&
|
||||
node.openingElement.attributes &&
|
||||
node.openingElement.attributes.some(
|
||||
attr =>
|
||||
attr.name &&
|
||||
attr.name.name === 'className' &&
|
||||
/fa fa-/.test(attr.value.value),
|
||||
)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
@@ -90,5 +66,3 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = plugin;
|
||||
@@ -22,20 +22,16 @@
|
||||
* @author Apache
|
||||
*/
|
||||
|
||||
import type { Rule } from 'eslint';
|
||||
|
||||
const { RuleTester } = require('eslint');
|
||||
const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
|
||||
const plugin = require('.');
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
const ruleTester = new RuleTester({
|
||||
parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } },
|
||||
});
|
||||
const rule: Rule.RuleModule = plugin.rules['no-fa-icons-usage'];
|
||||
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
|
||||
const rule = plugin.rules['no-fa-icons-usage'];
|
||||
|
||||
const errors: Array<{ message: string }> = [
|
||||
const errors = [
|
||||
{
|
||||
message:
|
||||
'FontAwesome icons should not be used. Use the src/components/Icons component instead.',
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "eslint-plugin-icons",
|
||||
"version": "1.0.0",
|
||||
"description": "Warns about direct usage of Ant Design icons",
|
||||
"main": "index.ts",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
// https://www.w3.org/wiki/CSS/Properties/color/keywords
|
||||
const COLOR_KEYWORDS: string[] = [
|
||||
module.exports = [
|
||||
'black',
|
||||
'silver',
|
||||
'gray',
|
||||
@@ -170,5 +170,3 @@ const COLOR_KEYWORDS: string[] = [
|
||||
'whitesmoke',
|
||||
'yellowgreen',
|
||||
];
|
||||
|
||||
export default COLOR_KEYWORDS;
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Rule to warn about literal colors
|
||||
* @author Apache
|
||||
*/
|
||||
|
||||
const COLOR_KEYWORDS = require('./colors');
|
||||
|
||||
function hasHexColor(quasi) {
|
||||
if (typeof quasi === 'string') {
|
||||
const regex = /#([a-f0-9]{3}|[a-f0-9]{4}(?:[a-f0-9]{2}){0,2})\b/gi;
|
||||
return !!quasi.match(regex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasRgbColor(quasi) {
|
||||
if (typeof quasi === 'string') {
|
||||
const regex = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/i;
|
||||
return !!quasi.match(regex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasLiteralColor(quasi, strict = false) {
|
||||
if (typeof quasi === 'string') {
|
||||
// matches literal colors at the start or end of a CSS prop
|
||||
return COLOR_KEYWORDS.some(color => {
|
||||
const regexColon = new RegExp(`: ${color}`);
|
||||
const regexSemicolon = new RegExp(` ${color};`);
|
||||
return (
|
||||
!!quasi.match(regexColon) ||
|
||||
!!quasi.match(regexSemicolon) ||
|
||||
(strict && quasi === color)
|
||||
);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const WARNING_MESSAGE =
|
||||
'Theme color variables are preferred over rgb(a)/hex/literal colors';
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
rules: {
|
||||
'no-literal-colors': {
|
||||
create(context) {
|
||||
const warned = [];
|
||||
return {
|
||||
TemplateElement(node) {
|
||||
const rawValue = node?.value?.raw;
|
||||
const isChildParentTagged =
|
||||
node?.parent?.parent?.type === 'TaggedTemplateExpression';
|
||||
const isChildParentArrow =
|
||||
node?.parent?.parent?.type === 'ArrowFunctionExpression';
|
||||
const isParentTemplateLiteral =
|
||||
node?.parent?.type === 'TemplateLiteral';
|
||||
const loc = node?.parent?.parent?.loc;
|
||||
const locId = loc && JSON.stringify(loc);
|
||||
const hasWarned = warned.includes(locId);
|
||||
if (
|
||||
!hasWarned &&
|
||||
(isChildParentTagged ||
|
||||
(isChildParentArrow && isParentTemplateLiteral)) &&
|
||||
rawValue &&
|
||||
(hasLiteralColor(rawValue) ||
|
||||
hasHexColor(rawValue) ||
|
||||
hasRgbColor(rawValue))
|
||||
) {
|
||||
context.report(node, loc, WARNING_MESSAGE);
|
||||
warned.push(locId);
|
||||
}
|
||||
},
|
||||
Literal(node) {
|
||||
const value = node?.value;
|
||||
const isParentProperty = node?.parent?.type === 'Property';
|
||||
const locId = JSON.stringify(node.loc);
|
||||
const hasWarned = warned.includes(locId);
|
||||
|
||||
if (
|
||||
!hasWarned &&
|
||||
isParentProperty &&
|
||||
value &&
|
||||
(hasLiteralColor(value, true) ||
|
||||
hasHexColor(value) ||
|
||||
hasRgbColor(value))
|
||||
) {
|
||||
context.report(node, node.loc, WARNING_MESSAGE);
|
||||
warned.push(locId);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,162 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Rule to warn about literal colors
|
||||
* @author Apache
|
||||
*/
|
||||
|
||||
import type { Rule } from 'eslint';
|
||||
import type { Node, SourceLocation } from 'estree';
|
||||
|
||||
import COLOR_KEYWORDS from './colors';
|
||||
|
||||
function hasHexColor(quasi: string): boolean {
|
||||
const regex = /#([a-f0-9]{3}|[a-f0-9]{4}(?:[a-f0-9]{2}){0,2})\b/gi;
|
||||
return !!quasi.match(regex);
|
||||
}
|
||||
|
||||
function hasRgbColor(quasi: string): boolean {
|
||||
const regex = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/i;
|
||||
return !!quasi.match(regex);
|
||||
}
|
||||
|
||||
function hasLiteralColor(quasi: string, strict: boolean = false): boolean {
|
||||
// matches literal colors at the start or end of a CSS prop
|
||||
return COLOR_KEYWORDS.some((color: string) => {
|
||||
const regexColon = new RegExp(`: ${color}`);
|
||||
const regexSemicolon = new RegExp(` ${color};`);
|
||||
return (
|
||||
!!quasi.match(regexColon) ||
|
||||
!!quasi.match(regexSemicolon) ||
|
||||
(strict && quasi === color)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const WARNING_MESSAGE: string =
|
||||
'Theme color variables are preferred over rgb(a)/hex/literal colors';
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
interface TemplateElementNode {
|
||||
type: string;
|
||||
value?: { raw: string };
|
||||
loc?: SourceLocation | null;
|
||||
parent?: {
|
||||
type: string;
|
||||
parent?: { type: string; loc?: SourceLocation | null };
|
||||
};
|
||||
}
|
||||
|
||||
interface LiteralNode {
|
||||
type: string;
|
||||
value?: unknown;
|
||||
loc?: SourceLocation | null;
|
||||
parent?: { type: string };
|
||||
}
|
||||
|
||||
const plugin: { rules: Record<string, Rule.RuleModule> } = {
|
||||
rules: {
|
||||
'no-literal-colors': {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow literal color values; use theme colors instead',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context: Rule.RuleContext): Rule.RuleListener {
|
||||
const warned: string[] = [];
|
||||
return {
|
||||
TemplateElement(node: Node): void {
|
||||
const templateNode = node as TemplateElementNode;
|
||||
const rawValue = templateNode?.value?.raw;
|
||||
const isChildParentTagged =
|
||||
templateNode?.parent?.parent?.type === 'TaggedTemplateExpression';
|
||||
const isChildParentArrow =
|
||||
templateNode?.parent?.parent?.type === 'ArrowFunctionExpression';
|
||||
const isParentTemplateLiteral =
|
||||
templateNode?.parent?.type === 'TemplateLiteral';
|
||||
const loc = templateNode?.parent?.parent?.loc;
|
||||
const locId = loc && JSON.stringify(loc);
|
||||
const hasWarned = locId ? warned.includes(locId) : false;
|
||||
if (
|
||||
!hasWarned &&
|
||||
(isChildParentTagged ||
|
||||
(isChildParentArrow && isParentTemplateLiteral)) &&
|
||||
rawValue &&
|
||||
(hasLiteralColor(rawValue) ||
|
||||
hasHexColor(rawValue) ||
|
||||
hasRgbColor(rawValue))
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
...(loc && { loc: loc as SourceLocation }),
|
||||
message: WARNING_MESSAGE,
|
||||
});
|
||||
if (locId) {
|
||||
warned.push(locId);
|
||||
}
|
||||
}
|
||||
},
|
||||
Literal(node: Node): void {
|
||||
const literalNode = node as LiteralNode;
|
||||
const value = literalNode?.value;
|
||||
// Only process string literals (not numbers, booleans, null, or RegExp)
|
||||
if (typeof value !== 'string') {
|
||||
return;
|
||||
}
|
||||
const parent = literalNode?.parent as Node & {
|
||||
type: string;
|
||||
value?: Node;
|
||||
};
|
||||
// Only check property values, not keys (e.g., { color: 'red' } not { red: 1 })
|
||||
const isPropertyValue =
|
||||
parent?.type === 'Property' && parent.value === node;
|
||||
const locId = node.loc ? JSON.stringify(node.loc) : null;
|
||||
const hasWarned = locId ? warned.includes(locId) : false;
|
||||
|
||||
if (
|
||||
!hasWarned &&
|
||||
isPropertyValue &&
|
||||
(hasLiteralColor(value, true) ||
|
||||
hasHexColor(value) ||
|
||||
hasRgbColor(value))
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
...(node.loc && { loc: node.loc as SourceLocation }),
|
||||
message: WARNING_MESSAGE,
|
||||
});
|
||||
if (locId) {
|
||||
warned.push(locId);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = plugin;
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "eslint-plugin-theme-colors",
|
||||
"version": "1.0.0",
|
||||
"description": "Warns about rgb(a)/hex/literal colors",
|
||||
"main": "index.ts",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
|
||||
@@ -36,13 +36,7 @@ module.exports = {
|
||||
'^@apache-superset/core/(.*)$': '<rootDir>/packages/superset-core/src/$1',
|
||||
},
|
||||
testEnvironment: '<rootDir>/spec/helpers/jsDomWithFetchAPI.ts',
|
||||
modulePathIgnorePatterns: [
|
||||
'<rootDir>/packages/generator-superset',
|
||||
'<rootDir>/packages/.*/esm',
|
||||
'<rootDir>/packages/.*/lib',
|
||||
'<rootDir>/plugins/.*/esm',
|
||||
'<rootDir>/plugins/.*/lib',
|
||||
],
|
||||
modulePathIgnorePatterns: ['<rootDir>/packages/generator-superset'],
|
||||
setupFilesAfterEnv: ['<rootDir>/spec/helpers/setup.ts'],
|
||||
snapshotSerializers: ['@emotion/jest/serializer'],
|
||||
testEnvironmentOptions: {
|
||||
@@ -65,7 +59,7 @@ module.exports = {
|
||||
],
|
||||
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
|
||||
'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
|
||||
700
superset-frontend/package-lock.json
generated
700
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -97,13 +97,6 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@apache-superset/core": "file:packages/superset-core",
|
||||
"@deck.gl/aggregation-layers": "~9.2.5",
|
||||
"@deck.gl/core": "~9.2.5",
|
||||
"@deck.gl/extensions": "~9.2.5",
|
||||
"@deck.gl/geo-layers": "~9.2.5",
|
||||
"@deck.gl/layers": "~9.2.5",
|
||||
"@deck.gl/mesh-layers": "~9.2.5",
|
||||
"@deck.gl/react": "~9.2.5",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -138,7 +131,6 @@
|
||||
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
|
||||
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
|
||||
"@types/d3-format": "^3.0.1",
|
||||
"@types/d3-selection": "^3.0.11",
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/react-google-recaptcha": "^2.1.9",
|
||||
"@visx/axis": "^3.8.0",
|
||||
@@ -178,12 +170,6 @@
|
||||
"json-bigint": "^1.0.0",
|
||||
"json-stringify-pretty-compact": "^2.0.0",
|
||||
"lodash": "^4.17.23",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
"@luma.gl/engine": "~9.2.5",
|
||||
"@luma.gl/gltf": "~9.2.5",
|
||||
"@luma.gl/shadertools": "~9.2.5",
|
||||
"@luma.gl/webgl": "~9.2.5",
|
||||
"mapbox-gl": "^3.18.1",
|
||||
"markdown-to-jsx": "^9.7.3",
|
||||
"match-sorter": "^6.3.4",
|
||||
@@ -192,6 +178,7 @@
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"ol": "^7.5.2",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "9.3.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^17.0.2",
|
||||
@@ -244,7 +231,7 @@
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
|
||||
"@babel/plugin-transform-runtime": "^7.29.0",
|
||||
"@babel/plugin-transform-runtime": "^7.28.5",
|
||||
"@babel/preset-env": "^7.29.0",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
@@ -297,6 +284,7 @@
|
||||
"@types/redux-localstorage": "^1.0.8",
|
||||
"@types/redux-mock-store": "^1.0.6",
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
@@ -306,6 +294,7 @@
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"babel-plugin-typescript-to-proptypes": "^2.0.0",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"cheerio": "1.2.0",
|
||||
"concurrently": "^9.2.1",
|
||||
@@ -344,7 +333,7 @@
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-html-reporter": "^4.3.0",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"jsdom": "^28.0.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"lerna": "^8.2.3",
|
||||
"lightningcss": "^1.31.1",
|
||||
"mini-css-extract-plugin": "^2.10.0",
|
||||
@@ -357,6 +346,7 @@
|
||||
"react-refresh": "^0.18.0",
|
||||
"react-resizable": "^3.1.3",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"sinon": "^18.0.0",
|
||||
"source-map": "^0.7.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.5.0",
|
||||
@@ -366,6 +356,7 @@
|
||||
"terser-webpack-plugin": "^5.3.16",
|
||||
"thread-loader": "^4.0.4",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-loader": "^9.5.4",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "5.4.5",
|
||||
@@ -399,20 +390,19 @@
|
||||
"underscore": "^1.13.7",
|
||||
"jspdf": "^4.0.0",
|
||||
"nwsapi": "^2.2.13",
|
||||
"@deck.gl/aggregation-layers": "~9.2.5",
|
||||
"@deck.gl/core": "~9.2.5",
|
||||
"@deck.gl/extensions": "~9.2.5",
|
||||
"@deck.gl/geo-layers": "~9.2.5",
|
||||
"@deck.gl/layers": "~9.2.5",
|
||||
"@deck.gl/mesh-layers": "~9.2.5",
|
||||
"@deck.gl/react": "~9.2.5",
|
||||
"@deck.gl/widgets": "~9.2.5",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
"@luma.gl/engine": "~9.2.5",
|
||||
"@luma.gl/gltf": "~9.2.5",
|
||||
"@luma.gl/shadertools": "~9.2.5",
|
||||
"@luma.gl/webgl": "~9.2.5"
|
||||
"@deck.gl/aggregation-layers": "~9.2.2",
|
||||
"@deck.gl/core": "~9.2.2",
|
||||
"@deck.gl/extensions": "~9.2.2",
|
||||
"@deck.gl/geo-layers": "~9.2.2",
|
||||
"@deck.gl/layers": "~9.2.2",
|
||||
"@deck.gl/mesh-layers": "~9.2.2",
|
||||
"@deck.gl/react": "~9.2.2",
|
||||
"@deck.gl/widgets": "~9.2.2",
|
||||
"@luma.gl/constants": "~9.2.2",
|
||||
"@luma.gl/core": "~9.2.2",
|
||||
"@luma.gl/engine": "~9.2.2",
|
||||
"@luma.gl/shadertools": "~9.2.2",
|
||||
"@luma.gl/webgl": "~9.2.2"
|
||||
},
|
||||
"readme": "ERROR: No README data found!",
|
||||
"scarfSettings": {
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore -- yeoman-test type resolution differs between local and Docker environments
|
||||
import helpers, { result } from 'yeoman-test';
|
||||
import appModule from '../generators/app';
|
||||
|
||||
@@ -18,17 +18,15 @@
|
||||
*/
|
||||
|
||||
import { dirname, join } from 'path';
|
||||
// @ts-ignore -- yeoman-test type resolution differs between local and Docker environments
|
||||
import helpers from 'yeoman-test';
|
||||
// @ts-ignore -- fs-extra/esm has no type declarations
|
||||
import helpers, { result } from 'yeoman-test';
|
||||
import { copySync } from 'fs-extra/esm';
|
||||
import { fileURLToPath } from 'url';
|
||||
import pluginChartModule from '../generators/plugin-chart';
|
||||
|
||||
test('generator-superset:plugin-chart:creates files', async () => {
|
||||
const result = await helpers
|
||||
await helpers
|
||||
.run(pluginChartModule)
|
||||
.onTargetDirectory((dir: string) => {
|
||||
.onTargetDirectory(dir => {
|
||||
// `dir` is the path to the new temporary directory
|
||||
const generatorDirname = dirname(fileURLToPath(import.meta.url));
|
||||
copySync(
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@apache-superset/core",
|
||||
"version": "0.0.1-rc10",
|
||||
"version": "0.0.1-rc9",
|
||||
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
|
||||
@@ -202,7 +202,7 @@ test('serializeThemeConfig defaults to "default" for unknown algorithms', () =>
|
||||
const unknownAlgorithm = () => ({});
|
||||
const config: AntdThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
algorithm: unknownAlgorithm,
|
||||
};
|
||||
|
||||
@@ -237,7 +237,7 @@ test('serializeThemeConfig defaults each unknown algorithm in array to "default"
|
||||
const unknownAlgorithm = () => ({});
|
||||
const config: AntdThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
algorithm: [antdThemeImport.darkAlgorithm, unknownAlgorithm],
|
||||
};
|
||||
|
||||
@@ -257,10 +257,10 @@ test('serializeThemeConfig handles mixed known and unknown algorithms in array',
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: [
|
||||
antdThemeImport.darkAlgorithm,
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
unknownAlgorithm1,
|
||||
antdThemeImport.compactAlgorithm,
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
unknownAlgorithm2,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -129,7 +129,7 @@ test('returns empty array if timeseries_limit_metric is an empty array', () => {
|
||||
expect(
|
||||
extractExtraMetrics({
|
||||
...baseFormData,
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
timeseries_limit_metric: [],
|
||||
}),
|
||||
).toEqual([]);
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('defineSavedMetrics', () => {
|
||||
uuid: '1',
|
||||
},
|
||||
]);
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(defineSavedMetrics({ ...dataset, metrics: undefined })).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ test('getColorFunction BETWEEN with target value right undefined', () => {
|
||||
test('getColorFunction unsupported operator', () => {
|
||||
const colorFunction = getColorFunction(
|
||||
{
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
operator: 'unsupported operator',
|
||||
targetValue: 50,
|
||||
colorScheme: '#FF0000',
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"classnames": "^2.5.1",
|
||||
"csstype": "^3.2.3",
|
||||
"core-js": "^3.48.0",
|
||||
"d3-format": "^3.1.2",
|
||||
"d3-format": "^1.3.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
@@ -85,7 +85,7 @@
|
||||
"fetch-mock": "^12.6.0",
|
||||
"jest-mock-console": "^2.0.0",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"timezone-mock": "1.4.0"
|
||||
"timezone-mock": "1.3.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^5.26.0",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { t } from '@apache-superset/core';
|
||||
import { SupersetTheme } from '@apache-superset/core/ui';
|
||||
import { FallbackPropsWithDimension } from './SuperChart';
|
||||
import { getErrorMessage } from 'react-error-boundary';
|
||||
|
||||
export type Props = Partial<FallbackPropsWithDimension>;
|
||||
|
||||
@@ -38,13 +39,7 @@ export default function FallbackComponent({ error, height, width }: Props) {
|
||||
<div>
|
||||
<b>{t('Oops! An error occurred!')}</b>
|
||||
</div>
|
||||
<code>
|
||||
{error instanceof Error
|
||||
? error.message
|
||||
: error
|
||||
? String(error)
|
||||
: t('Unknown Error')}
|
||||
</code>
|
||||
<code>{error ? getErrorMessage(error) : 'Unknown Error'}</code>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -119,7 +119,7 @@ export function AsyncEsmComponent<
|
||||
const Component = component || placeholder;
|
||||
return Component ? (
|
||||
// placeholder does not get the ref
|
||||
// @ts-expect-error: Suppress TypeScript error for ref assignment
|
||||
// @ts-ignore: Suppress TypeScript error for ref assignment
|
||||
<Component ref={Component === component ? ref : null} {...props} />
|
||||
) : null;
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { Divider } from '../Divider';
|
||||
import { Input } from '../Input';
|
||||
import { CronPicker } from '.';
|
||||
@@ -28,19 +28,22 @@ export default {
|
||||
};
|
||||
|
||||
export const InteractiveCronPicker = (props: CronProps) => {
|
||||
// @ts-ignore
|
||||
const inputRef = useRef<Input>(null);
|
||||
const [value, setValue] = useState(props.value);
|
||||
useEffect(() => {
|
||||
setValue(props.value);
|
||||
}, [props.value]);
|
||||
const customSetValue = useCallback((newValue: string) => {
|
||||
setValue(newValue);
|
||||
}, []);
|
||||
const customSetValue = useCallback(
|
||||
(newValue: string) => {
|
||||
setValue(newValue);
|
||||
inputRef.current?.setValue(newValue);
|
||||
},
|
||||
[inputRef],
|
||||
);
|
||||
const [error, onError] = useState<CronError>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={value}
|
||||
ref={inputRef}
|
||||
onBlur={event => {
|
||||
setValue(event.target.value);
|
||||
}}
|
||||
|
||||
@@ -26,7 +26,7 @@ const props = {
|
||||
describe('NoAnimationDropdown', () => {
|
||||
it('requires children', () => {
|
||||
expect(() => {
|
||||
// @ts-expect-error need to test the error case
|
||||
// @ts-ignore need to test the error case
|
||||
render(<NoAnimationDropdown {...props} />);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
@@ -17,14 +17,13 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { render, fireEvent, screen } from '@superset-ui/core/spec';
|
||||
import type { LabeledErrorBoundInputProps } from './types';
|
||||
import { LabeledErrorBoundInput } from './LabeledErrorBoundInput';
|
||||
|
||||
const defaultProps: LabeledErrorBoundInputProps = {
|
||||
id: '1',
|
||||
const defaultProps = {
|
||||
id: 1,
|
||||
label: 'Username',
|
||||
name: 'Username',
|
||||
validationMethods: { onBlur: () => {} },
|
||||
validationMethods: () => {},
|
||||
errorMessage: '',
|
||||
helpText: 'This is a line of example help text',
|
||||
hasTooltip: false,
|
||||
@@ -36,7 +36,7 @@ test('works with an onClick handler', () => {
|
||||
|
||||
// test stories from the storybook!
|
||||
test('renders all the storybook gallery variants', () => {
|
||||
// @ts-expect-error: Suppress TypeScript error for LabelGallery usage
|
||||
// @ts-ignore: Suppress TypeScript error for LabelGallery usage
|
||||
const { container } = render(<LabelGallery />);
|
||||
const nonInteractiveLabelCount = 4;
|
||||
const renderedLabelCount = options.length * 2 + nonInteractiveLabelCount;
|
||||
|
||||
@@ -479,10 +479,10 @@ const AsyncSelect = forwardRef(
|
||||
fullSelectOptions.filter(opt => set.has(opt.value)),
|
||||
);
|
||||
if (isSingleMode) {
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
onChange?.(selectValue, options[0]);
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
onChange?.(array, options);
|
||||
}
|
||||
}
|
||||
@@ -619,7 +619,7 @@ const AsyncSelect = forwardRef(
|
||||
onBlur={handleOnBlur}
|
||||
onDeselect={handleOnDeselect}
|
||||
onOpenChange={handleOnDropdownVisibleChange}
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
onPaste={onPaste}
|
||||
onPopupScroll={handlePagination}
|
||||
onSearch={showSearch ? handleOnSearch : undefined}
|
||||
|
||||
@@ -748,7 +748,7 @@ const Select = forwardRef(
|
||||
onBlur={handleOnBlur}
|
||||
onDeselect={handleOnDeselect}
|
||||
onOpenChange={handleOnDropdownVisibleChange}
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
onPaste={onPaste}
|
||||
onPopupScroll={undefined}
|
||||
onSearch={shouldShowSearch ? handleOnSearch : undefined}
|
||||
|
||||
@@ -45,20 +45,20 @@ const rows = [
|
||||
* 1 or greater means the first item comes before the second item
|
||||
*/
|
||||
test('alphabeticalSort sorts correctly', () => {
|
||||
// @ts-expect-error
|
||||
expect(alphabeticalSort('name', rows[0], rows[1])).toBeLessThan(0);
|
||||
// @ts-expect-error
|
||||
expect(alphabeticalSort('name', rows[1], rows[0])).toBeGreaterThan(0);
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(alphabeticalSort('name', rows[0], rows[1])).toBe(-1);
|
||||
// @ts-ignore
|
||||
expect(alphabeticalSort('name', rows[1], rows[0])).toBe(1);
|
||||
// @ts-ignore
|
||||
expect(alphabeticalSort('category', rows[1], rows[0])).toBe(0);
|
||||
});
|
||||
|
||||
test('numericalSort sorts correctly', () => {
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(numericalSort('cost', rows[1], rows[2])).toBe(0);
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(numericalSort('cost', rows[1], rows[0])).toBeLessThan(0);
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(numericalSort('cost', rows[4], rows[1])).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -68,10 +68,10 @@ test('numericalSort sorts correctly', () => {
|
||||
* In the case the sorter cannot perform the comparison it should return undefined and the next sort step will proceed without error
|
||||
*/
|
||||
test('alphabeticalSort bad inputs no errors', () => {
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(alphabeticalSort('name', null, null)).toBe(undefined);
|
||||
// incorrect non-object values
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(alphabeticalSort('name', 3, [])).toBe(undefined);
|
||||
// incorrect object values without specified key
|
||||
expect(alphabeticalSort('name', {}, {})).toBe(undefined);
|
||||
@@ -79,7 +79,7 @@ test('alphabeticalSort bad inputs no errors', () => {
|
||||
expect(
|
||||
alphabeticalSort(
|
||||
'name',
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
{ name: { title: 'the name attribute should not be an object' } },
|
||||
{ name: 'Doug' },
|
||||
),
|
||||
@@ -87,22 +87,22 @@ test('alphabeticalSort bad inputs no errors', () => {
|
||||
});
|
||||
|
||||
test('numericalSort bad inputs no errors', () => {
|
||||
// @ts-expect-error
|
||||
expect(numericalSort('name', undefined, undefined)).toBeNaN();
|
||||
// @ts-expect-error
|
||||
expect(numericalSort('name', null, null)).toBeNaN();
|
||||
// @ts-ignore
|
||||
expect(numericalSort('name', undefined, undefined)).toBe(NaN);
|
||||
// @ts-ignore
|
||||
expect(numericalSort('name', null, null)).toBe(NaN);
|
||||
// incorrect non-object values
|
||||
// @ts-expect-error
|
||||
expect(numericalSort('name', 3, [])).toBeNaN();
|
||||
// @ts-ignore
|
||||
expect(numericalSort('name', 3, [])).toBe(NaN);
|
||||
// incorrect object values without specified key
|
||||
expect(numericalSort('name', {}, {})).toBeNaN();
|
||||
expect(numericalSort('name', {}, {})).toBe(NaN);
|
||||
// Object as value for name when it should be a string
|
||||
expect(
|
||||
numericalSort(
|
||||
'name',
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
{ name: { title: 'the name attribute should not be an object' } },
|
||||
{ name: 'Doug' },
|
||||
),
|
||||
).toBeNaN();
|
||||
).toBe(NaN);
|
||||
});
|
||||
|
||||
@@ -46,12 +46,12 @@ test('withinRange unsupported negative numbers', async () => {
|
||||
test('withinRange invalid inputs', async () => {
|
||||
// Invalid inputs should return falsy and not throw an error
|
||||
// We need ts-ignore here to be able to pass invalid values and pass linting
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(withinRange(null, 60, undefined)).toBeFalsy();
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(withinRange([], 'hello', {})).toBeFalsy();
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(withinRange([], undefined, {})).toBeFalsy();
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(withinRange([], 'hello', {})).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ beforeEach(() => {
|
||||
parent: { child: 'Nested Value 3' },
|
||||
},
|
||||
];
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
const tableHookResult = renderHook(() => useTable({ columns, data }));
|
||||
tableHook = tableHookResult.result.current;
|
||||
defaultProps = {
|
||||
|
||||
@@ -47,6 +47,7 @@ export const TelemetryPixel = ({
|
||||
const pixelPath = `https://apachesuperset.gateway.scarf.sh/pixel/${PIXEL_ID}/${version}/${sha}/${build}`;
|
||||
return process.env.SCARF_ANALYTICS === 'false' ? null : (
|
||||
<img
|
||||
// @ts-ignore
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
src={pixelPath}
|
||||
width={0}
|
||||
|
||||
@@ -241,7 +241,7 @@ test('uses queueMicrotask when available', async () => {
|
||||
|
||||
test('falls back to setTimeout when queueMicrotask is not available', async () => {
|
||||
const originalQueueMicrotask = global.queueMicrotask;
|
||||
// @ts-expect-error - temporarily remove queueMicrotask for testing
|
||||
// @ts-ignore - temporarily remove queueMicrotask for testing
|
||||
delete global.queueMicrotask;
|
||||
|
||||
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
||||
|
||||
@@ -109,7 +109,7 @@ export function evalExpression(expression: string, value: number): number {
|
||||
parsedExpression = subExpressions[1] ?? subExpressions[0];
|
||||
// we can ignore the type requirement on `TOKENS`, as value is always `number`
|
||||
// and doesn't need to consider `number | undefined`.
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
return Number(mexp.eval(parsedExpression, TOKENS, { x: value }));
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
*/
|
||||
|
||||
export default class ExtensibleFunction extends Function {
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
constructor(fn: Function) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, no-constructor-return
|
||||
return Object.setPrototypeOf(fn, new.target.prototype);
|
||||
|
||||
@@ -23,5 +23,4 @@ export const DEFAULT_D3_FORMAT: FormatLocaleDefinition = {
|
||||
thousands: ',',
|
||||
grouping: [3],
|
||||
currency: ['$', ''],
|
||||
minus: '-', // Use ASCII hyphen for backward compatibility (d3-format v3 defaults to Unicode minus sign)
|
||||
};
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { formatLocale, FormatLocaleDefinition } from 'd3-format';
|
||||
import {
|
||||
format as d3Format,
|
||||
formatLocale,
|
||||
FormatLocaleDefinition,
|
||||
} from 'd3-format';
|
||||
import { isRequired } from '../../utils';
|
||||
import NumberFormatter from '../NumberFormatter';
|
||||
import { NumberFormatFunction } from '../types';
|
||||
import { DEFAULT_D3_FORMAT } from '../D3FormatConfig';
|
||||
|
||||
export default function createD3NumberFormatter(config: {
|
||||
description?: string;
|
||||
@@ -39,7 +42,10 @@ export default function createD3NumberFormatter(config: {
|
||||
let isInvalid = false;
|
||||
|
||||
try {
|
||||
formatFunc = formatLocale(locale ?? DEFAULT_D3_FORMAT).format(formatString);
|
||||
formatFunc =
|
||||
typeof locale === 'undefined'
|
||||
? d3Format(formatString)
|
||||
: formatLocale(locale).format(formatString);
|
||||
} catch (error) {
|
||||
formatFunc = value => `${value} (Invalid format: ${formatString})`;
|
||||
isInvalid = true;
|
||||
|
||||
@@ -17,9 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { formatLocale } from 'd3-format';
|
||||
import { format as d3Format } from 'd3-format';
|
||||
import NumberFormatter from '../NumberFormatter';
|
||||
import { DEFAULT_D3_FORMAT } from '../D3FormatConfig';
|
||||
|
||||
export default function createSiAtMostNDigitFormatter(
|
||||
config: {
|
||||
@@ -30,8 +29,7 @@ export default function createSiAtMostNDigitFormatter(
|
||||
} = {},
|
||||
) {
|
||||
const { description, n = 3, id, label } = config;
|
||||
const locale = formatLocale(DEFAULT_D3_FORMAT);
|
||||
const siFormatter = locale.format(`.${n}s`);
|
||||
const siFormatter = d3Format(`.${n}s`);
|
||||
|
||||
return new NumberFormatter({
|
||||
description,
|
||||
|
||||
@@ -17,15 +17,13 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { formatLocale } from 'd3-format';
|
||||
import { format as d3Format } from 'd3-format';
|
||||
import NumberFormatter from '../NumberFormatter';
|
||||
import NumberFormats from '../NumberFormats';
|
||||
import { DEFAULT_D3_FORMAT } from '../D3FormatConfig';
|
||||
|
||||
const locale = formatLocale(DEFAULT_D3_FORMAT);
|
||||
const siFormatter = locale.format(`.3~s`);
|
||||
const float2PointFormatter = locale.format(`.2~f`);
|
||||
const float4PointFormatter = locale.format(`.4~f`);
|
||||
const siFormatter = d3Format(`.3~s`);
|
||||
const float2PointFormatter = d3Format(`.2~f`);
|
||||
const float4PointFormatter = d3Format(`.4~f`);
|
||||
|
||||
function formatValue(value: number) {
|
||||
if (value === 0) {
|
||||
|
||||
@@ -19,6 +19,15 @@
|
||||
|
||||
import { DatasourceType } from './types/Datasource';
|
||||
|
||||
const DATASOURCE_TYPE_MAP: Record<string, DatasourceType> = {
|
||||
table: DatasourceType.Table,
|
||||
query: DatasourceType.Query,
|
||||
dataset: DatasourceType.Dataset,
|
||||
sl_table: DatasourceType.SlTable,
|
||||
saved_query: DatasourceType.SavedQuery,
|
||||
semantic_view: DatasourceType.SemanticView,
|
||||
};
|
||||
|
||||
export default class DatasourceKey {
|
||||
readonly id: number;
|
||||
|
||||
@@ -27,8 +36,7 @@ export default class DatasourceKey {
|
||||
constructor(key: string) {
|
||||
const [idStr, typeStr] = key.split('__');
|
||||
this.id = parseInt(idStr, 10);
|
||||
this.type = DatasourceType.Table; // default to SqlaTable model
|
||||
this.type = typeStr === 'query' ? DatasourceType.Query : this.type;
|
||||
this.type = DATASOURCE_TYPE_MAP[typeStr] ?? DatasourceType.Table;
|
||||
}
|
||||
|
||||
public toString() {
|
||||
|
||||
@@ -63,6 +63,7 @@ export default function normalizeOrderBy(
|
||||
) {
|
||||
return {
|
||||
...cloneQueryObject,
|
||||
// @ts-ignore
|
||||
orderby: [[queryObject.legacy_order_by, isAsc]],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function overrideExtraFormData(
|
||||
);
|
||||
EXTRA_FORM_DATA_OVERRIDE_EXTRA_KEYS.forEach(key => {
|
||||
if (key in overrideFormData) {
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
overriddenExtras[key] = overrideFormData[key];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ export enum DatasourceType {
|
||||
Dataset = 'dataset',
|
||||
SlTable = 'sl_table',
|
||||
SavedQuery = 'saved_query',
|
||||
SemanticView = 'semantic_view',
|
||||
}
|
||||
|
||||
export interface Currency {
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('SuperChartCore', () => {
|
||||
});
|
||||
|
||||
it('does not render if chartType is not set', async () => {
|
||||
// @ts-expect-error chartType is required
|
||||
// @ts-ignore chartType is required
|
||||
const { container } = render(<SuperChartCore />);
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -129,7 +129,7 @@ describe('reactify(renderFn)', () => {
|
||||
it('does not try to render if not mounted', () => {
|
||||
const anotherRenderFn = jest.fn();
|
||||
const AnotherChart = reactify(anotherRenderFn); // enables valid new AnotherChart() call
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
new AnotherChart({ id: 'test' }).execute();
|
||||
expect(anotherRenderFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -529,13 +529,15 @@ describe('SupersetClientClass', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
originalLocation = window.location;
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
delete window.location;
|
||||
// @ts-ignore
|
||||
window.location = {
|
||||
pathname: mockRequestPath,
|
||||
// @ts-ignore
|
||||
search: mockRequestSearch,
|
||||
href: mockHref,
|
||||
} as unknown as Location;
|
||||
};
|
||||
authSpy = jest
|
||||
.spyOn(SupersetClientClass.prototype, 'ensureAuth')
|
||||
.mockImplementation();
|
||||
@@ -566,11 +568,13 @@ describe('SupersetClientClass', () => {
|
||||
it('should not redirect again if already on login page', async () => {
|
||||
const client = new SupersetClientClass({});
|
||||
|
||||
// @ts-ignore
|
||||
window.location = {
|
||||
href: '/login?next=something',
|
||||
pathname: '/login',
|
||||
// @ts-ignore
|
||||
search: '?next=something',
|
||||
} as unknown as Location;
|
||||
};
|
||||
|
||||
let error;
|
||||
try {
|
||||
|
||||
@@ -67,18 +67,18 @@ test('CurrencyFormatter:hasValidCurrency', () => {
|
||||
expect(currencyFormatter.hasValidCurrency()).toBe(true);
|
||||
|
||||
const currencyFormatterWithoutPosition = new CurrencyFormatter({
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
currency: { symbol: 'USD' },
|
||||
});
|
||||
expect(currencyFormatterWithoutPosition.hasValidCurrency()).toBe(true);
|
||||
|
||||
const currencyFormatterWithoutSymbol = new CurrencyFormatter({
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
currency: { symbolPosition: 'prefix' },
|
||||
});
|
||||
expect(currencyFormatterWithoutSymbol.hasValidCurrency()).toBe(false);
|
||||
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
const currencyFormatterWithoutCurrency = new CurrencyFormatter({});
|
||||
expect(currencyFormatterWithoutCurrency.hasValidCurrency()).toBe(false);
|
||||
});
|
||||
@@ -129,12 +129,12 @@ test('CurrencyFormatter:format', () => {
|
||||
expect(currencyFormatterWithSuffix(VALUE)).toEqual('56.1M $');
|
||||
|
||||
const currencyFormatterWithoutPosition = new CurrencyFormatter({
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
currency: { symbol: 'USD' },
|
||||
});
|
||||
expect(currencyFormatterWithoutPosition(VALUE)).toEqual('56.1M $');
|
||||
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
const currencyFormatterWithoutCurrency = new CurrencyFormatter({});
|
||||
expect(currencyFormatterWithoutCurrency(VALUE)).toEqual('56.1M');
|
||||
|
||||
|
||||
@@ -28,10 +28,10 @@ const textToWidth = {
|
||||
export const SAMPLE_TEXT = Object.keys(textToWidth);
|
||||
|
||||
export function addDummyFill() {
|
||||
// @ts-expect-error - fix jsdom
|
||||
// @ts-ignore - fix jsdom
|
||||
originalFn = SVGElement.prototype.getBBox;
|
||||
|
||||
// @ts-expect-error - fix jsdom
|
||||
// @ts-ignore - fix jsdom
|
||||
SVGElement.prototype.getBBox = function getBBox() {
|
||||
let width =
|
||||
textToWidth[this.textContent as keyof typeof textToWidth] || 200;
|
||||
@@ -78,6 +78,6 @@ export function addDummyFill() {
|
||||
}
|
||||
|
||||
export function removeDummyFill() {
|
||||
// @ts-expect-error - fix jsdom
|
||||
// @ts-ignore - fix jsdom
|
||||
SVGElement.prototype.getBBox = originalFn;
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ describe('mergeMargin(margin1, margin2, mode?)', () => {
|
||||
mergeMargin(
|
||||
{
|
||||
top: 10,
|
||||
// @ts-expect-error to let us pass `null` for testing
|
||||
// @ts-ignore to let us pass `null` for testing
|
||||
left: null,
|
||||
bottom: 20,
|
||||
right: NaN,
|
||||
|
||||
@@ -54,9 +54,9 @@ describe('ExtensibleFunction', () => {
|
||||
x: unknown;
|
||||
|
||||
constructor(x: unknown) {
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
super(function customName() {
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
return customName.x;
|
||||
}); // named function
|
||||
this.x = x;
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('NumberFormatter', () => {
|
||||
it('requires config.id', () => {
|
||||
expect(
|
||||
() =>
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
new NumberFormatter({
|
||||
formatFunc: () => '',
|
||||
}),
|
||||
@@ -33,7 +33,7 @@ describe('NumberFormatter', () => {
|
||||
it('requires config.formatFunc', () => {
|
||||
expect(
|
||||
() =>
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
new NumberFormatter({
|
||||
id: 'my_format',
|
||||
}),
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('NumberFormatterRegistry', () => {
|
||||
});
|
||||
it('falls back to default format if format is null', () => {
|
||||
registry.setDefaultKey('.1f');
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
const formatter = registry.get(null);
|
||||
expect(formatter.format(100)).toEqual('100.0');
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ import { createD3NumberFormatter } from '@superset-ui/core';
|
||||
|
||||
describe('createD3NumberFormatter(config)', () => {
|
||||
it('requires config.formatString', () => {
|
||||
// @ts-expect-error -- intentionally pass invalid input
|
||||
// @ts-ignore -- intentionally pass invalid input
|
||||
expect(() => createD3NumberFormatter({})).toThrow();
|
||||
});
|
||||
describe('config.formatString', () => {
|
||||
@@ -85,18 +85,4 @@ describe('createD3NumberFormatter(config)', () => {
|
||||
expect(formatter(200)).toEqual('€200.00');
|
||||
});
|
||||
});
|
||||
describe('negative numbers', () => {
|
||||
it('uses ASCII hyphen-minus (U+002D) for negative numbers, not Unicode minus (U+2212)', () => {
|
||||
const formatter = createD3NumberFormatter({ formatString: ',d' });
|
||||
const result = formatter(-1234);
|
||||
// Verify the result contains ASCII hyphen-minus (char code 45), not Unicode minus (char code 8722)
|
||||
// This is important for backward compatibility after d3-format v3 upgrade
|
||||
expect(result).toEqual('-1,234');
|
||||
expect(result.charCodeAt(0)).toEqual(45); // ASCII hyphen-minus
|
||||
});
|
||||
it('formats negative decimals correctly', () => {
|
||||
const formatter = createD3NumberFormatter({ formatString: ',.2f' });
|
||||
expect(formatter(-1234.5)).toEqual('-1,234.50');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,12 +27,6 @@ describe('createSiAtMostNDigitFormatter({ n })', () => {
|
||||
const formatter = createSiAtMostNDigitFormatter({ n: 4 });
|
||||
expect(formatter).toBeInstanceOf(NumberFormatter);
|
||||
});
|
||||
it('uses ASCII hyphen-minus (U+002D) for negative numbers, not Unicode minus (U+2212)', () => {
|
||||
// This is important for backward compatibility after d3-format v3 upgrade
|
||||
const formatter = createSiAtMostNDigitFormatter({ n: 3 });
|
||||
const result = formatter(-1000);
|
||||
expect(result.charCodeAt(0)).toBe(45); // ASCII hyphen-minus
|
||||
});
|
||||
it('when n is specified, it formats number in SI format with at most n significant digits', () => {
|
||||
const formatter = createSiAtMostNDigitFormatter({ n: 2 });
|
||||
expect(formatter(10)).toBe('10');
|
||||
|
||||
@@ -66,11 +66,6 @@ describe('createSmartNumberFormatter(options)', () => {
|
||||
});
|
||||
});
|
||||
describe('for negative numbers', () => {
|
||||
it('uses ASCII hyphen-minus (U+002D), not Unicode minus (U+2212)', () => {
|
||||
// This is important for backward compatibility after d3-format v3 upgrade
|
||||
const result = formatter(-1000);
|
||||
expect(result.charCodeAt(0)).toBe(45); // ASCII hyphen-minus
|
||||
});
|
||||
it('formats billion with B in stead of G', () => {
|
||||
expect(formatter(-1000000000)).toBe('-1B');
|
||||
expect(formatter(-4560000000)).toBe('-4.56B');
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('makeApi()', () => {
|
||||
makeApi({
|
||||
method: 'POST',
|
||||
endpoint: '/test-formdata',
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
requestType: 'text',
|
||||
});
|
||||
}).toThrow('Invalid request payload type');
|
||||
|
||||
@@ -120,7 +120,7 @@ test('Handles Response that contains raw html be parsed as text', async () => {
|
||||
test('Handles TypeError Response', async () => {
|
||||
const error = new TypeError('Failed to fetch');
|
||||
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
const errorObj = await getClientErrorObject(error);
|
||||
expect(errorObj).toMatchObject({ error: 'Network error' });
|
||||
});
|
||||
@@ -184,15 +184,15 @@ test('Handles error with status text and message', async () => {
|
||||
const statusText = 'status';
|
||||
const message = 'message';
|
||||
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(await getClientErrorObject({ statusText, message })).toMatchObject({
|
||||
error: statusText,
|
||||
});
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(await getClientErrorObject({ message })).toMatchObject({
|
||||
error: message,
|
||||
});
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(await getClientErrorObject({})).toMatchObject({
|
||||
error: 'An error occurred',
|
||||
});
|
||||
|
||||
@@ -183,7 +183,7 @@ describe('normalizeOrderBy', () => {
|
||||
datasource: '5__table',
|
||||
viz_type: VizType.Table,
|
||||
time_range: '1 year ago : 2013',
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
orderby: [['count(*)', 'true']],
|
||||
};
|
||||
expect(normalizeOrderBy(query)).not.toHaveProperty('orderby');
|
||||
|
||||
@@ -39,8 +39,8 @@ const runTimezoneTest = (
|
||||
expected_result: string[],
|
||||
includeFutureOffsets = true,
|
||||
) => {
|
||||
timezoneMock.register(timezone);
|
||||
jest.setSystemTime(new Date(now_time));
|
||||
timezoneMock.register(timezone);
|
||||
const result = getTimeOffset({
|
||||
timeRangeFilter,
|
||||
shifts,
|
||||
@@ -53,8 +53,8 @@ const runTimezoneTest = (
|
||||
|
||||
test('should handle includeFutureOffsets is null', () => {
|
||||
jest.useFakeTimers();
|
||||
timezoneMock.register('Etc/GMT-2');
|
||||
jest.setSystemTime(new Date(NOW_UTC_IN_EUROPE));
|
||||
timezoneMock.register('Etc/GMT-2');
|
||||
const result = getTimeOffset({
|
||||
timeRangeFilter: {
|
||||
comparator: '2024-06-03 : 2024-06-10',
|
||||
@@ -109,7 +109,7 @@ test('should handle custom range with relative dates (now)', () => {
|
||||
timeRangeFilter,
|
||||
shifts,
|
||||
startDate,
|
||||
['5 days ago'],
|
||||
['4 days ago'],
|
||||
);
|
||||
runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
|
||||
'4 days ago',
|
||||
@@ -218,7 +218,7 @@ test('should handle null timeRangeFilter', () => {
|
||||
timeRangeFilter,
|
||||
shifts,
|
||||
startDate,
|
||||
['3 days ago'],
|
||||
['2 days ago'],
|
||||
);
|
||||
runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
|
||||
'2 days ago',
|
||||
@@ -304,7 +304,7 @@ test('should handle custom range with DATEADD function and relative start date',
|
||||
timeRangeFilter,
|
||||
shifts,
|
||||
startDate,
|
||||
['3 days ago'],
|
||||
['2 days ago'],
|
||||
);
|
||||
runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
|
||||
'2 days ago',
|
||||
@@ -362,7 +362,7 @@ test('should handle custom range with specific date and relative end date', () =
|
||||
timeRangeFilter,
|
||||
shifts,
|
||||
startDate,
|
||||
['8 days ago'],
|
||||
['9 days ago'],
|
||||
);
|
||||
runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
|
||||
'9 days ago',
|
||||
@@ -390,7 +390,7 @@ test('should handle custom range with specific date and specific end date', () =
|
||||
timeRangeFilter,
|
||||
shifts,
|
||||
startDate,
|
||||
['1 days ago'],
|
||||
['2 days ago'],
|
||||
);
|
||||
runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
|
||||
'2 days ago',
|
||||
@@ -474,7 +474,7 @@ test('should handle custom range with previous calendar week', () => {
|
||||
timeRangeFilter,
|
||||
shifts,
|
||||
startDate,
|
||||
['3 days ago'],
|
||||
['1 days ago'],
|
||||
);
|
||||
runTimezoneTest(
|
||||
'2024-06-05T00:06:00Z',
|
||||
@@ -507,7 +507,7 @@ test('should handle custom range with previous calendar month', () => {
|
||||
timeRangeFilter,
|
||||
shifts,
|
||||
startDate,
|
||||
['7 days ago'],
|
||||
['5 days ago'],
|
||||
);
|
||||
runTimezoneTest(
|
||||
'2024-06-05T00:06:00Z',
|
||||
@@ -541,7 +541,7 @@ test('should handle custom range with previous calendar year', () => {
|
||||
timeRangeFilter,
|
||||
shifts,
|
||||
startDate,
|
||||
['8 days ago'],
|
||||
['6 days ago'],
|
||||
);
|
||||
runTimezoneTest(
|
||||
'2024-06-05T00:06:00Z',
|
||||
@@ -760,7 +760,7 @@ test('should handle future custom shift with different format', () => {
|
||||
timeRangeFilter,
|
||||
shifts,
|
||||
startDate,
|
||||
['3 days after'],
|
||||
['4 days after'],
|
||||
);
|
||||
runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
|
||||
'4 days after',
|
||||
@@ -850,7 +850,7 @@ test('should handle custom range with relative dates (hour)', () => {
|
||||
timeRangeFilter,
|
||||
shifts,
|
||||
startDate,
|
||||
['2 days ago'],
|
||||
['4 days ago'],
|
||||
);
|
||||
runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
|
||||
'4 days ago',
|
||||
|
||||
@@ -38,8 +38,8 @@ const runTimezoneTest = (
|
||||
endDate = false,
|
||||
computingShift = false,
|
||||
) => {
|
||||
timezoneMock.register(timezone);
|
||||
jest.setSystemTime(new Date(now_time));
|
||||
timezoneMock.register(timezone);
|
||||
expect(parseDttmToDate(eval_time, endDate, computingShift)).toEqual(
|
||||
expected_result,
|
||||
);
|
||||
@@ -69,9 +69,9 @@ test('should return the current date for "today"', () => {
|
||||
'today',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-06-01T22:00:00Z'),
|
||||
new Date('2024-06-03T00:00:00+02:00'),
|
||||
);
|
||||
runTimezoneTest('today', NOW_IN_UTC, 'UTC', new Date('2024-06-02T00:00:00Z'));
|
||||
runTimezoneTest('today', NOW_IN_UTC, 'UTC', new Date('2024-06-03T00:00:00Z'));
|
||||
runTimezoneTest(
|
||||
'today',
|
||||
NOW_UTC_IN_PACIFIC,
|
||||
@@ -125,13 +125,13 @@ test('should return yesterday date for "Last day"', () => {
|
||||
'Last day',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-05-31T22:00:00Z'),
|
||||
new Date('2024-06-01T22:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last day',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-06-01T00:00:00Z'),
|
||||
new Date('2024-06-02T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last day',
|
||||
@@ -147,13 +147,13 @@ test('should return the date one week ago for "Last week"', () => {
|
||||
'Last week',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-05-25T22:00:00Z'),
|
||||
new Date('2024-05-26T22:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last week',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-05-26T00:00:00Z'),
|
||||
new Date('2024-05-27T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last week',
|
||||
@@ -169,13 +169,13 @@ test('should return the date one month ago for "Last month"', () => {
|
||||
'Last month',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-05-01T22:00:00Z'),
|
||||
new Date('2024-05-02T22:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last month',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-05-02T00:00:00Z'),
|
||||
new Date('2024-05-03T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last month',
|
||||
@@ -191,13 +191,13 @@ test('should return the date three months ago for "Last quarter"', () => {
|
||||
'Last quarter',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-03-01T22:00:00Z'),
|
||||
new Date('2024-03-02T22:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last quarter',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-03-02T00:00:00Z'),
|
||||
new Date('2024-03-03T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last quarter',
|
||||
@@ -213,13 +213,13 @@ test('should return the date one year ago for "Last year"', () => {
|
||||
'Last year',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2023-06-01T22:00:00Z'),
|
||||
new Date('2023-06-02T22:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last year',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2023-06-02T00:00:00Z'),
|
||||
new Date('2023-06-03T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last year',
|
||||
@@ -235,19 +235,19 @@ test('should return the date for "previous calendar week"', () => {
|
||||
'previous calendar week',
|
||||
'2024-06-04T22:00:00Z',
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-05-27T16:00:00Z'),
|
||||
new Date('2024-05-26T22:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'previous calendar week',
|
||||
'2024-06-05T00:00:00Z',
|
||||
'UTC',
|
||||
new Date('2024-05-27T20:00:00Z'),
|
||||
new Date('2024-05-27T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'previous calendar week',
|
||||
'2024-06-05T08:00:00Z',
|
||||
'Etc/GMT+8',
|
||||
new Date('2024-05-27T12:00:00Z'),
|
||||
new Date('2024-05-27T08:00:00Z'),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -257,19 +257,19 @@ test('should return the date for "previous calendar month"', () => {
|
||||
'previous calendar month',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-05-01T10:00:00Z'),
|
||||
new Date('2024-04-30T22:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'previous calendar month',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-05-01T16:00:00Z'),
|
||||
new Date('2024-05-01T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'previous calendar month',
|
||||
NOW_UTC_IN_PACIFIC,
|
||||
'Etc/GMT+8',
|
||||
new Date('2024-05-01T16:00:00Z'),
|
||||
new Date('2024-05-01T08:00:00Z'),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -279,19 +279,19 @@ test('should return the date for "previous calendar year"', () => {
|
||||
'previous calendar year',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2023-01-01T16:00:00Z'),
|
||||
new Date('2022-12-31T22:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'previous calendar year',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2023-01-01T20:00:00Z'),
|
||||
new Date('2023-01-01T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'previous calendar year',
|
||||
NOW_UTC_IN_PACIFIC,
|
||||
'Etc/GMT+8',
|
||||
new Date('2023-01-01T12:00:00Z'),
|
||||
new Date('2023-01-01T08:00:00Z'),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -301,13 +301,13 @@ test('should return the date for "1 day ago"', () => {
|
||||
'1 day ago',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-05-31T22:00:00Z'),
|
||||
new Date('2024-06-01T22:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'1 day ago',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-06-01T00:00:00Z'),
|
||||
new Date('2024-06-02T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'1 day ago',
|
||||
@@ -323,13 +323,13 @@ test('should return the date for "1 week ago"', () => {
|
||||
'1 week ago',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-05-25T22:00:00Z'),
|
||||
new Date('2024-05-26T22:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'1 week ago',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-05-26T00:00:00Z'),
|
||||
new Date('2024-05-27T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'1 week ago',
|
||||
@@ -345,13 +345,13 @@ test('should return the date for "1 month ago"', () => {
|
||||
'1 month ago',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-05-01T22:00:00Z'),
|
||||
new Date('2024-05-02T22:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'1 month ago',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-05-02T00:00:00Z'),
|
||||
new Date('2024-05-03T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'1 month ago',
|
||||
@@ -367,13 +367,13 @@ test('should return the date for "1 year ago"', () => {
|
||||
'1 year ago',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2023-06-01T22:00:00Z'),
|
||||
new Date('2023-06-02T22:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'1 year ago',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2023-06-02T00:00:00Z'),
|
||||
new Date('2023-06-03T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'1 year ago',
|
||||
@@ -389,13 +389,13 @@ test('should return the date for "2024-03-09"', () => {
|
||||
'2024-03-09',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-03-07T22:00:00.000Z'),
|
||||
new Date('2024-03-08T22:00:00.000Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'2024-03-09',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-03-08T00:00:00.000Z'),
|
||||
new Date('2024-03-09T00:00:00.000Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'2024-03-09',
|
||||
@@ -411,14 +411,14 @@ test('should return the current date for "Last day" with isEndDate true', () =>
|
||||
'Last day',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-06-01T22:00:00Z'),
|
||||
new Date('2024-06-02T22:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last day',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-06-02T00:00:00Z'),
|
||||
new Date('2024-06-03T00:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
@@ -436,14 +436,14 @@ test('should return the current date for "Last week" with isEndDate true', () =>
|
||||
'Last week',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-06-01T22:00:00Z'),
|
||||
new Date('2024-06-02T22:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last week',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-06-02T00:00:00Z'),
|
||||
new Date('2024-06-03T00:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
@@ -461,14 +461,14 @@ test('should return the current date for "Last quarter" with isEndDate true', ()
|
||||
'Last quarter',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-06-01T22:00:00Z'),
|
||||
new Date('2024-06-02T22:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last quarter',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-06-02T00:00:00Z'),
|
||||
new Date('2024-06-03T00:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
@@ -486,14 +486,14 @@ test('should return the current date for "Last year" with isEndDate true', () =>
|
||||
'Last year',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-06-01T22:00:00Z'),
|
||||
new Date('2024-06-02T22:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
'Last year',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-06-02T00:00:00Z'),
|
||||
new Date('2024-06-03T00:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
@@ -511,21 +511,21 @@ test('should return the date for "previous calendar week" with isEndDate true',
|
||||
'previous calendar week',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-06-03T16:00:00Z'),
|
||||
new Date('2024-06-02T22:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
'previous calendar week',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-06-03T20:00:00Z'),
|
||||
new Date('2024-06-03T00:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
'previous calendar week',
|
||||
NOW_UTC_IN_PACIFIC,
|
||||
'Etc/GMT+8',
|
||||
new Date('2024-06-03T12:00:00Z'),
|
||||
new Date('2024-06-03T08:00:00Z'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -536,21 +536,21 @@ test('should return the date for "previous calendar month" with isEndDate true',
|
||||
'previous calendar month',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-06-01T16:00:00Z'),
|
||||
new Date('2024-05-31T22:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
'previous calendar month',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-06-01T20:00:00Z'),
|
||||
new Date('2024-06-01T00:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
'previous calendar month',
|
||||
NOW_UTC_IN_PACIFIC,
|
||||
'Etc/GMT+8',
|
||||
new Date('2024-06-01T12:00:00Z'),
|
||||
new Date('2024-06-01T08:00:00Z'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -561,21 +561,21 @@ test('should return the date for "previous calendar year" with isEndDate true',
|
||||
'previous calendar year',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-01-01T16:00:00Z'),
|
||||
new Date('2023-12-31T22:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
'previous calendar year',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-01-01T20:00:00Z'),
|
||||
new Date('2024-01-01T00:00:00Z'),
|
||||
true,
|
||||
);
|
||||
runTimezoneTest(
|
||||
'previous calendar year',
|
||||
NOW_UTC_IN_PACIFIC,
|
||||
'Etc/GMT+8',
|
||||
new Date('2024-01-01T12:00:00Z'),
|
||||
new Date('2024-01-01T08:00:00Z'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -586,9 +586,9 @@ test('should return the date for "2024" with parts.length === 1', () => {
|
||||
'2024',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2023-12-30T22:00:00.000Z'),
|
||||
new Date('2023-12-31T22:00:00.000Z'),
|
||||
);
|
||||
runTimezoneTest('2024', NOW_IN_UTC, 'UTC', new Date('2023-12-31T00:00:00Z'));
|
||||
runTimezoneTest('2024', NOW_IN_UTC, 'UTC', new Date('2024-01-01T00:00:00Z'));
|
||||
runTimezoneTest(
|
||||
'2024',
|
||||
NOW_UTC_IN_PACIFIC,
|
||||
@@ -603,13 +603,13 @@ test('should return the date for "2024-03" with parts.length === 2', () => {
|
||||
'2024-03',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-02-28T22:00:00.000Z'),
|
||||
new Date('2024-02-29T22:00:00.000Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'2024-03',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-02-29T00:00:00Z'),
|
||||
new Date('2024-03-01T00:00:00Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'2024-03',
|
||||
@@ -625,13 +625,13 @@ test('should return the date for "2024-03-06" with parts.length === 3', () => {
|
||||
'2024-03-06',
|
||||
NOW_UTC_IN_EUROPE,
|
||||
'Etc/GMT-2',
|
||||
new Date('2024-03-04T22:00:00.000Z'),
|
||||
new Date('2024-03-05T22:00:00.000Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'2024-03-06',
|
||||
NOW_IN_UTC,
|
||||
'UTC',
|
||||
new Date('2024-03-05T00:00:00.000Z'),
|
||||
new Date('2024-03-06T00:00:00.000Z'),
|
||||
);
|
||||
runTimezoneTest(
|
||||
'2024-03-06',
|
||||
@@ -643,7 +643,7 @@ test('should return the date for "2024-03-06" with parts.length === 3', () => {
|
||||
|
||||
test('should return the date for "2024-03-06" with computingShifts true', () => {
|
||||
jest.useFakeTimers();
|
||||
const expectedDate = new Date('2024-03-05T22:00:00Z');
|
||||
const expectedDate = new Date('2024-03-06T22:00:00Z');
|
||||
expectedDate.setHours(-expectedDate.getTimezoneOffset() / 60, 0, 0, 0);
|
||||
runTimezoneTest(
|
||||
'2024-03-06',
|
||||
@@ -657,7 +657,7 @@ test('should return the date for "2024-03-06" with computingShifts true', () =>
|
||||
|
||||
test('should return the date for "2024-03-06" with computingShifts true and isEndDate true', () => {
|
||||
jest.useFakeTimers();
|
||||
const expectedDate = new Date('2024-03-05T22:00:00Z');
|
||||
const expectedDate = new Date('2024-03-06T22:00:00Z');
|
||||
expectedDate.setHours(-expectedDate.getTimezoneOffset() / 60, 0, 0, 0);
|
||||
runTimezoneTest(
|
||||
'2024-03-06',
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('TimeFormatter', () => {
|
||||
it('requires config.id', () => {
|
||||
expect(
|
||||
() =>
|
||||
// @ts-expect-error -- intentionally pass invalid input
|
||||
// @ts-ignore -- intentionally pass invalid input
|
||||
new TimeFormatter({
|
||||
formatFunc: () => 'test',
|
||||
}),
|
||||
@@ -33,7 +33,7 @@ describe('TimeFormatter', () => {
|
||||
it('requires config.formatFunc', () => {
|
||||
expect(
|
||||
() =>
|
||||
// @ts-expect-error -- intentionally pass invalid input
|
||||
// @ts-ignore -- intentionally pass invalid input
|
||||
new TimeFormatter({
|
||||
id: 'my_format',
|
||||
}),
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('TimeFormatterRegistry', () => {
|
||||
});
|
||||
it('falls back to default format if format is null', () => {
|
||||
registry.setDefaultKey(TimeFormats.INTERNATIONAL_DATE);
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
const formatter = registry.get(null);
|
||||
expect(formatter.format(PREVIEW_TIME)).toEqual('14/02/2017');
|
||||
});
|
||||
|
||||
@@ -71,9 +71,9 @@ const thLocale: TimeLocaleDefinition = {
|
||||
|
||||
describe('createD3TimeFormatter(config)', () => {
|
||||
it('requires config.formatString', () => {
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(() => createD3TimeFormatter()).toThrow();
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
expect(() => createD3TimeFormatter({})).toThrow();
|
||||
});
|
||||
describe('config.useLocalTime', () => {
|
||||
|
||||
@@ -27,7 +27,7 @@ test('Returns null if Selection object is null', () => {
|
||||
test('Returns selection text if Selection object is not null', () => {
|
||||
jest
|
||||
.spyOn(window, 'getSelection')
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
.mockImplementationOnce(() => ({ toString: () => 'test string' }));
|
||||
expect(getSelectedText()).toEqual('test string');
|
||||
jest.restoreAllMocks();
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
TableChartFormData,
|
||||
TableChartProps,
|
||||
} from '@superset-ui/plugin-chart-table';
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line import/extensions
|
||||
// @ts-ignore -- TS6307: this file is outside the tsconfig project scope, @ts-expect-error does not suppress project-level errors
|
||||
import birthNamesJson from './birthNames.json';
|
||||
|
||||
export const birthNames = birthNamesJson as unknown as TableChartProps;
|
||||
|
||||
@@ -16,48 +16,46 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import { extent as d3Extent, range as d3Range } from 'd3-array';
|
||||
import { select as d3Select } from 'd3-selection';
|
||||
import { getSequentialSchemeRegistry } from '@superset-ui/core';
|
||||
import { SupersetTheme, t } from '@apache-superset/core/ui';
|
||||
import CalHeatMapImport from './vendor/cal-heatmap';
|
||||
import { t } from '@apache-superset/core/ui';
|
||||
import CalHeatMap from './vendor/cal-heatmap';
|
||||
import { convertUTCTimestampToLocal } from './utils';
|
||||
|
||||
// The vendor file is @ts-nocheck, so its export lacks type info.
|
||||
// Define a minimal constructor interface for use in this file.
|
||||
interface CalHeatMapInstance {
|
||||
init(config: Record<string, unknown>): void;
|
||||
}
|
||||
const CalHeatMap = CalHeatMapImport as unknown as new () => CalHeatMapInstance;
|
||||
const propTypes = {
|
||||
data: PropTypes.shape({
|
||||
// Object hashed by metric name,
|
||||
// then hashed by timestamp (in seconds, not milliseconds) as float
|
||||
// the innermost value is count
|
||||
// e.g. { count_distinct_something: { 1535034236.0: 3 } }
|
||||
data: PropTypes.object,
|
||||
domain: PropTypes.string,
|
||||
range: PropTypes.number,
|
||||
// timestamp in milliseconds
|
||||
start: PropTypes.number,
|
||||
subdomain: PropTypes.string,
|
||||
}),
|
||||
height: PropTypes.number,
|
||||
// eslint-disable-next-line react/sort-prop-types
|
||||
cellPadding: PropTypes.number,
|
||||
// eslint-disable-next-line react/sort-prop-types
|
||||
cellRadius: PropTypes.number,
|
||||
// eslint-disable-next-line react/sort-prop-types
|
||||
cellSize: PropTypes.number,
|
||||
linearColorScheme: PropTypes.string,
|
||||
showLegend: PropTypes.bool,
|
||||
showMetricName: PropTypes.bool,
|
||||
showValues: PropTypes.bool,
|
||||
steps: PropTypes.number,
|
||||
timeFormatter: PropTypes.func,
|
||||
valueFormatter: PropTypes.func,
|
||||
verboseMap: PropTypes.object,
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
interface CalendarData {
|
||||
data: Record<string, Record<string, number>>;
|
||||
domain: string;
|
||||
range: number;
|
||||
start: number;
|
||||
subdomain: string;
|
||||
}
|
||||
|
||||
interface CalendarProps {
|
||||
data: CalendarData;
|
||||
height: number;
|
||||
cellPadding?: number;
|
||||
cellRadius?: number;
|
||||
cellSize?: number;
|
||||
domainGranularity: string;
|
||||
linearColorScheme: string;
|
||||
showLegend: boolean;
|
||||
showMetricName: boolean;
|
||||
showValues: boolean;
|
||||
steps: number;
|
||||
subdomainGranularity: string;
|
||||
timeFormatter: (ts: number | string) => string;
|
||||
valueFormatter: (value: number) => string;
|
||||
verboseMap: Record<string, string>;
|
||||
theme: SupersetTheme;
|
||||
}
|
||||
|
||||
function Calendar(element: HTMLElement, props: CalendarProps) {
|
||||
function Calendar(element, props) {
|
||||
const {
|
||||
data,
|
||||
height,
|
||||
@@ -84,7 +82,7 @@ function Calendar(element: HTMLElement, props: CalendarProps) {
|
||||
const div = container.append('div');
|
||||
|
||||
const subDomainTextFormat = showValues
|
||||
? (_date: Date, value: number) => valueFormatter(value)
|
||||
? (date, value) => valueFormatter(value)
|
||||
: null;
|
||||
|
||||
const metricsData = data.data;
|
||||
@@ -97,21 +95,11 @@ function Calendar(element: HTMLElement, props: CalendarProps) {
|
||||
calContainer.text(`${METRIC_TEXT}: ${verboseMap[metric] || metric}`);
|
||||
}
|
||||
const timestamps = metricsData[metric];
|
||||
const rawExtents = d3Extent(
|
||||
Object.keys(timestamps),
|
||||
key => timestamps[key],
|
||||
);
|
||||
// Guard against undefined extents (empty data)
|
||||
const extents: [number, number] =
|
||||
rawExtents[0] != null && rawExtents[1] != null
|
||||
? [rawExtents[0], rawExtents[1]]
|
||||
: [0, 1];
|
||||
// Guard against division by zero when steps <= 1
|
||||
const step = steps > 1 ? (extents[1] - extents[0]) / (steps - 1) : 0;
|
||||
const colorScheme = getSequentialSchemeRegistry().get(linearColorScheme);
|
||||
const colorScale = colorScheme
|
||||
? colorScheme.createLinearScale(extents)
|
||||
: (v: number) => '#ccc'; // fallback if scheme not found
|
||||
const extents = d3Extent(Object.keys(timestamps), key => timestamps[key]);
|
||||
const step = (extents[1] - extents[0]) / (steps - 1);
|
||||
const colorScale = getSequentialSchemeRegistry()
|
||||
.get(linearColorScheme)
|
||||
.createLinearScale(extents);
|
||||
|
||||
const legend = d3Range(steps).map(i => extents[0] + step * i);
|
||||
const legendColors = legend.map(x => colorScale(x));
|
||||
@@ -150,5 +138,6 @@ function Calendar(element: HTMLElement, props: CalendarProps) {
|
||||
}
|
||||
|
||||
Calendar.displayName = 'Calendar';
|
||||
Calendar.propTypes = propTypes;
|
||||
|
||||
export default Calendar;
|
||||
@@ -16,27 +16,15 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import { reactify } from '@superset-ui/core';
|
||||
import { styled, css, useTheme } from '@apache-superset/core/ui';
|
||||
import { Global } from '@emotion/react';
|
||||
import Component from './Calendar';
|
||||
|
||||
// Type-erase the render function to allow flexible prop spreading in the wrapper.
|
||||
// The Calendar render function has typed props, but the wrapper passes props via spread
|
||||
// which TypeScript cannot verify at compile time. Props are validated at runtime.
|
||||
const ReactComponent = reactify(
|
||||
Component as unknown as (
|
||||
container: HTMLDivElement,
|
||||
props: Record<string, unknown>,
|
||||
) => void,
|
||||
);
|
||||
const ReactComponent = reactify(Component);
|
||||
|
||||
interface CalendarWrapperProps {
|
||||
className?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const Calendar = ({ className, ...otherProps }: CalendarWrapperProps) => {
|
||||
const Calendar = ({ className, ...otherProps }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div className={className}>
|
||||
@@ -100,6 +88,15 @@ const Calendar = ({ className, ...otherProps }: CalendarWrapperProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
Calendar.defaultProps = {
|
||||
otherProps: {},
|
||||
};
|
||||
|
||||
Calendar.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
otherProps: PropTypes.objectOf(PropTypes.any),
|
||||
};
|
||||
|
||||
export default styled(Calendar)`
|
||||
${({ theme }) => `
|
||||
.superset-legacy-chart-calendar {
|
||||
@@ -17,10 +17,10 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ChartProps, getNumberFormatter } from '@superset-ui/core';
|
||||
import { getNumberFormatter } from '@superset-ui/core';
|
||||
import { getFormattedUTCTime } from './utils';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
export default function transformProps(chartProps) {
|
||||
const { height, formData, queriesData, datasource } = chartProps;
|
||||
const {
|
||||
cellPadding,
|
||||
@@ -38,8 +38,7 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
} = formData;
|
||||
|
||||
const { verboseMap } = datasource;
|
||||
const timeFormatter = (ts: number | string) =>
|
||||
getFormattedUTCTime(ts, xAxisTimeFormat);
|
||||
const timeFormatter = ts => getFormattedUTCTime(ts, xAxisTimeFormat);
|
||||
const valueFormatter = getNumberFormatter(yAxisFormat);
|
||||
|
||||
return {
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-nocheck
|
||||
// [LICENSE TBD]
|
||||
/* Copied and altered from http://cal-heatmap.com/ , alterations around:
|
||||
* - tuning tooltips
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
@@ -19,26 +18,24 @@
|
||||
*/
|
||||
/* eslint-disable no-param-reassign, react/sort-prop-types */
|
||||
import d3 from 'd3';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
getNumberFormatter,
|
||||
CategoricalColorNamespace,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
interface ChordData {
|
||||
matrix: number[][];
|
||||
nodes: string[];
|
||||
}
|
||||
const propTypes = {
|
||||
data: PropTypes.shape({
|
||||
matrix: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)),
|
||||
nodes: PropTypes.arrayOf(PropTypes.string),
|
||||
}),
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
colorScheme: PropTypes.string,
|
||||
numberFormat: PropTypes.string,
|
||||
};
|
||||
|
||||
interface ChordProps {
|
||||
data: ChordData;
|
||||
width: number;
|
||||
height: number;
|
||||
colorScheme: string;
|
||||
numberFormat: string;
|
||||
sliceId: number;
|
||||
}
|
||||
|
||||
function Chord(element: HTMLElement, props: ChordProps) {
|
||||
function Chord(element, props) {
|
||||
const { data, width, height, numberFormat, colorScheme, sliceId } = props;
|
||||
|
||||
element.innerHTML = '';
|
||||
@@ -52,10 +49,7 @@ function Chord(element: HTMLElement, props: ChordProps) {
|
||||
const outerRadius = Math.min(width, height) / 2 - 10;
|
||||
const innerRadius = outerRadius - 24;
|
||||
|
||||
// d3 v3 data-bound selections use any for the datum generic parameter
|
||||
// because the d3 v3 typings cannot represent the actual chord layout data types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let chord: d3.Selection<any>;
|
||||
let chord;
|
||||
|
||||
const arc = d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius);
|
||||
|
||||
@@ -98,7 +92,6 @@ function Chord(element: HTMLElement, props: ChordProps) {
|
||||
const groupPath = group
|
||||
.append('path')
|
||||
.attr('id', (d, i) => `group${i}`)
|
||||
// @ts-expect-error -- d3 v3 arc layout is callable at runtime but not typed as Primitive
|
||||
.attr('d', arc)
|
||||
.style('fill', (d, i) => colorFn(nodes[i], sliceId));
|
||||
|
||||
@@ -111,10 +104,9 @@ function Chord(element: HTMLElement, props: ChordProps) {
|
||||
.text((d, i) => nodes[i]);
|
||||
// Remove the labels that don't fit. :(
|
||||
groupText
|
||||
.filter(function filter(this: SVGTextElement, d, i) {
|
||||
.filter(function filter(d, i) {
|
||||
return (
|
||||
(groupPath[0][i] as SVGPathElement).getTotalLength() / 2 - 16 <
|
||||
this.getComputedTextLength()
|
||||
groupPath[0][i].getTotalLength() / 2 - 16 < this.getComputedTextLength()
|
||||
);
|
||||
})
|
||||
.remove();
|
||||
@@ -130,7 +122,6 @@ function Chord(element: HTMLElement, props: ChordProps) {
|
||||
chord.classed('fade', p => p !== d);
|
||||
})
|
||||
.style('fill', d => colorFn(nodes[d.source.index], sliceId))
|
||||
// @ts-expect-error -- d3 v3 chord layout is callable at runtime but not typed as Primitive
|
||||
.attr('d', path);
|
||||
|
||||
// Add an elaborate mouseover title for each chord.
|
||||
@@ -138,14 +129,15 @@ function Chord(element: HTMLElement, props: ChordProps) {
|
||||
.append('title')
|
||||
.text(
|
||||
d =>
|
||||
`${nodes[d.source.index]} \u2192 ${nodes[d.target.index]}: ${f(
|
||||
`${nodes[d.source.index]} → ${nodes[d.target.index]}: ${f(
|
||||
d.target.value,
|
||||
)}\n${nodes[d.target.index]} \u2192 ${nodes[d.source.index]}: ${f(
|
||||
)}\n${nodes[d.target.index]} → ${nodes[d.source.index]}: ${f(
|
||||
d.source.value,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
Chord.displayName = 'Chord';
|
||||
Chord.propTypes = propTypes;
|
||||
|
||||
export default Chord;
|
||||
@@ -18,29 +18,26 @@
|
||||
*/
|
||||
import { reactify } from '@superset-ui/core';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import Component from './Chord';
|
||||
|
||||
// Type-erase the render function to allow flexible prop spreading in the wrapper.
|
||||
// The Chord render function has typed props, but the wrapper passes props via spread
|
||||
// which TypeScript cannot verify at compile time. Props are validated at runtime.
|
||||
const ReactComponent = reactify(
|
||||
Component as unknown as (
|
||||
container: HTMLDivElement,
|
||||
props: Record<string, unknown>,
|
||||
) => void,
|
||||
);
|
||||
const ReactComponent = reactify(Component);
|
||||
|
||||
interface ChordWrapperProps {
|
||||
className?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const Chord = ({ className, ...otherProps }: ChordWrapperProps) => (
|
||||
const Chord = ({ className, ...otherProps }) => (
|
||||
<div className={className}>
|
||||
<ReactComponent {...otherProps} />
|
||||
</div>
|
||||
);
|
||||
|
||||
Chord.defaultProps = {
|
||||
otherProps: {},
|
||||
};
|
||||
|
||||
Chord.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
otherProps: PropTypes.objectOf(PropTypes.any),
|
||||
};
|
||||
|
||||
export default styled(Chord)`
|
||||
${({ theme }) => `
|
||||
.superset-legacy-chart-chord svg #circle circle {
|
||||
@@ -16,9 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
export default function transformProps(chartProps) {
|
||||
const { width, height, formData, queriesData } = chartProps;
|
||||
const { yAxisFormat, colorScheme, sliceId } = formData;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
@@ -19,6 +18,7 @@
|
||||
*/
|
||||
/* eslint-disable react/sort-prop-types */
|
||||
import d3 from 'd3';
|
||||
import PropTypes from 'prop-types';
|
||||
import { extent as d3Extent } from 'd3-array';
|
||||
import {
|
||||
getNumberFormatter,
|
||||
@@ -27,47 +27,25 @@ import {
|
||||
} from '@superset-ui/core';
|
||||
import countries, { countryOptions } from './countries';
|
||||
|
||||
/**
|
||||
* Escape HTML special characters to prevent XSS attacks
|
||||
*/
|
||||
function escapeHtml(text: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
const propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
country_id: PropTypes.string,
|
||||
metric: PropTypes.number,
|
||||
}),
|
||||
),
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
country: PropTypes.string,
|
||||
colorScheme: PropTypes.string,
|
||||
linearColorScheme: PropTypes.string,
|
||||
mapBaseUrl: PropTypes.string,
|
||||
numberFormat: PropTypes.string,
|
||||
};
|
||||
|
||||
interface CountryMapDataItem {
|
||||
country_id: string;
|
||||
metric: number;
|
||||
}
|
||||
const maps = {};
|
||||
|
||||
interface GeoFeature {
|
||||
properties: {
|
||||
ISO: string;
|
||||
ID_2?: string;
|
||||
NAME_1?: string;
|
||||
NAME_2?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GeoData {
|
||||
features: GeoFeature[];
|
||||
}
|
||||
|
||||
interface CountryMapProps {
|
||||
data: CountryMapDataItem[];
|
||||
width: number;
|
||||
height: number;
|
||||
country: string;
|
||||
linearColorScheme: string;
|
||||
numberFormat: string;
|
||||
colorScheme: string;
|
||||
sliceId: number;
|
||||
}
|
||||
|
||||
const maps: Record<string, GeoData> = {};
|
||||
|
||||
function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
function CountryMap(element, props) {
|
||||
const {
|
||||
data,
|
||||
width,
|
||||
@@ -81,24 +59,18 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
|
||||
const container = element;
|
||||
const format = getNumberFormatter(numberFormat);
|
||||
const rawExtents = d3Extent(data, v => v.metric);
|
||||
const extents: [number, number] =
|
||||
rawExtents[0] != null && rawExtents[1] != null
|
||||
? [rawExtents[0], rawExtents[1]]
|
||||
: [0, 1];
|
||||
const colorSchemeObj = getSequentialSchemeRegistry().get(linearColorScheme);
|
||||
const linearColorScale = colorSchemeObj
|
||||
? colorSchemeObj.createLinearScale(extents)
|
||||
: () => '#ccc'; // fallback if scheme not found
|
||||
const linearColorScale = getSequentialSchemeRegistry()
|
||||
.get(linearColorScheme)
|
||||
.createLinearScale(d3Extent(data, v => v.metric));
|
||||
const colorScale = CategoricalColorNamespace.getScale(colorScheme);
|
||||
|
||||
const colorMap: Record<string, string> = {};
|
||||
const colorMap = {};
|
||||
data.forEach(d => {
|
||||
colorMap[d.country_id] = colorScheme
|
||||
? colorScale(d.country_id, sliceId)
|
||||
: (linearColorScale(d.metric) ?? '');
|
||||
: linearColorScale(d.metric);
|
||||
});
|
||||
const colorFn = (d: GeoFeature) => colorMap[d.properties.ISO] || 'none';
|
||||
const colorFn = d => colorMap[d.properties.ISO] || 'none';
|
||||
|
||||
const path = d3.geo.path();
|
||||
const div = d3.select(container);
|
||||
@@ -120,13 +92,13 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
const mapLayer = g.append('g').classed('map-layer', true);
|
||||
const hoverPopup = div.append('div').attr('class', 'hover-popup');
|
||||
|
||||
let centered: GeoFeature | null;
|
||||
let centered;
|
||||
|
||||
const clicked = function clicked(d: GeoFeature) {
|
||||
const clicked = function clicked(d) {
|
||||
const hasCenter = d && centered !== d;
|
||||
let x: number;
|
||||
let y: number;
|
||||
let k: number;
|
||||
let x;
|
||||
let y;
|
||||
let k;
|
||||
const halfWidth = width / 2;
|
||||
const halfHeight = height / 2;
|
||||
|
||||
@@ -152,21 +124,19 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
|
||||
backgroundRect.on('click', clicked);
|
||||
|
||||
const getNameOfRegion = function getNameOfRegion(
|
||||
feature: GeoFeature,
|
||||
): string {
|
||||
const getNameOfRegion = function getNameOfRegion(feature) {
|
||||
if (feature && feature.properties) {
|
||||
if (feature.properties.ID_2) {
|
||||
return feature.properties.NAME_2 || '';
|
||||
return feature.properties.NAME_2;
|
||||
}
|
||||
return feature.properties.NAME_1 || '';
|
||||
return feature.properties.NAME_1;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const mouseenter = function mouseenter(this: SVGPathElement, d: GeoFeature) {
|
||||
const mouseenter = function mouseenter(d) {
|
||||
// Darken color
|
||||
let c: string = colorFn(d);
|
||||
let c = colorFn(d);
|
||||
if (c !== 'none') {
|
||||
c = d3.rgb(c).darker().toString();
|
||||
}
|
||||
@@ -193,12 +163,12 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
.style('left', `${position[0]}px`);
|
||||
};
|
||||
|
||||
const mouseout = function mouseout(this: SVGPathElement) {
|
||||
const mouseout = function mouseout() {
|
||||
d3.select(this).style('fill', colorFn);
|
||||
hoverPopup.style('display', 'none');
|
||||
};
|
||||
|
||||
function drawMap(mapData: GeoData) {
|
||||
function drawMap(mapData) {
|
||||
const { features } = mapData;
|
||||
const center = d3.geo.centroid(mapData);
|
||||
const scale = 100;
|
||||
@@ -243,21 +213,13 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
if (map) {
|
||||
drawMap(map);
|
||||
} else {
|
||||
const url = (countries as Record<string, string>)[country];
|
||||
if (!url) {
|
||||
const countryName =
|
||||
countryOptions.find(x => x[0] === country)?.[1] || country;
|
||||
d3.select(element).html(
|
||||
`<div class="alert alert-danger">No map data available for ${escapeHtml(countryName)}</div>`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
d3.json(url, (error: unknown, mapData: GeoData) => {
|
||||
const url = countries[country];
|
||||
d3.json(url, (error, mapData) => {
|
||||
if (error) {
|
||||
const countryName =
|
||||
countryOptions.find(x => x[0] === country)?.[1] || country;
|
||||
d3.select(element).html(
|
||||
`<div class="alert alert-danger">Could not load map data for ${escapeHtml(countryName)}</div>`,
|
||||
`<div class="alert alert-danger">Could not load map data for ${countryName}</div>`,
|
||||
);
|
||||
} else {
|
||||
maps[country] = mapData;
|
||||
@@ -268,5 +230,6 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
}
|
||||
|
||||
CountryMap.displayName = 'CountryMap';
|
||||
CountryMap.propTypes = propTypes;
|
||||
|
||||
export default CountryMap;
|
||||
@@ -20,25 +20,9 @@ import { reactify } from '@superset-ui/core';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
import Component from './CountryMap';
|
||||
|
||||
// Type-erase the render function to allow flexible prop spreading in the wrapper.
|
||||
// The CountryMap render function has typed props, but the wrapper passes props via spread
|
||||
// which TypeScript cannot verify at compile time. Props are validated at runtime.
|
||||
const ReactComponent = reactify(
|
||||
Component as unknown as (
|
||||
container: HTMLDivElement,
|
||||
props: Record<string, unknown>,
|
||||
) => void,
|
||||
);
|
||||
const ReactComponent = reactify(Component);
|
||||
|
||||
interface CountryMapWrapperProps {
|
||||
className?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const CountryMap = ({
|
||||
className = '',
|
||||
...otherProps
|
||||
}: CountryMapWrapperProps) => (
|
||||
const CountryMap = ({ className = '', ...otherProps }) => (
|
||||
<div className={className}>
|
||||
<ReactComponent {...otherProps} />
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user