Compare commits

..

1 Commits

Author SHA1 Message Date
Evan
e06720b067 chore(ci): correct codeql-action version comment to match pinned SHA
The github/codeql-action/init and analyze steps were pinned to SHA
8aad20d150bbac5944a9f9d289da16a4b0d87c1e but annotated with a '# v4'
comment. That SHA is the v4.36.2 release, and the moving 'v4' tag now
resolves to a different commit, so zizmor's ref-version-mismatch rule
flags the comment as untruthful. Update the comment to '# v4.36.2' to
reflect the actual pinned ref, matching how other actions in this
workflow annotate their exact versions (e.g. actions/checkout # v7.0.0).

Resolves code-scanning alert #2560

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:34:36 -07:00
22 changed files with 347 additions and 1863 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

@@ -297,7 +297,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

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

@@ -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

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

@@ -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