Compare commits

..
Author SHA1 Message Date
Joe Li e1ef4f06cc Merge remote-tracking branch 'origin/master' into HEAD 2026-08-17 07:09:31 -07:00
Endi Monan a8216e3787 fix(calendar): localize date labels (#43163) 2026-08-17 09:44:14 -03:00
anopsandEvan Rusackas d114eb638b fix(datasets): preserve metric warning_markdown when extra is absent (#42763)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-16 00:22:24 -07:00
Abdrihman Hussein 70d06e3d77 docs(faq): convert flask fab freeze question to a proper heading (#43221) 2026-08-15 23:21:13 +07:00
Joe Li a2c004266b fix(ci): update vulnerable transitive nanoid (#43166) 2026-08-15 23:09:19 +07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Đỗ Trọng Hải
2773bc94eb chore(deps-dev): bump sqlalchemy-bigquery from 1.17.1 to 1.17.2 (#43209)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-08-15 00:59:55 -07:00
94459ae885 ci: declare top-level permissions on the remaining workflows (#43197)
Co-authored-by: bikashJMV <bikash@jmv.co.in>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-08-15 14:48:43 +07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ad3103fdb0 chore(deps-dev): bump fastmcp from 3.4.5 to 3.4.7 (#43210)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 00:43:03 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
065342f8c9 chore(deps-dev): bump pandas-gbq from 0.35.0 to 0.35.1 (#43212)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 00:42:19 -07:00
Joe Li 74a364dc13 Merge remote-tracking branch 'origin/master' into HEAD 2026-08-14 07:05:15 -07:00
Joe Li e4bb1bc825 Merge remote-tracking branch 'origin/master' into HEAD 2026-08-13 07:04:11 -07:00
Joe Li 05938977ab Merge remote-tracking branch 'origin/master' into HEAD 2026-08-12 07:05:21 -07:00
Joe Li 411f25859e Merge remote-tracking branch 'origin/master' into HEAD 2026-08-11 07:04:36 -07:00
Joe Li 8bd06c462c Merge remote-tracking branch 'origin/master' into HEAD 2026-08-10 07:05:32 -07:00
Joe Li fd95e60abd chore: fix spacing in AGENTS.md 2026-08-07 09:02:41 -07:00
31 changed files with 356 additions and 29 deletions
@@ -21,6 +21,9 @@ on:
#schedule:
# - cron: '0 0 * * *' # Runs daily at midnight UTC
permissions:
contents: read
jobs:
bump-python-package:
runs-on: ubuntu-slim
@@ -16,6 +16,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
check_db_migration_conflict:
name: Check DB migration conflict
+3
View File
@@ -14,6 +14,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
changes:
runs-on: ubuntu-26.04
@@ -33,6 +33,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
enforce-single-migration-head:
runs-on: ubuntu-26.04
+3
View File
@@ -7,6 +7,9 @@ on:
pull_request:
types: [opened, edited]
permissions:
contents: read
jobs:
superbot-orglabel:
runs-on: ubuntu-slim
+3
View File
@@ -7,6 +7,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
labeler:
permissions:
+3
View File
@@ -3,6 +3,9 @@ on:
release:
types: [published] # This makes it run only when a new released is published
permissions:
contents: read
jobs:
latest-release:
name: Add/update tag to new release
+3
View File
@@ -19,6 +19,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: false
permissions:
contents: read
jobs:
lint-check:
runs-on: ubuntu-slim
+3
View File
@@ -17,6 +17,9 @@ env:
GITHUB_ORG: ${{ github.repository_owner }}
GITHUB_REPO: ${{ github.event.repository.name }}
permissions:
contents: read
jobs:
cleanup-expired:
name: Clean up expired showtime environments
+3
View File
@@ -26,6 +26,9 @@ env:
GITHUB_REPO: ${{ github.event.repository.name }}
GITHUB_ACTOR: ${{ github.actor }}
permissions:
contents: read
jobs:
sync:
name: 🎪 Sync PR to desired state
+3
View File
@@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
changes:
runs-on: ubuntu-26.04
@@ -27,6 +27,9 @@ concurrency:
group: helm-release
cancel-in-progress: false
permissions:
contents: read
jobs:
release:
runs-on: ubuntu-26.04
@@ -22,6 +22,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
changes:
runs-on: ubuntu-26.04
@@ -14,6 +14,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
changes:
runs-on: ubuntu-26.04
+3
View File
@@ -13,6 +13,9 @@ on:
required: true
type: string
permissions:
contents: read
jobs:
supersetbot:
runs-on: ubuntu-26.04
+3
View File
@@ -5,6 +5,9 @@ on:
pull_request_target:
types: [opened]
permissions:
contents: read
jobs:
welcome:
runs-on: ubuntu-slim
+1 -1
View File
@@ -304,7 +304,7 @@ pre-commit run eslint # Frontend linting
## Platform-Specific Instructions
- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
- **[GEMINI.md](GEMINI.md)** - For Google Gemini tools
- **[GPT.md](GPT.md)** - For OpenAI/ChatGPT tools
- **[.cursor/rules/dev-standard.mdc](.cursor/rules/dev-standard.mdc)** - For Cursor editor
@@ -97,6 +97,54 @@ for more information on how to configure it.
At the very least, you'll want to change `SECRET_KEY` and `SQLALCHEMY_DATABASE_URI`. Continue reading for more about each of these.
## Localizing D3 date and time labels
`BABEL_DEFAULT_LOCALE` controls Superset's application translations, while
`D3_TIME_FORMAT` provides localized date and time names to visualizations that
use the D3 formatter registry, including Calendar Heatmap. Configure both when
you want the application and chart labels to use the same locale.
`D3_TIME_FORMAT` accepts partial overrides. For example, Russian month names
can be configured in `superset_config.py` as follows:
```python
BABEL_DEFAULT_LOCALE = "ru"
D3_TIME_FORMAT = {
"months": [
"Январь",
"Февраль",
"Март",
"Апрель",
"Май",
"Июнь",
"Июль",
"Август",
"Сентябрь",
"Октябрь",
"Ноябрь",
"Декабрь",
],
"shortMonths": [
"Янв",
"Фев",
"Мар",
"Апр",
"Май",
"Июн",
"Июл",
"Авг",
"Сен",
"Окт",
"Ноя",
"Дек",
],
}
```
Restart Superset after changing `superset_config.py` so the frontend receives
the updated formatter configuration.
## Chart-data query timing
Set `CHART_DATA_INCLUDE_TIMING = True` to add an optional versioned timing object
+49 -5
View File
@@ -46,13 +46,43 @@ import FAQSchema from '@site/src/components/FAQSchema';
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: 'How to limit the timed refresh on a dashboard?',
answer:
'To exclude specific slices from the timed refresh process, add the timed_refresh_immune_slices key to the dashboard JSON Metadata field with the slice IDs to exclude.',
},
{
question: "Why does 'flask fab' or Superset freeze, hang, or not respond when started (my home directory is NFS mounted)?",
answer:
'By default, Superset creates and uses an SQLite database at ~/.superset/superset.db. SQLite is known to not work well if used on NFS due to broken file locking. Override the path with the SUPERSET_HOME environment variable or change SQLALCHEMY_DATABASE_URI in superset_config.py.',
},
{
question: 'What if the table schema changed?',
answer:
'Go to Data -> Datasets, click the edit icon next to the dataset whose schema has changed, and hit Sync columns from source from the Columns tab. The new columns will get merged.',
},
{
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?',
question: 'How can I configure OAuth authentication and authorization?',
answer:
'Take a look at the Flask-AppBuilder OAuth configuration example, which shows how to configure OAuth authentication and authorization in Superset.',
},
{
question: 'Is there a way to force the dashboard to use specific colors?',
answer:
'It is possible on a per-dashboard basis by providing a mapping of labels to colors in the JSON Metadata attribute using the label_colors key. You can use a full hex color, a named color, or the index in the current color palette.',
},
{
question: 'How do I expand all chart descriptions on a dashboard by default?',
answer:
'Add the expand_all_slices key to the dashboard JSON Metadata field with a value of true. Charts that have already been manually expanded or collapsed keep that per-chart override regardless of the setting.',
},
{
question: 'Does Superset work with [insert database engine here]?',
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.',
},
@@ -61,15 +91,30 @@ import FAQSchema from '@site/src/components/FAQSchema';
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: 'How can I see usage statistics (e.g., monthly active users)?',
answer:
'This functionality is not included with Superset, but you can extract and analyze the logs table in Superset\u2019s metadata database to see what actions have occurred.',
},
{
question: 'What does Hours Offset in the Edit Dataset view do?',
answer:
'In the Edit Dataset view, the hours offset lets you configure the number of hours to be added or subtracted from the time column. This can be used, for example, to convert UTC time to local time.',
},
{
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?',
question: 'Does Superset have an archive panel or trash bin from which a user can 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.',
'No. Currently, there is no way to recover a deleted Superset dashboard, chart, dataset, or database from the UI. It is recommended to take periodic backups of the metadata database and use export functionality for recovery.',
},
{
question: 'I ran a security scan of the Superset container image and it showed dozens of "high" and "critical" vulnerabilities! Can you release a version of Superset without these?',
answer:
'These are dependency CVEs in software that Superset uses, mostly in the Linux kernel or Python. Superset addresses them by regularly updating dependencies and welcomes pull requests that fix dependency CVEs. The Superset security team focuses primarily on vulnerabilities in Superset itself.',
},
]}
/>
@@ -214,8 +259,7 @@ frontend falls back to a built-in default of `5000` milliseconds.
SUPERSET_DASHBOARD_MANUAL_REFRESH_STAGGER_MS = 0
```
**Why does flask fab or superset freeze/hang/not responding when started (my home directory is
NFS mounted)?**
## Why does `flask fab` or Superset freeze, hang, or not respond when started (my home directory is NFS mounted)?
By default, Superset creates and uses an SQLite database at `~/.superset/superset.db`. SQLite is
known to [not work well if used on NFS](https://www.sqlite.org/lockingv3.html) due to broken file
+3 -3
View File
@@ -135,10 +135,10 @@ athena = ["pyathena[pandas]>=3.35.4, <4"]
# superset/db_engine_specs/aurora.py's known_incompatibilities metadata.
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.35.0",
"pandas-gbq>=0.35.1",
# 1.17.1 is likely the final release: googleapis/python-bigquery-sqlalchemy
# was archived 2026-05-16. Both 1.17.0 and 1.17.1 support SQLAlchemy 1.4/2.0.
"sqlalchemy-bigquery>=1.17.1",
"sqlalchemy-bigquery>=1.17.2",
"google-cloud-bigquery>=3.42.3",
]
clickhouse = ["clickhouse-connect>=1.6.0, <2.0"]
@@ -185,7 +185,7 @@ excel = ["xlrd>=2.0.2, <2.1"]
# installing this extra is only required to actually run exports.
excel-export = ["boto3"]
fastmcp = [
"fastmcp>=3.4.5,<4.0",
"fastmcp>=3.4.6,<4.0",
# tiktoken backs the response-size-guard token estimator. Without
# it, the middleware falls back to a coarser character-based
# heuristic that under-counts JSON-heavy MCP responses.
+4 -4
View File
@@ -239,9 +239,9 @@ et-xmlfile==2.0.0
# openpyxl
exceptiongroup==1.3.0
# via fastmcp-slim
fastmcp==3.4.5
fastmcp==3.4.7
# via apache-superset
fastmcp-slim==3.4.5
fastmcp-slim==3.4.7
# via fastmcp
filelock==3.20.3
# via
@@ -640,7 +640,7 @@ pandas==2.3.3
# db-dtypes
# pandas-gbq
# prophet
pandas-gbq==0.35.0
pandas-gbq==0.35.1
# via apache-superset
parameterized==0.9.0
# via apache-superset
@@ -964,7 +964,7 @@ sqlalchemy==2.0.51
# sqlalchemy-bigquery
# sqlalchemy-continuum
# sqlalchemy-utils
sqlalchemy-bigquery==1.17.1
sqlalchemy-bigquery==1.17.2
# via apache-superset
sqlalchemy-continuum==1.7.0
# via
+1 -1
View File
@@ -14,7 +14,7 @@
"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.
under the License
-->
# Change Log
+6 -6
View File
@@ -33246,9 +33246,9 @@
"license": "MIT"
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"dev": true,
"funding": [
{
@@ -34955,9 +34955,9 @@
}
},
"node_modules/react-checkbox-tree/node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"funding": [
{
"type": "github",
+1
View File
@@ -415,6 +415,7 @@
"minimatch@>=10": {
"brace-expansion": ">=5.0.8"
},
"nanoid@>=3 <4": "3.3.18",
"nwsapi": "^2.2.13",
"puppeteer": "^22.4.1",
"tar": "^7.5.16",
-4
View File
@@ -47,10 +47,6 @@ export default defineConfig({
// Retry logic - 2 retries in CI, 0 locally
retries: process.env.CI ? 2 : 0,
// Disable capturing Git commit info as the project's history is increasingly dense
// and breach Playwright's default 3-seconds `git` command timeout limit
captureGitInfo: { commit: false, diff: false },
// Reporter configuration - multiple reporters for better visibility
reporter: process.env.CI
? [
@@ -22,7 +22,7 @@ import { getSequentialSchemeRegistry } from '@superset-ui/core';
import { SupersetTheme } from '@apache-superset/core/theme';
import { t } from '@apache-superset/core/translation';
import CalHeatMapImport from './vendor/cal-heatmap';
import { convertUTCTimestampToLocal } from './utils';
import { convertUTCTimestampToLocal, getFormattedUTCTime } from './utils';
// The vendor file is @ts-nocheck, so its export lacks type info.
// Define a minimal constructor interface for use in this file.
@@ -103,6 +103,8 @@ function Calendar(element: HTMLElement, props: CalendarProps) {
const subDomainTextFormat = showValues
? (_date: Date, value: number) => valueFormatter(value)
: null;
const dateFormatter = (date: Date, format: string) =>
getFormattedUTCTime(date.getTime(), format);
const metricsData = data.data;
@@ -166,6 +168,7 @@ function Calendar(element: HTMLElement, props: CalendarProps) {
itemName: '',
valueFormatter,
timeFormatter,
dateFormatter,
subDomainTextFormat,
});
});
@@ -76,6 +76,8 @@ var CalHeatMap = function () {
timeFormatter: d => d,
dateFormatter: null,
domain: 'hour',
subDomain: 'min',
@@ -1990,10 +1992,14 @@ CalHeatMap.prototype = {
if (typeof format === 'function') {
return format(d);
} else {
var f = d3.time.format(format);
return f(d);
}
if (typeof this.options.dateFormatter === 'function') {
return this.options.dateFormatter(d, format);
}
var f = d3.time.format(format);
return f(d);
},
getSubDomainTitle: function (d) {
@@ -25,9 +25,11 @@ import {
waitFor,
} from 'spec/helpers/testing-library';
import { CALENDAR_TOOLTIP_CLASS } from '../src/tooltip';
import { convertUTCTimestampToLocal } from '../src/utils';
interface MockCalHeatMapConfig {
itemSelector: Element;
dateFormatter?: (date: Date, format: string) => string;
}
type MetricNameInput = string | string[];
@@ -38,6 +40,7 @@ let mockInitCallCount = 0;
let mockThrowOnInitCall: number | null = null;
let mockDestroyCallCount = 0;
let mockDestroyedInstanceIds: string[] = [];
let mockDateFormatter: MockCalHeatMapConfig['dateFormatter'];
const mockTheme = {
colorBgElevated: '#ffffff',
@@ -56,6 +59,7 @@ jest.mock('../src/vendor/cal-heatmap', () => ({
} = require('../src/tooltip');
mockInitCallCount += 1;
mockDateFormatter = config.dateFormatter;
if (mockThrowOnInitCall === mockInitCallCount) {
throw new Error('Mock CalHeatMap init failure');
}
@@ -284,9 +288,28 @@ afterEach(() => {
mockThrowOnInitCall = null;
mockDestroyCallCount = 0;
mockDestroyedInstanceIds = [];
mockDateFormatter = undefined;
document.body.innerHTML = '';
});
test('Calendar provides a timezone-safe date formatter to CalHeatMap', () => {
const calendarOwner = document.createElement('div');
document.body.appendChild(calendarOwner);
Calendar(calendarOwner, {
...createCalendarProps('localized-metric'),
theme: mockTheme,
});
if (!mockDateFormatter) {
throw new Error('Expected Calendar to configure a date formatter');
}
const localDate = new Date(convertUTCTimestampToLocal(Date.UTC(2024, 0, 1)));
expect(mockDateFormatter(localDate, '%Y-%m-%d')).toBe('2024-01-01');
});
test('rerender and unmount clean up only the affected calendar tooltips', () => {
jest.useFakeTimers();
@@ -0,0 +1,61 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import CalHeatMapImport from '../src/vendor/cal-heatmap';
type DateFormatter = (date: Date, format: string) => string;
type FunctionalDateFormat = (date: Date) => string;
interface CalHeatMapInstance {
options: {
dateFormatter: DateFormatter | null;
};
formatDate(date: Date, format: string | FunctionalDateFormat): string;
}
const CalHeatMap = CalHeatMapImport as unknown as new () => CalHeatMapInstance;
test('CalHeatMap delegates string date formats to the configured formatter', () => {
const calendar = new CalHeatMap();
const date = new Date(2024, 0, 1);
const dateFormatter = jest.fn<string, [Date, string]>(() => 'Январь');
calendar.options.dateFormatter = dateFormatter;
expect(calendar.formatDate(date, '%B')).toBe('Январь');
expect(dateFormatter).toHaveBeenCalledWith(date, '%B');
});
test('CalHeatMap preserves functional formatters over the configured formatter', () => {
const calendar = new CalHeatMap();
const date = new Date(2024, 0, 1);
const dateFormatter = jest.fn<string, [Date, string]>(() => 'localized');
const functionalFormat = jest.fn<string, [Date]>(() => 'custom');
calendar.options.dateFormatter = dateFormatter;
expect(calendar.formatDate(date, functionalFormat)).toBe('custom');
expect(functionalFormat).toHaveBeenCalledWith(date);
expect(dateFormatter).not.toHaveBeenCalled();
});
test('CalHeatMap keeps the D3 formatter fallback', () => {
const calendar = new CalHeatMap();
const date = new Date(2024, 0, 1);
expect(calendar.formatDate(date, '%B')).toBe('January');
});
@@ -867,7 +867,7 @@ function DatasourceEditor({
return {
...metric,
certification_details: certificationDetails || details,
warning_markdown: warningMarkdown || '',
warning_markdown: warningMarkdown || metric.warning_markdown || '',
certified_by: certifiedBy || certifiedByMetric,
};
}),
@@ -0,0 +1,97 @@
/**
* 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 fetchMock from 'fetch-mock';
import { screen, userEvent } from 'spec/helpers/testing-library';
import {
createProps,
DATASOURCE_ENDPOINT,
setupDatasourceEditorMocks,
cleanupAsyncOperations,
fastRender,
dismissDatasourceWarning,
} from './DatasourceEditor.test.utils';
// Stub the Ace-backed control with a plain textarea. Ace spreads its document
// across many spans and keeps only the keystroke buffer in its own textarea,
// so asserting on the value the control receives is less brittle than
// reaching into Ace's DOM.
jest.mock('src/explore/components/controls/TextAreaControl', () => ({
__esModule: true,
default: ({
controlId,
value,
onChange,
}: {
controlId?: string;
value?: string;
onChange?: (value: string) => void;
}) => (
<textarea
data-test={`mock-textarea-${controlId}`}
value={value ?? ''}
onChange={event => onChange?.(event.target.value)}
/>
),
}));
beforeEach(() => {
fetchMock.get(DATASOURCE_ENDPOINT, [], { name: DATASOURCE_ENDPOINT });
setupDatasourceEditorMocks();
});
afterEach(async () => {
await cleanupAsyncOperations();
fetchMock.clearHistory().removeRoutes();
});
// Regression test for #42704. Explore's datasource payload (SqlMetric.data on
// the backend) exposes warning_markdown as a flattened top-level field and
// omits the raw `extra` JSON string that the /api/v1/dataset/{id} endpoint
// backing the Datasets page provides. Deriving warning_markdown purely from
// `extra` therefore dropped the saved text when the modal was opened from
// Explore, leaving the Warning field blank on reopen.
test('keeps a pre-existing top-level warning_markdown when the metric has no extra', async () => {
const baseProps = createProps();
const testProps = {
...baseProps,
datasource: {
...baseProps.datasource,
metrics: [
{
...baseProps.datasource.metrics[0],
warning_markdown: 'existing warning',
extra: undefined,
},
],
},
};
fastRender(testProps);
await dismissDatasourceWarning();
const metricsTab = await screen.findByTestId('collection-tab-Metrics');
await userEvent.click(metricsTab);
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
expect(
await screen.findByTestId('mock-textarea-warning_markdown'),
).toHaveValue('existing warning');
});