Compare commits

..

1 Commits

Author SHA1 Message Date
Evan
3d1c08976c chore(ci): correct codeql-action version pin comment to v4.36.2
The pinned SHA 8aad20d150bbac5944a9f9d289da16a4b0d87c1e resolves to
release v4.36.2, but the trailing comment claimed the generic "v4"
major tag. zizmor's ref-version-mismatch flags this because the
comment must truthfully name the version the SHA points to, matching
the full-patch-version convention used for every other pinned action
in this repo (e.g. actions/checkout # v7.0.0).

Resolves code-scanning alert #2561

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:34:25 -07:00
24 changed files with 353 additions and 1878 deletions

View File

@@ -64,7 +64,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -75,6 +75,6 @@ jobs:
# queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:${{matrix.language}}"

View File

@@ -141,7 +141,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
COPY superset/translations/ /app/translations_mo/
RUN if [ "${BUILD_TRANSLATIONS}" = "true" ]; then \
pybabel compile --use-fuzzy -d /app/translations_mo || true; \
pybabel compile -d /app/translations_mo | true; \
fi; \
rm -f /app/translations_mo/*/*/*.[po,json]

View File

@@ -332,28 +332,15 @@ cd superset-frontend
npm run build-translation
# Backend
pybabel compile --use-fuzzy -d superset/translations
pybabel compile -d superset/translations
```
`--use-fuzzy` includes `#, fuzzy` entries in the compiled `.mo` files. Superset
serves fuzzy translations on purpose: the frontend build (`po2json --fuzzy`)
already includes them, `flask fab babel-compile` (used by the release images)
compiles with `-f`, and the production `Dockerfile` compiles with `--use-fuzzy`
as well. This keeps machine-generated (and other draft) translations visible in
the UI rather than falling back to English while they await review.
### Backfilling missing translations with AI
For languages with many untranslated strings, the repo includes a script that
uses Claude AI to generate draft translations for any missing entries. All
AI-generated strings are marked `#, fuzzy` and tagged with an attribution
comment so that human reviewers know they need to be checked.
Note that `#, fuzzy` marks a translation as *needing review*, not as *withheld*:
both the frontend and backend builds serve fuzzy entries (see [Applying
translations](#applying-translations) above), so an AI-generated string is shown
in the UI as soon as it is built and deployed. Reviewers should verify each
entry and remove the `#, fuzzy` flag to promote it to a confirmed translation.
comment so that human reviewers know they need to be checked before merging.
#### Prerequisites

View File

@@ -182,7 +182,7 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.61.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.6",
"@storybook/addon-docs": "10.4.5",
"@storybook/addon-links": "10.4.4",
"@storybook/react-webpack5": "10.4.4",
"@storybook/test-runner": "0.24.4",
@@ -9718,16 +9718,16 @@
"license": "MIT"
},
"node_modules/@storybook/addon-docs": {
"version": "10.4.6",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.6.tgz",
"integrity": "sha512-aWAfP5JMiT5a3zBJizwroCRzOCqZwDTJmvsYvwMD3ilIEa/kT1vhf6Xrbk4XIPhDwbh8Hpb/Gfnka1xBYEISWg==",
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.5.tgz",
"integrity": "sha512-9mIV0maIxixfuvdpNhr3QMeU/gbJKeaBcWhPYuf176cqDZAG9EUhZ50TIinxeFRbyEGRJqaLPoiYwIu4GJu3jA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdx-js/react": "^3.0.0",
"@storybook/csf-plugin": "10.4.6",
"@storybook/csf-plugin": "10.4.5",
"@storybook/icons": "^2.0.2",
"@storybook/react-dom-shim": "10.4.6",
"@storybook/react-dom-shim": "10.4.5",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"ts-dedent": "^2.0.0"
@@ -9738,7 +9738,7 @@
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.6"
"storybook": "^10.4.5"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -9747,9 +9747,9 @@
}
},
"node_modules/@storybook/addon-docs/node_modules/@storybook/react-dom-shim": {
"version": "10.4.6",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.6.tgz",
"integrity": "sha512-iGNmKzrq9vgl2PDrYAnZKI+yvac3Ym+lJXXuQaqlFRS23zA5MNm4EBX+rAG7WulqchoK6NaZ0KQOs2mAgEpTMg==",
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.5.tgz",
"integrity": "sha512-fKdikHC7cDgSuaBirPwvgFBmfO//3cln0y3GmDEQchUV2VFDrZ7ZL1/iH7dA21XuiFFhQcDRRkArJmvMAGG5Cg==",
"dev": true,
"license": "MIT",
"funding": {
@@ -9761,7 +9761,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.6"
"storybook": "^10.4.5"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -9800,9 +9800,9 @@
}
},
"node_modules/@storybook/csf-plugin": {
"version": "10.4.6",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.6.tgz",
"integrity": "sha512-NILLxDqpA/JR/AazGWpsz+4fadJwRU4uhHephGtYpVOWnQA/DkJfKT6zpcJVq8+QA8A2zKMLX3GVKsXIrxjuDA==",
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.5.tgz",
"integrity": "sha512-OsSsSLulBmdKTz7MIKLgoWADZB8bjYaAjZZy/THdI50G/TTd6FVSXQMCM7GO7xQZ/EguRY1PmjOVCLbgcnXsDA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9815,7 +9815,7 @@
"peerDependencies": {
"esbuild": "*",
"rollup": "*",
"storybook": "^10.4.6",
"storybook": "^10.4.5",
"vite": "*",
"webpack": "*"
},

View File

@@ -265,7 +265,7 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.61.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.6",
"@storybook/addon-docs": "10.4.5",
"@storybook/addon-links": "10.4.4",
"@storybook/react-webpack5": "10.4.4",
"@storybook/test-runner": "0.24.4",

View File

@@ -713,5 +713,4 @@ export interface DataColumnMeta {
isChildColumn?: boolean;
description?: string;
currencyCodeColumn?: string;
isFilterable?: boolean;
}

View File

@@ -67,7 +67,7 @@
"rison": "^0.1.1",
"seedrandom": "^3.0.5",
"xss": "^1.0.15",
"lodash-es": "^4.18.1"
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@emotion/styled": "^11.14.1",

View File

@@ -29,7 +29,6 @@ import { waitForPost } from '../../helpers/api/intercepts';
import { expectStatusOneOf } from '../../helpers/api/assertions';
import { getDatabaseByName } from '../../helpers/api/database';
import { apiExecuteSql } from '../../helpers/api/sqllab';
import { TIMEOUT } from '../../utils/constants';
interface ExamplesSetupResult {
tableName: string;
@@ -117,7 +116,7 @@ async function dropTempTable(
// Uses test.describe only because Playwright's serial mode API requires it -
// (Deviation from "avoid describe" guideline is necessary for functional reasons)
test.describe('create dataset wizard', () => {
test.describe.configure({ mode: 'serial', timeout: TIMEOUT.SLOW_TEST });
test.describe.configure({ mode: 'serial' });
test('should create a dataset via wizard', async ({ page, testAssets }) => {
const { tableName, dbId, createDatasetPage } = await setupExamplesDataset(

View File

@@ -33,9 +33,8 @@ import { getLayerConfig } from '../util/controlPanelUtil';
export default class CartodiagramPlugin extends ChartPlugin {
constructor(opts: CartodiagramPluginConstructorOpts) {
const metadata = new ChartMetadata({
description: t(
description:
'Display charts on a map. For using this plugin, users first have to create any other chart that can then be placed on the map.',
),
name: t('Cartodiagram'),
thumbnail,
thumbnailDark,

View File

@@ -28,9 +28,8 @@ export default class PopKPIPlugin extends ChartPlugin {
constructor() {
const metadata = new ChartMetadata({
category: t('KPI'),
description: t(
description:
'Showcases a metric along with a comparison of value, change, and percent change for a selected time period.',
),
name: t('Big Number with Time Period Comparison'),
tags: [
t('Comparison'),

View File

@@ -1204,11 +1204,8 @@ export default function TableChart<D extends DataRecord = DataRecord>(
onClick:
emitCrossFilters && !valueRange && !isMetric
? () => {
const isFilterable = columnsMeta.find(
(cm: DataColumnMeta) => cm.key === key,
)?.isFilterable;
// allow selecting text in a cell
if (!getSelectedText() && isFilterable !== false) {
if (!getSelectedText()) {
toggleFilter(key, value);
}
}

View File

@@ -232,9 +232,6 @@ const processColumns = memoizeOne(function processColumns(
const metricsSet = new Set(metrics);
const percentMetricsSet = new Set(percentMetrics);
const rawPercentMetricsSet = new Set(rawPercentMetrics);
const columnsByName = new Map(
(props.datasource.columns ?? []).map(col => [col.column_name, col]),
);
const columns: DataColumnMeta[] = (colnames || [])
.filter(
@@ -247,7 +244,6 @@ const processColumns = memoizeOne(function processColumns(
const config = columnConfig[key] || {};
// for the purpose of presentation, only numeric values are treated as metrics
// because users can also add things like `MAX(str_col)` as a metric.
const isFilterable = columnsByName.get(key)?.filterable;
const isMetric = metricsSet.has(key) && isNumeric(key, records);
const isPercentMetric = percentMetricsSet.has(key);
const label =
@@ -330,7 +326,6 @@ const processColumns = memoizeOne(function processColumns(
isPercentMetric,
formatter,
config,
isFilterable,
description,
currencyCodeColumn,
};

View File

@@ -2534,33 +2534,3 @@ test('sorts genuinely string columns alphanumerically', () => {
const values = Array.from(cells).map(td => td.textContent);
expect(values).toEqual(['apple', 'banana', 'cherry']);
});
test('TableChart should NOT emit cross-filter when clicking a cell in a not-filterable column', () => {
const setDataMask = jest.fn();
const props = transformProps({
...testData.basic,
datasource: {
...testData.basic.datasource,
columns: [{ column_name: 'name', filterable: false } as any],
},
hooks: { setDataMask },
emitCrossFilters: true,
});
render(
<ProviderWrapper>
<TableChart
{...props}
emitCrossFilters
setDataMask={setDataMask}
sticky={false}
/>
</ProviderWrapper>,
);
fireEvent.click(screen.getByText('Michael'));
const crossFilterCall = setDataMask.mock.calls.find(
(call: any[]) => call[0]?.filterState?.filters,
);
expect(crossFilterCall).toBeUndefined();
});

View File

@@ -22,7 +22,6 @@ import {
createStore,
render,
screen,
userEvent,
waitFor,
} from 'spec/helpers/testing-library';
import reducerIndex from 'spec/helpers/reducerIndex';
@@ -31,7 +30,7 @@ import {
useDashboardCharts,
useDashboardDatasets,
} from 'src/hooks/apiResources';
import { SupersetApiError, SupersetClient } from '@superset-ui/core';
import { SupersetClient } from '@superset-ui/core';
import CrudThemeProvider from 'src/components/CrudThemeProvider';
import { hydrateDashboard } from 'src/dashboard/actions/hydrate';
import {
@@ -560,48 +559,6 @@ test('does not overwrite filterState when modern native_filters URL format is us
).toBeUndefined();
});
test('renders a not-found state instead of throwing when the dashboard 404s', async () => {
mockUseDashboard.mockReturnValue({
result: null,
error: new SupersetApiError({ status: 404, message: 'Not found' }),
});
mockUseDashboardCharts.mockReturnValue({
result: null,
error: new SupersetApiError({ status: 404, message: 'Not found' }),
});
mockUseDashboardDatasets.mockReturnValue({
result: null,
error: new SupersetApiError({ status: 404, message: 'Not found' }),
status: 'error',
});
render(
<Suspense fallback="loading">
<DashboardPage idOrSlug="404" />
</Suspense>,
{
useRedux: true,
useRouter: true,
initialState: {
dashboardInfo: {},
dashboardState: { sliceIds: [] },
nativeFilters: { filters: {} },
dataMask: {},
},
},
);
expect(
await screen.findByText('This dashboard does not exist'),
).toBeInTheDocument();
expect(screen.queryByTestId('dashboard-builder')).not.toBeInTheDocument();
await userEvent.click(
screen.getByRole('button', { name: 'See all dashboards' }),
);
expect(window.location.pathname).toBe('/dashboard/list/');
});
test('clears undo history after hydrating the dashboard', async () => {
render(
<Suspense fallback="loading">

View File

@@ -24,7 +24,7 @@ import { useTheme } from '@apache-superset/core/theme';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from '@reduxjs/toolkit';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import { EmptyState, Loading } from '@superset-ui/core/components';
import { Loading } from '@superset-ui/core/components';
import {
useDashboard,
useDashboardCharts,
@@ -67,8 +67,7 @@ import SyncDashboardState, {
getDashboardContextLocalStorage,
} from '../components/SyncDashboardState';
import { AutoRefreshProvider } from '../contexts/AutoRefreshContext';
import { Filter, PartialFilters, SupersetApiError } from '@superset-ui/core';
import { RoutePaths } from 'src/views/routePaths';
import { Filter, PartialFilters } from '@superset-ui/core';
import {
parseRisonFilters,
risonFiltersToExtraFormDataFilters,
@@ -152,9 +151,6 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
const isDashboardHydrated = useRef(false);
const error = dashboardApiError || chartsApiError;
// Only 404 gets a graceful not-found state; a 403 (access denied) still
// surfaces through the error boundary.
const isNotFoundError = (error as SupersetApiError | null)?.status === 404;
const readyToRender = Boolean(dashboard && charts);
const { dashboard_title, id = 0 } = dashboard || {};
@@ -369,21 +365,18 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
useEffect(() => {
if (datasetsApiError) {
// A missing dashboard also 404s its datasets; the not-found state covers it.
if (!isNotFoundError) {
addDangerToast(
t('Error loading chart datasources. Filters may not work correctly.'),
);
}
addDangerToast(
t('Error loading chart datasources. Filters may not work correctly.'),
);
} else {
dispatch(setDatasources(datasets));
}
}, [addDangerToast, datasets, datasetsApiError, dispatch, isNotFoundError]);
}, [addDangerToast, datasets, datasetsApiError, dispatch]);
const relevantDataMask = useSelector(selectRelevantDatamask);
const activeFilters = useSelector(selectActiveFilters);
if (error && !isNotFoundError) throw error; // caught in error boundary
if (error) throw error; // caught in error boundary
const globalStyles = useMemo(
() => [
@@ -396,25 +389,9 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
[theme],
);
if (error && !isNotFoundError) throw error; // caught in error boundary
if (error) throw error; // caught in error boundary
const DashboardBuilderComponent = useMemo(() => <DashboardBuilder />, []);
if (isNotFoundError) {
return (
<EmptyState
size="large"
image="empty-dashboard.svg"
title={t('This dashboard does not exist')}
description={t(
'The dashboard you are looking for may have been deleted or moved.',
)}
buttonText={t('See all dashboards')}
buttonAction={() => history.push(RoutePaths.DASHBOARD_LIST)}
/>
);
}
return (
<>
<Global styles={globalStyles} />

View File

@@ -34,40 +34,3 @@ test('Render a FilterInput', async () => {
expect(onChangeHandler).toHaveBeenCalledTimes(4);
});
test('FilterInput auto-focuses when a non-editable element (e.g. a tab) has focus', () => {
const onChangeHandler = jest.fn();
const button = document.createElement('button');
document.body.appendChild(button);
try {
button.focus();
expect(document.activeElement).toBe(button);
render(<FilterInput onChangeHandler={onChangeHandler} shouldFocus />);
const filterInput = screen.getByPlaceholderText('Search');
// Auto-focus should fire — a button is not an editable element
expect(document.activeElement).toBe(filterInput);
} finally {
document.body.removeChild(button);
}
});
test('FilterInput does not steal focus when another input already has focus', () => {
const onChangeHandler = jest.fn();
const otherInput = document.createElement('input');
document.body.appendChild(otherInput);
try {
otherInput.focus();
expect(document.activeElement).toBe(otherInput);
render(<FilterInput onChangeHandler={onChangeHandler} shouldFocus />);
const filterInput = screen.getByPlaceholderText('Search');
// FilterInput should not have stolen focus from the already-focused input
expect(document.activeElement).not.toBe(filterInput);
expect(document.activeElement).toBe(otherInput);
} finally {
document.body.removeChild(otherInput);
}
});

View File

@@ -98,20 +98,9 @@ export const FilterInput = ({
const inputRef: RefObject<any> = useRef(null);
useEffect(() => {
// Focus the input element when the component mounts
if (inputRef.current && shouldFocus) {
// Skip auto-focus only when an editable element already has focus (e.g.
// user is typing in a form control when this pane remounts after a data
// refresh). Non-editable focused elements like tabs/buttons still allow
// auto-focus so the search box focuses on first open.
const activeEl = document.activeElement;
const editableFocused =
activeEl instanceof HTMLElement &&
(activeEl.tagName === 'INPUT' ||
activeEl.tagName === 'TEXTAREA' ||
activeEl.isContentEditable);
if (!editableFocused) {
inputRef.current.focus();
}
inputRef.current.focus();
}
}, []);

View File

@@ -541,9 +541,8 @@ function SavedQueryList({
key: 'search',
input: 'search',
operator: FilterOperator.AllText,
toolTipDescription: t(
toolTipDescription:
'Searches all text fields: Name, Description, Database & Schema',
),
},
{
Header: t('Database'),

View File

@@ -133,9 +133,8 @@ function TagList(props: TagListProps) {
const emptyState = {
title: t('No Tags created'),
image: 'dashboard.svg',
description: t(
description:
'Create a new tag and assign it to existing entities like charts or dashboards',
),
buttonAction: () => setShowTagModal(true),
buttonIcon: <Icons.PlusOutlined iconSize="m" data-test="add-rule-empty" />,
buttonText: t('Create a new Tag'),

View File

@@ -8250,16 +8250,16 @@ msgid "List"
msgstr "Auflisten"
msgid "List Groups"
msgstr "Gruppen auflisten"
msgstr "Gruppen"
msgid "List Roles"
msgstr "Rollen auflisten"
msgstr "Rollen"
msgid "List Unique Values"
msgstr "Eindeutige Werte auflisten"
msgid "List Users"
msgstr "Benutzer*innen auflisten"
msgstr "Benutzer*innen"
msgid "List of extra columns made available in JavaScript functions"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,6 @@
"""Unit tests for the MCP get_dashboard_datasets tool."""
from importlib import import_module
from unittest.mock import Mock, patch
import pytest
@@ -30,10 +29,6 @@ from superset.mcp_service.utils.sanitization import (
)
from superset.utils import json
get_dashboard_datasets_module = import_module(
"superset.mcp_service.dashboard.tool.get_dashboard_datasets"
)
def _wrapped(value: str) -> str:
return f"{LLM_CONTEXT_OPEN_DELIMITER}\n{value}\n{LLM_CONTEXT_CLOSE_DELIMITER}"
@@ -147,8 +142,8 @@ def mock_dataset_access():
@pytest.fixture(autouse=True)
def allow_data_model_metadata():
"""Keep tests in the metadata-allowed path unless a test overrides it."""
with patch.object(
get_dashboard_datasets_module,
with patch(
"superset.mcp_service.dashboard.tool.get_dashboard_datasets."
"user_can_view_data_model_metadata",
return_value=True,
) as mock_allow:

View File

@@ -15,7 +15,6 @@
# specific language governing permissions and limitations
# under the License.
from datetime import datetime
from importlib import import_module
from importlib.util import find_spec
from unittest.mock import patch
@@ -27,8 +26,6 @@ from superset.utils.core import DTTM_ALIAS
from superset.utils.pandas_postprocessing import prophet
from tests.unit_tests.fixtures.dataframes import prophet_df
prophet_module = import_module("superset.utils.pandas_postprocessing.prophet")
def test_prophet_valid():
df = prophet(df=prophet_df, time_grain="P1M", periods=3, confidence_interval=0.9)
@@ -210,7 +207,9 @@ def test_prophet_fit_error():
if find_spec("prophet") is None:
pytest.skip("prophet not installed")
with patch.object(prophet_module, "_prophet_fit_and_predict") as mock_fit:
with patch(
"superset.utils.pandas_postprocessing.prophet._prophet_fit_and_predict"
) as mock_fit:
mock_fit.side_effect = InvalidPostProcessingError(
"Unable to generate forecast: Dataframe has fewer than 2 non-NaN rows."
)

View File

@@ -2983,9 +2983,8 @@ def test_coerce_integer_rejects_non_integer_float() -> None:
def test_coerce_integer_rejects_other_types() -> None:
raw: Any = [1]
with pytest.raises(ValueError, match="Invalid integer value"):
_coerce_scalar_filter_value(raw, _dim(pa.int64()))
_coerce_scalar_filter_value([1], _dim(pa.int64()))
@pytest.mark.parametrize(
@@ -3009,9 +3008,8 @@ def test_coerce_floating_invalid_string_raises() -> None:
def test_coerce_floating_rejects_other_types() -> None:
raw: Any = [1.0]
with pytest.raises(ValueError, match="Invalid numeric value"):
_coerce_scalar_filter_value(raw, _dim(pa.float64()))
_coerce_scalar_filter_value([1.0], _dim(pa.float64()))
def test_coerce_date_from_datetime() -> None: