diff --git a/.github/actions/file-changes-action b/.github/actions/file-changes-action deleted file mode 160000 index a6ca26c1427..00000000000 --- a/.github/actions/file-changes-action +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a6ca26c14274c33b15e6499323aac178af06ad4b diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fe22327f248..dff11ed37c9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,7 +11,15 @@ updates: - package-ecosystem: "npm" ignore: - dependency-name: "@rjsf/*" - # TODO: remove below entries until React >= 19.0.0 + # TODO: remove below entries once the application supports React >= 19.0.0 + - dependency-name: "react" + update-types: ["version-update:semver-major"] + - dependency-name: "react-dom" + update-types: ["version-update:semver-major"] + - dependency-name: "@types/react" + update-types: ["version-update:semver-major"] + - dependency-name: "@types/react-dom" + update-types: ["version-update:semver-major"] - dependency-name: "react-icons" # JSDOM v30 doesn't play well with Jest v30 # Source: https://jestjs.io/blog#known-issues diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0297ea465bc..fab54f311a6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -64,7 +64,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 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@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index c9f675554b2..9901a92fd13 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -7,6 +7,11 @@ on: - "[0-9].[0-9]*" pull_request: types: [synchronize, opened, reopened, ready_for_review] + # Nightly full-tree sweep. Per-PR runs only lint changed files, so a change + # that invalidates an untouched file (e.g. a type change that breaks an + # importing test) can pass every PR yet leave master red. This catches that. + schedule: + - cron: "0 6 * * *" permissions: contents: read @@ -32,6 +37,9 @@ jobs: with: persist-credentials: false submodules: recursive + # Full history so we can diff a PR/push against its base commit to + # determine changed files (see "Determine changed files" below). + fetch-depth: 0 - name: Setup Python uses: ./.github/actions/setup-backend/ @@ -69,19 +77,82 @@ jobs: restore-keys: | pre-commit-v2-${{ runner.os }}-py${{ matrix.python-version }}- - - name: Get changed files + - name: Determine changed files id: changed_files - uses: ./.github/actions/file-changes-action - with: - output: " " + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + BEFORE_SHA: ${{ github.event.before }} + run: | + set -euo pipefail + + # Scheduled runs check the whole tree (see the pre-commit step). + if [ "${EVENT_NAME}" = "schedule" ]; then + echo "mode=all" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Resolve the commit to diff against. + base="" + if [ "${EVENT_NAME}" = "pull_request" ]; then + base="${BASE_SHA}" + elif [ -n "${BEFORE_SHA:-}" ] && \ + [ "${BEFORE_SHA}" != "0000000000000000000000000000000000000000" ]; then + base="${BEFORE_SHA}" + fi + + # Fail closed: if the diff base can't be resolved, check every file + # instead of silently checking nothing. Previously an empty file list + # made `pre-commit run --files` a no-op that still reported success, + # which let unlinted code reach master. + if [ -z "${base}" ] || ! git cat-file -e "${base}^{commit}" 2>/dev/null; then + echo "::notice::Could not resolve a diff base; falling back to --all-files." + echo "mode=all" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Files present in HEAD that changed since the base (drop deletions). + files="$(git diff --name-only --diff-filter=ACMRT "${base}...HEAD")" + + if [ -z "${files}" ]; then + echo "mode=none" >> "$GITHUB_OUTPUT" + else + echo "mode=files" >> "$GITHUB_OUTPUT" + { + echo "files<<__CHANGED_FILES_EOF__" + echo "${files}" + echo "__CHANGED_FILES_EOF__" + } >> "$GITHUB_OUTPUT" + fi - name: pre-commit env: + MODE: ${{ steps.changed_files.outputs.mode }} CHANGED_FILES: ${{ steps.changed_files.outputs.files }} run: | set +e # Don't exit immediately on failure export SKIP=type-checking-frontend - pre-commit run --files $CHANGED_FILES + + case "${MODE}" in + all) + echo "ℹ️ Running pre-commit on all files." + pre-commit run --all-files + ;; + files) + echo "ℹ️ Running pre-commit on changed files:" + echo "${CHANGED_FILES}" + # shellcheck disable=SC2086 + pre-commit run --files ${CHANGED_FILES} + ;; + none) + echo "ℹ️ No source files changed; nothing for pre-commit to check." + exit 0 + ;; + *) + echo "⚠️ Unrecognized changed-files mode '${MODE}'; checking all files." + pre-commit run --all-files + ;; + esac PRE_COMMIT_EXIT_CODE=$? git diff --quiet --exit-code GIT_DIFF_EXIT_CODE=$? diff --git a/.gitmodules b/.gitmodules index bf3f8a1b37e..54a0fb6b8f4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -21,9 +21,6 @@ [submodule ".github/actions/pr-lint-action"] path = .github/actions/pr-lint-action url = https://github.com/morrisoncole/pr-lint-action -[submodule ".github/actions/file-changes-action"] - path = .github/actions/file-changes-action - url = https://github.com/trilom/file-changes-action [submodule ".github/actions/cached-dependencies"] path = .github/actions/cached-dependencies url = https://github.com/apache-superset/cached-dependencies diff --git a/pyproject.toml b/pyproject.toml index 81a299759db..19defd25b14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ # without the ``base.txt`` lock file (#40962). "cachetools>=7.1.4, <8", "celery>=5.6.3, <6.0.0", - "click>=8.4.0", + "click>=8.4.2", "click-option-group", "colorama", "flask-cors>=6.0.5, <7.0", @@ -155,7 +155,7 @@ elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"] exasol = ["sqlalchemy-exasol>=2.4.0, <8.0"] excel = ["xlrd>=2.0.2, <2.1"] fastmcp = [ - "fastmcp>=3.4.2,<4.0", + "fastmcp>=3.4.3,<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. @@ -199,7 +199,7 @@ risingwave = ["sqlalchemy-risingwave"] shillelagh = ["shillelagh[all]>=1.4.4, <2"] singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"] snowflake = ["snowflake-sqlalchemy>=1.10.2, <2"] -sqlite = ["syntaqlite>=0.6.0,<0.7.0"] +sqlite = ["syntaqlite>=0.7.0,<0.8.0"] spark = [ "pyhive[hive]>=0.6.5;python_version<'3.11'", "pyhive[hive_pure_sasl]>=0.7", @@ -244,7 +244,7 @@ development = [ "ruff", "sqloxide", "statsd", - "syntaqlite>=0.6.0,<0.7.0", + "syntaqlite>=0.7.0,<0.8.0", ] [project.urls] diff --git a/requirements/base.txt b/requirements/base.txt index 92cb6e85d33..6c9d48b693a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -60,7 +60,7 @@ cffi==2.0.0 # pynacl charset-normalizer==3.4.2 # via requests -click==8.4.1 +click==8.4.2 # via # apache-superset (pyproject.toml) # celery diff --git a/requirements/development.txt b/requirements/development.txt index fd3f37740a1..3ce4ca6a10b 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -130,7 +130,7 @@ charset-normalizer==3.4.2 # via # -c requirements/base-constraint.txt # requests -click==8.4.1 +click==8.4.2 # via # -c requirements/base-constraint.txt # apache-superset @@ -238,9 +238,9 @@ et-xmlfile==2.0.0 # openpyxl exceptiongroup==1.3.0 # via fastmcp-slim -fastmcp==3.4.2 +fastmcp==3.4.4 # via apache-superset -fastmcp-slim==3.4.2 +fastmcp-slim==3.4.4 # via fastmcp filelock==3.20.3 # via @@ -1017,7 +1017,7 @@ starlette==1.3.1 # mcp statsd==4.0.1 # via apache-superset -syntaqlite==0.6.0 +syntaqlite==0.7.1 # via apache-superset tabulate==0.10.0 # via diff --git a/superset-frontend/.storybook/shared/Expandable.tsx b/superset-frontend/.storybook/shared/Expandable.tsx index 37b177f325b..367ea969264 100644 --- a/superset-frontend/.storybook/shared/Expandable.tsx +++ b/superset-frontend/.storybook/shared/Expandable.tsx @@ -17,45 +17,38 @@ * under the License. */ -import { Component, ReactNode } from 'react'; +import { useState, useCallback, ReactNode } from 'react'; export type Props = { children: ReactNode; expandableWhat?: string; }; -type State = { - open: boolean; -}; +export default function Expandable({ children, expandableWhat }: Props) { + const [open, setOpen] = useState(false); -export default class Expandable extends Component { - constructor(props: Props) { - super(props); - this.state = { open: false }; - this.handleToggle = this.handleToggle.bind(this); - } + const handleToggle = useCallback(() => { + setOpen(prevOpen => !prevOpen); + }, []); - handleToggle() { - this.setState(({ open }) => ({ open: !open })); - } + const label = expandableWhat + ? `${open ? 'Hide' : 'Show'} ${expandableWhat}` + : open + ? 'Hide' + : 'Show'; - render() { - const { open } = this.state; - const { children, expandableWhat } = this.props; - - return ( -
- -
-
- {open ? children : null} -
- ); - } + return ( +
+ +
+
+ {open ? children : null} +
+ ); } diff --git a/superset-frontend/.storybook/shared/VerifyCORS.tsx b/superset-frontend/.storybook/shared/VerifyCORS.tsx index 78fb78c6fb2..9dd96472f3c 100644 --- a/superset-frontend/.storybook/shared/VerifyCORS.tsx +++ b/superset-frontend/.storybook/shared/VerifyCORS.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { Component, ReactNode } from 'react'; +import { useState, useEffect, useCallback, useRef, ReactNode } from 'react'; import { t } from '@apache-superset/core/translation'; import { SupersetClient, @@ -36,12 +36,6 @@ export type Props = { postPayload?: string; }; -type State = { - didVerify: boolean; - error?: Error | SupersetApiError; - payload?: object; -}; - export const renderError = (error: Error) => (
The following error occurred, make sure you have
@@ -54,29 +48,37 @@ export const renderError = (error: Error) => (
); -export default class VerifyCORS extends Component { - constructor(props: Props) { - super(props); - this.state = { didVerify: false }; - this.handleVerify = this.handleVerify.bind(this); - } +export default function VerifyCORS({ + children, + endpoint, + host, + method, + postPayload, +}: Props): JSX.Element { + const [didVerify, setDidVerify] = useState(false); + const [error, setError] = useState( + undefined, + ); + const [payload, setPayload] = useState(undefined); - componentDidUpdate(prevProps: Props) { - const { endpoint, host, postPayload, method } = this.props; + const prevPropsRef = useRef({ endpoint, host, postPayload, method }); + + useEffect(() => { + const prevProps = prevPropsRef.current; if ( - (this.state.didVerify || this.state.error) && + (didVerify || error) && (prevProps.endpoint !== endpoint || prevProps.host !== host || prevProps.postPayload !== postPayload || prevProps.method !== method) ) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ didVerify: false, error: undefined }); + setDidVerify(false); + setError(undefined); } - } + prevPropsRef.current = { endpoint, host, postPayload, method }; + }, [endpoint, host, postPayload, method, didVerify, error]); - handleVerify() { - const { endpoint, host, postPayload, method } = this.props; + const handleVerify = useCallback(() => { SupersetClient.reset(); SupersetClient.configure({ credentials: 'include', @@ -94,43 +96,40 @@ export default class VerifyCORS extends Component { } return { error: 'Must provide valid endpoint and payload.' }; }) - .then(result => - this.setState({ didVerify: true, error: undefined, payload: result }), - ) - .catch(error => this.setState({ error })); - } + .then(result => { + setDidVerify(true); + setError(undefined); + setPayload(result); + }) + .catch(err => setError(err)); + }, [endpoint, host, method, postPayload]); - render() { - const { didVerify, error, payload } = this.state; - const { children } = this.props; - - return didVerify ? ( - children({ payload }) - ) : ( -
-
- This example requires CORS requests from this domain.
-
- 1) enable CORS requests in your Superset App from{' '} - {`${window.location.origin}`} -
- 2) configure your Superset App host name below
- 3) click below to verify authentication. You may debug CORS further - using the `@superset-ui/connection` story.
-
- -
-
-
- - {error && ( -
- -
- )} + return didVerify ? ( + <>{children({ payload })} + ) : ( +
+
+ This example requires CORS requests from this domain.
+
+ 1) enable CORS requests in your Superset App from{' '} + {`${window.location.origin}`} +
+ 2) configure your Superset App host name below
+ 3) click below to verify authentication. You may debug CORS further + using the `@superset-ui/connection` story.
+
+ +
+
- ); - } + + {error && ( +
+ +
+ )} +
+ ); } diff --git a/superset-frontend/packages/superset-ui-core/src/number-format/stories/NumberFormat.stories.tsx b/superset-frontend/packages/superset-ui-core/src/number-format/stories/NumberFormat.stories.tsx index 718a1f17cfb..f473a8f8df2 100644 --- a/superset-frontend/packages/superset-ui-core/src/number-format/stories/NumberFormat.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/number-format/stories/NumberFormat.stories.tsx @@ -17,126 +17,108 @@ * under the License. */ -import { PureComponent } from 'react'; +import { useState, useCallback } from 'react'; import { formatNumber } from '@superset-ui/core'; -interface NumberFormatValidatorState { - formatString: string; - testValues: (number | null | undefined)[]; -} +const testValues: (number | null | undefined)[] = [ + 987654321, + 12345.6789, + 3000, + 400.14, + 70.00002, + 1, + 0, + -1, + -70.00002, + -400.14, + -3000, + -12345.6789, + -987654321, + Number.POSITIVE_INFINITY, + Number.NEGATIVE_INFINITY, + NaN, + null, + undefined, +]; -class NumberFormatValidator extends PureComponent< - Record, - NumberFormatValidatorState -> { - state: NumberFormatValidatorState = { - formatString: '.3~s', - testValues: [ - 987654321, - 12345.6789, - 3000, - 400.14, - 70.00002, - 1, - 0, - -1, - -70.00002, - -400.14, - -3000, - -12345.6789, - -987654321, - Number.POSITIVE_INFINITY, - Number.NEGATIVE_INFINITY, - NaN, - null, - undefined, - ], - }; +function NumberFormatValidator() { + const [formatString, setFormatString] = useState('.3~s'); - constructor(props: Record) { - super(props); + const handleFormatChange = useCallback( + (event: React.ChangeEvent) => { + setFormatString(event.target.value); + }, + [], + ); - this.handleFormatChange = this.handleFormatChange.bind(this); - } - - handleFormatChange(event: React.ChangeEvent) { - this.setState({ - formatString: event.target.value, - }); - } - - render() { - const { formatString, testValues } = this.state; - - return ( -
-
-
-

- This @superset-ui/number-format package enriches{' '} - d3-format - to handle invalid formats as well as edge case values. Use the - validator below to preview outputs from the specified format - string. See - - D3 Format Reference - - for how to write a D3 format string. -

-
-
-
-
-
-
-
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - -
-
-
-
-
-
-
- - - - - - - - - {testValues.map((v, index) => ( - - - - - ))} - -
Input (number)Formatted output (string)
- {`${v}`} - - "{formatNumber(formatString, v)}" -
-
+ return ( +
+
+
+

+ This @superset-ui/number-format package enriches{' '} + d3-format + to handle invalid formats as well as edge case values. Use the + validator below to preview outputs from the specified format string. + See + + D3 Format Reference + + for how to write a D3 format string. +

- ); - } +
+
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +
+
+
+
+
+
+
+ + + + + + + + + {testValues.map((v, index) => ( + + + + + ))} + +
Input (number)Formatted output (string)
+ {`${v}`} + + "{formatNumber(formatString, v)}" +
+
+
+
+ ); } export default { diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/stories/TimeFormat.stories.tsx b/superset-frontend/packages/superset-ui-core/src/time-format/stories/TimeFormat.stories.tsx index 2798113b587..34ee21dee46 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-format/stories/TimeFormat.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/time-format/stories/TimeFormat.stories.tsx @@ -17,115 +17,96 @@ * under the License. */ -import { PureComponent } from 'react'; +import { useState, useCallback } from 'react'; import { formatTime } from '@superset-ui/core'; -interface TimeFormatValidatorState { - formatString: string; - testValues: (Date | number | null | undefined)[]; -} +const testValues: (Date | number | null | undefined)[] = [ + new Date(Date.UTC(1986, 5, 14, 8, 30, 53)), + new Date(Date.UTC(2001, 9, 27, 13, 45, 2, 678)), + new Date(Date.UTC(2009, 1, 1, 0, 0, 0)), + new Date(Date.UTC(2018, 1, 1, 10, 20, 33)), + 0, + null, + undefined, +]; -class TimeFormatValidator extends PureComponent< - Record, - TimeFormatValidatorState -> { - state: TimeFormatValidatorState = { - formatString: '%Y-%m-%d %H:%M:%S', - testValues: [ - new Date(Date.UTC(1986, 5, 14, 8, 30, 53)), - new Date(Date.UTC(2001, 9, 27, 13, 45, 2, 678)), - new Date(Date.UTC(2009, 1, 1, 0, 0, 0)), - new Date(Date.UTC(2018, 1, 1, 10, 20, 33)), - 0, - null, - undefined, - ], - }; +function TimeFormatValidator() { + const [formatString, setFormatString] = useState('%Y-%m-%d %H:%M:%S'); - constructor(props: Record) { - super(props); - this.handleFormatChange = this.handleFormatChange.bind(this); - } + const handleFormatChange = useCallback( + (event: React.ChangeEvent) => { + setFormatString(event.target.value); + }, + [], + ); - handleFormatChange(event: React.ChangeEvent) { - this.setState({ - formatString: event.target.value, - }); - } - - render() { - const { formatString, testValues } = this.state; - - return ( -
-
-
-

- This @superset-ui/time-format package enriches - d3-time-format to handle invalid formats as well as - edge case values. Use the validator below to preview outputs from - the specified format string. See   - - D3 Time Format Reference - -  for how to write a D3 time format string. -

-
-
-
-
-
-
-
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - -
-
-
-
-
-
-
- - - - - - - - - {testValues.map((v, index) => ( - - - - - ))} - -
Input (time)Formatted output (string)
- - {v instanceof Date ? v.toUTCString() : `${v}`} - - - "{formatTime(formatString, v)}" -
-
+ return ( +
+
+
+

+ This @superset-ui/time-format package enriches + d3-time-format to handle invalid formats as well as + edge case values. Use the validator below to preview outputs from + the specified format string. See   + + D3 Time Format Reference + +  for how to write a D3 time format string. +

- ); - } +
+
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +
+
+
+
+
+
+
+ + + + + + + + + {testValues.map((v, index) => ( + + + + + ))} + +
Input (time)Formatted output (string)
+ {v instanceof Date ? v.toUTCString() : `${v}`} + + "{formatTime(formatString, v)}" +
+
+
+
+ ); } export default { diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx index 7c3352d110f..86d02243b40 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx @@ -26,9 +26,17 @@ import { import { FeatureFlag, VizType } from '@superset-ui/core'; import mockState from 'spec/fixtures/mockState'; import { cachedSupersetGet } from 'src/utils/cachedSupersetGet'; +import downloadAsImage from 'src/utils/downloadAsImage'; +import downloadAsPdf from 'src/utils/downloadAsPdf'; import SliceHeaderControls, { SliceHeaderControlsProps } from '.'; jest.mock('src/utils/cachedSupersetGet'); +jest.mock('src/utils/downloadAsImage', () => + jest.fn(() => jest.fn().mockResolvedValue(undefined)), +); +jest.mock('src/utils/downloadAsPdf', () => + jest.fn(() => jest.fn().mockResolvedValue(undefined)), +); const mockCachedSupersetGet = cachedSupersetGet as jest.MockedFunction< typeof cachedSupersetGet @@ -127,6 +135,12 @@ const openMenu = () => { userEvent.click(screen.getByRole('button', { name: 'More Options' })); }; +const mockDownloadAsImage = downloadAsImage as jest.MockedFunction< + typeof downloadAsImage +>; +const mockDownloadAsPdf = downloadAsPdf as jest.MockedFunction< + typeof downloadAsPdf +>; const mockFullscreenElement = (getElement: () => Element | null) => { Object.defineProperty(document, 'fullscreenElement', { configurable: true, @@ -136,6 +150,8 @@ const mockFullscreenElement = (getElement: () => Element | null) => { beforeEach(() => { mockCachedSupersetGet.mockClear(); + mockDownloadAsImage.mockClear(); + mockDownloadAsPdf.mockClear(); mockCachedSupersetGet.mockResolvedValue({ response: {} as Response, json: { @@ -675,6 +691,109 @@ test('Should pass formData to Share menu for embed code feature', () => { expect(screen.getByText('Share')).toBeInTheDocument(); }); +test('Download submenu shows standardized export screenshot and PDF labels', async () => { + const props = createProps(); + renderWrapper(props); + openMenu(); + userEvent.hover(screen.getByText('Download')); + expect( + await screen.findByText('Export screenshot (jpeg)'), + ).toBeInTheDocument(); + expect(screen.getByText('Export screenshot (png)')).toBeInTheDocument(); + expect(screen.getByText('Export as PDF')).toBeInTheDocument(); +}); + +test('Clicking "Export screenshot (jpeg)" calls downloadAsImage and logEvent', async () => { + const props = createProps(); + renderWrapper(props); + openMenu(); + userEvent.hover(screen.getByText('Download')); + userEvent.click(await screen.findByText('Export screenshot (jpeg)')); + expect(downloadAsImage).toHaveBeenCalledWith( + `.dashboard-chart-id-${SLICE_ID}`, + props.slice.slice_name, + true, + expect.anything(), + ); + expect(props.logEvent).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ chartId: SLICE_ID }), + ); +}); + +test('Export screenshot (png) submenu shows Transparent and Solid options', async () => { + const props = createProps(); + renderWrapper(props); + openMenu(); + userEvent.hover(screen.getByText('Download')); + userEvent.hover(await screen.findByText('Export screenshot (png)')); + expect(await screen.findByText('Transparent background')).toBeInTheDocument(); + expect(screen.getByText('Solid background')).toBeInTheDocument(); +}); + +test('Clicking "Transparent background" calls downloadAsImage with transparent option and logEvent', async () => { + const props = createProps(); + renderWrapper(props); + openMenu(); + userEvent.hover(screen.getByText('Download')); + userEvent.hover(await screen.findByText('Export screenshot (png)')); + userEvent.click(await screen.findByText('Transparent background')); + expect(downloadAsImage).toHaveBeenCalledWith( + `.dashboard-chart-id-${SLICE_ID}`, + props.slice.slice_name, + true, + expect.anything(), + { format: 'png', backgroundType: 'transparent' }, + ); + expect(props.logEvent).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + chartId: SLICE_ID, + backgroundType: 'transparent', + }), + ); +}); + +test('Clicking "Solid background" calls downloadAsImage with solid option and logEvent', async () => { + const props = createProps(); + renderWrapper(props); + openMenu(); + userEvent.hover(screen.getByText('Download')); + userEvent.hover(await screen.findByText('Export screenshot (png)')); + userEvent.click(await screen.findByText('Solid background')); + expect(downloadAsImage).toHaveBeenCalledWith( + `.dashboard-chart-id-${SLICE_ID}`, + props.slice.slice_name, + true, + expect.anything(), + { format: 'png', backgroundType: 'solid' }, + ); + expect(props.logEvent).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + chartId: SLICE_ID, + backgroundType: 'solid', + }), + ); +}); + +test('Clicking "Export as PDF" calls downloadAsPdf and logEvent', async () => { + const props = createProps(); + renderWrapper(props); + openMenu(); + userEvent.hover(screen.getByText('Download')); + userEvent.click(await screen.findByText('Export as PDF')); + expect(downloadAsPdf).toHaveBeenCalledWith( + `.dashboard-chart-id-${SLICE_ID}`, + props.slice.slice_name, + true, + ); + expect(props.logEvent).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ chartId: SLICE_ID }), + ); +}); + test('Should show single fetched query tooltip with timestamp', async () => { const updatedDttm = Date.parse('2024-01-28T10:00:00.000Z'); const props = createProps(); diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index 77cf1e197e9..a892427d746 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -50,12 +50,17 @@ import { } from '@superset-ui/core/components'; import { useShareMenuItems } from 'src/dashboard/components/menu/ShareMenuItems'; import downloadAsImage from 'src/utils/downloadAsImage'; +import downloadAsPdf from 'src/utils/downloadAsPdf'; import { getSliceHeaderTooltip } from 'src/dashboard/util/getSliceHeaderTooltip'; import { Icons } from '@superset-ui/core/components/Icons'; import ViewQueryModal from 'src/explore/components/controls/ViewQueryModal'; import { ResultsPaneOnDashboard } from 'src/explore/components/DataTablesPane'; import { useDrillDetailMenuItems } from 'src/components/Chart/useDrillDetailMenuItems'; -import { LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils'; +import { + LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE, + LOG_ACTIONS_CHART_DOWNLOAD_AS_PNG, + LOG_ACTIONS_CHART_DOWNLOAD_AS_PDF, +} from 'src/logger/LogUtils'; import { MenuKeys, RootState } from 'src/dashboard/types'; import DrillDetailModal from 'src/components/Chart/DrillDetail/DrillDetailModal'; import { openInNewTab } from 'src/utils/navigationUtils'; @@ -302,29 +307,90 @@ const SliceHeaderControls = ( props.exportXLSX?.(props.slice.slice_id); break; case MenuKeys.DownloadAsImage: { - // menu closes with a delay, we need to hide it manually, - // so that we don't capture it on the screenshot + // Hide the dropdown menu so it is not captured in the screenshot const menu = document.querySelector( '.ant-dropdown:not(.ant-dropdown-hidden)', - ) as HTMLElement; + ) as HTMLElement | null; if (menu) { menu.style.visibility = 'hidden'; } - downloadAsImage( - getScreenshotNodeSelector(props.slice.slice_id), - props.slice.slice_name, - true, - theme, - )(domEvent).then(() => { + Promise.resolve( + downloadAsImage( + getScreenshotNodeSelector(props.slice.slice_id), + props.slice.slice_name, + true, + theme, + )(domEvent), + ).finally(() => { if (menu) { menu.style.visibility = 'visible'; } }); + // eslint-disable-next-line no-unused-expressions props.logEvent?.(LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE, { chartId: props.slice.slice_id, }); break; } + case MenuKeys.DownloadAsPngTransparent: + case MenuKeys.DownloadAsPngSolid: { + const menu = document.querySelector( + '.ant-dropdown:not(.ant-dropdown-hidden)', + ) as HTMLElement | null; + if (menu) { + menu.style.visibility = 'hidden'; + } + + const backgroundType = + key === MenuKeys.DownloadAsPngTransparent ? 'transparent' : 'solid'; + + Promise.resolve( + downloadAsImage( + getScreenshotNodeSelector(props.slice.slice_id), + props.slice.slice_name, + true, + theme, + { format: 'png', backgroundType }, + )(domEvent), + ).finally(() => { + if (menu) { + menu.style.visibility = 'visible'; + } + }); + + // eslint-disable-next-line no-unused-expressions + props.logEvent?.(LOG_ACTIONS_CHART_DOWNLOAD_AS_PNG, { + chartId: props.slice.slice_id, + backgroundType, + }); + break; + } + case MenuKeys.DownloadAsPdf: { + const menu = document.querySelector( + '.ant-dropdown:not(.ant-dropdown-hidden)', + ) as HTMLElement | null; + if (menu) { + menu.style.visibility = 'hidden'; + } + + Promise.resolve( + downloadAsPdf( + getScreenshotNodeSelector(props.slice.slice_id), + props.slice.slice_name, + true, + )(domEvent), + ).finally(() => { + if (menu) { + menu.style.visibility = 'visible'; + } + }); + + // eslint-disable-next-line no-unused-expressions + props.logEvent?.(LOG_ACTIONS_CHART_DOWNLOAD_AS_PDF, { + chartId: props.slice.slice_id, + }); + break; + } case MenuKeys.ExportPivotXlsx: { const sliceSelector = `#chart-id-${props.slice.slice_id}`; props.exportPivotExcel?.( @@ -614,9 +680,30 @@ const SliceHeaderControls = ( : []), { key: MenuKeys.DownloadAsImage, - label: t('Download as image'), + label: t('Export screenshot (jpeg)'), icon: , }, + { + type: 'submenu', + key: 'download_as_png_submenu', + label: t('Export screenshot (png)'), + icon: , + children: [ + { + key: MenuKeys.DownloadAsPngTransparent, + label: t('Transparent background'), + }, + { + key: MenuKeys.DownloadAsPngSolid, + label: t('Solid background'), + }, + ], + }, + { + key: MenuKeys.DownloadAsPdf, + label: t('Export as PDF'), + icon: , + }, ], }); } diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index 24a4eb9ab9d..67b7704f8ca 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -367,6 +367,9 @@ export interface SliceEntitiesState { export enum MenuKeys { DownloadAsImage = 'download_as_image', + DownloadAsPngTransparent = 'download_as_png_transparent', + DownloadAsPngSolid = 'download_as_png_solid', + DownloadAsPdf = 'download_as_pdf', ExploreChart = 'explore_chart', ExportCsv = 'export_csv', ExportPivotCsv = 'export_pivot_csv', diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx index e95496aef74..a8f8f32e98f 100644 --- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx +++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx @@ -49,6 +49,7 @@ import { useToasts } from 'src/components/MessageToasts/withToasts'; import { DEFAULT_CSV_STREAMING_ROW_THRESHOLD } from 'src/constants'; import { exportChart, getChartKey } from 'src/explore/exploreUtils'; import downloadAsImage from 'src/utils/downloadAsImage'; +import downloadAsPdf from 'src/utils/downloadAsPdf'; import { getChartPermalink } from 'src/utils/urlUtils'; import copyTextToClipboard from 'src/utils/copy'; import { useHeaderReportMenuItems } from 'src/features/reports/ReportModal/HeaderReportDropdown'; @@ -56,6 +57,8 @@ import { MenuItemTooltip } from 'src/components/Chart/DisabledMenuItemTooltip'; import { logEvent } from 'src/logger/actions'; import { LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE, + LOG_ACTIONS_CHART_DOWNLOAD_AS_PNG, + LOG_ACTIONS_CHART_DOWNLOAD_AS_PDF, LOG_ACTIONS_CHART_DOWNLOAD_AS_JSON, LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV, LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV_PIVOTED, @@ -88,9 +91,15 @@ const MENU_KEYS = { EXPORT_TO_JSON: 'export_to_json', EXPORT_TO_XLSX: 'export_to_xlsx', EXPORT_ALL_SCREENSHOT: 'export_all_screenshot', + EXPORT_ALL_PNG_TRANSPARENT: 'export_all_png_transparent', + EXPORT_ALL_PNG_SOLID: 'export_all_png_solid', + EXPORT_ALL_PDF: 'export_all_pdf', EXPORT_CURRENT_TO_CSV: 'export_current_to_csv', EXPORT_CURRENT_TO_JSON: 'export_current_to_json', EXPORT_CURRENT_SCREENSHOT: 'export_current_screenshot', + EXPORT_CURRENT_PNG_TRANSPARENT: 'export_current_png_transparent', + EXPORT_CURRENT_PNG_SOLID: 'export_current_png_solid', + EXPORT_CURRENT_PDF: 'export_current_pdf', EXPORT_CURRENT_XLSX: 'export_current_xlsx', SHARE_SUBMENU: 'share_submenu', COPY_PERMALINK: 'copy_permalink', @@ -107,6 +116,98 @@ const MENU_KEYS = { const VIZ_TYPES_PIVOTABLE = [VizType.PivotTable]; +const CHART_EXPORT_SELECTOR = '.panel-body .chart-container'; + +export function getExportScreenshotMenuItems({ + chartSelector, + sliceName, + chartId, + theme, + setIsDropdownVisible, + dispatch, + submenuKey, + transparentKey, + solidKey, + pdfKey, +}: { + chartSelector: string; + sliceName: string; + chartId?: number; + theme: ReturnType; + setIsDropdownVisible: (visible: boolean) => void; + dispatch: Dispatch; + submenuKey: string; + transparentKey: string; + solidKey: string; + pdfKey: string; +}) { + return [ + { + type: 'submenu' as const, + key: submenuKey, + label: t('Export screenshot (png)'), + icon: , + children: [ + { + key: transparentKey, + label: t('Transparent background'), + onClick: (e: { + domEvent: React.MouseEvent | React.KeyboardEvent; + }) => { + downloadAsImage(chartSelector, sliceName, true, theme, { + format: 'png', + backgroundType: 'transparent', + })(e.domEvent); + setIsDropdownVisible(false); + dispatch( + logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_PNG, { + chartId, + chartName: sliceName, + backgroundType: 'transparent', + }), + ); + }, + }, + { + key: solidKey, + label: t('Solid background'), + onClick: (e: { + domEvent: React.MouseEvent | React.KeyboardEvent; + }) => { + downloadAsImage(chartSelector, sliceName, true, theme, { + format: 'png', + backgroundType: 'solid', + })(e.domEvent); + setIsDropdownVisible(false); + dispatch( + logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_PNG, { + chartId, + chartName: sliceName, + backgroundType: 'solid', + }), + ); + }, + }, + ], + }, + { + key: pdfKey, + label: t('Export as PDF'), + icon: , + onClick: (e: { domEvent: React.MouseEvent | React.KeyboardEvent }) => { + downloadAsPdf(chartSelector, sliceName, true)(e.domEvent); + setIsDropdownVisible(false); + dispatch( + logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_PDF, { + chartId, + chartName: sliceName, + }), + ); + }, + }, + ]; +} + export const MenuItemWithCheckboxContainer = styled.div` ${({ theme }) => css` display: flex; @@ -811,7 +912,7 @@ export const useExploreAdditionalActionsMenu = ( disabled: imageExportDisabled, onClick: (e: { domEvent: React.MouseEvent | React.KeyboardEvent }) => { downloadAsImage( - '.panel-body .chart-container', + CHART_EXPORT_SELECTOR, slice?.slice_name ?? t('New chart'), true, theme, @@ -825,6 +926,18 @@ export const useExploreAdditionalActionsMenu = ( ); }, }, + ...getExportScreenshotMenuItems({ + chartSelector: CHART_EXPORT_SELECTOR, + sliceName: slice?.slice_name ?? t('New chart'), + chartId: slice?.slice_id, + theme, + setIsDropdownVisible, + dispatch, + submenuKey: 'export_all_png_submenu', + transparentKey: MENU_KEYS.EXPORT_ALL_PNG_TRANSPARENT, + solidKey: MENU_KEYS.EXPORT_ALL_PNG_SOLID, + pdfKey: MENU_KEYS.EXPORT_ALL_PDF, + }), { key: MENU_KEYS.EXPORT_TO_XLSX, label: dataExportLabel(t('Export to Excel')), @@ -920,7 +1033,7 @@ export const useExploreAdditionalActionsMenu = ( disabled: imageExportDisabled, onClick: (e: { domEvent: React.MouseEvent | React.KeyboardEvent }) => { downloadAsImage( - '.panel-body .chart-container', + CHART_EXPORT_SELECTOR, slice?.slice_name ?? t('New chart'), true, theme, @@ -934,6 +1047,18 @@ export const useExploreAdditionalActionsMenu = ( ); }, }, + ...getExportScreenshotMenuItems({ + chartSelector: CHART_EXPORT_SELECTOR, + sliceName: slice?.slice_name ?? t('New chart'), + chartId: slice?.slice_id, + theme, + setIsDropdownVisible, + dispatch, + submenuKey: 'export_current_png_submenu', + transparentKey: MENU_KEYS.EXPORT_CURRENT_PNG_TRANSPARENT, + solidKey: MENU_KEYS.EXPORT_CURRENT_PNG_SOLID, + pdfKey: MENU_KEYS.EXPORT_CURRENT_PDF, + }), { key: MENU_KEYS.EXPORT_CURRENT_XLSX, label: dataExportLabel(t('Export to Excel')), diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/useExploreAdditionalActionsMenu.test.tsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/useExploreAdditionalActionsMenu.test.tsx index 282bf532034..d4d57c7a043 100644 --- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/useExploreAdditionalActionsMenu.test.tsx +++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/useExploreAdditionalActionsMenu.test.tsx @@ -19,7 +19,12 @@ import { ComponentType } from 'react'; import { render, screen, waitFor } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; -import { useExploreAdditionalActionsMenu } from './index'; +import downloadAsImage from 'src/utils/downloadAsImage'; +import downloadAsPdf from 'src/utils/downloadAsPdf'; +import { + useExploreAdditionalActionsMenu, + getExportScreenshotMenuItems, +} from './index'; import * as exploreUtils from 'src/explore/exploreUtils'; jest.mock('src/explore/exploreUtils', () => ({ @@ -29,6 +34,22 @@ jest.mock('src/explore/exploreUtils', () => ({ getChartKey: jest.fn(() => 'test_chart_key'), })); +jest.mock('src/utils/downloadAsImage', () => ({ + __esModule: true, + default: jest.fn(() => jest.fn()), +})); +jest.mock('src/utils/downloadAsPdf', () => ({ + __esModule: true, + default: jest.fn(() => jest.fn()), +})); + +const mockDownloadAsImage = downloadAsImage as jest.MockedFunction< + typeof downloadAsImage +>; +const mockDownloadAsPdf = downloadAsPdf as jest.MockedFunction< + typeof downloadAsPdf +>; + const mockExportChart = exploreUtils.exportChart as jest.Mock; const mockAddDangerToast = jest.fn(); @@ -148,3 +169,83 @@ test('shows 413 error toast when Export Current View CSV server path fails with ); }); }); + +const CHART_SELECTOR = '.panel-body .chart-container'; +const SLICE_NAME = 'My chart'; +const CHART_ID = 42; +const domEvent = {} as React.MouseEvent; + +const buildScreenshotItems = () => { + const setIsDropdownVisible = jest.fn(); + const dispatch = jest.fn(); + const items = getExportScreenshotMenuItems({ + chartSelector: CHART_SELECTOR, + sliceName: SLICE_NAME, + chartId: CHART_ID, + theme: {} as any, + setIsDropdownVisible, + dispatch, + submenuKey: 'export_png_submenu', + transparentKey: 'export_png_transparent', + solidKey: 'export_png_solid', + pdfKey: 'export_pdf', + }) as any[]; + return { items, setIsDropdownVisible, dispatch }; +}; + +test('getExportScreenshotMenuItems builds the PNG submenu and PDF item with the provided keys', () => { + const { items } = buildScreenshotItems(); + const [pngSubmenu, pdfItem] = items; + + expect(pngSubmenu.key).toBe('export_png_submenu'); + expect(pngSubmenu.children).toHaveLength(2); + expect(pngSubmenu.children[0].key).toBe('export_png_transparent'); + expect(pngSubmenu.children[1].key).toBe('export_png_solid'); + expect(pdfItem.key).toBe('export_pdf'); +}); + +test('getExportScreenshotMenuItems transparent option downloads a transparent PNG and dispatches a log event', () => { + const { items, setIsDropdownVisible, dispatch } = buildScreenshotItems(); + + items[0].children[0].onClick({ domEvent }); + + expect(mockDownloadAsImage).toHaveBeenCalledWith( + CHART_SELECTOR, + SLICE_NAME, + true, + expect.anything(), + { format: 'png', backgroundType: 'transparent' }, + ); + expect(setIsDropdownVisible).toHaveBeenCalledWith(false); + expect(dispatch).toHaveBeenCalledTimes(1); +}); + +test('getExportScreenshotMenuItems solid option downloads a solid PNG and dispatches a log event', () => { + const { items, setIsDropdownVisible, dispatch } = buildScreenshotItems(); + + items[0].children[1].onClick({ domEvent }); + + expect(mockDownloadAsImage).toHaveBeenCalledWith( + CHART_SELECTOR, + SLICE_NAME, + true, + expect.anything(), + { format: 'png', backgroundType: 'solid' }, + ); + expect(setIsDropdownVisible).toHaveBeenCalledWith(false); + expect(dispatch).toHaveBeenCalledTimes(1); +}); + +test('getExportScreenshotMenuItems PDF option calls downloadAsPdf and dispatches a log event', () => { + const { items, setIsDropdownVisible, dispatch } = buildScreenshotItems(); + + items[1].onClick({ domEvent }); + + expect(mockDownloadAsPdf).toHaveBeenCalledWith( + CHART_SELECTOR, + SLICE_NAME, + true, + ); + expect(setIsDropdownVisible).toHaveBeenCalledWith(false); + expect(dispatch).toHaveBeenCalledTimes(1); +}); diff --git a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx index cbd60ecddd8..dfd9f55d5d5 100644 --- a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx +++ b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx @@ -861,9 +861,9 @@ test('opens Schedule Section on click', async () => { useRedux: true, }); userEvent.click(screen.getByTestId('schedule-panel')); - const scheduleHeader = within( + const [scheduleHeader] = within( screen.getByRole('tab', { expanded: true }), - ).queryAllByText(/schedule/i)[0]; + ).queryAllByText(/schedule/i); expect(scheduleHeader).toBeInTheDocument(); }); test('renders default Schedule fields', async () => { @@ -929,9 +929,9 @@ test('opens Notification Method Section on click', async () => { useRedux: true, }); userEvent.click(screen.getByTestId('notification-method-panel')); - const notificationMethodHeader = within( + const [notificationMethodHeader] = within( screen.getByRole('tab', { expanded: true }), - ).queryAllByText(/notification method/i)[0]; + ).queryAllByText(/notification method/i); expect(notificationMethodHeader).toBeInTheDocument(); }); diff --git a/superset-frontend/src/logger/LogUtils.ts b/superset-frontend/src/logger/LogUtils.ts index 784be5af50e..24277df3900 100644 --- a/superset-frontend/src/logger/LogUtils.ts +++ b/superset-frontend/src/logger/LogUtils.ts @@ -54,6 +54,8 @@ export const LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE = export const LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF = 'dashboard_download_as_pdf'; export const LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE = 'chart_download_as_image'; +export const LOG_ACTIONS_CHART_DOWNLOAD_AS_PNG = 'chart_download_as_png'; +export const LOG_ACTIONS_CHART_DOWNLOAD_AS_PDF = 'chart_download_as_pdf'; export const LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV = 'chart_download_as_csv'; export const LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV_PIVOTED = 'chart_download_as_csv_pivoted'; @@ -112,6 +114,8 @@ export const LOG_EVENT_TYPE_USER = new Set([ LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE, LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF, LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE, + LOG_ACTIONS_CHART_DOWNLOAD_AS_PNG, + LOG_ACTIONS_CHART_DOWNLOAD_AS_PDF, ]); export const LOG_EVENT_DATASET_TYPE_DATASET_CREATION = [ diff --git a/superset-frontend/src/utils/downloadAsImage.test.ts b/superset-frontend/src/utils/downloadAsImage.test.ts index 0959a8eaa22..7660667a6f9 100644 --- a/superset-frontend/src/utils/downloadAsImage.test.ts +++ b/superset-frontend/src/utils/downloadAsImage.test.ts @@ -24,7 +24,7 @@ import downloadAsImageOptimized, { jest.mock('dom-to-image-more', () => ({ __esModule: true, - default: { toJpeg: jest.fn() }, + default: { toJpeg: jest.fn(), toPng: jest.fn() }, })); jest.mock('src/components/MessageToasts/actions', () => ({ @@ -36,6 +36,7 @@ jest.mock('@apache-superset/core/translation', () => ({ })); const mockToJpeg = domToImage.toJpeg as jest.Mock; +const mockToPng = domToImage.toPng as jest.Mock; const mockAddWarningToast = addWarningToast as jest.Mock; // document.fonts.ready is not implemented in jsdom; provide a resolved promise @@ -81,6 +82,7 @@ function attachMockApi( beforeEach(() => { jest.clearAllMocks(); mockToJpeg.mockResolvedValue('data:image/jpeg;base64,test'); + mockToPng.mockResolvedValue('data:image/png;base64,test'); }); afterEach(() => { @@ -650,6 +652,55 @@ test('ag-grid path uses theme colorBgContainer as background', async () => { jest.useRealTimers(); }); +test('ag-grid path exports PNG (transparent) via toPng when format is png', async () => { + jest.useFakeTimers(); + const { container, agContainer, cleanup } = buildAgGridElement(); + attachMockApi(agContainer); + + const theme = { colorBgContainer: '#1a1a2e' } as any; + const handler = downloadAsImageOptimized('div', 'My Chart', false, theme, { + format: 'png', + backgroundType: 'transparent', + }); + const exportPromise = handler(syntheticEventFor(container)); + await jest.runAllTimersAsync(); + await exportPromise; + + // format=png must route to toPng (not toJpeg) with a transparent background + expect(mockToPng).toHaveBeenCalledWith( + expect.any(HTMLElement), + expect.objectContaining({ bgcolor: 'transparent' }), + ); + expect(mockToJpeg).not.toHaveBeenCalled(); + + cleanup(); + jest.useRealTimers(); +}); + +test('ag-grid path exports PNG (solid) via toPng using theme background', async () => { + jest.useFakeTimers(); + const { container, agContainer, cleanup } = buildAgGridElement(); + attachMockApi(agContainer); + + const theme = { colorBgContainer: '#1a1a2e' } as any; + const handler = downloadAsImageOptimized('div', 'My Chart', false, theme, { + format: 'png', + backgroundType: 'solid', + }); + const exportPromise = handler(syntheticEventFor(container)); + await jest.runAllTimersAsync(); + await exportPromise; + + expect(mockToPng).toHaveBeenCalledWith( + expect.any(HTMLElement), + expect.objectContaining({ bgcolor: '#1a1a2e' }), + ); + expect(mockToJpeg).not.toHaveBeenCalled(); + + cleanup(); + jest.useRealTimers(); +}); + test('ag-grid path falls back to white background when theme is absent', async () => { jest.useFakeTimers(); const { container, agContainer, cleanup } = buildAgGridElement(); diff --git a/superset-frontend/src/utils/downloadAsImage.tsx b/superset-frontend/src/utils/downloadAsImage.tsx index 24b9408288c..a3be38ba9b8 100644 --- a/superset-frontend/src/utils/downloadAsImage.tsx +++ b/superset-frontend/src/utils/downloadAsImage.tsx @@ -25,6 +25,8 @@ import { addWarningToast } from 'src/components/MessageToasts/actions'; import type { AgGridContainerElement } from '@superset-ui/core/components'; const IMAGE_DOWNLOAD_QUALITY = 0.95; +const PNG_SCALE = 2; // Higher quality for PNG +export type BackgroundType = 'transparent' | 'solid'; const TRANSPARENT_RGBA = 'transparent'; const POLL_INTERVAL_MS = 100; @@ -268,6 +270,13 @@ const createEnhancedClone = ( return { clone, cleanup }; }; +export type ImageFormat = 'jpeg' | 'png'; + +export interface DownloadImageOptions { + format?: ImageFormat; + backgroundType?: BackgroundType; +} + // Polls until scrollHeight is stable for minStablePolls consecutive intervals or maxMs elapses. // ag-grid has no "layout settled" event, so polling is the recommended workaround. export const waitForStableScrollHeight = ( @@ -312,7 +321,10 @@ export default function downloadAsImageOptimized( description: string, isExactSelector = false, theme?: SupersetTheme, + options: DownloadImageOptions = {}, ) { + const { format = 'jpeg', backgroundType = 'solid' } = options; + return async (event: SyntheticEvent) => { const elementToPrint = isExactSelector ? document.querySelector(selector) @@ -331,6 +343,13 @@ export default function downloadAsImageOptimized( !node.className.includes('header-controls') : true; + const isPng = format === 'png'; + const scale = isPng ? PNG_SCALE : 1; + const bgcolor = + isPng && backgroundType === 'transparent' + ? 'transparent' + : theme?.colorBgContainer; + // Only apply ag-grid path for single-chart captures. // Skip entirely for dashboard-level exports (selector targets the .dashboard root). const isDashboardCapture = ( @@ -420,17 +439,29 @@ export default function downloadAsImageOptimized( const imageHeight = agRootWrapper.scrollHeight; - const dataUrl = await domToImage.toJpeg(agRootWrapper, { - bgcolor: theme?.colorBgContainer, + const agImageOptions = { + bgcolor, filter, quality: IMAGE_DOWNLOAD_QUALITY, - height: imageHeight, - width: originalWidth, + height: imageHeight * scale, + width: originalWidth * scale, cacheBust: true, - }); + ...(isPng && { + style: { + transform: `scale(${PNG_SCALE})`, + transformOrigin: 'top left', + width: `${originalWidth}px`, + height: `${imageHeight}px`, + }, + }), + }; + + const dataUrl = isPng + ? await domToImage.toPng(agRootWrapper, agImageOptions) + : await domToImage.toJpeg(agRootWrapper, agImageOptions); const link = document.createElement('a'); - link.download = `${generateFileStem(description)}.jpg`; + link.download = `${generateFileStem(description)}.${isPng ? 'png' : 'jpg'}`; link.href = dataUrl; link.click(); } catch (error) { @@ -466,20 +497,33 @@ export default function downloadAsImageOptimized( ); cleanup = cleanupFn; - const dataUrl = await domToImage.toJpeg(clone, { - bgcolor: theme?.colorBgContainer, + const imageOptions = { + bgcolor, filter, quality: IMAGE_DOWNLOAD_QUALITY, - height: clone.scrollHeight, - width: clone.scrollWidth, + height: clone.scrollHeight * scale, + width: clone.scrollWidth * scale, cacheBust: true, - }); + ...(isPng && { + style: { + transform: `scale(${PNG_SCALE})`, + transformOrigin: 'top left', + width: `${clone.scrollWidth}px`, + height: `${clone.scrollHeight}px`, + }, + }), + }; + + const dataUrl = isPng + ? await domToImage.toPng(clone, imageOptions) + : await domToImage.toJpeg(clone, imageOptions); cleanup(); cleanup = null; + const extension = isPng ? 'png' : 'jpg'; const link = document.createElement('a'); - link.download = `${generateFileStem(description)}.jpg`; + link.download = `${generateFileStem(description)}.${extension}`; link.href = dataUrl; link.click(); } catch (error) { diff --git a/superset-frontend/src/utils/export.test.ts b/superset-frontend/src/utils/export.test.ts index 15d5d89bb8a..ed5b61be2e7 100644 --- a/superset-frontend/src/utils/export.test.ts +++ b/superset-frontend/src/utils/export.test.ts @@ -444,9 +444,8 @@ test.each(doublePrefixTestCases)( const expectedEndpoint = `/api/v1/${resource}/export/?q=!(${ids.join(',')})`; // Explicitly verify no prefix in endpoint - this will fail if ensureAppRoot is used - const callArgs = (SupersetClient.get as jest.Mock).mock.calls.slice( - -1, - )[0][0]; + const [lastCall] = (SupersetClient.get as jest.Mock).mock.calls.slice(-1); + const [callArgs] = lastCall; expect(callArgs.endpoint).not.toContain(appRoot); expect(callArgs.endpoint).toBe(expectedEndpoint); diff --git a/superset/commands/chart/exceptions.py b/superset/commands/chart/exceptions.py index 43fef980249..2e5ae7c36a0 100644 --- a/superset/commands/chart/exceptions.py +++ b/superset/commands/chart/exceptions.py @@ -103,6 +103,19 @@ class DatasourceTypeUpdateRequiredValidationError(ValidationError): ) +class ChartQueryContextDatasourceMismatchValidationError(ValidationError): + """ + Raised when a query-context-only update carries a datasource that does not + match the chart's own datasource. + """ + + def __init__(self) -> None: + super().__init__( + _("The query context datasource does not match the chart datasource"), + field_name="query_context", + ) + + class ChartNotFoundError(CommandException): message = "Chart not found." diff --git a/superset/commands/chart/update.py b/superset/commands/chart/update.py index cc862d1e198..38f44d0ff9a 100644 --- a/superset/commands/chart/update.py +++ b/superset/commands/chart/update.py @@ -29,6 +29,7 @@ from superset.commands.chart.exceptions import ( ChartForbiddenError, ChartInvalidError, ChartNotFoundError, + ChartQueryContextDatasourceMismatchValidationError, ChartUpdateFailedError, DashboardsForbiddenError, DashboardsNotFoundValidationError, @@ -46,6 +47,7 @@ from superset.exceptions import SupersetSecurityException from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.tags.models import ObjectType +from superset.utils import json from superset.utils.decorators import on_error, transaction logger = logging.getLogger(__name__) @@ -106,6 +108,55 @@ class UpdateChartCommand(UpdateMixin, BaseCommand): if not security_manager.is_editor(dash): raise DashboardsForbiddenError() + def _validate_query_context_datasource( + self, exceptions: list[ValidationError] + ) -> None: + """ + Ensure a query-context-only update keeps the chart's own datasource. + + The submitted query context is only verified when it carries a parseable + ``datasource`` object; a payload that references a different datasource than + the chart's persisted one is rejected. Payloads without a datasource fall + back to the chart's datasource at execution time and need no check. + """ + if not self._model: + return + + raw_query_context = self._properties.get("query_context") + if not raw_query_context: + return + + try: + query_context = json.loads(raw_query_context) + except (TypeError, ValueError): + # TypeError covers a query_context that isn't a string (e.g. an + # already-parsed dict); that shape is intentionally out of scope + # here since the schema serializes it as a JSON string. ValueError + # covers a string that failed to parse. Either way, an unverifiable + # payload is left for downstream handling rather than guessed at. + return + + datasource = ( + query_context.get("datasource") if isinstance(query_context, dict) else None + ) + if not isinstance(datasource, dict): + return + + try: + ids_match = int(datasource["id"]) == self._model.datasource_id + except (KeyError, TypeError, ValueError): + ids_match = False + + # A datasource object must carry a type that matches the chart's own. + # Treating a missing type as valid would let an id-only payload through, + # and query-context loading reads datasource["type"] directly, so that + # payload raises KeyError when the saved context is later replayed. + datasource_type = datasource.get("type") + types_match = str(datasource_type) == self._model.datasource_type + + if not ids_match or not types_match: + exceptions.append(ChartQueryContextDatasourceMismatchValidationError()) + def validate(self) -> None: # noqa: C901 exceptions: list[ValidationError] = [] dashboard_ids = self._properties.get("dashboards") @@ -140,6 +191,9 @@ class UpdateChartCommand(UpdateMixin, BaseCommand): security_manager.raise_for_access(chart=self._model) except SupersetSecurityException as ex: raise ChartForbiddenError() from ex + # Keep the refreshed payload bound to the chart's own datasource so it + # cannot be repointed at an unrelated one. + self._validate_query_context_datasource(exceptions) # validate tags try: diff --git a/superset/commands/report/alert.py b/superset/commands/report/alert.py index 0d67a231e71..eecbce16ac9 100644 --- a/superset/commands/report/alert.py +++ b/superset/commands/report/alert.py @@ -37,6 +37,7 @@ from superset.commands.report.exceptions import ( AlertQueryMultipleRowsError, AlertQueryTimeout, AlertValidatorConfigError, + ReportScheduleExecutorNotFoundError, ) from superset.reports.models import ReportSchedule, ReportScheduleValidatorType from superset.tasks.utils import get_executor @@ -193,6 +194,7 @@ class AlertCommand(BaseCommand): database=self._report_schedule.database ) rendered_sql = sql_template.process_template(self._report_schedule.sql) + try: limited_rendered_sql = self._report_schedule.database.apply_limit_to_sql( rendered_sql, ALERT_SQL_LIMIT @@ -210,6 +212,13 @@ class AlertCommand(BaseCommand): model=self._report_schedule, ) user = security_manager.find_user(username) + # A deleted/disabled executor user makes find_user return None. Raise + # the dedicated error so the handler below re-surfaces it instead of + # masking it as an opaque AlertQueryError (or letting the missing user + # surface as a NoneType error from the downstream auth flow). + if user is None: + raise ReportScheduleExecutorNotFoundError(username) + with override_user(user): start = default_timer() df = self._report_schedule.database.get_df(sql=limited_rendered_sql) @@ -223,6 +232,10 @@ class AlertCommand(BaseCommand): except SoftTimeLimitExceeded as ex: logger.warning("A timeout occurred while executing the alert query: %s", ex) raise AlertQueryTimeout() from ex + except ReportScheduleExecutorNotFoundError: + # A missing executor user is a configuration problem, not a transient + # query error; surface the typed error rather than masking it. + raise except Exception as ex: logger.warning("An error occurred when running alert query") # The exception message here can reveal to much information to malicious diff --git a/superset/db_engine_specs/databricks.py b/superset/db_engine_specs/databricks.py index d4b1f23444c..255a8c15909 100644 --- a/superset/db_engine_specs/databricks.py +++ b/superset/db_engine_specs/databricks.py @@ -17,7 +17,9 @@ from __future__ import annotations import logging +import re from datetime import datetime +from re import Pattern from typing import Any, Callable, cast, TYPE_CHECKING, TypedDict, Union from apispec import APISpec @@ -56,6 +58,10 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +INSUFFICIENT_PERMISSIONS_REGEX: Pattern[str] = re.compile( + r"\[INSUFFICIENT_PERMISSIONS\]|\bSQLSTATE:\s*42501\b" +) + try: from databricks.sql.utils import ParamEscaper @@ -263,6 +269,25 @@ class DatabricksBaseEngineSpec(BaseEngineSpec): def epoch_to_dttm(cls) -> str: return HiveEngineSpec.epoch_to_dttm() + @classmethod + def extract_errors( + cls, + ex: Exception, + context: dict[str, Any] | None = None, + database_name: str | None = None, + ) -> list[SupersetError]: + raw_message = cls._extract_error_message(ex) + if INSUFFICIENT_PERMISSIONS_REGEX.search(raw_message): + return [ + SupersetError( + error_type=SupersetErrorType.CONNECTION_DATABASE_PERMISSIONS_ERROR, + message=raw_message, + level=ErrorLevel.WARNING, + extra={"engine_name": cls.engine_name}, + ) + ] + return super().extract_errors(ex, context, database_name) + class DatabricksODBCEngineSpec(DatabricksBaseEngineSpec): """Databricks engine spec using ODBC driver for SQL Endpoints.""" @@ -490,46 +515,15 @@ class DatabricksDynamicBaseEngineSpec(BasicParametersMixin, DatabricksBaseEngine context: dict[str, Any] | None = None, database_name: str | None = None, ) -> list[SupersetError]: - raw_message = cls._extract_error_message(ex) - context = context or {} # access_token isn't currently parseable from the # databricks error response, but adding it in here # for reference if their error message changes - for key, value in cls.context_key_mapping.items(): context[key] = context.get(value) - db_engine_custom_errors = cls.get_database_custom_errors(database_name) - if not isinstance(db_engine_custom_errors, dict): - db_engine_custom_errors = {} - - for regex, (message, error_type, extra) in [ - *db_engine_custom_errors.items(), - *cls.custom_errors.items(), - ]: - match = regex.search(raw_message) - if match: - params = {**context, **match.groupdict()} - extra["engine_name"] = cls.engine_name - return [ - SupersetError( - error_type=error_type, - message=message % params, - level=ErrorLevel.ERROR, - extra=extra, - ) - ] - - return [ - SupersetError( - error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR, - message=cls._extract_error_message(ex), - level=ErrorLevel.ERROR, - extra={"engine_name": cls.engine_name}, - ) - ] + return super().extract_errors(ex, context, database_name) @classmethod def validate_parameters( # type: ignore diff --git a/superset/mcp_service/chart/schemas.py b/superset/mcp_service/chart/schemas.py index 1213250fdae..e337b5cca26 100644 --- a/superset/mcp_service/chart/schemas.py +++ b/superset/mcp_service/chart/schemas.py @@ -306,6 +306,8 @@ class GetChartInfoRequest(BaseModel): current chart configuration from cache. """ + model_config = ConfigDict(populate_by_name=True) + identifier: Annotated[ int | str | None, Field( @@ -314,6 +316,7 @@ class GetChartInfoRequest(BaseModel): "Chart identifier - can be numeric ID or UUID string. " "Optional when form_data_key is provided (for unsaved charts)." ), + validation_alias=AliasChoices("identifier", "id", "chart_id"), ), ] form_data_key: str | None = Field( @@ -344,6 +347,7 @@ class GetChartInfoRequest(BaseModel): "set that excludes 'form_data' (the full chart config, can be 50KB+). " "Add 'form_data' explicitly when you need the raw chart configuration." ), + validation_alias=AliasChoices("select_columns", "columns"), ), ] @@ -1937,6 +1941,8 @@ class ChartRequestNormalizerMixin(BaseModel): class ListChartsRequest(EditedByMeMixin, CreatedByMeMixin, MetadataCacheControl): """Request schema for list_charts with clear, unambiguous types.""" + model_config = ConfigDict(populate_by_name=True) + filters: Annotated[ List[ChartFilter], Field( @@ -1952,6 +1958,7 @@ class ListChartsRequest(EditedByMeMixin, CreatedByMeMixin, MetadataCacheControl) default_factory=list, description="List of columns to select. Defaults to common columns if not " "specified.", + validation_alias=AliasChoices("select_columns", "columns"), ), ] @@ -2129,6 +2136,8 @@ class GenerateChartRequest(ChartRequestNormalizerMixin, QueryCacheControl): class GenerateExploreLinkRequest(ChartRequestNormalizerMixin, FormDataCacheControl): + model_config = ConfigDict(populate_by_name=True) + dataset_id: int | str = Field(..., description="Dataset identifier (ID, UUID)") config: ChartConfig | None = Field( None, @@ -2143,7 +2152,11 @@ class GenerateExploreLinkRequest(ChartRequestNormalizerMixin, FormDataCacheContr class UpdateChartRequest(ChartRequestNormalizerMixin, QueryCacheControl): model_config = ConfigDict(populate_by_name=True) - identifier: int | str = Field(..., description="Chart ID or UUID") + identifier: int | str = Field( + ..., + description="Chart ID or UUID", + validation_alias=AliasChoices("identifier", "id", "chart_id"), + ) config: ChartConfig | None = Field( None, description="Chart configuration. Optional; omit to only update chart_name.", @@ -2188,6 +2201,8 @@ class UpdateChartRequest(ChartRequestNormalizerMixin, QueryCacheControl): class UpdateChartPreviewRequest(ChartRequestNormalizerMixin, FormDataCacheControl): + model_config = ConfigDict(populate_by_name=True) + form_data_key: str | None = Field( None, description=( @@ -2215,12 +2230,15 @@ class GetChartDataRequest(QueryCacheControl): the current chart configuration from cache. """ + model_config = ConfigDict(populate_by_name=True) + identifier: int | str | None = Field( default=None, description=( "Chart identifier (ID, UUID). " "Optional when form_data_key is provided (for unsaved charts)." ), + validation_alias=AliasChoices("identifier", "id", "chart_id"), ) form_data_key: str | None = Field( default=None, @@ -2345,12 +2363,15 @@ class GetChartPreviewRequest(QueryCacheControl): using the current chart configuration from cache. """ + model_config = ConfigDict(populate_by_name=True) + identifier: int | str | None = Field( default=None, description=( "Chart identifier (ID, UUID). " "Optional when form_data_key is provided (for unsaved charts)." ), + validation_alias=AliasChoices("identifier", "id", "chart_id"), ) form_data_key: str | None = Field( default=None, @@ -2564,12 +2585,15 @@ class GetChartSqlRequest(BaseModel): to get the SQL for the unsaved chart state from the Explore view. """ + model_config = ConfigDict(populate_by_name=True) + identifier: int | str | None = Field( default=None, description=( "Chart identifier - can be numeric ID or UUID string. " "Optional when form_data_key is provided (for unsaved charts)." ), + validation_alias=AliasChoices("identifier", "id", "chart_id"), ) form_data_key: str | None = Field( default=None, diff --git a/superset/mcp_service/dashboard/schemas.py b/superset/mcp_service/dashboard/schemas.py index 133ba830dd3..29b7839e546 100644 --- a/superset/mcp_service/dashboard/schemas.py +++ b/superset/mcp_service/dashboard/schemas.py @@ -195,6 +195,8 @@ class DashboardFilter(ColumnOperator): class ListDashboardsRequest(EditedByMeMixin, CreatedByMeMixin, MetadataCacheControl): """Request schema for list_dashboards with clear, unambiguous types.""" + model_config = ConfigDict(populate_by_name=True) + filters: Annotated[ List[DashboardFilter], Field( @@ -210,6 +212,7 @@ class ListDashboardsRequest(EditedByMeMixin, CreatedByMeMixin, MetadataCacheCont default_factory=list, description="List of columns to select. Defaults to common columns " "if not specified.", + validation_alias=AliasChoices("select_columns", "columns"), ), ] @@ -321,10 +324,15 @@ class GetDashboardInfoRequest(MetadataCacheControl): in a dashboard but the URL contains a permalink_key. """ + model_config = ConfigDict(populate_by_name=True) + identifier: Annotated[ int | str, Field( - description="Dashboard identifier - can be numeric ID, UUID string, or slug" + description=( + "Dashboard identifier - can be numeric ID, UUID string, or slug" + ), + validation_alias=AliasChoices("identifier", "id", "dashboard_id"), ), ] permalink_key: str | None = Field( @@ -347,6 +355,7 @@ class GetDashboardInfoRequest(MetadataCacheControl): "to override, e.g. ['id','dashboard_title','charts'] for minimal " "output, or add 'css' to include raw dashboard CSS." ), + validation_alias=AliasChoices("select_columns", "columns"), ), ] @@ -585,10 +594,18 @@ class DashboardList(BaseModel): class AddChartToDashboardRequest(BaseModel): """Request schema for adding a chart to an existing dashboard.""" + model_config = ConfigDict(populate_by_name=True) + dashboard_id: int = Field( - ..., description="ID of the dashboard to add the chart to" + ..., + description="ID of the dashboard to add the chart to", + validation_alias=AliasChoices("dashboard_id", "dashboard", "id"), + ) + chart_id: int = Field( + ..., + description="ID of the chart to add to the dashboard", + validation_alias=AliasChoices("chart_id", "chart"), ) - chart_id: int = Field(..., description="ID of the chart to add to the dashboard") target_tab: str | None = Field( None, min_length=1, diff --git a/superset/mcp_service/dataset/schemas.py b/superset/mcp_service/dataset/schemas.py index adbf10cd210..27b5df2d5cb 100644 --- a/superset/mcp_service/dataset/schemas.py +++ b/superset/mcp_service/dataset/schemas.py @@ -25,6 +25,7 @@ from datetime import datetime, timezone from typing import Annotated, Any, Dict, List, Literal from pydantic import ( + AliasChoices, BaseModel, ConfigDict, Field, @@ -262,6 +263,8 @@ class DatasetList(BaseModel): class ListDatasetsRequest(EditedByMeMixin, CreatedByMeMixin, MetadataCacheControl): """Request schema for list_datasets with clear, unambiguous types.""" + model_config = ConfigDict(populate_by_name=True) + filters: Annotated[ List[DatasetFilter], Field( @@ -277,6 +280,7 @@ class ListDatasetsRequest(EditedByMeMixin, CreatedByMeMixin, MetadataCacheContro default_factory=list, description="List of columns to select. Defaults to common columns if not " "specified.", + validation_alias=AliasChoices("select_columns", "columns"), ), ] search: Annotated[ @@ -370,9 +374,14 @@ DEFAULT_GET_DATASET_INFO_COLUMN_FIELDS: List[str] = [ class GetDatasetInfoRequest(MetadataCacheControl): """Request schema for get_dataset_info with support for ID or UUID.""" + model_config = ConfigDict(populate_by_name=True) + identifier: Annotated[ int | str, - Field(description="Dataset identifier - can be numeric ID or UUID string"), + Field( + description="Dataset identifier - can be numeric ID or UUID string", + validation_alias=AliasChoices("identifier", "id", "dataset_id"), + ), ] select_columns: Annotated[ List[str], @@ -391,6 +400,7 @@ class GetDatasetInfoRequest(MetadataCacheControl): "Pass an explicit list to select only what you need " "(e.g. ['id', 'table_name', 'columns', 'metrics'])." ), + validation_alias=AliasChoices("select_columns", "columns"), ), ] column_fields: Annotated[ diff --git a/superset/migrations/versions/2026-06-30_00-00_d24e6b0a9c7f_strip_metricsqlexpressions_from_ag_grid_params.py b/superset/migrations/versions/2026-06-30_00-00_d24e6b0a9c7f_strip_metricsqlexpressions_from_ag_grid_params.py new file mode 100644 index 00000000000..4e408132af1 --- /dev/null +++ b/superset/migrations/versions/2026-06-30_00-00_d24e6b0a9c7f_strip_metricsqlexpressions_from_ag_grid_params.py @@ -0,0 +1,110 @@ +# 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. +"""strip metricSqlExpressions from ag_grid_table params + +Before PR #41555 the AG Grid table plugin leaked ``metricSqlExpressions`` +(a mapping of every datasource metric/column name to its SQL expression) into +``extra_form_data`` on every filter interaction, which was then serialised into +the ``params`` and ``query_context`` columns on save. For datasources with +many metrics this bloated each chart record by hundreds of MB. + +This migration strips the field from existing rows so those records are no +longer loaded eagerly on every dashboard request. + +Revision ID: d24e6b0a9c7f +Revises: 8f3a1b2c4d5e +Create Date: 2026-06-30 00:00:00.000000 + +""" + +from alembic import op +from sqlalchemy import Column, Integer, String, Text +from sqlalchemy.orm import declarative_base + +from superset import db +from superset.utils import json + +# revision identifiers, used by Alembic. +revision = "d24e6b0a9c7f" +down_revision = "8f3a1b2c4d5e" + +Base = declarative_base() + +_FIELD = "metricSqlExpressions" +_VIZ_TYPE = "ag_grid_table" + + +class Slice(Base): + __tablename__ = "slices" + id = Column(Integer, primary_key=True) + viz_type = Column(String(250)) + params = Column(Text) + query_context = Column(Text) + + +def _strip_params(slc: Slice) -> bool: + """Remove _FIELD from extra_form_data in params. Returns True if changed.""" + if not slc.params: + return False + try: + params = json.loads(slc.params) + except Exception: + return False + + extra = params.get("extra_form_data", {}) + if _FIELD not in extra: + return False + + del extra[_FIELD] + params["extra_form_data"] = extra + slc.params = json.dumps(params) + return True + + +def _strip_query_context(slc: Slice) -> bool: + """Remove _FIELD from query_context.form_data.extra_form_data.""" + if not slc.query_context: + return False + try: + qc = json.loads(slc.query_context) + except Exception: + return False + + extra = qc.get("form_data", {}).get("extra_form_data", {}) + if _FIELD not in extra: + return False + + del extra[_FIELD] + slc.query_context = json.dumps(qc) + return True + + +def upgrade() -> None: + bind = op.get_bind() + session = db.Session(bind=bind) + + for slc in session.query(Slice).filter(Slice.viz_type == _VIZ_TYPE): + _strip_params(slc) + _strip_query_context(slc) + + session.flush() + + +def downgrade() -> None: + # The stripped data was runtime-derived and is regenerated automatically + # by the chart plugin on the next render — there is nothing to restore. + pass diff --git a/superset/sqllab/sql_json_executer.py b/superset/sqllab/sql_json_executer.py index d65dbc071d0..690e0d1b682 100644 --- a/superset/sqllab/sql_json_executer.py +++ b/superset/sqllab/sql_json_executer.py @@ -107,9 +107,13 @@ class SynchronousSqlJsonExecutor(SqlJsonExecutorBase): if data.get("status") == QueryStatus.FAILED: # type: ignore # new error payload with rich context if data["errors"]: # type: ignore - raise SupersetErrorsException( - [SupersetError(**params) for params in data["errors"]] # type: ignore + errors = [SupersetError(**params) for params in data["errors"]] # type: ignore + status = ( + 500 + if any(error.level == ErrorLevel.ERROR for error in errors) + else 400 ) + raise SupersetErrorsException(errors, status=status) # old string-only error message raise SupersetGenericDBErrorException(data["error"]) # type: ignore diff --git a/superset/translations/ja/LC_MESSAGES/messages.po b/superset/translations/ja/LC_MESSAGES/messages.po index 56e7b558955..612b0bc2784 100644 --- a/superset/translations/ja/LC_MESSAGES/messages.po +++ b/superset/translations/ja/LC_MESSAGES/messages.po @@ -34,6 +34,9 @@ msgid "" "Restore it via POST /api/v1/dataset/%(uuid)s/restore before creating a new " "dataset over this table, or use a different table name." msgstr "" +"ソフト削除されたデータセット (uuid %(uuid)s) が、すでにこのテーブルを参照しています。" +"このテーブル上に新しいデータセットを作成する前に、POST /api/v1/dataset/%(uuid)s/restore を使用して復元するか、" +"別のテーブル名を使用してください。" #, python-format msgid "" @@ -41,6 +44,9 @@ msgid "" "Restore it via POST /api/v1/dataset/%(uuid)s/restore before uploading, or " "upload to a different table name." msgstr "" +"ソフト削除されたデータセット(uuid %(uuid)s)が、すでにこのテーブルを参照しています。" +"アップロードする前に、POST /api/v1/dataset/%(uuid)s/restore を使用して復元するか、" +"別のテーブル名にアップロードしてください。" msgid "" "Cannot delete a database whose only remaining datasets are soft-deleted. " @@ -48,12 +54,17 @@ msgid "" "permanently once a purge capability ships, or remove the underlying rows " "out-of-band, before deleting the database." msgstr "" +"残っているデータセットがすべてソフト削除されているデータベースは削除できません。" +"それらを復元(POST /api/v1/dataset//restore)し、パージ機能が提供されたら完全に削除するか、" +"データベースを削除する前に、別の手順で基となる行を削除してください。" msgid "" "Dataset cannot be restored because another active dataset already references " "the same physical table (same database, catalog, schema, and table name). " "Delete the duplicate or rename the table before restoring." msgstr "" +"別のアクティブなデータセットがすでに同じ物理テーブル(同じデータベース、カタログ、スキーマ、およびテーブル名)を参照しているため、" +"データセットを復元できません。復元する前に、重複しているデータセットを削除するか、テーブルの名前を変更してください。" msgid "" "\n" @@ -225,9 +236,7 @@ msgstr "%(object)s はこのデータベース内に存在しません。" msgid "%(prefix)s %(title)s" msgstr "%(prefix)s %(title)s" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, es, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "" "%(prefix)sResults truncated to %(row_count)s rows due to memory constraints." msgstr "%(prefix)sメモリの制限により、結果が%(row_count)s行に切り詰められました。" @@ -262,13 +271,13 @@ msgstr "" "クエリを再確認してください。\n" "例外: %(ex)s" -#, fuzzy, python-format +#, python-format msgid "%s %s" -msgstr "%s%s" +msgstr "%s %s" -#, fuzzy, python-format +#, python-format msgid "%s ENCRYPTED EXTRA" -msgstr "セキュア設定" +msgstr "%s 暗号化された追加設定" #, python-format msgid "%s Error" @@ -278,9 +287,9 @@ msgstr "%s エラー" msgid "%s PASSWORD" msgstr "%s パスワード" -#, fuzzy, python-format +#, python-format msgid "%s Physical" -msgstr "物理" +msgstr "%s 物理" #, python-format msgid "%s SSH TUNNEL PASSWORD" @@ -298,9 +307,9 @@ msgstr "%s SSHトンネル 秘密鍵パスワード" msgid "%s Selected" msgstr "%s 個選択済み" -#, fuzzy, python-format +#, python-format msgid "%s Selected (%s)" -msgstr "%s 個選択済み" +msgstr "%s 個選択済み (%s)" #, python-format msgid "%s Selected (Physical)" @@ -310,9 +319,7 @@ msgstr "%s 個選択済み (物理)" msgid "%s Selected (Virtual)" msgstr "%s 個選択済み (仮想)" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, es, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "%s Semantic View" msgstr "%s セマンティックビュー" @@ -320,15 +327,15 @@ msgstr "%s セマンティックビュー" msgid "%s URL" msgstr "%s URL" -#, fuzzy, python-format +#, python-format msgid "%s Virtual" -msgstr "仮想" +msgstr "%s 仮想" #, python-format msgid "%s aggregates(s)" msgstr "%s 個の集計" -#, fuzzy, python-format +#, python-format msgid "%s column" msgid_plural "%s columns" msgstr[0] "%s 個の列" @@ -337,21 +344,21 @@ msgstr[0] "%s 個の列" msgid "%s column(s)" msgstr "%s 個の列" -#, fuzzy, python-format +#, python-format msgid "%s day ago" msgid_plural "%s days ago" -msgstr[0] "%s日前" +msgstr[0] "%s 日前" -#, fuzzy, python-format +#, python-format msgid "%s hr ago" msgid_plural "%s hr ago" -msgstr[0] "%s 行" +msgstr[0] "%s 時間前" -#, fuzzy, python-format +#, python-format msgid "%s imported" -msgstr "データをインポートしました" +msgstr "%s をインポートしました" -#, fuzzy, python-format +#, python-format msgid "%s item" msgid_plural "%s items" msgstr[0] "%s 個の項目" @@ -366,15 +373,15 @@ msgid "" "selected objects." msgstr "選択したすべてのオブジェクトに対する編集権限がないため、%s 個の項目にタグを付けられませんでした。" -#, fuzzy, python-format +#, python-format msgid "%s metric" msgid_plural "%s metrics" msgstr[0] "%s件のメトリック" -#, fuzzy, python-format +#, python-format msgid "%s min ago" msgid_plural "%s min ago" -msgstr[0] "%s分前" +msgstr[0] "%s 分前" #, python-format msgid "%s operator(s)" @@ -389,19 +396,19 @@ msgstr[0] "%s 個のオプション" msgid "%s option(s)" msgstr "%s 個のオプション" -#, fuzzy, python-format +#, python-format msgid "%s out of %s column" msgid_plural "%s out of %s columns" msgstr[0] "%s / %s 列" -#, fuzzy, python-format +#, python-format msgid "%s out of %s metric" msgid_plural "%s out of %s metrics" msgstr[0] "%s / %s メトリック" -#, fuzzy, python-format +#, python-format msgid "%s out of %s selected" -msgstr "%s タブが選択されました" +msgstr "%s / %s 選択済み" #, python-format msgid "%s recipients" @@ -412,45 +419,39 @@ msgid "%s record" msgid_plural "%s records..." msgstr[0] "%s 件のレコード" -#, fuzzy, python-format +#, python-format msgid "%s record..." msgid_plural "%s records..." -msgstr[0] "%s 件のレコード" +msgstr[0] "%s 件のレコード..." #, python-format msgid "%s row" msgid_plural "%s rows" msgstr[0] "%s 行" -#, fuzzy, python-format +#, python-format msgid "%s s ago" msgid_plural "%s s ago" -msgstr[0] "%s秒前" +msgstr[0] "%s 秒前" #, python-format msgid "%s saved metric(s)" msgstr "%s 個の保存済みメトリック" -#, fuzzy, python-format +#, python-format msgid "%s second" msgid_plural "%s seconds" -msgstr[0] "%s秒" +msgstr[0] "%s 秒" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, es, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "%s semantic view(s) added" msgstr "%s 件のセマンティックビューが追加されました" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, es, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "%s semantic view(s) failed to add" msgstr "%s 件のセマンティックビューの追加に失敗しました" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "%s semantic view(s) failed to add: %s" msgstr "%s 件のセマンティックビューの追加に失敗しました: %s" @@ -526,13 +527,13 @@ msgstr "" "-- 注: クエリを保存しない限り、ブラウザのCookieを削除したりブラウザを変更したりすると、これらのタブは保持されません。\n" "\n" -#, fuzzy, python-format +#, python-format msgid "... and %d more" -msgstr "... 他 %s 件" +msgstr "... 他 %d 件" -#, fuzzy, python-format +#, python-format msgid "... and %s more records" -msgstr "... 他 %s 件" +msgstr "... 他 %s 件のレコード" #, python-format msgid "... and %s others" @@ -787,9 +788,7 @@ msgstr "ラベル設定オブジェクトを生成するJavaScript関数" msgid "A JavaScript function that generates an icon configuration object" msgstr "アイコン設定オブジェクトを生成するJavaScript関数" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-brace-format, fuzzy +#, python-brace-format msgid "" "A JavaScript object that adheres to the ECharts options specification, " "overriding other control options with higher precedence. (i.e. { title: { " @@ -816,7 +815,7 @@ msgid "A date is required when using custom date shift" msgstr "カスタム日付シフトを使用する場合は、日付の指定が必要です" msgid "A description for your dashboard" -msgstr "" +msgstr "ダッシュボードの説明" #, python-brace-format msgid "" @@ -869,7 +868,7 @@ msgstr "色に使用するメトリック" msgid "" "A multiplier applied to the point radius. Use this to uniformly scale all" " points." -msgstr "" +msgstr "ポイントの半径に適用される倍率です。これを使用すると、すべてのポイントを均一に拡大・縮小できます。" msgid "A new chart and dashboard will be created." msgstr "新しいチャートとダッシュボードが作成されます。" @@ -936,30 +935,23 @@ msgstr "キー順 (a-z)" msgid "AND" msgstr "かつ" -#, fuzzy msgid "API Key Created" -msgstr "を作成しました" +msgstr "APIキーが作成されました" -#, fuzzy msgid "API Keys" -msgstr "秘密鍵" +msgstr "APIキー" -#, fuzzy msgid "API key created successfully" -msgstr "ロールを正常に作成しました。" +msgstr "APIキーが正常に作成されました" -#, fuzzy msgid "API key name is required" -msgstr "テーブル名は必須です" +msgstr "APIキー名は必須です" msgid "API key revoked successfully" msgstr "APIキーが正常に無効化されました" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, es, sr, -# sr_Latn] -#, fuzzy msgid "API keys allow scoped programmatic access to Superset." -msgstr "API キーを使用すると、Superset へのスコープ付きプログラムアクセスが可能になります。" +msgstr "APIキーを使用すると、Supersetへのスコープ付きプログラムアクセスが可能になります。" msgid "APPLY" msgstr "適用" @@ -973,13 +965,11 @@ msgstr "非同期クエリ実行" msgid "AUG" msgstr "8月" -#, fuzzy msgid "Aborted" -msgstr "開始済み" +msgstr "中止" -#, fuzzy msgid "Aborting" -msgstr "処理中" +msgstr "中止中" msgid "About" msgstr "詳細" @@ -1029,9 +1019,9 @@ msgstr "適応型形式" msgid "Add" msgstr "追加" -#, fuzzy, python-format +#, python-format msgid "Add %s view(s)" -msgstr "%s 個の項目" +msgstr "%s 個のビューを追加" msgid "Add BCC Recipients" msgstr "BCCの追加" @@ -1064,9 +1054,8 @@ msgstr "ロールを追加" msgid "Add Rule" msgstr "ルールを追加" -#, fuzzy msgid "Add Semantic View" -msgstr "項目を追加" +msgstr "セマンティックビューを追加" msgid "Add Tag" msgstr "タグを追加" @@ -1119,9 +1108,8 @@ msgstr "このダッシュボードに認証情報を追加" msgid "Add color for positive/negative change" msgstr "増減に応じた色分けを追加" -#, fuzzy msgid "Add colors to cell bars for +/- for all columns" -msgstr "セルのバーに +/- の色分けを適用" +msgstr "すべての列のセルバーに +/- の色分けを追加" msgid "Add cross-filter" msgstr "クロスフィルターを追加" @@ -1180,9 +1168,8 @@ msgstr "新しいカラー形式を追加" msgid "Add new formatter" msgstr "新しい形式を追加" -#, fuzzy msgid "Add numbered column" -msgstr "計算列を追加する" +msgstr "番号付き列を追加" msgid "Add or edit display controls" msgstr "表示コントロールを追加または編集" @@ -1417,9 +1404,8 @@ msgstr "アラートとレポート" msgid "Align +/-" msgstr "+/- を揃える" -#, fuzzy msgid "Align +/- for all columns" -msgstr "すべての列を表示" +msgstr "すべての列で +/- を揃える" msgid "All" msgstr "すべて" @@ -1437,9 +1423,8 @@ msgstr "すべてのチャート" msgid "All charts/global scoping" msgstr "すべてのチャート(グローバルスコープ)" -#, fuzzy msgid "All dimensions" -msgstr "ディメンション" +msgstr "すべてのディメンション" msgid "All filters" msgstr "すべてのフィルター" @@ -1569,9 +1554,8 @@ msgstr "拡張機能へのアクセス中にエラーが発生しました。" msgid "An error occurred while accessing the value." msgstr "値へのアクセス中にエラーが発生しました。" -#, fuzzy msgid "An error occurred while adding semantic views" -msgstr "SQLの読み込み中にエラーが発生しました" +msgstr "セマンティックビューの追加中にエラーが発生しました" msgid "" "An error occurred while collapsing the table schema. Please contact your " @@ -1591,9 +1575,8 @@ msgstr "データソースの作成中にエラーが発生しました" msgid "An error occurred while creating the extension." msgstr "拡張機能の作成中にエラーが発生しました。" -#, fuzzy msgid "An error occurred while creating the semantic layer" -msgstr "値の作成中にエラーが発生しました。" +msgstr "セマンティックレイヤーの作成中にエラーが発生しました" msgid "An error occurred while creating the value." msgstr "値の作成中にエラーが発生しました。" @@ -1609,25 +1592,25 @@ msgid "" "administrator." msgstr "テーブルスキーマの展開中にエラーが発生しました。管理者に連絡してください。" -#, fuzzy, python-format +#, python-format msgid "An error occurred while fetching %s" -msgstr "%ss の取得中にエラーが発生しました: %s" +msgstr "%s の取得中にエラーが発生しました" #, python-format msgid "An error occurred while fetching %s info: %s" msgstr "%s 情報の取得中にエラーが発生しました: %s" -#, fuzzy, python-format +#, python-format msgid "An error occurred while fetching %s related data" -msgstr "データセット関連データの取得中にエラーが発生しました" +msgstr "%s 関連データの取得中にエラーが発生しました" -#, fuzzy, python-format +#, python-format msgid "An error occurred while fetching %s values: %s" -msgstr "ユーザー値の取得中にエラーが発生しました: %s" +msgstr "%s の値の取得中にエラーが発生しました: %s" -#, fuzzy, python-format +#, python-format msgid "An error occurred while fetching %s: %s" -msgstr "%ss の取得中にエラーが発生しました: %s" +msgstr "%s の取得中にエラーが発生しました: %s" #, python-format msgid "An error occurred while fetching %ss: %s" @@ -1639,9 +1622,8 @@ msgstr "利用可能なCSSテンプレートの取得中にエラーが発生し msgid "An error occurred while fetching available themes" msgstr "利用可能なテーマの取得中にエラーが発生しました" -#, fuzzy msgid "An error occurred while fetching available views" -msgstr "利用可能なテーマの取得中にエラーが発生しました" +msgstr "利用可能なビューの取得中にエラーが発生しました" #, python-format msgid "An error occurred while fetching chart editor values: %s" @@ -1651,13 +1633,12 @@ msgstr "チャート編集者の値を取得中にエラーが発生しました msgid "An error occurred while fetching chart viewer values: %s" msgstr "チャート閲覧者の値を取得中にエラーが発生しました: %s" -#, fuzzy msgid "An error occurred while fetching connections" -msgstr "関数名の取得中にエラーが発生しました。" +msgstr "接続の取得中にエラーが発生しました" #, fuzzy, python-format msgid "An error occurred while fetching created by values: %s" -msgstr "スキーマの値を取得中にエラーが発生しました: %s" +msgstr "作成者の値を取得中にエラーが発生しました: %s" #, python-format msgid "An error occurred while fetching dashboard editor values: %s" @@ -1705,36 +1686,30 @@ msgstr "関数名の取得中にエラーが発生しました。" msgid "An error occurred while fetching schema values: %s" msgstr "スキーマの値を取得中にエラーが発生しました: %s" -#, fuzzy msgid "An error occurred while fetching semantic layer types" -msgstr "利用可能なテーマの取得中にエラーが発生しました" +msgstr "セマンティックレイヤーの種類を取得中にエラーが発生しました" -#, fuzzy msgid "An error occurred while fetching semantic layers" -msgstr "スキーマの値を取得中にエラーが発生しました: %s" +msgstr "セマンティックレイヤーの取得中にエラーが発生しました" msgid "An error occurred while fetching tab state" msgstr "タブの状態を取得中にエラーが発生しました" -#, fuzzy, python-format +#, python-format msgid "An error occurred while fetching table metadata for %s" -msgstr "テーブルのメタデータを取得中にエラーが発生しました" +msgstr "%s のテーブルメタデータの取得中にエラーが発生しました" -#, fuzzy msgid "An error occurred while fetching the configuration schema" -msgstr "関数名の取得中にエラーが発生しました。" +msgstr "構成スキーマの取得中にエラーが発生しました" -#, fuzzy msgid "An error occurred while fetching the runtime schema" -msgstr "関数名の取得中にエラーが発生しました。" +msgstr "ランタイムスキーマの取得中にエラーが発生しました" -#, fuzzy msgid "An error occurred while fetching the semantic layer" -msgstr "タブの状態を取得中にエラーが発生しました" +msgstr "セマンティックレイヤーの取得中にエラーが発生しました" -#, fuzzy msgid "An error occurred while fetching the semantic view structure" -msgstr "タブの状態を取得中にエラーが発生しました" +msgstr "セマンティックビュー構造の取得中にエラーが発生しました" #, python-format msgid "An error occurred while fetching theme datasource values: %s" @@ -1757,7 +1732,7 @@ msgstr "%s のインポート中にエラーが発生しました: %s" msgid "" "An error occurred while loading SQL Lab. This may be caused by a " "corrupted query state." -msgstr "" +msgstr "SQL Labの読み込み中にエラーが発生しました。これは、クエリの状態が破損していることが原因である可能性があります。" msgid "An error occurred while loading dashboard information." msgstr "ダッシュボード情報の読み込み中にエラーが発生しました。" @@ -1774,9 +1749,8 @@ msgstr "キーの解析中にエラーが発生しました。" msgid "An error occurred while pruning logs " msgstr "ログの削除(プルーニング)中にエラーが発生しました " -#, fuzzy msgid "An error occurred while refreshing the configuration schema" -msgstr "視覚化(ビジュアライゼーション)のレンダリング中にエラーが発生しました: %s" +msgstr "構成スキーマの更新中にエラーが発生しました" msgid "" "An error occurred while removing query. Please contact your administrator." @@ -1791,9 +1765,8 @@ msgstr "テーブルスキーマの削除中にエラーが発生しました。 msgid "An error occurred while rendering the visualization: %s" msgstr "視覚化(ビジュアライゼーション)のレンダリング中にエラーが発生しました: %s" -#, fuzzy msgid "An error occurred while saving the semantic view" -msgstr "SQLの読み込み中にエラーが発生しました" +msgstr "セマンティックビューの保存中にエラーが発生しました" msgid "An error occurred while starring this chart" msgstr "このチャートにお気に入りを設定中にエラーが発生しました" @@ -1810,9 +1783,8 @@ msgstr "%s の権限同期中にエラーが発生しました: %s" msgid "An error occurred while updating the extension." msgstr "拡張機能の更新中にエラーが発生しました。" -#, fuzzy msgid "An error occurred while updating the semantic layer" -msgstr "値の更新中にエラーが発生しました。" +msgstr "セマンティックレイヤーの更新中にエラーが発生しました" msgid "An error occurred while updating the value." msgstr "値の更新中にエラーが発生しました。" @@ -1991,9 +1963,6 @@ msgid "" "query satisfies the minimum periods defined in the rolling window." msgstr "適用されたローリングウィンドウの結果が空です。ソースクエリが、ローリングウィンドウで定義された最小期間を満たしているか確認してください。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "Applies only when \"Cell bars\" formatting is selected: the background of " "the histogram columns is displayed if the \"Show cell bars\" flag is " @@ -2116,7 +2085,7 @@ msgstr "「%s」をシステムのダークテーマに設定してもよろし msgid "" "Are you sure you want to set \"%s\" as the system default theme? This will " "apply to all users who haven't set a personal preference." -msgstr "「%s」をシステムのデフォルトテーマとして設定してもよろしいですか?これは個人設定を行っていないすべてのユーザーに適用されます。" +msgstr "「%s」をシステムの既定テーマとして設定してもよろしいですか?これは個人設定を行っていないすべてのユーザーに適用されます。" msgid "Area" msgstr "面" @@ -2165,21 +2134,17 @@ msgstr "自動" msgid "Auto Zoom" msgstr "自動ズーム" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "Auto refresh paused (set to %s seconds)" msgstr "自動更新が一時停止されました(%s 秒に設定)" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "Auto refresh paused - tab inactive (set to %s seconds)" msgstr "自動更新が一時停止されました - タブが非アクティブ(%s 秒に設定)" -#, fuzzy, python-format +#, python-format msgid "Auto refresh set to %s seconds" -msgstr "更新間隔は少なくとも %s 秒以上にする必要があります" +msgstr "自動更新が %s 秒に設定されました" msgid "Auto-detect" msgstr "自動検出" @@ -2223,9 +2188,8 @@ msgstr "平均" msgid "Average value" msgstr "平均値" -#, fuzzy msgid "Awaiting filter selection" -msgstr "選択を反転" +msgstr "フィルターの選択を待機中" msgid "Axis" msgstr "軸" @@ -2302,20 +2266,16 @@ msgstr "基本の指数" msgid "Base height" msgstr "基本の高さ" -#, fuzzy msgid "Base layer map style. Accepts a MapLibre-compatible style URL." -msgstr "ベースレイヤーのマップスタイル。Mapboxのドキュメントを参照: %s" +msgstr "ベースレイヤーのマップスタイル。MapLibre 互換のスタイル URL を受け付けます。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "Base layer map style. Accepts a Mapbox style URL (mapbox://styles/...)." -msgstr "ベースレイヤーのマップスタイル。Mapbox スタイル URL(mapbox://styles/...)を受け付けます。" +msgstr "ベースレイヤーのマップスタイル。Mapbox スタイルの URL (mapbox://styles/...) を受け付けます。" -#, fuzzy, python-format +#, python-format msgid "Base layer map style. See MapLibre documentation: %s" -msgstr "ベースレイヤーのマップスタイル。Mapboxのドキュメントを参照: %s" +msgstr "ベースレイヤーのマップスタイル。MapLibre のドキュメントを参照してください: %s" msgid "Base slope" msgstr "基本の勾配" @@ -2446,9 +2406,8 @@ msgstr "箱ひげ図" msgid "Breakdowns" msgstr "内訳" -#, fuzzy msgid "Breakpoint Metric" -msgstr "メトリックに基づく" +msgstr "ブレークポイントのメトリック" msgid "" "Breaks down the series by the category specified in this control.\n" @@ -2544,9 +2503,6 @@ msgstr "CSSテンプレート" msgid "CSS applied to the chart" msgstr "チャートに適用されたCSS" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "CSS styles may be removed by server-side HTML sanitization. If styles are " "not applying, ask your Superset administrator to adjust the HTML " @@ -2660,16 +2616,12 @@ msgstr "複数値の選択が可能" msgid "Cancel" msgstr "キャンセル" -#, fuzzy msgid "Cancel Task" -msgstr "キャンセル" +msgstr "タスクをキャンセル" msgid "Cancel query on window unload event" msgstr "ウィンドウのアンロード時にクエリをキャンセル" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, es, sr, -# sr_Latn] -#, fuzzy msgid "Cancellation not available due to missing abort handler" msgstr "中止ハンドラーが存在しないため、キャンセルは使用できません" @@ -2711,7 +2663,6 @@ msgstr "時間文字列 [%(human_readable)s] を解析できません" msgid "Captcha" msgstr "キャプチャ" -#, fuzzy msgid "Cartodiagram" msgstr "カルトダイアグラム" @@ -2849,17 +2800,14 @@ msgstr "このデータソースの変更は禁止されています" msgid "Changing this report is forbidden" msgstr "このレポートの変更は禁止されています" -#, fuzzy msgid "Changing this semantic layer is forbidden" -msgstr "このチャートの変更は禁止されています" +msgstr "このセマンティックレイヤーの変更は禁止されています" -#, fuzzy msgid "Changing this semantic view is forbidden" -msgstr "このデータセットの変更は禁止されています" +msgstr "このセマンティックビューの変更は禁止されています" -#, fuzzy msgid "Changing this task is forbidden" -msgstr "このデータセットの変更は禁止されています" +msgstr "このタスクの変更は禁止されています" msgid "Character to interpret as decimal point" msgstr "小数点として解釈する文字" @@ -2867,9 +2815,7 @@ msgstr "小数点として解釈する文字" msgid "Chart" msgstr "チャート" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "Chart %(chart_id)s is not on dashboard %(dashboard_id)s" msgstr "チャート %(chart_id)s はダッシュボード %(dashboard_id)s にありません" @@ -2926,7 +2872,7 @@ msgid "Chart could not be created." msgstr "チャートを作成できませんでした。" msgid "Chart could not be restored." -msgstr "" +msgstr "チャートを復元できませんでした。" msgid "Chart could not be updated." msgstr "チャートを更新できませんでした。" @@ -3029,9 +2975,9 @@ msgstr "[ラベル] で選択した項目は [グループ化] に含まれて msgid "Choice of [Point Radius] must be present in [Group By]" msgstr "[ポイント半径] で選択した項目は [グループ化] に含まれている必要があります" -#, fuzzy, python-format +#, python-format msgid "Choose a %s" -msgstr "ソースを選択" +msgstr "%s を選択" msgid "Choose a chart for displaying on the map" msgstr "マップ上に表示するチャートを選択" @@ -3158,9 +3104,9 @@ msgstr "条件" msgid "Clear" msgstr "クリア" -#, fuzzy, python-format +#, python-format msgid "Clear %s filter" -msgstr "すべてのフィルターをクリア" +msgstr "%s フィルターをクリア" msgid "Clear Sort" msgstr "並べ替えをクリア" @@ -3285,9 +3231,8 @@ msgstr "コード" msgid "Code Copied!" msgstr "コードをコピーしました!" -#, fuzzy msgid "Collapse" -msgstr "行を折りたたむ" +msgstr "折りたたむ" msgid "Collapse All" msgstr "すべて折りたたむ" @@ -3310,13 +3255,11 @@ msgstr "色" msgid "Color +/-" msgstr "色 +/-" -#, fuzzy msgid "Color By X-Axis" -msgstr "色分けの基準" +msgstr "X軸で色分け" -#, fuzzy msgid "Color By Y-Axis" -msgstr "色分けの基準" +msgstr "Y軸で色分け" msgid "Color Metric" msgstr "色のメトリック" @@ -3330,17 +3273,11 @@ msgstr "配色種類" msgid "Color Steps" msgstr "配色ステップ数" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Color bars by x-axis" -msgstr "X 軸でバーを色付けする" +msgstr "X軸でバーを色分け" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Color bars by y-axis" -msgstr "Y 軸でバーを色付けする" +msgstr "Y軸でバーを色分け" msgid "Color bounds" msgstr "色の範囲" @@ -3351,9 +3288,6 @@ msgstr "色の境界点" msgid "Color by" msgstr "色分けの基準" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Color field must be a hex color (#rrggbb) or 'rgb(r, g, b)'" msgstr "カラーフィールドは16進数カラー(#rrggbb)または 'rgb(r, g, b)' である必要があります" @@ -3450,16 +3384,14 @@ msgstr "列" msgid "Columns (%s)" msgstr "列 (%s)" -#, fuzzy msgid "Columns (horizontal layout)" -msgstr "水平レイアウト (列方向)" +msgstr "列 (水平レイアウト)" msgid "Columns and metrics" msgstr "列とメトリック" -#, fuzzy msgid "Columns and metrics should be inside folders" -msgstr "列とメトリック" +msgstr "列とメトリックはフォルダー内に配置する必要があります" msgid "Columns folder can only contain column items" msgstr "列フォルダーには列項目のみを含めることができます" @@ -3472,11 +3404,8 @@ msgstr "データセットに存在しない列: %(invalid_columns)s" msgid "Columns missing in datasource: %(invalid_columns)s" msgstr "データソースに存在しない列: %(invalid_columns)s" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, es, sr, -# sr_Latn] -#, fuzzy msgid "Columns should be inside folders" -msgstr "列はフォルダ内に配置する必要があります" +msgstr "列はフォルダー内に配置する必要があります" msgid "Columns subtotal position" msgstr "列の小計表示位置" @@ -3523,7 +3452,7 @@ msgstr "" "に指定した値と一致させる必要があります。" msgid "Common (unit per pixel at zoom 0)" -msgstr "" +msgstr "共通 (ズーム0でのピクセル単位)" msgid "Comparator option" msgstr "比較オプション" @@ -3588,9 +3517,9 @@ msgstr "信頼区間は 0 から 1 の間の数値である必要があります msgid "Configuration" msgstr "設定" -#, fuzzy, python-format +#, python-format msgid "Configure %s" -msgstr "保存の確認" +msgstr "%s を設定" msgid "Configure Advanced Time Range " msgstr "高度な時間範囲設定 " @@ -3685,11 +3614,9 @@ msgstr "接続に失敗しました。接続設定を確認してください。 msgid "Contains" msgstr "含む" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "Contains text (ILIKE %x%)" -msgstr "テキストを含む(ILIKE %x%)" +msgstr "テキストを含む (ILIKE %x%)" msgid "Content format" msgstr "コンテンツ形式" @@ -3724,9 +3651,8 @@ msgstr "ラベル付きコントロール" msgid "Copied to clipboard!" msgstr "クリップボードにコピーしました!" -#, fuzzy msgid "Copied!" -msgstr "コードをコピーしました!" +msgstr "コピーしました!" msgid "Copy" msgstr "コピー" @@ -3746,9 +3672,8 @@ msgstr "JSON形式の認証情報をコピーして貼り付け" msgid "Copy code to clipboard" msgstr "コードをクリップボードにコピー" -#, fuzzy msgid "Copy column name" -msgstr "列名" +msgstr "列名をコピー" #, python-format msgid "Copy of %s" @@ -3807,7 +3732,7 @@ msgid "Could not find viz object" msgstr "視覚化(Viz)オブジェクトが見つかりませんでした" msgid "Could not load SQL Lab" -msgstr "" +msgstr "SQL Lab を読み込めませんでした" msgid "Could not load database driver" msgstr "データベースドライバーを読み込めませんでした" @@ -3856,9 +3781,8 @@ msgstr "国別地図" msgid "Create" msgstr "作成" -#, fuzzy msgid "Create API Key" -msgstr "作成者" +msgstr "APIキーを作成" msgid "Create Tag" msgstr "タグを作成" @@ -3904,9 +3828,9 @@ msgstr "作成者" msgid "Created by me" msgstr "自分が作成" -#, fuzzy, python-format +#, python-format msgid "Created by: %s" -msgstr "作成者" +msgstr "作成者: %s" msgid "Created on" msgstr "作成日" @@ -3923,9 +3847,8 @@ msgstr "作成者" msgid "Crimson" msgstr "クリムゾン" -#, fuzzy msgid "Cross-filter column" -msgstr "クロスフィルターの適用範囲設定" +msgstr "クロスフィルター列" msgid "" "Cross-filter will be applied to all of the charts that use this dataset." @@ -3964,9 +3887,8 @@ msgstr "通貨記号" msgid "Current" msgstr "現在" -#, fuzzy msgid "Current Zoom" -msgstr "現在" +msgstr "現在のズーム" msgid "Current day" msgstr "当日" @@ -4002,15 +3924,11 @@ msgstr "カスタムSQL" msgid "Custom SQL ad-hoc metrics are not enabled for this dataset" msgstr "このデータセットではカスタムSQLアドホックメトリックが有効になっていません" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Custom SQL fields cannot be parsed as a single SQL statement." -msgstr "カスタム SQL フィールドは単一の SQL ステートメントとして解析できません。" +msgstr "カスタムSQLフィールドは単一のSQLステートメントとして解析できません。" -#, fuzzy msgid "Custom SQL fields cannot contain set operations." -msgstr "カスタムSQLフィールドにサブクエリを含めることはできません。" +msgstr "カスタムSQLフィールドに集合演算を含めることはできません。" msgid "Custom SQL fields cannot contain sub-queries." msgstr "カスタムSQLフィールドにサブクエリを含めることはできません。" @@ -4039,9 +3957,8 @@ msgstr "スクリーンショットの幅(ピクセル単位)" msgid "Custom..." msgstr "カスタム..." -#, fuzzy msgid "Customization and styling" -msgstr "カスタマイズ種類" +msgstr "カスタマイズとスタイリング" msgid "Customization type" msgstr "カスタマイズ種類" @@ -4134,9 +4051,8 @@ msgstr "日次の季節性" msgid "Dark" msgstr "ダーク" -#, fuzzy msgid "Dark (Carto)" -msgstr "レーダーチャート" +msgstr "ダーク (Carto)" msgid "Dark Cyan" msgstr "ダークシアン" @@ -4147,9 +4063,9 @@ msgstr "ダークモード" msgid "Dashboard" msgstr "ダッシュボード" -#, fuzzy, python-format +#, python-format msgid "Dashboard %(dashboard_id)s not found" -msgstr "チャート (ID: %(id)s) が見つかりません" +msgstr "ダッシュボード %(dashboard_id)s が見つかりません" msgid "Dashboard Filter" msgstr "ダッシュボードフィルター" @@ -4180,7 +4096,7 @@ msgid "Dashboard could not be deleted." msgstr "ダッシュボードを削除できませんでした。" msgid "Dashboard could not be restored." -msgstr "" +msgstr "ダッシュボードを復元できませんでした。" msgid "Dashboard could not be updated." msgstr "ダッシュボードを更新できませんでした。" @@ -4229,9 +4145,9 @@ msgstr "" msgid "Dashboard title" msgstr "ダッシュボードタイトル" -#, fuzzy, python-format +#, python-format msgid "Dashboard updated %s" -msgstr "最終更新日: %s" +msgstr "ダッシュボードが更新されました %s" msgid "Dashboard usage" msgstr "ダッシュボードの利用状況" @@ -4251,9 +4167,8 @@ msgstr "破線" msgid "Data" msgstr "データ" -#, fuzzy msgid "Data Connections" -msgstr "データベース接続" +msgstr "データ接続" msgid "Data Export Options" msgstr "データエクスポートオプション" @@ -4267,13 +4182,11 @@ msgstr "データ URI は許可されていません。" msgid "Data Zoom" msgstr "データズーム" -#, fuzzy msgid "Data connection" -msgstr "データベース接続" +msgstr "データ接続" -#, fuzzy msgid "Data connections" -msgstr "データベース接続" +msgstr "データ接続" msgid "" "Data could not be deserialized from the results backend. The storage format " @@ -4286,9 +4199,8 @@ msgid "" " original query." msgstr "バックエンドからデータを取得できませんでした。クエリを再実行する必要があります。" -#, fuzzy msgid "Data error" -msgstr "データベースエラー" +msgstr "データエラー" #, python-format msgid "Data for %s" @@ -4373,9 +4285,8 @@ msgstr "データベースパスワード" msgid "Database port" msgstr "データベースポート" -#, fuzzy msgid "Database reference is not allowed on a report" -msgstr "CSVアップロード時にデータベーススキーマは指定できません。" +msgstr "レポートでのデータベース参照は許可されていません" msgid "Database schema is not allowed for csv uploads." msgstr "CSVアップロード時にデータベーススキーマは指定できません。" @@ -4386,10 +4297,8 @@ msgstr "データベース設定を更新しました" msgid "Database type does not support file uploads." msgstr "このデータベース種類はファイルアップロードをサポートしていません。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs] -#, fuzzy msgid "Database upload file exceeds the maximum allowed size." -msgstr "データベースのアップロードファイルが許可される最大サイズを超えています。" +msgstr "データベースへのアップロードファイルが許可される最大サイズを超えています。" msgid "Database upload file failed" msgstr "データベースへのファイルアップロードに失敗しました" @@ -4423,7 +4332,7 @@ msgid "Dataset could not be duplicated." msgstr "データセットを複製できませんでした。" msgid "Dataset could not be restored." -msgstr "" +msgstr "データセットを復元できませんでした。" msgid "Dataset could not be updated." msgstr "データセットを更新できませんでした。" @@ -4473,9 +4382,8 @@ msgstr "データソースとチャート種類" msgid "Datasource does not exist" msgstr "データソースが存在しません" -#, fuzzy msgid "Datasource is required" -msgstr "データセットが必要です" +msgstr "データソースが必要です" msgid "Datasource is required for validation" msgstr "検証にはデータソースが必要です" @@ -4486,7 +4394,6 @@ msgstr "データソース種類が無効です" msgid "Datasource type is required when datasource_id is given" msgstr "データソースIDを指定する場合、データソース種類は必須です" -#, fuzzy msgid "Datasources" msgstr "データソース" @@ -4571,9 +4478,8 @@ msgstr "Deck.gl - スクリーングリッド" msgid "Deck.gl Layer Visibility" msgstr "Deck.gl レイヤーの表示設定" -#, fuzzy msgid "Deckgl" -msgstr "Deckgl" +msgstr "Deck.gl" msgid "Decrease" msgstr "減少" @@ -4710,9 +4616,8 @@ msgid "" "between two data points" msgstr "2つのデータポイント間において、ステップ(段差)を最初、中間、最後のどこに表示するかを定義します" -#, fuzzy msgid "Definition" -msgstr "偏差" +msgstr "定義" #, python-format msgid "Delayed (missed %s refresh)" @@ -4722,9 +4627,9 @@ msgstr[0] "遅延(%s 回の更新が失敗しました)" msgid "Delete" msgstr "削除" -#, fuzzy, python-format +#, python-format msgid "Delete %s" -msgstr "%s を削除しました" +msgstr "%s を削除" #, python-format msgid "Delete %s?" @@ -4748,13 +4653,11 @@ msgstr "レポートを削除しますか?" msgid "Delete Role?" msgstr "ロールを削除しますか?" -#, fuzzy msgid "Delete Semantic Layer?" -msgstr "レイヤーを削除しますか?" +msgstr "セマンティックレイヤーを削除しますか?" -#, fuzzy msgid "Delete Semantic View?" -msgstr "テンプレートを削除しますか?" +msgstr "セマンティックビューを削除しますか?" msgid "Delete Template?" msgstr "テンプレートを削除しますか?" @@ -4852,10 +4755,10 @@ msgid "Deleted %(num)d saved query" msgid_plural "Deleted %(num)d saved queries" msgstr[0] "%(num)d 件の保存済みクエリを削除しました" -#, fuzzy, python-format +#, python-format msgid "Deleted %(num)d semantic view" msgid_plural "Deleted %(num)d semantic views" -msgstr[0] "%(num)d 個のCSSテンプレートを削除しました" +msgstr[0] "%(num)d 件のセマンティックビューを削除しました" #, python-format msgid "Deleted %(num)d theme" @@ -4866,9 +4769,9 @@ msgstr[0] "%(num)d 件のテーマを削除しました" msgid "Deleted %s" msgstr "%s を削除しました" -#, fuzzy, python-format +#, python-format msgid "Deleted %s item(s)" -msgstr "項目を削除" +msgstr "%s 個の項目を削除しました" #, python-format msgid "Deleted group: %s" @@ -4946,16 +4849,13 @@ msgstr "詳細" msgid "Details of the certification" msgstr "認証の詳細情報" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "Determines how the filter matches values. \"Exact match\" uses the IN " "operator (default). ILIKE options enable partial text matching with a free-" "text input. Warning: ILIKE queries may be slow on large datasets as they " "cannot use indexes effectively." msgstr "" -"フィルターの値照合方法を決定します。「完全一致」は IN 演算子を使用します(デフォルト)。ILIKE " +"フィルターの値照合方法を決定します。「完全一致」は IN 演算子を使用します(既定)。ILIKE " "オプションは、フリーテキスト入力による部分的なテキスト照合を有効にします。警告: ILIKE " "クエリはインデックスを効果的に使用できないため、大規模なデータセットでは低速になる場合があります。" @@ -4982,9 +4882,6 @@ msgstr "ディム・グレー" msgid "Dimension" msgstr "ディメンション" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "Dimension column emitted as a cross-filter when a feature is clicked. Other " "charts on the dashboard match against this column. If unset, falls back to " @@ -5013,9 +4910,9 @@ msgstr "ディメンション値" msgid "Dimensions" msgstr "ディメンション" -#, fuzzy, python-format +#, python-format msgid "Dimensions (%s)" -msgstr "ディメンション" +msgstr "ディメンション (%s)" msgid "" "Dimensions contain qualitative values such as names, dates, or geographical " @@ -5170,9 +5067,8 @@ msgstr "ドメイン" msgid "Don't refresh" msgstr "更新しない" -#, fuzzy msgid "Done" -msgstr "なし" +msgstr "完了" msgid "Donut" msgstr "ドーナツ型" @@ -5220,9 +5116,8 @@ msgid "" msgstr "" "列やメトリックをここにドラッグして、ツールチップの内容をカスタマイズします。配置した順序でツールチップに表示されます。ボタンをクリックして手動で選択することも可能です。" -#, fuzzy msgid "Drag to reorder" -msgstr "未加工レコード" +msgstr "ドラッグして並べ替え" msgid "Draw a marker on data points. Only applicable for line types." msgstr "データポイントにマーカーを描画します(折れ線グラフのみ)。" @@ -5362,9 +5257,8 @@ msgstr "所要時間(ミリ秒) (10500 => 0:00:10.5)" msgid "Duration in ms (66000 => 1m 6s)" msgstr "所要時間(ミリ秒) (66000 => 1分6秒)" -#, fuzzy msgid "Duration in seconds" -msgstr "所要時間を秒単位で入力" +msgstr "所要時間 (秒)" msgid "Dynamic" msgstr "動的" @@ -5372,16 +5266,14 @@ msgstr "動的" msgid "Dynamic Aggregation Function" msgstr "動的集計関数" -#, fuzzy msgid "Dynamic Section Label" -msgstr "方向性あり" +msgstr "動的セクションラベル" msgid "Dynamic group by" msgstr "動的グループ化" -#, fuzzy msgid "Dynamic section description" -msgstr "動的集計関数" +msgstr "動的セクションの説明" msgid "Dynamically search all filter values" msgstr "すべてのフィルター値を動的に検索" @@ -5392,11 +5284,8 @@ msgstr "データセットからグループ化列を動的に選択" msgid "ECharts" msgstr "ECharts" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "ECharts Options (JS object literals)" -msgstr "ECharts オプション(JS オブジェクトリテラル)" +msgstr "ECharts オプション (JS オブジェクトリテラル)" msgid "EMAIL_REPORTS_CTA" msgstr "メールレポートのCTA" @@ -5419,9 +5308,9 @@ msgstr "エッジの太さ" msgid "Edit" msgstr "編集" -#, fuzzy, python-format +#, python-format msgid "Edit %s" -msgstr "ユーザーを編集" +msgstr "%s を編集" #, python-format msgid "Edit %s in modal" @@ -5524,7 +5413,7 @@ msgid "Editable" msgstr "編集可能" msgid "Edited" -msgstr "編集" +msgstr "編集済み" msgid "Editing 1 filter:" msgstr "1個のフィルターを編集しています:" @@ -5643,18 +5532,15 @@ msgstr "グラフのローミング(移動)を有効にする" msgid "Enable icon JavaScript mode" msgstr "アイコンのJavaScriptモードを有効にする" -#, fuzzy msgid "Enable icons" -msgstr "アイコンを表示" +msgstr "アイコンを有効にする" msgid "Enable label JavaScript mode" msgstr "ラベルのJavaScriptモードを有効にする" -#, fuzzy msgid "Enable labels" -msgstr "ラベルを表示" +msgstr "ラベルを有効にする" -#, fuzzy msgid "Enable matrixify" msgstr "マトリックス化を有効にする" @@ -5687,9 +5573,8 @@ msgid "" " please consider filtering those out" msgstr "無効なNULLの空間データが検出されました。これらを除外するためのフィルター検討してください" -#, fuzzy msgid "Encrypted extra fields" -msgstr "共有クエリフィールド" +msgstr "暗号化された追加フィールド" msgid "End" msgstr "終了" @@ -5721,9 +5606,9 @@ msgstr "終了日は開始日より後の日付である必要があります" msgid "Ends With" msgstr "で終わる" -#, fuzzy, python-format +#, python-format msgid "Ends with (ILIKE %x)" -msgstr "で終わる" +msgstr "で終わる (ILIKE %x)" #, python-format msgid "Engine \"%(engine)s\" cannot be configured through parameters." @@ -5826,15 +5711,13 @@ msgstr "タグ付けされたオブジェクトの取得エラー" msgid "Error deleting %s" msgstr "%s の削除エラー" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, es, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "Error disabling fullscreen: %s" -msgstr "全画面表示の無効化エラー: %s" +msgstr "全画面表示の無効化中にエラーが発生しました: %s" -#, fuzzy, python-format +#, python-format msgid "Error enabling fullscreen: %s" -msgstr "%s の削除エラー" +msgstr "全画面表示の有効化中にエラーが発生しました: %s" msgid "Error executing query. " msgstr "クエリ実行エラー。 " @@ -5959,11 +5842,8 @@ msgstr "推移・変遷" msgid "Exact" msgstr "完全一致" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Exact match (IN)" -msgstr "完全一致(IN)" +msgstr "完全一致 (IN)" msgid "Example" msgstr "例" @@ -6022,9 +5902,8 @@ msgstr "データパネルを開く" msgid "Expand row" msgstr "行を展開" -#, fuzzy msgid "Expand row to edit" -msgstr "行を展開" +msgstr "行を展開して編集" msgid "" "Expects a formula with depending time parameter 'x'\n" @@ -6037,9 +5916,8 @@ msgstr "" msgid "Experimental" msgstr "試験的" -#, fuzzy msgid "Expired" -msgstr "探索" +msgstr "期限切れ" msgid "Explore" msgstr "探索" @@ -6095,7 +5973,6 @@ msgstr "CSVとしてエクスポート" msgid "Export to .JSON" msgstr "JSONとしてエクスポート" -#, fuzzy msgid "Export to CSV" msgstr "CSVとしてエクスポート" @@ -6129,9 +6006,8 @@ msgstr "SQL Labでデータベースを公開する" msgid "Expose in SQL Lab" msgstr "SQL Labで公開" -#, fuzzy msgid "Expression" -msgstr "SQL式" +msgstr "式" msgid "Expression cannot be empty" msgstr "式を空にすることはできません" @@ -6142,9 +6018,6 @@ msgstr "拡張機能" msgid "Extent" msgstr "範囲" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "External link warning" msgstr "外部リンクの警告" @@ -6188,9 +6061,8 @@ msgstr "押し出し表示" msgid "FEB" msgstr "2月" -#, fuzzy msgid "FIT DATA" -msgstr "データを合わせる" +msgstr "データに合わせる" msgid "FRI" msgstr "金曜日" @@ -6213,17 +6085,14 @@ msgstr "結果の取得に失敗しました" msgid "Failed to apply theme: Invalid JSON" msgstr "テーマの適用失敗: JSONが無効です" -#, fuzzy msgid "Failed to copy API key to clipboard" -msgstr "パーティションクエリをクリップボードにコピー" +msgstr "APIキーをクリップボードにコピーできませんでした" -#, fuzzy msgid "Failed to copy stack trace to clipboard" -msgstr "コードをクリップボードにコピー" +msgstr "スタックトレースをクリップボードにコピーできませんでした" -#, fuzzy msgid "Failed to create API key" -msgstr "レポートの作成に失敗しました" +msgstr "APIキーの作成に失敗しました" msgid "Failed to create report" msgstr "レポートの作成に失敗しました" @@ -6232,16 +6101,15 @@ msgstr "レポートの作成に失敗しました" msgid "Failed to execute %(query)s" msgstr "%(query)s の実行に失敗しました" -#, fuzzy msgid "Failed to fetch API keys" -msgstr "項目へのタグ付けに失敗しました" +msgstr "APIキーの取得に失敗しました" msgid "Failed to generate chart edit URL" msgstr "チャート編集用URLの生成に失敗しました" -#, fuzzy, python-format +#, python-format msgid "Failed to generate download: %s" -msgstr "チャート編集用URLの生成に失敗しました" +msgstr "ダウンロードの生成に失敗しました: %s" msgid "Failed to load chart data" msgstr "チャートデータの読み込みに失敗しました" @@ -6249,9 +6117,9 @@ msgstr "チャートデータの読み込みに失敗しました" msgid "Failed to load chart data." msgstr "チャートデータの読み込みに失敗しました。" -#, fuzzy, python-format +#, python-format msgid "Failed to load columns for %s %s" -msgstr "データセット %s の列情報の読み込みに失敗しました" +msgstr "%s %s の列情報の読み込みに失敗しました" msgid "Failed to load top values" msgstr "上位値の読み込みに失敗しました" @@ -6324,9 +6192,8 @@ msgstr "偽 (False)" msgid "Favorite" msgstr "お気に入り" -#, fuzzy msgid "Feature Not Enabled" -msgstr "SSHトンネリングが有効になっていません" +msgstr "機能が有効になっていません" msgid "Featured" msgstr "注目" @@ -6347,9 +6214,8 @@ msgstr "%s 取得しました" msgid "Fetching" msgstr "取得中" -#, fuzzy msgid "Fetching data..." -msgstr "取得中" +msgstr "データを取得中..." #, python-format msgid "Field cannot be decoded by JSON. %(json_error)s" @@ -6359,9 +6225,8 @@ msgstr "フィールドをJSONでデコードできません。 %(json_error)s" msgid "Field cannot be decoded by JSON. %(msg)s" msgstr "フィールドをJSONでデコードできません。 %(msg)s" -#, fuzzy msgid "Field cannot be empty." -msgstr "空にできません" +msgstr "フィールドを空にすることはできません。" msgid "Field is required" msgstr "このフィールドは必須です" @@ -6423,9 +6288,8 @@ msgid "" "Filter only displays values relevant to selections made in other filters." msgstr "他のフィルターでの選択に関連する値のみを表示します。" -#, fuzzy msgid "Filter options" -msgstr "フィルター設定" +msgstr "フィルターオプション" msgid "Filter results" msgstr "結果をフィルター" @@ -6491,7 +6355,6 @@ msgstr "検索" msgid "Finish" msgstr "完了" -#, fuzzy msgid "First" msgstr "最初" @@ -6571,9 +6434,9 @@ msgid "" "filter will be applied to. For base filters, these are the subjects that " "the filter DOES NOT apply to, e.g. Admin if admin should see all data." msgstr "" -"通常フィルターの場合、これらはフィルターを適用するサブジェクト(ユーザー、ロール、グループ)です。" -"基本フィルターの場合、これらはフィルターを「適用しない」サブジェクトです。" -"例:管理者がすべてのデータを閲覧できるようにする場合のAdmin。" +"通常フィルターの場合、これらはフィルターが適用される対象(ユーザー、ロール、グループ)です。" +"基本フィルターの場合、これらはフィルターが適用されない対象です。" +"(例:管理者がすべてのデータを閲覧できるようにする場合のAdminとなります。)" msgid "Forbidden" msgstr "禁止されています" @@ -6584,11 +6447,8 @@ msgstr "強制" msgid "Force Time Grain as Max Interval" msgstr "時間単位を最大間隔として強制" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Force abort (stops task for all subscribers)" -msgstr "強制中止(すべてのサブスクライバーのタスクを停止します)" +msgstr "強制中止 (すべてのサブスクライバーのタスクを停止します)" msgid "" "Force all tables and views to be created in this schema when clicking CTAS " @@ -6634,19 +6494,17 @@ msgstr "キャッシュ内にフォームデータが見つかりません。チ msgid "Form data not found in cache, reverting to dataset metadata." msgstr "キャッシュ内にフォームデータが見つかりません。データセットのメタデータを使用します。" -#, fuzzy msgid "Format" -msgstr "整形済み" +msgstr "書式" -#, fuzzy msgid "Format JSON configuration" -msgstr "JSON設定" +msgstr "JSON設定の書式" msgid "Format SQL" -msgstr "SQL整形" +msgstr "SQLの書式" msgid "Format SQL query" -msgstr "SQLクエリを整形" +msgstr "SQLクエリの書式" #, python-brace-format msgid "" @@ -6673,13 +6531,11 @@ msgstr "整形済みの値" msgid "Formatting" msgstr "書式設定" -#, fuzzy msgid "Formatting column" -msgstr "書式設定" +msgstr "列の書式設定" -#, fuzzy msgid "Formatting object" -msgstr "書式設定" +msgstr "オブジェクトの書式設定" msgid "Formula" msgstr "数式" @@ -6711,9 +6567,8 @@ msgstr "開始日は終了日より後の日付にできません" msgid "Full name" msgstr "氏名" -#, fuzzy msgid "Fullscreen is not supported in this browser." -msgstr "このチャート種類ではドリルバイはまだサポートされていません" +msgstr "このブラウザでは全画面表示はサポートされていません。" msgid "Funnel Chart" msgstr "ファンネルチャート" @@ -6789,7 +6644,6 @@ msgstr "Googleスプレッドシートの名前とURL" msgid "Grace period" msgstr "猶予期間" -#, fuzzy msgid "Grain" msgstr "時間粒度" @@ -6823,9 +6677,8 @@ msgstr "グリッド" msgid "Grid Size" msgstr "グリッドサイズ" -#, fuzzy msgid "Grid view" -msgstr "グリッドサイズ" +msgstr "グリッドビュー" msgid "Group" msgstr "グループ" @@ -6984,7 +6837,7 @@ msgid "ISO 8601" msgstr "ISO 8601" msgid "Icon JavaScript config generator" -msgstr "アイコン用JavaScript設定生成ツール" +msgstr "アイコン用JavaScript設定ジェネレーター" msgid "Icon URL" msgstr "アイコンURL" @@ -7093,9 +6946,8 @@ msgstr "テーマをインポート" msgid "In" msgstr "次を含む" -#, fuzzy msgid "In Progress" -msgstr "進捗" +msgstr "進行中" msgid "In Range" msgstr "範囲内" @@ -7108,9 +6960,8 @@ msgstr "非公開のスプレッドシートに接続するには、サービス msgid "In this view you can preview the first 25 rows. " msgstr "このビューでは最初の25行をプレビューできます。" -#, fuzzy msgid "Inactive" -msgstr "有効" +msgstr "非アクティブ" msgid "Include Series" msgstr "系列を含める" @@ -7246,9 +7097,8 @@ msgstr "無効な接続文字列です。'ocient://user:pass@host:port/database' msgid "Invalid JSON" msgstr "無効なJSON" -#, fuzzy msgid "Invalid JSON configuration" -msgstr "緯度・経度の設定が無効です。" +msgstr "無効なJSON設定" msgid "Invalid JSON metadata" msgstr "無効なJSONメタデータ" @@ -7357,9 +7207,9 @@ msgstr "無効な空間点が見つかりました: %(latlong)s" msgid "Invalid state." msgstr "無効な状態です。" -#, fuzzy, python-format +#, python-format msgid "Invalid tab ids: %(tab_ids)s" -msgstr "無効なタブID: %s(tab_ids)" +msgstr "無効なタブID: %(tab_ids)s" msgid "Invalid username or password" msgstr "ユーザー名またはパスワードが正しくありません" @@ -7407,7 +7257,7 @@ msgid "Is temporal" msgstr "時間情報(時間列)" msgid "Is true" -msgstr "本当です" +msgstr "真 (True)" msgid "Isoband" msgstr "等値帯" @@ -7469,13 +7319,13 @@ msgid "January" msgstr "1月" msgid "JavaScript data interceptor" -msgstr "JavaScript データ・インターセプター" +msgstr "JavaScript データインターセプター" msgid "JavaScript onClick href" msgstr "JavaScript onClick href" msgid "JavaScript tooltip generator" -msgstr "JavaScript ツールチップ生成ツール" +msgstr "JavaScript ツールチップジェネレーター" msgid "Jinja templating" msgstr "Jinjaテンプレート" @@ -7498,16 +7348,12 @@ msgstr "編集を続ける" msgid "Key" msgstr "キー" -#, fuzzy msgid "Key Prefix" -msgstr "接頭辞" +msgstr "キーの接頭辞" msgid "Keyboard shortcuts" msgstr "キーボードショートカット" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Keys are shown only once at creation. Store them securely." msgstr "キーは作成時に一度だけ表示されます。安全に保管してください。" @@ -7521,7 +7367,7 @@ msgid "Label Contents" msgstr "ラベルの内容" msgid "Label JavaScript config generator" -msgstr "ラベル用JavaScript設定生成ツール" +msgstr "ラベル用JavaScript設定ジェネレーター" msgid "Label Line" msgstr "ラベル線" @@ -7600,9 +7446,8 @@ msgstr "最終更新日: %s" msgid "Last Updated %s by %s" msgstr "最終更新日: %s (更新者: %s)" -#, fuzzy msgid "Last Used" -msgstr "ユーザー一覧" +msgstr "最終使用日" #, python-format msgid "Last available value seen on %s" @@ -7635,9 +7480,9 @@ msgstr "最終実行日時" msgid "Last run" msgstr "最終実行" -#, fuzzy, python-format +#, python-format msgid "Last updated %s ago" -msgstr "最終更新日: %s" +msgstr "最終更新: %s 前" msgid "Last week" msgstr "先週" @@ -7738,9 +7583,6 @@ msgstr "以下 (≦)" msgid "Less than (<)" msgstr "未満 (<)" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Liberty (OpenFreeMap)" msgstr "Liberty (OpenFreeMap)" @@ -7750,9 +7592,8 @@ msgstr "リフト(向上率)の精度" msgid "Light" msgstr "ライト" -#, fuzzy msgid "Light (Carto)" -msgstr "折れ線グラフ" +msgstr "ライト (Carto)" msgid "Light mode" msgstr "ライトモード" @@ -7869,9 +7710,8 @@ msgstr "三角形でマークする値のリスト" msgid "List updated" msgstr "リストを更新しました" -#, fuzzy msgid "List view" -msgstr "ユーザー一覧" +msgstr "リストビュー" msgid "Live render" msgstr "ライブ描画" @@ -7885,13 +7725,11 @@ msgstr "読み込まれたデータがキャッシュされました" msgid "Loaded from cache" msgstr "キャッシュから読み込みました" -#, fuzzy msgid "Loading" msgstr "読み込み中..." -#, fuzzy msgid "Loading filter values" -msgstr "フィルター値を並べ替え" +msgstr "フィルター値を読み込み中" msgid "Loading timezones..." msgstr "タイムゾーンを読み込み中..." @@ -7951,7 +7789,6 @@ msgstr "ログアウト" msgid "Logs" msgstr "ログ" -#, fuzzy msgid "Lon" msgstr "経度" @@ -7991,9 +7828,8 @@ msgstr "月" msgid "Main" msgstr "メイン" -#, fuzzy msgid "Main navigation" -msgstr "アニメーション" +msgstr "メインナビゲーション" msgid "" "Make sure that the controls are configured properly and the datasource " @@ -8016,14 +7852,11 @@ msgstr "ダッシュボード編集者とアクセス権限を管理" msgid "Manage email report" msgstr "メールレポートの管理" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "Manage filters and customizations to set scoping, descriptions, and " "limitations. Create new elements for better dashboard insights." msgstr "" -"フィルターとカスタマイズを管理して、スコープ、説明、制限を設定します。より良いダッシュボードのインサイトのために新しい要素を作成してください。" +"フィルターとカスタマイズを管理して、スコープ、説明、制限を設定します。より良いダッシュボードの分析精度のために新しい要素を作成してください。" msgid "Manage your databases" msgstr "データベースの管理" @@ -8040,40 +7873,29 @@ msgstr "地図" msgid "Map Options" msgstr "地図設定" -#, fuzzy msgid "Map Renderer" -msgstr "ライブ描画" +msgstr "マップレンダラー" msgid "Map Style" msgstr "地図スタイル" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "MapLibre (open-source)" -msgstr "MapLibre(オープンソース)" +msgstr "MapLibre (オープンソース)" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "MapLibre is open-source and requires no API key. Mapbox requires " "MAPBOX_API_KEY to be configured on the server." msgstr "" -"MapLibre はオープンソースで API キーは必要ありません。Mapbox はサーバーで MAPBOX_API_KEY を設定する必要があります。" +"MapLibre はオープンソースであり、API キーは不要です。Mapbox はサーバーに MAPBOX_API_KEY を設定する必要があります。" msgid "Mapbox" msgstr "Mapbox" -#, fuzzy msgid "Mapbox (API key required)" -msgstr "メールアドレスは必須です" +msgstr "Mapbox (APIキー必須)" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Mapbox requires a MAPBOX_API_KEY to be configured on the server." -msgstr "Mapbox はサーバーで MAPBOX_API_KEY を設定する必要があります。" +msgstr "Mapbox を使用するには、サーバーに MAPBOX_API_KEY を設定する必要があります。" msgid "March" msgstr "3月" @@ -8114,9 +7936,8 @@ msgstr "システム設定に合わせる" msgid "Match time shift color with original series" msgstr "タイムシフト(比較用)の色を元の系列に合わせる" -#, fuzzy msgid "Match type" -msgstr "データ種類" +msgstr "一致種類" msgid "Matrixify" msgstr "マトリックス化" @@ -8142,9 +7963,8 @@ msgstr "最大フォントサイズ" msgid "Maximum Radius" msgstr "最大半径" -#, fuzzy msgid "Maximum Width" -msgstr "最小幅" +msgstr "最大幅" msgid "Maximum folder nesting depth reached" msgstr "フォルダーの入れ子の深さが上限に達しました" @@ -8163,9 +7983,8 @@ msgstr "最大値" msgid "Maximum value on the gauge axis" msgstr "ゲージ軸の最大値" -#, fuzzy msgid "Maximum width size of the path, in pixels or meters." -msgstr "正方形セルのサイズ (ピクセル単位)" +msgstr "パスの最大幅サイズ (ピクセルまたはメートル単位)。" msgid "May" msgstr "5月" @@ -8318,24 +8137,18 @@ msgstr "系列または行の数に制限がある際、上位系列を並べ替 msgid "Metrics" msgstr "メトリック" -#, fuzzy, python-format +#, python-format msgid "Metrics (%s)" -msgstr "メトリック" +msgstr "メトリック (%s)" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Metrics can't be used for both rows and columns at the same time" -msgstr "メトリクスは行と列の両方に同時に使用することはできません" +msgstr "メトリックは行と列の両方に同時に使用することはできません" msgid "Metrics folder can only contain metric items" msgstr "メトリックフォルダーにはメトリック項目のみを含めることができます" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Metrics should be inside folders" -msgstr "メトリクスはフォルダ内に配置する必要があります" +msgstr "メトリックはフォルダー内に配置する必要があります" msgid "Metrics to show in the tooltip." msgstr "ツールチップに表示するメトリック" @@ -8385,7 +8198,6 @@ msgstr "最小フォントサイズ" msgid "Minimum Radius" msgstr "最小半径" -#, fuzzy msgid "Minimum Width" msgstr "最小幅" @@ -8409,9 +8221,8 @@ msgstr "グラフに表示するラベルの最小値。" msgid "Minimum value on the gauge axis" msgstr "ゲージ軸の最小値" -#, fuzzy msgid "Minimum width size of the path, in pixels or meters." -msgstr "正方形セルのサイズ (ピクセル単位)" +msgstr "パスの最小幅サイズ (ピクセルまたはメートル単位)。" msgid "Minor Split Line" msgstr "補助分割線" @@ -8426,14 +8237,13 @@ msgstr "分" msgid "Minutes %s" msgstr "%s 分" -#, fuzzy, python-format +#, python-format msgid "Missing %s" -msgstr "データセットがありません" +msgstr "%s が見つかりません" msgid "Missing OAuth2 token" msgstr "OAuth2トークンがありません" -#, fuzzy msgid "Missing URL parameter" msgstr "URLパラメータがありません" @@ -8488,9 +8298,8 @@ msgstr "さらにフィルターを表示" msgid "MotherDuck token" msgstr "MotherDuckトークン" -#, fuzzy msgid "Move icon" -msgstr "移動のみ" +msgstr "移動アイコン" msgid "Move only" msgstr "移動のみ" @@ -8521,12 +8330,10 @@ msgstr "複数の形式に対応しています。詳細はPythonのgeopy.points msgid "Multiplier" msgstr "乗数" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] msgid "" "Must be a chart editor to overwrite this chart. Save as a new chart " "instead." -msgstr "このチャートを上書きするには、チャートの編集者である必要があります。代わりに新しいチャートとして保存してください。" +msgstr "このチャートを上書きするにはチャートの編集者である必要があります。代わりに新しいチャートとして保存してください。" msgid "Must be unique" msgstr "一意である必要があります" @@ -8582,9 +8389,8 @@ msgstr "親ノードのIDを含む列の名前" msgid "Name of the id column" msgstr "ID列の名前" -#, fuzzy msgid "Name of the semantic layer" -msgstr "ID列の名前" +msgstr "セマンティックレイヤーの名前" msgid "Name of the source nodes" msgstr "ソースノードの名前" @@ -8628,9 +8434,8 @@ msgstr "ネットワークエラー" msgid "New" msgstr "新規" -#, fuzzy msgid "New Semantic Layer" -msgstr "注釈レイヤーなし" +msgstr "新しいセマンティックレイヤー" msgid "New chart" msgstr "新しいチャート" @@ -8656,7 +8461,6 @@ msgstr "新しいタブ (Ctrl + t)" msgid "Next" msgstr "次へ" -#, fuzzy msgid "Nightingale" msgstr "ナイチンゲール (鶏頭図)" @@ -8709,9 +8513,9 @@ msgstr "利用可能なフィルターはありません。" msgid "No columns found" msgstr "列が見つかりません" -#, fuzzy, python-format +#, python-format msgid "No compatible %s found" -msgstr "互換性のあるスキーマが見つかりません" +msgstr "互換性のある %s が見つかりません" msgid "No compatible catalog found" msgstr "互換性のあるカタログが見つかりません" @@ -8752,16 +8556,12 @@ msgstr "フィルターが選択されていません。" msgid "No filters" msgstr "フィルターなし" -#, fuzzy msgid "No filters applied" -msgstr "フィルターなし" +msgstr "適用されているフィルターはありません" msgid "No filters are currently added to this dashboard." msgstr "このダッシュボードにはまだフィルターが追加されていません。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "No filters or customizations created yet" msgstr "フィルターまたはカスタマイズはまだ作成されていません" @@ -8786,9 +8586,8 @@ msgstr "一致するレコードが見つかりません" msgid "No matching results found" msgstr "一致する結果が見つかりません" -#, fuzzy msgid "No multilayer deck.gl charts are currently added to this dashboard." -msgstr "このダッシュボードにはまだフィルターが追加されていません。" +msgstr "このダッシュボードにはマルチレイヤーdeck.glチャートはまだ追加されていません。" msgid "No records found" msgstr "レコードが見つかりません" @@ -8818,13 +8617,13 @@ msgstr "ロールなし" msgid "No roles yet" msgstr "ロールはまだありません" -#, fuzzy, python-format +#, python-format msgid "No rows were returned for this %s" -msgstr "このデータセットに対して行が返されませんでした" +msgstr "この %s に対して行が返されませんでした" -#, fuzzy, python-format +#, python-format msgid "No samples were returned for this %s" -msgstr "このデータセットのサンプルが返されませんでした" +msgstr "この %s のサンプルが返されませんでした" msgid "No saved expressions found" msgstr "保存済み数式が見つかりません" @@ -8841,9 +8640,8 @@ msgstr "該当する列が見つかりません。メトリックでフィルタ msgid "No table columns" msgstr "テーブル列がありません" -#, fuzzy msgid "No tasks yet" -msgstr "%s はまだありません" +msgstr "タスクはまだありません" msgid "No temporal columns found" msgstr "時間列が見つかりません" @@ -8926,9 +8724,8 @@ msgstr "未定義" msgid "Not equal to (≠)" msgstr "等しくない (≠)" -#, fuzzy msgid "Not found" -msgstr "チャートが見つかりません" +msgstr "見つかりません" msgid "Not in" msgstr "次を含まない (Not in)" @@ -9133,15 +8930,9 @@ msgstr "「ラベルの種類」がパーセンテージ以外の場合にのみ msgid "Only applies when \"Label Type\" is set to show values." msgstr "「ラベルの種類」が「値を表示」に設定されている場合にのみ適用されます。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Only exact match is available for non-string columns." msgstr "文字列以外の列では完全一致のみが使用できます。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Only proceed if you trust the destination or its source." msgstr "宛先またはそのソースを信頼する場合にのみ続行してください。" @@ -9184,10 +8975,10 @@ msgid "Open SQL Lab in a new tab" msgstr "新しいタブでSQL Labを開く" msgid "Open chart in new tab" -msgstr "" +msgstr "チャートを新しいタブで開く" msgid "Open dashboard in new tab" -msgstr "" +msgstr "ダッシュボードを新しいタブで開く" msgid "Open in SQL Lab" msgstr "SQL Labで開く" @@ -9353,9 +9144,8 @@ msgstr "ページサイズ:" msgid "Page length" msgstr "表示行数" -#, fuzzy msgid "Page navigation" -msgstr "集計" +msgstr "ページナビゲーション" msgid "Paired t-test Table" msgstr "対応のあるt検定テーブル" @@ -9416,7 +9206,7 @@ msgstr "パスワードは必須です" #, python-format msgid "Password must be at least %(min_length)s characters long." -msgstr "" +msgstr "パスワードは、少なくとも %(min_length)s 文字以上である必要があります。" msgid "Password:" msgstr "パスワード:" @@ -9439,28 +9229,21 @@ msgstr "共有可能なGoogleスプレッドシートのURLをここに貼り付 msgid "Paste your access token here" msgstr "アクセストークンをここに貼り付けてください" -#, fuzzy msgid "Path Color" -msgstr "ポイントの色" +msgstr "パスの色" -#, fuzzy msgid "Path Size" -msgstr "ポイントのサイズ" +msgstr "パスのサイズ" msgid "Pattern" msgstr "パターン" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Pause auto refresh if tab is inactive" -msgstr "タブが非アクティブの場合、自動更新を一時停止する" +msgstr "タブが非アクティブな場合は自動更新を一時停止する" -#, fuzzy msgid "Pause auto-refresh" -msgstr "自動更新を設定" +msgstr "自動更新を一時停止" -#, fuzzy msgid "Pending" msgstr "保留中" @@ -9590,9 +9373,8 @@ msgstr "右側にピン留め" msgid "Pin to the result panel" msgstr "結果パネルにピン留め" -#, fuzzy msgid "Pin to top" -msgstr "上から下へ" +msgstr "上部にピン留め" msgid "Pivot Mode" msgstr "ピボットモード" @@ -9704,7 +9486,7 @@ msgstr "ファイルを再エクスポートし、インポートを再試行し msgid "Please reach out to the Chart Editor for assistance." msgid_plural "Please reach out to the Chart Editors for assistance." -msgstr[0] "サポートが必要な場合は、チャート編集者にお問い合わせください。" +msgstr[0] "サポートが必要な場合は、チャートの編集者にお問い合わせください。" msgid "Please save your chart first, then try creating a new email report." msgstr "まずチャートを保存してから、新しいメールレポートを作成してください。" @@ -9716,9 +9498,9 @@ msgstr "まずダッシュボードを保存してから、新しいメールレ msgid "Please select at least one role or group" msgstr "少なくとも1つのロールまたはグループを選択してください" -#, fuzzy, python-format +#, python-format msgid "Please select both a %s and a Chart type to proceed" -msgstr "続行するには、データセットとチャート種類の両方を選択してください" +msgstr "続行するには、%s とチャート種類の両方を選択してください" #, python-format msgid "" @@ -9742,9 +9524,8 @@ msgstr "" msgid "Plugins" msgstr "プラグイン" -#, fuzzy msgid "Point Cluster Map" -msgstr "ポイントの色" +msgstr "ポイントクラスターマップ" msgid "Point Color" msgstr "ポイントの色" @@ -9759,7 +9540,7 @@ msgid "Point Radius Unit" msgstr "ポイントの半径単位" msgid "Point Radius Units" -msgstr "" +msgstr "ポイントの半径単位" msgid "Point Size" msgstr "ポイントのサイズ" @@ -9870,9 +9651,8 @@ msgstr "第1 Y軸の境界" msgid "Primary y-axis format" msgstr "第1 Y軸の形式" -#, fuzzy msgid "Private" -msgstr "秘密鍵" +msgstr "プライベート" msgid "Private Channels (Bot in channel)" msgstr "プライベートチャンネル (チャンネル内のボット)" @@ -9908,15 +9688,9 @@ msgstr "プロジェクトID" msgid "Proportional" msgstr "比例" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Public and privately shared sheets" msgstr "公開および非公開で共有されたシート" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Publicly shared sheets only" msgstr "公開共有シートのみ" @@ -9970,7 +9744,6 @@ msgstr "クエリの状態" msgid "Query cannot be loaded." msgstr "クエリを読み込めません。" -#, fuzzy msgid "Query data in SQL Lab" msgstr "SQL Labでデータをクエリする" @@ -10085,9 +9858,8 @@ msgstr "最近" msgid "Recipients are separated by \",\" or \";\"" msgstr "受信者は「,」または「;」で区切ります" -#, fuzzy msgid "Records" -msgstr "未加工レコード" +msgstr "レコード" msgid "Rectangle" msgstr "長方形" @@ -10117,7 +9889,6 @@ msgstr "参照" msgid "Referenced columns not available in DataFrame." msgstr "参照された列は DataFrame 内で使用できません。" -#, fuzzy msgid "Referrer" msgstr "リファラ" @@ -10127,9 +9898,8 @@ msgstr "結果を再取得" msgid "Refresh dashboard" msgstr "ダッシュボードを更新" -#, fuzzy msgid "Refresh delayed" -msgstr "ダッシュボードを更新" +msgstr "更新が遅延しています" msgid "Refresh frequency" msgstr "更新頻度" @@ -10202,7 +9972,7 @@ msgid "Reload" msgstr "再読み込み" msgid "Reload SQL Lab" -msgstr "" +msgstr "SQL Labを再読み込み" msgid "Remove" msgstr "削除" @@ -10216,13 +9986,11 @@ msgstr "システムの既定テーマを解除" msgid "Remove cross-filter" msgstr "クロスフィルターを削除" -#, fuzzy msgid "Remove customization" -msgstr "カスタマイズ種類" +msgstr "カスタマイズを削除" -#, fuzzy msgid "Remove filter" -msgstr "項目を削除" +msgstr "フィルターを削除" msgid "Remove item" msgstr "項目を削除" @@ -10316,7 +10084,7 @@ msgid "Report name" msgstr "レポート名" msgid "Report not yet run" -msgstr "" +msgstr "レポートはまだ実行されていません" msgid "Report schedule client error" msgstr "レポートスケジュールのクライアントエラー" @@ -10372,7 +10140,7 @@ msgstr "リサンプリング" #, python-format msgid "Resample method '%(method)s' is not supported." -msgstr "" +msgstr "リサンプリングメソッド「%(method)s」はサポートされていません。" msgid "Resample method should be in " msgstr "リサンプリング方法は以下の通りです " @@ -10401,7 +10169,6 @@ msgstr "パスワードをリセット" msgid "Reset state" msgstr "状態をリセット" -#, fuzzy msgid "Reset to default folders?" msgstr "既定のフォルダーにリセットしますか?" @@ -10433,9 +10200,8 @@ msgstr "結果バックエンドが設定されていません。" msgid "Results backend needed for asynchronous queries is not configured." msgstr "非同期クエリに必要な結果バックエンドが設定されていません。" -#, fuzzy msgid "Resume auto-refresh" -msgstr "自動更新を設定" +msgstr "自動更新を再開" msgid "Retry" msgstr "再試行" @@ -10443,9 +10209,8 @@ msgstr "再試行" msgid "Retry fetching results" msgstr "結果の取得を再試行" -#, fuzzy msgid "Return to Superset" -msgstr "特定の日時に戻ります。" +msgstr "Superset に戻る" msgid "Return to specific datetime." msgstr "特定の日時に戻ります。" @@ -10571,9 +10336,8 @@ msgstr "行数制限" msgid "Rows" msgstr "行" -#, fuzzy msgid "Rows (vertical layout)" -msgstr "垂直レイアウト (行)" +msgstr "行 (垂直レイアウト)" msgid "Rows per page, 0 means no pagination" msgstr "1ページあたりの行数(0 はページネーションなし)" @@ -10623,7 +10387,7 @@ msgstr "選択範囲を実行" msgid "Running" msgstr "実行中" -#, fuzzy, python-format +#, python-format msgid "Running block %(block_num)s out of %(block_count)s" msgstr "全 %(block_count)s ブロック中、%(block_num)s 番目のブロックを実行中" @@ -10639,9 +10403,6 @@ msgstr "SQL" msgid "SQL Lab" msgstr "SQL Lab" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "SQL Lab cannot authorise a statement that could not be fully parsed. Qualify" " tables explicitly and avoid dynamic SQL inside stored-procedure or vendor-" @@ -10770,9 +10531,9 @@ msgstr "上書き保存" msgid "Save as" msgstr "名前を付けて保存" -#, fuzzy, python-format +#, python-format msgid "Save as %s" -msgstr "名前を付けて保存" +msgstr "%sとして保存" msgid "Save as Dataset" msgstr "データセットとして保存" @@ -10816,11 +10577,8 @@ msgstr "データセットを保存または上書き" msgid "Save query" msgstr "クエリを保存" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Save this API key securely" -msgstr "この API キーを安全に保存してください" +msgstr "この API キーを安全に保管してください" msgid "Save this query as a virtual dataset to continue exploring" msgstr "探索を続けるために、このクエリを仮想データセットとして保存してください" @@ -10855,11 +10613,8 @@ msgstr "保存中..." msgid "Scale and Move" msgstr "拡大縮小と移動" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Scale factor applied to metric-driven line widths" -msgstr "メトリクスに基づく線幅に適用されるスケールファクター" +msgstr "メトリックに基づく線幅に適用されるスケールファクター" msgid "Scale only" msgstr "拡大縮小のみ" @@ -10950,26 +10705,23 @@ msgstr "すべてのメトリックと列を検索" msgid "Search box" msgstr "検索ボックス" -#, fuzzy msgid "Search by" -msgstr "検索ボックス" +msgstr "検索条件" msgid "Search by query text" msgstr "クエリテキストで検索" -#, fuzzy msgid "Search calculated columns by name" -msgstr "計算列" +msgstr "計算列を名前で検索" msgid "Search charts by name, editor, or dashboard" -msgstr "名前、編集者、またはダッシュボードでチャートを検索" +msgstr "チャートを名前、編集者、またはダッシュボードで検索" msgid "Search columns" msgstr "列を検索" -#, fuzzy msgid "Search columns by name" -msgstr "列を検索" +msgstr "列を名前で検索" msgid "Search columns..." msgstr "列を検索..." @@ -10980,9 +10732,8 @@ msgstr "編集者を検索" msgid "Search in filters" msgstr "フィルター内を検索" -#, fuzzy msgid "Search metrics by key or label" -msgstr "メトリックと列を検索" +msgstr "メトリックをキーまたはラベルで検索" msgid "Search tags" msgstr "タグを検索" @@ -11024,9 +10775,8 @@ msgstr "セキュア設定" msgid "Security" msgstr "セキュリティ" -#, fuzzy msgid "See " -msgstr "系列" +msgstr "参照 " #, python-format msgid "See all %(tableName)s" @@ -11044,9 +10794,9 @@ msgstr "クエリの詳細を表示" msgid "Select" msgstr "選択" -#, fuzzy, python-format +#, python-format msgid "Select %s or type to search %s" -msgstr "スキーマを選択、または入力してスキーマを検索" +msgstr "%s を選択、または入力して %s を検索" msgid "Select ..." msgstr "選択..." @@ -11069,9 +10819,9 @@ msgstr "タグを選択" msgid "Select Value" msgstr "値を選択" -#, fuzzy, python-format +#, python-format msgid "Select a %s" -msgstr "タグを選択" +msgstr "%s を選択" msgid "Select a CSS template" msgstr "CSSテンプレートを選択" @@ -11106,7 +10856,6 @@ msgstr "このデータの区切り文字を選択してください" msgid "Select a dimension" msgstr "ディメンションを選択" -#, fuzzy msgid "Select a linear color scheme" msgstr "線形配色を選択" @@ -11127,13 +10876,11 @@ msgstr "スキーマを選択" msgid "Select a schema if the database supports this" msgstr "データベースがサポートしている場合はスキーマを選択してください" -#, fuzzy msgid "Select a semantic layer" -msgstr "スキーマを選択" +msgstr "セマンティックレイヤーを選択" -#, fuzzy msgid "Select a semantic layer type" -msgstr "チャート種類を選択" +msgstr "セマンティックレイヤーの種類を選択" msgid "Select a sheet name from the uploaded file" msgstr "アップロードしたファイルからシート名を選択してください" @@ -11188,9 +10935,8 @@ msgstr "配色を選択してください" msgid "Select column" msgstr "列を選択" -#, fuzzy msgid "Select column name" -msgstr "列を選択" +msgstr "列名を選択" msgid "" "Select columns that will be displayed in the table. You can multiselect " @@ -11277,9 +11023,8 @@ msgstr "" msgid "Select layers to hide" msgstr "非表示にするレイヤーを選択" -#, fuzzy msgid "Select object name" -msgstr "件名を選択" +msgstr "オブジェクト名を選択" msgid "" "Select one or many metrics to display, that will be displayed in the " @@ -11310,9 +11055,8 @@ msgstr "データセット名を選択または入力" msgid "Select page size" msgstr "ページサイズを選択" -#, fuzzy msgid "Select permissions" -msgstr "権限の同期" +msgstr "権限を選択" msgid "Select roles" msgstr "ロールを選択" @@ -11323,16 +11067,14 @@ msgstr "保存済みメトリックを選択" msgid "Select saved queries" msgstr "保存済みクエリを選択" -#, fuzzy msgid "Select schema" msgstr "スキーマを選択" msgid "Select scheme" msgstr "スキームを選択" -#, fuzzy msgid "Select semantic views" -msgstr "スキームを選択" +msgstr "セマンティックビューを選択" msgid "" "Select shape for computing values. \"FIXED\" sets all zoom levels to the " @@ -11397,22 +11139,16 @@ msgstr "固定色を選択" msgid "Select the geojson column" msgstr "GeoJSON列を選択してください" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "Select the map tile provider. MapLibre is open-source and requires no API " "key. Mapbox requires MAPBOX_API_KEY to be configured in Superset." msgstr "" -"マップタイルプロバイダーを選択してください。MapLibreはオープンソースであり、APIキーは不要です。MapboxにはSupersetでMAPBOX_API_KEYの設定が必要です。" +"マップタイルプロバイダーを選択してください。MapLibreはオープンソースであり、APIキーは不要です。Mapboxを使用するにはSupersetでMAPBOX_API_KEYを設定する必要があります。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "Select the metric used to determine which color breakpoint range each path " "falls into." -msgstr "各パスがどのカラーブレーク範囲に属するかを決定するために使用するメトリクスを選択してください。" +msgstr "各パスがどのカラーブレークポイント範囲に属するかを決定するために使用するメトリックを選択してください。" msgid "Select the type of color scheme to use." msgstr "使用する配色の種類を選択してください。" @@ -11429,18 +11165,14 @@ msgid "" "query by clicking on the %s button." msgstr "コントロールパネルの強調表示されたフィールドで値を選択してください。その後、%s ボタンをクリックしてクエリを実行してください。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "Select which time grains are available in the filter control. This is a UI " "allow list only and does not add extra conditions to the underlying queries." msgstr "" -"フィルターコントロールで利用可能な時間粒度を選択してください。これはUIの許可リストのみであり、基となるクエリに追加の条件は付与されません。" +"フィルターコントロールで利用可能な時間粒度を選択してください。これはUI上の許可リストのみであり、基となるクエリに追加の条件は付与されません。" -#, fuzzy msgid "Selected" -msgstr "未選択" +msgstr "選択済み" msgid "Selecting a database is required" msgstr "データベースの選択は必須です" @@ -11448,91 +11180,63 @@ msgstr "データベースの選択は必須です" msgid "Selection method" msgstr "選択方法" -#, fuzzy msgid "Semantic" -msgstr "メールアドレス" +msgstr "セマンティック" -#, fuzzy msgid "Semantic Layer" -msgstr "注釈レイヤー" +msgstr "セマンティックレイヤー" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Semantic View" msgstr "セマンティックビュー" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Semantic Views" -msgstr "セマンティックビュー一覧" +msgstr "セマンティックビュー" -#, fuzzy msgid "Semantic layer" -msgstr "注釈レイヤー" +msgstr "セマンティックレイヤー" -#, fuzzy msgid "Semantic layer could not be created." -msgstr "注釈レイヤーを作成できませんでした。" +msgstr "セマンティックレイヤーを作成できませんでした。" -#, fuzzy msgid "Semantic layer could not be deleted." -msgstr "注釈レイヤーを削除できませんでした。" +msgstr "セマンティックレイヤーを削除できませんでした。" -#, fuzzy msgid "Semantic layer could not be updated." -msgstr "注釈レイヤーを更新できませんでした。" +msgstr "セマンティックレイヤーを更新できませんでした。" -#, fuzzy msgid "Semantic layer created" -msgstr "注釈テンプレートを作成しました" +msgstr "セマンティックレイヤーを作成しました" -#, fuzzy msgid "Semantic layer does not exist" -msgstr "チャートが存在しません" +msgstr "セマンティックレイヤーが存在しません" -#, fuzzy msgid "Semantic layer parameters are invalid." -msgstr "注釈レイヤーパラメータが無効です。" +msgstr "セマンティックレイヤーパラメータが無効です。" -#, fuzzy msgid "Semantic layer type" -msgstr "注釈レイヤーの種類" +msgstr "セマンティックレイヤーの種類" -#, fuzzy msgid "Semantic layer updated" -msgstr "注釈テンプレートを更新しました" +msgstr "セマンティックレイヤーを更新しました" -#, fuzzy msgid "Semantic view could not be created." -msgstr "データセットを作成できませんでした。" +msgstr "セマンティックビューを作成できませんでした。" -#, fuzzy msgid "Semantic view could not be deleted." -msgstr "CSSテンプレートを削除できませんでした。" +msgstr "セマンティックビューを削除できませんでした。" -#, fuzzy msgid "Semantic view could not be updated." -msgstr "データセットを更新できませんでした。" +msgstr "セマンティックビューを更新できませんでした。" -#, fuzzy msgid "Semantic view does not exist" -msgstr "データセットが存在しません" +msgstr "セマンティックビューが存在しません" -#, fuzzy msgid "Semantic view parameters are invalid." -msgstr "データセットパラメータが無効です。" +msgstr "セマンティックビューパラメータが無効です。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Semantic view updated" msgstr "セマンティックビューが更新されました" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Semantic views" msgstr "セマンティックビュー" @@ -11676,7 +11380,6 @@ msgstr "チャートをメールで共有" msgid "Share permalink by email" msgstr "固定リンクをメールで共有" -#, fuzzy msgid "Shared" msgstr "共有" @@ -11747,9 +11450,8 @@ msgstr "メトリック名を表示" msgid "Show Range Filter" msgstr "範囲フィルターを表示" -#, fuzzy msgid "Show SQL" -msgstr "ログを表示" +msgstr "SQLを表示" msgid "Show Timestamp" msgstr "タイムスタンプを表示" @@ -11789,9 +11491,8 @@ msgstr "軸線の目盛りを表示" msgid "Show cell bars" msgstr "セルバーを表示" -#, fuzzy msgid "Show cell bars for all columns" -msgstr "すべての列を表示" +msgstr "すべての列のセルバーを表示" msgid "Show chart description" msgstr "チャートの説明を表示" @@ -11820,7 +11521,7 @@ msgstr "1ページあたりの表示件数" msgid "" "Show hierarchical relationships of data, with the value represented by area," " showing proportion and contribution to the whole." -msgstr "データの階層関係を、面積で表される値によって示し、全体に対する割合や寄与度を視覚化します。" +msgstr "データの階層関係を面積で表される値によって示し、全体に対する割合や寄与度を視覚化します。" msgid "Show info tooltip" msgstr "情報ツールチップを表示" @@ -11931,11 +11632,9 @@ msgstr "その時点で利用可能なすべての系列をリスト表示しま msgid "Shows or hides markers for the time series" msgstr "時系列データのマーカーを表示または非表示にします" -#, fuzzy msgid "Sign in" msgstr "ログイン" -#, fuzzy msgid "Sign in with" msgstr "でログイン" @@ -11988,11 +11687,9 @@ msgstr "区切り文字の後のスペースをスキップ" msgid "Skipped %d system themes that cannot be deleted" msgstr "削除できない %d 個のシステムテーマをスキップしました" -#, fuzzy msgid "Slice Id" msgstr "スライスID" -#, fuzzy msgid "Slider" msgstr "スライダー" @@ -12019,21 +11716,12 @@ msgstr "平滑線は折れ線グラフのバリエーションです。角や急 msgid "Solid" msgstr "実線" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Some groups could not be resolved and are shown as IDs." msgstr "一部のグループを解決できなかったため、IDとして表示されています。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Some permissions could not be resolved and are shown as IDs." msgstr "一部の権限を解決できなかったため、IDとして表示されています。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "Some required filters on other tabs have values and will not be cleared" msgstr "他のタブの一部の必須フィルターには値が設定されており、クリアされません" @@ -12134,13 +11822,11 @@ msgstr "データで並ベ替え" msgid "Sort by metric" msgstr "メトリックで並べ替える" -#, fuzzy msgid "Sort by original table order" -msgstr "基テーブルの列順を維持" +msgstr "元のテーブルの順序で並べ替える" -#, fuzzy msgid "Sort by series" -msgstr "系列の並べ替え基準" +msgstr "系列で並べ替え" msgid "Sort columns alphabetically" msgstr "列をアルファベット順に並べ替え" @@ -12151,9 +11837,8 @@ msgstr "列の並べ替え基準" msgid "Sort descending" msgstr "降順で並べ替え" -#, fuzzy msgid "Sort display control values" -msgstr "表示制御値の並べ替え" +msgstr "表示コントロール値を並べ替え" msgid "Sort filter values" msgstr "フィルター値を並べ替え" @@ -12167,15 +11852,12 @@ msgstr "並べ替えメトリック" msgid "Sort query by" msgstr "クエリの並べ替え基準" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "Sort results by series name in ascending order. When combined with \"Sort by" " metric\", this acts as a tiebreaker for equal metric values. Adding this " "sort may reduce query performance on some databases." msgstr "" -"結果をシリーズ名の昇順に並び替えます。「メトリクスで並び替え」と組み合わせると、メトリクス値が等しい場合のタイブレーカーとして機能します。この並び替えを追加すると、一部のデータベースではクエリのパフォーマンスが低下する可能性があります。" +"結果を系列名の昇順で並べ替えます。「メトリックで並べ替え」と組み合わせると、メトリック値が等しい場合のタイブレーカーとして機能します。この並べ替えを追加すると、一部のデータベースではクエリのパフォーマンスが低下する可能性があります。" msgid "Sort rows by" msgstr "行の並べ替え基準" @@ -12200,7 +11882,7 @@ msgstr "ソースカテゴリ" #, fuzzy msgid "Source location" -msgstr "ソースカテゴリ" +msgstr "ソースの場所" msgid "Sparkline" msgstr "スパークライン" @@ -12302,9 +11984,8 @@ msgstr "開始済み" msgid "Starts With" msgstr "で始まる" -#, fuzzy msgid "Starts with (ILIKE x%)" -msgstr "で始まる" +msgstr "で始まる (ILIKE x%)" msgid "State" msgstr "状態" @@ -12359,12 +12040,11 @@ msgstr "ストリーム" msgid "Streets" msgstr "ストリート" -#, fuzzy msgid "Streets (Carto)" -msgstr "ツリーチャート" +msgstr "ストリート (Carto)" msgid "Streets (OSM)" -msgstr "" +msgstr "ストリート (OSM)" msgid "Strength to pull the graph toward center" msgstr "グラフを中心へ引き寄せる強さ" @@ -12381,9 +12061,6 @@ msgstr "線あり (ストローク)" msgid "Structural" msgstr "構造的" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Structure is managed by the upstream semantic layer and is read-only." msgstr "構造は上流のセマンティックレイヤーによって管理されており、読み取り専用です。" @@ -12406,7 +12083,7 @@ msgid "Subject" msgstr "サブジェクト" msgid "Subject type" -msgstr "サブジェクトタイプ" +msgstr "サブジェクト種類" msgid "Subjects" msgstr "サブジェクト" @@ -12417,9 +12094,8 @@ msgstr "サブジェクトが無効です" msgid "Submit" msgstr "送信" -#, fuzzy msgid "Subscribers" -msgstr "サブカテゴリ" +msgstr "サブスクライバー" msgid "Subtitle" msgstr "サブタイトル" @@ -12430,9 +12106,9 @@ msgstr "小計" msgid "Success" msgstr "成功" -#, fuzzy, python-format +#, python-format msgid "Successfully changed %s!" -msgstr "データセットを正常に変更しました!" +msgstr "%s を正常に変更しました!" msgid "Suffix" msgstr "接尾辞" @@ -12488,9 +12164,9 @@ msgstr "Superset で予期しないエラーが発生しました。" msgid "Supported databases" msgstr "サポートされているデータベース" -#, fuzzy, python-format +#, python-format msgid "Swap %s" -msgstr "データセットを入れ替え" +msgstr "%s を入れ替え" msgid "Swap rows and columns" msgstr "行と列を入れ替え" @@ -12679,45 +12355,33 @@ msgstr "目標のカテゴリー" msgid "Target value" msgstr "目標値" -#, fuzzy msgid "Task" -msgstr "タグ" +msgstr "タスク" -#, fuzzy, python-format +#, python-format msgid "Task cancelled: %s" -msgstr "エクスポートがキャンセルされました" +msgstr "タスクがキャンセルされました: %s" -#, fuzzy msgid "Task could not be aborted." -msgstr "タグを更新できませんでした。" +msgstr "タスクを中止できませんでした。" -#, fuzzy msgid "Task could not be created." -msgstr "タグを作成できませんでした。" +msgstr "タスクを作成できませんでした。" -#, fuzzy msgid "Task could not be updated." -msgstr "タグを更新できませんでした。" +msgstr "タスクを更新できませんでした。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "Task is not abortable. The task is in progress but has not registered an " "abort handler." -msgstr "タスクを中止できません。タスクは実行中ですが、中止ハンドラーが登録されていません。" +msgstr "タスクは中止できません。タスクは進行中ですが、中止ハンドラーが登録されていません。" -#, fuzzy msgid "Task parameters are invalid." -msgstr "タグのパラメータが無効です。" +msgstr "タスクのパラメータが無効です。" -#, fuzzy msgid "Tasks" -msgstr "タグ" +msgstr "タスク" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Tasks will appear here as background operations are executed." msgstr "バックグラウンド操作が実行されると、タスクがここに表示されます。" @@ -12773,13 +12437,13 @@ msgstr "テキストの配置" msgid "Text embedded in email" msgstr "メールに埋め込まれるテキスト" -#, fuzzy, python-format +#, python-format msgid "The %s" -msgstr "%s 取得しました" +msgstr "%s" -#, fuzzy, python-format +#, python-format msgid "The %s linked to this chart may have been deleted." -msgstr "このチャートにリンクされているデータセットが削除された可能性があります。" +msgstr "このチャートにリンクされている %s は削除された可能性があります。" #, python-format msgid "The API response from %s does not match the IDatabaseTable interface." @@ -12805,29 +12469,22 @@ msgstr "" "GeoJsonLayer は GeoJSON " "形式のデータを受け取り、インタラクティブなポリゴン、線、ポイント(円、アイコン、テキスト)としてレンダリングします。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "The Global Task Framework is not enabled. Please contact your administrator " "to enable the GLOBAL_TASK_FRAMEWORK feature flag." msgstr "" -"グローバルタスクフレームワークが有効になっていません。GLOBAL_TASK_FRAMEWORKフィーチャーフラグを有効にするには、管理者にお問い合わせください。" +"グローバルタスクフレームワークが有効になっていません。GLOBAL_TASK_FRAMEWORK 機能フラグを有効にするように管理者に依頼してください。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "The Global Task Framework is not enabled. Set GLOBAL_TASK_FRAMEWORK=True in " "your feature flags to use @task. See " "https://superset.apache.org/docs/configuration/async-queries-celery for " "configuration details." msgstr "" -"グローバルタスクフレームワークが有効になっていません。@taskを使用するには、フィーチャーフラグでGLOBAL_TASK_FRAMEWORK=Trueを設定してください。設定の詳細については、https://superset.apache.org/docs/configuration/async-" -"queries-celery を参照してください。" +"グローバルタスクフレームワークが有効になっていません。@task を使用するには、機能フラグで GLOBAL_TASK_FRAMEWORK=True を設定してください。設定の詳細については、https://superset.apache.org/docs/configuration/async-queries-celery を参照してください。" msgid "The SSH server host key could not be verified against the expected key." -msgstr "" +msgstr "SSH サーバーのホストキーが、期待されるキーと照合できませんでした。" msgid "" "The Sankey chart visually tracks the movement and transformation of values across\n" @@ -12865,13 +12522,10 @@ msgid "" "with more than one category, only the first will be used." msgstr "色の割り当てに使用されるソースノードのカテゴリ。ノードが複数のカテゴリに関連付けられている場合、最初のカテゴリのみが使用されます。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "The chart this report targets was deleted. Restore the chart, or update " "the report to point at an active chart." -msgstr "" +msgstr "このレポートの対象であるチャートは削除されました。チャートを復元するか、レポートを更新して有効なチャートを指定してください。" msgid "The chart is still loading. Please wait a moment and try again." msgstr "チャートはまだ読み込み中です。しばらく待ってから再試行してください。" @@ -12949,7 +12603,7 @@ msgstr "ダッシュボードを保存しました" msgid "" "The dashboard this report targets was deleted. Restore the dashboard, or " "update the report to point at an active dashboard." -msgstr "" +msgstr "このレポートの対象であるダッシュボードは削除されました。ダッシュボードを復元するか、レポートを更新して有効なダッシュボードを指定してください。" msgid "The data source seems to have been deleted" msgstr "データソースが削除されたようです" @@ -13030,7 +12684,7 @@ msgstr "接続に使用される既定のスキーマ" msgid "" "The description can be displayed as widget headers in the dashboard view. " "Supports markdown." -msgstr "説明は、ダッシュボード ビューのウィジェット ヘッダーとして表示できます。マークダウンをサポートします。" +msgstr "説明は、ダッシュボード表示画面でウィジェットヘッダーとして表示できます。Markdownをサポートしています。" msgid "The display name of your dashboard" msgstr "ダッシュボードの表示名" @@ -13063,7 +12717,7 @@ msgid "" "extent so that all data points are included in the viewport. CUSTOM allows " "users to define the extent manually." msgstr "" -"アプリ起動時のマップの表示範囲。「データをあわせる」は、すべてのデータポイントがビューポート内に収まるよう自動調整します。「カスタム」は手動での設定を可能にします。" +"アプリ起動時のマップの表示範囲。「データに合わせる」は、すべてのデータポイントがビューポート内に収まるよう自動調整します。「カスタム(CUSTOM)」は手動での設定を可能にします。" msgid "The feature property to use for point labels" msgstr "ポイントラベルに使用するプロパティ" @@ -13074,9 +12728,6 @@ msgid "" "%(columns)s. " msgstr "`series_columns` に含まれる以下の項目が `columns` にありません: %(columns)s" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "The following fields contain sensitive information that was masked during " "export. Please provide the values to import this database." @@ -13293,9 +12944,6 @@ msgid "" msgstr "" "保存済みクエリと一緒にインポートするには、以下のデータベースのパスワードが必要です。なお、データベース設定の「セキュア設定」および「証明書」セクションはエクスポートファイルには含まれません。必要に応じて、インポート後に手動で追加してください。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "The passwords for the databases below are needed in order to import them." msgstr "以下のデータベースをインポートするには、それぞれのパスワードが必要です。" @@ -13384,6 +13032,8 @@ msgid "" "The radius of point features, in the units specified below. The final " "rendered size is this value multiplied by Point Radius Scale." msgstr "" +"ポイントフィーチャの半径は以下の単位で指定されたものとなります。" +"最終的にレンダリングされるサイズは、この値に「ポイント半径スケール」を乗じたものになります。" msgid "The report has been created" msgstr "レポートを作成しました" @@ -13408,7 +13058,7 @@ msgid "" msgstr "バックエンドに保存された結果が異なる形式であるため、デシリアライズ(復元)できません。" msgid "The rich tooltip shows a list of all series for that point in time" -msgstr "高度なツールチップには、その時点のすべての系列のリストが表示されます" +msgstr "詳細なツールチップには、その時点のすべての系列のリストが表示されます" msgid "The role has been created successfully." msgstr "ロールを正常に作成しました。" @@ -13525,11 +13175,10 @@ msgid "" "the timezone per the ISO 8601 format if specifying either the start and/or " "end time." msgstr "" -"視覚化の時間範囲。すべての相対時間、例: 「先月」、「過去 7 日間」、「現在」などは、サーバーの現地時間 (タイムゾーンなし) " +"視覚化の時間範囲。すべての相対時間(例: 「先月」「過去 7 日間」「現在」など)は、サーバーの現地時間 (タイムゾーンなし) " "を使用してサーバー上で評価されます。すべてのツールチップとプレースホルダーの時間は UTC (タイムゾーンなし) " -"で表されます。次に、タイムスタンプは、エンジンのローカル " -"タイムゾーンを使用してデータベースによって評価されます。開始時刻と終了時刻、あるいはその両方を指定する場合は、ISO 8601 " -"形式に従ってタイムゾーンを明示的に設定できることに注意してください。" +"で表されます。次に、タイムスタンプは、エンジンのローカルタイムゾーンを使用してデータベースによって評価されます。" +"開始時刻や終了時刻を指定する場合は、ISO 8601 形式に従ってタイムゾーンを明示的に設定できることに注意してください。" msgid "" "The time unit for each block. Should be a smaller unit than " @@ -13557,7 +13206,7 @@ msgstr "ラベルサイズの単位" msgid "" "The unit for point radius. Use \"pixels\" for consistent screen-space " "sizing regardless of zoom level." -msgstr "" +msgstr "ポイント半径の単位。ズームレベルに関係なく、画面空間でのサイズを統一するには「ピクセル」を使用してください。" msgid "The unit of measure for the specified point radius" msgstr "指定されたポイント半径の測定単位" @@ -13565,9 +13214,8 @@ msgstr "指定されたポイント半径の測定単位" msgid "The upper limit of the threshold range of the Isoband" msgstr "等値帯のしきい値範囲の上限" -#, fuzzy msgid "The user has been created successfully." -msgstr "ユーザーを正常に更新しました。" +msgstr "ユーザーを正常に作成しました。" msgid "The user has been updated successfully." msgstr "ユーザーを正常に更新しました。" @@ -13613,13 +13261,10 @@ msgstr "要素の境界線の幅" msgid "The width of the lines" msgstr "線の太さ" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "The width of the lines as either a fixed value or variable width based on a " "metric." -msgstr "固定値またはメトリクスに基づく可変幅として指定する線の幅。" +msgstr "固定値またはメトリックに基づく可変幅として指定する線の幅。" msgid "Theme" msgstr "テーマ" @@ -13673,13 +13318,11 @@ msgid "" "increasing the destination width." msgstr "このコンポーネント用のスペースが足りません。幅を狭くするか、配置先の幅を広げてください。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "" "There was a problem refreshing your dashboard. We'll try again in %s, as " "scheduled." -msgstr "ダッシュボードの更新中に問題が発生しました。予定通り%s後に再試行します。" +msgstr "ダッシュボードの更新中に問題が発生しました。予定通り %s 後に再試行します。" msgid "There was an error creating the group. Please, try again." msgstr "グループ作成中にエラーが発生しました。再度お試しください。" @@ -13706,13 +13349,11 @@ msgstr "お気に入り状態の取得中にエラーが発生しました: %s" msgid "There was an error fetching the filtered charts and dashboards:" msgstr "フィルタリングされたチャートおよびダッシュボードの取得中にエラーが発生しました:" -#, fuzzy msgid "There was an error loading groups." -msgstr "ユーザーの読み込み中にエラーが発生しました。" +msgstr "グループの読み込み中にエラーが発生しました。" -#, fuzzy msgid "There was an error loading permissions." -msgstr "ユーザーの読み込み中にエラーが発生しました。" +msgstr "権限の読み込み中にエラーが発生しました。" msgid "There was an error loading the catalogs" msgstr "カタログの読み込み中にエラーが発生しました" @@ -13745,13 +13386,11 @@ msgstr "ロール更新中にエラーが発生しました。再度お試しく msgid "There was an error updating the user. Please, try again." msgstr "ユーザー更新中にエラーが発生しました。再度お試しください。" -#, fuzzy msgid "There was an error while fetching groups" -msgstr "ユーザーの取得中にエラーが発生しました" +msgstr "グループの取得中にエラーが発生しました" -#, fuzzy msgid "There was an error while fetching permissions" -msgstr "ユーザーの取得中にエラーが発生しました" +msgstr "権限の取得中にエラーが発生しました" msgid "There was an error while fetching users" msgstr "ユーザーの取得中にエラーが発生しました" @@ -13759,9 +13398,9 @@ msgstr "ユーザーの取得中にエラーが発生しました" msgid "There was an error with your request" msgstr "リクエストにエラーがありました" -#, fuzzy, python-format +#, python-format msgid "There was an issue cancelling the task: %s" -msgstr "%s の削除中に問題が発生しました: %s" +msgstr "タスクのキャンセル中に問題が発生しました: %s" #, python-format msgid "There was an issue deleting %s" @@ -13779,9 +13418,9 @@ msgstr "ユーザーの登録削除中に問題が発生しました: %s" msgid "There was an issue deleting rules: %s" msgstr "ルールの削除中に問題が発生しました: %s" -#, fuzzy, python-format +#, python-format msgid "There was an issue deleting the selected %s" -msgstr "選択した %s の削除中に問題が発生しました: %s" +msgstr "選択した %s の削除中に問題が発生しました" #, python-format msgid "There was an issue deleting the selected %s: %s" @@ -13814,21 +13453,21 @@ msgstr "選択したテーマの削除中に問題が発生しました: %s" msgid "There was an issue deleting: %s" msgstr "削除中に問題が発生しました: %s" -#, fuzzy, python-format +#, python-format msgid "There was an issue duplicating the %s." -msgstr "データセットの複製中に問題が発生しました。" +msgstr "%s の複製中に問題が発生しました。" -#, fuzzy, python-format +#, python-format msgid "There was an issue duplicating the selected %s: %s" -msgstr "選択した %s の削除中に問題が発生しました: %s" +msgstr "選択した %s の複製中に問題が発生しました: %s" -#, fuzzy, python-format +#, python-format msgid "There was an issue exporting the %s" -msgstr "データベースのエクスポート中に問題が発生しました" +msgstr "%s のエクスポート中に問題が発生しました" -#, fuzzy, python-format +#, python-format msgid "There was an issue exporting the selected %s" -msgstr "選択したテーマのエクスポート中に問題が発生しました" +msgstr "選択した %s のエクスポート中に問題が発生しました" msgid "There was an issue exporting the selected charts" msgstr "選択したチャートのエクスポート中に問題が発生しました" @@ -13924,9 +13563,8 @@ msgstr "このチャートは、同じ名前の列を含むデータセットを msgid "This chart has been moved to a different filter scope." msgstr "このチャートは別のフィルタースコープへ移動されました。" -#, fuzzy msgid "This chart is managed externally and can't be overwritten in Superset." -msgstr "このチャートは外部で管理されているため、Superset では編集できません" +msgstr "このチャートは外部で管理されているため、Superset では上書きできません" msgid "This chart is managed externally, and can't be edited in Superset" msgstr "このチャートは外部で管理されているため、Superset では編集できません" @@ -13940,9 +13578,9 @@ msgid "" "source. " msgstr "保存されていないクエリをソースとして使用する場合、このチャートタイプはサポートされません。" -#, fuzzy, python-format +#, python-format msgid "This column might be incompatible with current %s" -msgstr "この列は現在のデータセットと互換性がない可能性があります" +msgstr "この列は現在の %s と互換性がない可能性があります" msgid "This column might be incompatible with current dataset" msgstr "この列は現在のデータセットと互換性がない可能性があります" @@ -14018,9 +13656,6 @@ msgid "" "table." msgstr "このデータベーステーブルにはデータが含まれていません。別のテーブルを選択してください。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "This database uses OAuth2 for authentication. Please click the link above to" " grant Apache Superset permission to access the data. Your personal access " @@ -14056,20 +13691,18 @@ msgstr "このフィールドは、メトリックをチャートに紐付ける msgid "This filter already exist on the report" msgstr "このフィルターは既にレポート内に存在します" -#, fuzzy, python-format +#, python-format msgid "This filter might be incompatible with current %s" -msgstr "このフィルターは現在のデータセットと互換性がない可能性があります" +msgstr "このフィルターは現在の %s と互換性がない可能性があります" msgid "This folder is currently empty" msgstr "このフォルダーは現在空です" -#, fuzzy msgid "This folder only supports columns" -msgstr "このフォルダーは列のみを受け入れます" +msgstr "このフォルダーは列のみをサポートしています" -#, fuzzy msgid "This folder only supports metrics" -msgstr "このフォルダーはメトリックのみを受け入れます" +msgstr "このフォルダーはメトリックのみをサポートしています" msgid "" "This functionality is disabled in your environment for security reasons." @@ -14110,11 +13743,8 @@ msgstr "これは既定のフォルダーです" msgid "This is the default light theme" msgstr "これは既定のライトテーマです" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "This is the only time you will see this key. Store it securely." -msgstr "このキーを表示できるのは今回限りです。安全な場所に保存してください。" +msgstr "このキーを表示できるのは今回限りです。安全に保管してください。" msgid "" "This json object describes the positioning of the widgets in the dashboard. " @@ -14125,15 +13755,12 @@ msgstr "" "オブジェクトは、ダッシュボード内のウィジェットの配置を記述します。ダッシュボード表示画面でドラッグ&ドロップによりサイズや位置を調整すると自動生成されます" msgid "This link cannot be followed because its address is unsafe." -msgstr "" +msgstr "このリンクのアドレスは安全ではないため、アクセスできません。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "This link will take you to an external website. We cannot guarantee the " "safety of external destinations." -msgstr "このリンクをクリックすると外部サイトに移動します。外部先の安全性は保証できません。" +msgstr "このリンクは外部ウェブサイトへ移動します。外部の遷移先の安全性は保証できません。" msgid "This markdown component has an error." msgstr "このマークダウンコンポーネントにエラーがあります。" @@ -14150,9 +13777,9 @@ msgstr "これは拡張機能が有効化されていないか、コンテンツ msgid "This may be triggered by:" msgstr "以下の原因が考えられます:" -#, fuzzy, python-format +#, python-format msgid "This metric might be incompatible with current %s" -msgstr "このメトリックは現在のデータセットと互換性がない可能性があります" +msgstr "このメトリックは現在の %s と互換性がない可能性があります" msgid "This name is already taken. Please choose another one." msgstr "この名前は既に使われています。別のものを選択してください。" @@ -14166,7 +13793,7 @@ msgid "" msgstr "このページは iframe 内への埋め込みを想定していますが、現在はその状態ではないようです。" msgid "This password is too common; please choose a less guessable one." -msgstr "" +msgstr "このパスワードはありふれています。推測されにくいものを選んでください。" msgid "" "This section allows you to configure how to use the slice\n" @@ -14221,11 +13848,9 @@ msgid "This was triggered by:" msgid_plural "This may be triggered by:" msgstr[0] "原因として以下が考えられます:" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "This will abort (stop) the task for all %s subscriber(s)." -msgstr "これにより、%s人の全購読者のタスクが中止(停止)されます。" +msgstr "これにより、%s 人のすべてのサブスクライバーのタスクが中止(停止)されます。" msgid "" "This will be applied to the whole table. Arrows (↑ and ↓) will be added to " @@ -14234,9 +13859,6 @@ msgid "" msgstr "" "これはテーブル全体に適用されます。主要な列には増加・減少を示す矢印(↑/↓)が付きます。基本的な条件付き書式は、以下の個別設定で上書き可能です。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "This will cancel the task." msgstr "これによりタスクがキャンセルされます。" @@ -14376,9 +13998,8 @@ msgstr "時間粒度フィルタープラグイン" msgid "Time grain missing" msgstr "時間粒度がありません" -#, fuzzy msgid "Time grain options" -msgstr "時系列オプション" +msgstr "時間粒度のオプション" msgid "Time granularity" msgstr "時間粒度" @@ -14423,20 +14044,17 @@ msgstr "時系列 - 期間ピボット" msgid "Time-series Table" msgstr "時系列 - テーブル" -#, fuzzy msgid "Timed Out" -msgstr "時間の形式" +msgstr "タイムアウト" msgid "Timeline" msgstr "タイムライン" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "" "Timeout configured (%s seconds) but no abort handler defined. Task will " "continue running past the timeout." -msgstr "タイムアウトが設定されています(%s秒)が、中止ハンドラーが定義されていません。タスクはタイムアウト後も実行を継続します。" +msgstr "タイムアウトが設定されています(%s 秒)が、中止ハンドラーが定義されていません。タスクはタイムアウト後も実行を継続します。" msgid "Timeout error" msgstr "タイムアウトエラー" @@ -14493,12 +14111,16 @@ msgid "" "Too many sub-slices requested. The maximum allowed is %(max)s, but " "%(count)s were requested." msgstr "" +"要求されたサブスライスの数が多すぎます。" +"許可される最大数は %(max)s ですが、%(count)s が要求されました。" #, python-format msgid "" "Too many time comparisons requested. The maximum allowed is %(max)s, but " "%(count)s were requested." msgstr "" +"時間比較の要求が多すぎます。" +"許可される最大数は %(max)s ですが、%(count)s が要求されました。" msgid "Tool Panel" msgstr "ツールパネル" @@ -14629,9 +14251,6 @@ msgid "" "Truncates the specified date to the accuracy specified by the date unit." msgstr "指定された日付を、指定された単位の精度で切り捨てます。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Trust this URL and don't ask again" msgstr "このURLを信頼して次回から確認しない" @@ -14660,18 +14279,14 @@ msgstr "値を入力してください" msgid "Type a value here" msgstr "ここに値を入力してください" -#, fuzzy msgid "Type a value..." -msgstr "値を入力してください" +msgstr "値を入力..." msgid "Type is required" msgstr "種類は必須です" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Type of Google Sheets allowed" -msgstr "許可されるGoogle スプレッドシートのタイプ" +msgstr "許可される Google スプレッドシートの種類" msgid "Type of chart to display in sparkline" msgstr "スパークラインに表示するチャートの種類" @@ -14679,21 +14294,14 @@ msgstr "スパークラインに表示するチャートの種類" msgid "Type of comparison, value difference or percentage" msgstr "比較の種類 (値の差またはパーセンテージ)" -#, fuzzy msgid "Type to search (contains)..." -msgstr "列を検索..." +msgstr "入力して検索 (部分一致)..." -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Type to search (ends with)..." -msgstr "入力して検索(末尾一致)..." +msgstr "入力して検索 (後方一致)..." -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Type to search (starts with)..." -msgstr "入力して検索(前方一致)..." +msgstr "入力して検索 (前方一致)..." msgid "UI Configuration" msgstr "UI 設定" @@ -14704,9 +14312,8 @@ msgstr "UI テーマ管理が有効になっていません。" msgid "URL" msgstr "URL" -#, fuzzy msgid "URL Filters" -msgstr "すべてのフィルター" +msgstr "URL フィルター" msgid "URL Parameters" msgstr "URL パラメータ" @@ -14738,15 +14345,13 @@ msgstr "" "Job " "User」のロールがあり、且つ「bigquery.readsessions.create」「bigquery.readsessions.getData」の権限があることを確認してください。" -#, fuzzy msgid "" "Unable to connect. Verify that the following roles are set on the service " "account: \"Cloud Datastore Viewer\", \"Cloud Datastore User\", \"Cloud " "Datastore Creator\"" msgstr "" -"接続できません。サービスアカウントに「BigQuery Data Viewer」「BigQuery Metadata Viewer」「BigQuery " -"Job " -"User」のロールがあり、且つ「bigquery.readsessions.create」「bigquery.readsessions.getData」の権限があることを確認してください。" +"接続できません。サービスアカウントに「Cloud Datastore Viewer」「Cloud Datastore User」「Cloud " +"Datastore Creator」のロールが設定されていることを確認してください。" msgid "Unable to create chart without a query id." msgstr "クエリ ID がないためチャートを作成できません。" @@ -14764,9 +14369,6 @@ msgstr "値をデコードできません" msgid "Unable to encode value" msgstr "値をエンコードできません" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Unable to fetch semantic views. Check the layer configuration." msgstr "セマンティックビューを取得できません。レイヤーの設定を確認してください。" @@ -14774,9 +14376,8 @@ msgstr "セマンティックビューを取得できません。レイヤーの msgid "Unable to find such a holiday: [%(holiday)s]" msgstr "指定された祝日が見つかりませんでした: [%(holiday)s]" -#, fuzzy msgid "Unable to generate download payload" -msgstr "ダッシュボードを読み込めませんでした" +msgstr "ダウンロード用ペイロードを生成できません" msgid "" "Unable to identify temporal column for date range time comparison.Please " @@ -14830,9 +14431,8 @@ msgstr "操作を元に戻す" msgid "Undo?" msgstr "元に戻しますか?" -#, fuzzy msgid "Unexpected HTTP 401 response. Check your credentials." -msgstr "予期しないエラーが発生しました。詳細についてはログを確認してください" +msgstr "予期しない HTTP 401 応答を受け取りました。認証情報を確認してください。" msgid "Unexpected error" msgstr "予期しないエラー" @@ -14890,9 +14490,6 @@ msgstr "不明なエラー" msgid "Unknown input format" msgstr "不明な入力形式" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Unknown tokens will be highlighted as warnings." msgstr "不明なトークンは警告としてハイライト表示されます。" @@ -14905,18 +14502,14 @@ msgstr "不明な値" msgid "Unpin" msgstr "固定を解除" -#, fuzzy msgid "Unpin from the result panel" -msgstr "結果パネルにピン留め" +msgstr "結果パネルのピン留めを解除" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Unpin from top" -msgstr "上部からピン留めを解除" +msgstr "上部のピン留めを解除" msgid "Unsafe link blocked" -msgstr "" +msgstr "安全でないリンクがブロックされました" #, python-format msgid "Unsafe return type for function %(func)s: %(value_type)s" @@ -14958,16 +14551,14 @@ msgstr "無題のクエリ" msgid "Untitled query" msgstr "無題のクエリ" -#, fuzzy msgid "Unverified" msgstr "未検証" msgid "Update" msgstr "更新" -#, fuzzy msgid "Update auto-refresh" -msgstr "自動更新を設定" +msgstr "自動更新の設定を更新" msgid "Update chart" msgstr "チャートを更新" @@ -15170,11 +14761,8 @@ msgid "" msgstr "" "円を用いてシステム内の異なるステージを通るデータの流れを視覚化します。パスをホバーすると各値がたどった段階を確認できます。複数ステージやグループが混在するファネルやパイプラインの視覚化に役立ちます。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Uses the first 25 values if the dimension has more." -msgstr "ディメンションにそれ以上の値がある場合、最初の25件の値を使用します。" +msgstr "ディメンションに値が25件以上ある場合、最初の25件の値を使用します。" msgid "Valid SQL expression" msgstr "有効なSQL式" @@ -15421,9 +15009,6 @@ msgstr "WFS" msgid "WMS" msgstr "WMS" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "Waiting for first refresh" msgstr "初回更新を待機中" @@ -15451,9 +15036,6 @@ msgid "" "exist." msgstr "警告!メタデータが存在しない場合、データセットを変更するとチャートが壊れる可能性があります。" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "" "Warning: ILIKE queries may be slow on large datasets as they cannot use " "indexes effectively." @@ -15577,7 +15159,7 @@ msgid "" msgstr "「計算種類」が「変化率」に設定されている場合、Y 軸の形式は強制的に「.1%」になります" msgid "When a secondary metric is provided, a linear color scale is used." -msgstr "二次メトリックが提供されている場合、線形カラー スケールが使用されます。" +msgstr "第2メトリックが提供されている場合、線形カラースケールが使用されます。" msgid "When checked, the map will zoom to your data after each query" msgstr "有効にすると、クエリ実行のたびにデータがある範囲へマップがズームします" @@ -15629,7 +15211,6 @@ msgstr "「グループ化」使用時は、選択できるメトリックは1 msgid "When using other than adaptive formatting, labels may overlap" msgstr "アダプティブ形式以外を使用する場合、ラベルが重なる可能性があります" -#, fuzzy msgid "" "When using this option, default value can't be set. Using this option may " "impact the load times for your dashboard." @@ -15706,9 +15287,8 @@ msgstr "X軸の最小値と最大値を表示するかどうか" msgid "Whether to display the min and max values of the Y-axis" msgstr "Y軸の最小値と最大値を表示するかどうか" -#, fuzzy msgid "Whether to display the numbered column" -msgstr "トレンドラインを表示するかどうか" +msgstr "番号付き列を表示するかどうか" msgid "Whether to display the numerical values within the cells" msgstr "セル内の数値を表示するかどうか" @@ -15808,7 +15388,6 @@ msgstr "ホバー時にどの関連項目を強調表示するか" msgid "Whisker/outlier options" msgstr "ひげ / 外れ値のオプション" -#, fuzzy msgid "Why do I need to create a database?" msgstr "なぜデータベースを作成する必要があるのですか?" @@ -15821,9 +15400,8 @@ msgstr "信頼区間の幅。0 から 1 の間で指定してください" msgid "Width of the sparkline" msgstr "スパークラインの幅" -#, fuzzy msgid "Width scale multiplier" -msgstr "乗数" +msgstr "幅のスケール乗数" msgid "Window must be > 0" msgstr "ウィンドウサイズは 0 より大きくする必要があります" @@ -15973,7 +15551,6 @@ msgstr "%s 年" msgid "Yes" msgstr "はい" -#, fuzzy msgid "Yes, Cancel" msgstr "はい、キャンセルします" @@ -15985,7 +15562,7 @@ msgstr "はい、変更を上書きします" #, python-format msgid "You are adding tags to %s %ss" -msgstr "%s %ss にタグを追加しています" +msgstr "%s 個の %s にタグを追加しています" msgid "You are editing a query from the virtual dataset " msgstr "仮想データセットのクエリを編集しています " @@ -16087,9 +15664,8 @@ msgstr "このチャートを編集する権限がありません" msgid "You do not have permission to edit this dashboard" msgstr "このダッシュボードを編集する権限がありません" -#, fuzzy msgid "You do not have permission to perform this operation" -msgstr "このチャートを編集する権限がありません" +msgstr "この操作を実行する権限がありません" msgid "You do not have permission to read tags" msgstr "タグを読み取る権限がありません" @@ -16101,7 +15677,7 @@ msgid "You do not have sufficient permissions to edit the chart" msgstr "チャートを編集するための十分な権限がありません" msgid "You don't have access to one or more of the referenced datasources." -msgstr "" +msgstr "参照されているデータソースのうち、1つ以上に対してアクセス権がありません。" msgid "You don't have access to this chart." msgstr "このチャートへのアクセス権がありません。" @@ -16115,51 +15691,46 @@ msgstr "このデータセットへのアクセス権がありません。" msgid "You don't have access to this embedded dashboard config." msgstr "この埋め込みダッシュボード設定へのアクセス権がありません。" -#, fuzzy msgid "You don't have access to this semantic view." -msgstr "このデータセットへのアクセス権がありません。" +msgstr "このセマンティックビューへのアクセス権がありません。" -#, fuzzy msgid "You don't have permission to copy to clipboard" -msgstr "値を変更する権限がありません。" +msgstr "クリップボードにコピーする権限がありません" -#, fuzzy msgid "You don't have permission to export data" -msgstr "タグを読み取る権限がありません" +msgstr "データをエクスポートする権限がありません" -#, fuzzy msgid "You don't have permission to export images" -msgstr "タグを読み取る権限がありません" +msgstr "画像をエクスポートする権限がありません" msgid "You don't have permission to modify the value." msgstr "値を変更する権限がありません。" #, python-format msgid "You don't have the rights to alter %(resource)s" -msgstr "%(resource)s を変更する権利がありません" +msgstr "%(resource)s を変更する権限がありません" msgid "You don't have the rights to alter this chart" -msgstr "このチャートを変更する権利がありません" +msgstr "このチャートを変更する権限がありません" msgid "You don't have the rights to alter this dashboard" -msgstr "このダッシュボードを変更する権利がありません" +msgstr "このダッシュボードを変更する権限がありません" msgid "You don't have the rights to alter this title." -msgstr "このタイトルを変更する権利がありません。" +msgstr "このタイトルを変更する権限がありません。" msgid "You don't have the rights to create a chart" -msgstr "チャートを作成する権利がありません" +msgstr "チャートを作成する権限がありません" msgid "You don't have the rights to create a dashboard" -msgstr "ダッシュボードを作成する権利がありません" +msgstr "ダッシュボードを作成する権限がありません" -#, fuzzy msgid "You don't have the rights to export data" -msgstr "ダッシュボードを作成する権利がありません" +msgstr "データをエクスポートする権限がありません" -#, fuzzy, python-format +#, python-format msgid "You have been removed from task: %s" -msgstr "このフィルターを削除しました。" +msgstr "タスクから削除されました: %s" msgid "You have removed this filter." msgstr "このフィルターを削除しました。" @@ -16191,7 +15762,7 @@ msgid "" msgstr "編集するにはデータセットの編集者である必要があります。変更または編集アクセスをリクエストするには、データセットの編集者に連絡してください。" msgid "You must change your password before continuing." -msgstr "" +msgstr "続行するには、パスワードを変更する必要があります。" msgid "You must pick a name for the new dashboard" msgstr "新しいダッシュボードの名前を指定してください" @@ -16204,6 +15775,8 @@ msgid "" "You need access to the following tables: %(tables)s, " "'all_database_access' or 'all_datasource_access' permission" msgstr "" +"以下のテーブルへのアクセス権、または「all_database_access」か「all_datasource_access」" +"権限が必要です:%(tables)s" msgid "You need to" msgstr "次のことを行う必要があります" @@ -16214,13 +15787,11 @@ msgid "" msgstr "" "コントロールパネルの値を更新しましたが、チャートには自動反映されていません。「チャートを更新」ボタンをクリックするか、以下を実行してください" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "" "You'll be removed from this task. It will continue running for %s other " "subscriber(s)." -msgstr "このタスクから削除されます。タスクは他の%s人の購読者のために実行を継続します。" +msgstr "あなたはこのタスクから削除されます。タスクは他の %s 人のサブスクライバーのために引き続き実行されます。" msgid "" "You've changed datasets. Any controls with data (columns, metrics) that " @@ -16272,14 +15843,14 @@ msgid "Your report could not be deleted" msgstr "レポートを削除できませんでした" msgid "Your session has ended. Please sign in again." -msgstr "" +msgstr "セッションが終了しました。再度ログインしてください。" msgid "Your user information" msgstr "ユーザー情報" #, fuzzy msgid "Z-A" -msgstr "キー逆順 (z-a)" +msgstr "Z-A" msgid "ZIP file contains multiple file types" msgstr "ZIPファイルに複数のファイル形式が含まれています" @@ -16350,10 +15921,9 @@ msgstr "" msgid "`operation` property of post processing object undefined" msgstr "後処理オブジェクトの `operation` プロパティが未定義です" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs] -#, python-format, fuzzy +#, python-format msgid "`periods` must be between 1 and %(max)s" -msgstr "`periods`は1から%(max)sの間でなければなりません" +msgstr "`periods` は 1 から %(max)s の間である必要があります" msgid "`prophet` package not installed" msgstr "`prophet` パッケージがインストールされていません" @@ -16374,11 +15944,10 @@ msgid "`width` must be greater or equal to 0" msgstr "`width` は 0 以上である必要があります" msgid "`window` must be between 1 and 10000" -msgstr "" +msgstr "`window` は 1 から 10000 の間の値でなければなりません" -#, fuzzy msgid "a few seconds" -msgstr "5秒" +msgstr "数秒" msgid "aggregate" msgstr "集計" @@ -16408,7 +15977,7 @@ msgid "annotation_layer" msgstr "注釈レイヤー" msgid "apple" -msgstr "" +msgstr "リンゴ" msgid "asfreq" msgstr "アス周波数" @@ -16422,12 +15991,11 @@ msgstr "自動" msgid "background" msgstr "背景" -#, fuzzy msgid "background color" msgstr "背景色" msgid "banana" -msgstr "" +msgstr "バナナ" msgid "basis" msgstr "基準" @@ -16451,9 +16019,8 @@ msgstr "bfill" msgid "bolt" msgstr "ボルト" -#, fuzzy msgid "boolean" -msgstr "真偽値 (BOOLEAN)" +msgstr "真偽値" msgid "boolean type icon" msgstr "真偽値型アイコン" @@ -16473,9 +16040,8 @@ msgstr "空にできません" msgid "cardinal" msgstr "基底" -#, fuzzy msgid "cell bar" -msgstr "セルバーを表示" +msgstr "セルバー" msgid "change" msgstr "変化" @@ -16535,7 +16101,6 @@ msgstr "新しいチャートを作成" msgid "create dataset from SQL query" msgstr "SQL クエリからデータセットを作成" -#, fuzzy msgid "crontab" msgstr "crontab" @@ -16554,18 +16119,15 @@ msgstr "ダッシュボード" msgid "dashboards" msgstr "ダッシュボード" -#, fuzzy msgid "data connection" -msgstr "データベース接続" +msgstr "データ接続" -#, fuzzy msgid "data connections" -msgstr "データベース接続" +msgstr "データ接続" msgid "database" msgstr "データベース" -#, fuzzy msgid "databases" msgstr "データベース" @@ -16575,15 +16137,12 @@ msgstr "データセット" msgid "dataset name" msgstr "データセット名" -#, fuzzy msgid "datasets" msgstr "データセット" -#, fuzzy msgid "datasource" msgstr "データソース" -#, fuzzy msgid "datasources" msgstr "データソース" @@ -16695,9 +16254,6 @@ msgstr "例: world_population" msgid "e.g. xy12345.us-east-2.aws" msgstr "例: xy12345.us-east-2.aws" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "e.g., CI/CD Pipeline, Analytics Script" msgstr "例: CI/CDパイプライン、分析スクリプト" @@ -16713,9 +16269,8 @@ msgstr "メールの件名" msgid "ends with" msgstr "で終わる" -#, fuzzy msgid "entire row" -msgstr "行全体に対して" +msgstr "行全体" msgid "entries" msgstr "件" @@ -16750,18 +16305,12 @@ msgstr "毎月" msgid "expand" msgstr "展開" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "extra.dashboard must be an object" -msgstr "extra.dashboardはオブジェクトである必要があります" +msgstr "extra.dashboard はオブジェクトである必要があります" msgid "failed" msgstr "失敗" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "farewell" msgstr "さようなら" @@ -16774,14 +16323,12 @@ msgstr "ffill" msgid "flat" msgstr "フラット" -#, fuzzy msgid "for a list of available helpers." -msgstr "利用可能なフィルターはありません。" +msgstr "利用可能なヘルパーのリストについてはこちら。" msgid "for more information on how to structure your URI." msgstr "URI の構造化に関する詳細情報はこちら。" -#, fuzzy msgid "formatted" msgstr "整形済み" @@ -16793,7 +16340,7 @@ msgstr "GeoHash (正方形)" #, fuzzy msgid "greeting" -msgstr "ログ保持期間" +msgstr "挨拶" msgid "heatmap" msgstr "ヒートマップ" @@ -16801,9 +16348,6 @@ msgstr "ヒートマップ" msgid "heatmap: values are normalized across the entire heatmap" msgstr "ヒートマップ: ヒートマップ全体で値を正規化します" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "hello" msgstr "こんにちは" @@ -16816,16 +16360,12 @@ msgstr "時間" msgid "in" msgstr "次を含む" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "in order to run this operation." msgstr "この操作を実行するために。" msgid "invalid email" msgstr "無効なメールアドレス" -#, fuzzy msgid "is" msgstr "である" @@ -16842,9 +16382,8 @@ msgstr "は数値である必要があります" msgid "is expected to be an integer" msgstr "は整数である必要があります" -#, fuzzy msgid "is false" -msgstr "は偽 (False) です" +msgstr "偽 (False) である" #, python-format msgid "" @@ -16897,9 +16436,8 @@ msgstr "{min} {name} 未満" msgid "linear" msgstr "線形" -#, fuzzy msgid "locale_key" -msgstr "ローカル" +msgstr "ロケールキー" msgid "log" msgstr "対数" @@ -16924,7 +16462,6 @@ msgstr "メートル" msgid "metric" msgstr "メトリック" -#, fuzzy msgid "metric type icon" msgstr "メトリック型のアイコン" @@ -16953,43 +16490,30 @@ msgstr "値を設定してください" msgid "name" msgstr "名前" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "nativeFilters must be a list" -msgstr "nativeFiltersはリストである必要があります" +msgstr "nativeFilters はリストである必要があります" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "nativeFilters[%(idx)s] missing required keys: %(keys)s" -msgstr "nativeFilters[%(idx)s]に必須キーがありません: %(keys)s" +msgstr "nativeFilters[%(idx)s] に必須キーがありません: %(keys)s" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "nativeFilters[%(idx)s] must be an object" -msgstr "nativeFilters[%(idx)s]はオブジェクトである必要があります" +msgstr "nativeFilters[%(idx)s] はオブジェクトである必要があります" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "nativeFilters[%(idx)s].filterValues must be a list" -msgstr "nativeFilters[%(idx)s].filterValuesはリストである必要があります" +msgstr "nativeFilters[%(idx)s].filterValues はリストである必要があります" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "" "nativeFilters[%(idx)s].nativeFilterId '%(filter_id)s' does not exist on the " "dashboard" -msgstr "nativeFilters[%(idx)s].nativeFilterId '%(filter_id)s'はダッシュボードに存在しません" +msgstr "nativeFilters[%(idx)s].nativeFilterId '%(filter_id)s' はダッシュボード上に存在しません" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, python-format, fuzzy +#, python-format msgid "nativeFilters[%(idx)s].nativeFilterId must be a non-empty string" -msgstr "nativeFilters[%(idx)s].nativeFilterId は空でない文字列でなければなりません" +msgstr "nativeFilters[%(idx)s].nativeFilterId は空でない文字列である必要があります" msgid "no SQL validator is configured" msgstr "SQL バリデータが設定されていません" @@ -17001,9 +16525,8 @@ msgstr "%(engine_spec)s 用の SQL バリデータが設定されていません msgid "not containing" msgstr "を含まない" -#, fuzzy msgid "numeric" -msgstr "数値型" +msgstr "数値" msgid "numeric type icon" msgstr "数値型アイコン" @@ -17081,7 +16604,7 @@ msgstr "前年" #, fuzzy msgid "provide authorization" -msgstr "認証が必要です" +msgstr "認証を提供する" msgid "quarter" msgstr "四半期" @@ -17163,7 +16686,6 @@ msgstr "停止済み" msgid "stream" msgstr "ストリーム" -#, fuzzy msgid "string" msgstr "文字列" @@ -17185,36 +16707,29 @@ msgstr "構文。" msgid "tag" msgstr "タグ" -#, fuzzy msgid "task" -msgstr "タグ" +msgstr "タスク" msgid "temporal type icon" msgstr "時間型アイコン" -#, fuzzy msgid "text color" -msgstr "テキストの色へ" +msgstr "テキストの色" msgid "textarea" msgstr "テキストエリア" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "the Handlebars chart documentation" msgstr "Handlebars チャートのドキュメント" msgid "theme" msgstr "テーマ" -#, fuzzy msgid "timestamp" -msgstr "タイムスタンプを表示" +msgstr "タイムスタンプ" -#, fuzzy msgid "to" -msgstr "件表示" +msgstr "まで" msgid "top" msgstr "上" @@ -17272,9 +16787,8 @@ msgstr "土曜日締めの週" msgid "week starting Sunday" msgstr "日曜日始まりの週" -#, fuzzy msgid "working timeout" -msgstr "処理時間切れ" +msgstr "処理タイムアウト" msgid "x" msgstr "x" @@ -17300,8 +16814,5 @@ msgstr "ズームエリア" msgid "© Layer attribution" msgstr "© レイヤー帰属情報" -# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr, -# sr_Latn] -#, fuzzy msgid "№" msgstr "№" diff --git a/superset/versioning/changes/listener.py b/superset/versioning/changes/listener.py index 56854310640..d8875eae9f6 100644 --- a/superset/versioning/changes/listener.py +++ b/superset/versioning/changes/listener.py @@ -299,6 +299,17 @@ def _inject_action_meta_record( logger.exception("version_changes: malformed ACTION_META_KEY payload") +def _write_action_kind( + session: Session, tx_table: sa.Table, tx_id: int, action_kind: str +) -> None: + """Write action metadata through the transaction's existing connection.""" + session.connection().execute( + sa.update(tx_table) + .where(tx_table.c.id == tx_id) + .values(action_kind=action_kind) + ) + + def _stamp_action_kind_on_transaction(session: Session, tx_id: int) -> None: """Pop the per-tx action_kind from ``session.info`` and stamp it onto the ``version_transaction`` row identified by *tx_id*. @@ -312,10 +323,11 @@ def _stamp_action_kind_on_transaction(session: Session, tx_id: int) -> None: The action_kind is popped (not just read) so a long-lived session can't accidentally carry the value into the next transaction. A - failed stamp is logged and swallowed — action_kind is a - descriptive enrichment, not a correctness invariant; refusing to - write change records because an UPDATE on a single column failed - would punish the user save for an audit-log nicety. + failed stamp is rolled back to a SAVEPOINT, logged, and swallowed: + action_kind is descriptive enrichment, not a correctness invariant. + The SAVEPOINT is opened after the final flush, so a failed metadata + statement cannot poison the user transaction or disturb Continuum's + canonical shadows. """ # pylint: disable=import-outside-toplevel from sqlalchemy_continuum import versioning_manager @@ -325,11 +337,8 @@ def _stamp_action_kind_on_transaction(session: Session, tx_id: int) -> None: return tx_tbl = versioning_manager.transaction_cls.__table__ try: - session.connection().execute( - sa.update(tx_tbl) - .where(tx_tbl.c.id == tx_id) - .values(action_kind=action_kind) - ) + with session.connection().begin_nested(): + _write_action_kind(session, tx_tbl, tx_id, action_kind) except Exception: # pylint: disable=broad-except logger.exception( "version_changes: failed to stamp action_kind=%s on tx %s", diff --git a/tests/integration_tests/versioning/transaction_correctness_tests.py b/tests/integration_tests/versioning/transaction_correctness_tests.py index 9e55fae4c90..0cb3d9b1afa 100644 --- a/tests/integration_tests/versioning/transaction_correctness_tests.py +++ b/tests/integration_tests/versioning/transaction_correctness_tests.py @@ -16,6 +16,8 @@ # under the License. """Transaction-level correctness tests for entity version history.""" +from unittest.mock import patch + import pytest import sqlalchemy as sa @@ -23,6 +25,8 @@ from superset import db from superset.connectors.sqla.models import SqlaTable from superset.models.slice import Slice from superset.utils import json +from superset.versioning.changes import listener +from superset.versioning.changes.listener import ACTION_KIND_KEY from superset.versioning.changes.table import version_changes_table from tests.integration_tests.base_tests import SupersetTestCase from tests.integration_tests.fixtures.birth_names_dashboard import ( # noqa: F401 @@ -222,3 +226,83 @@ class TestVersionHistoryTransactionCorrectness(SupersetTestCase): def test_nested_rollback_preserves_outer_initial_state(self) -> None: """Rolling back a SAVEPOINT keeps the outer transaction registry.""" self._assert_nested_transaction_preserves_outer_initial_state("rollback") + + def test_action_kind_sql_failure_is_isolated_and_consumed(self) -> None: + """Failed descriptive metadata cannot poison or leak from the save.""" + chart = self._chart("Girls") + chart_id = chart.id + boundary = db.session.scalar( + sa.text("SELECT coalesce(max(id), 0) FROM version_transaction") + ) + + def fail_action_kind(*_args: object, **_kwargs: object) -> None: + db.session.connection().execute( + sa.text("UPDATE __missing_action_kind_target__ SET value = 1") + ) + + try: + db.session.info[ACTION_KIND_KEY] = "restore" + chart.slice_name = "Girls survives action metadata failure" + with patch.object(listener, "_write_action_kind", fail_action_kind): + db.session.commit() + + db.session.expire_all() + saved = db.session.get(Slice, chart_id) + assert saved is not None + assert saved.slice_name == "Girls survives action metadata failure" + assert ACTION_KIND_KEY not in db.session.info + + first_rows = db.session.execute( + sa.text( + "SELECT sv.transaction_id, vt.action_kind " + "FROM slices_version sv " + "JOIN version_transaction vt ON vt.id = sv.transaction_id " + "WHERE sv.id = :id AND sv.operation_type = 1 " + "AND sv.slice_name = :name " + "AND sv.transaction_id > :boundary" + ), + { + "id": chart_id, + "name": "Girls survives action metadata failure", + "boundary": boundary, + }, + ).all() + assert len(first_rows) == 1 + assert first_rows[0].action_kind is None + failed_metadata_tx_id = first_rows[0].transaction_id + + semantic_changes = _changes_for_chart( + chart_id, after_transaction_id=boundary + ) + name_changes = [ + row + for row in semantic_changes + if row["path"] == ["slice_name"] + and row["to_value"] == "Girls survives action metadata failure" + ] + assert len(name_changes) == 1 + assert name_changes[0]["transaction_id"] == failed_metadata_tx_id + + saved.slice_name = "Girls next transaction" + db.session.commit() + next_rows = db.session.execute( + sa.text( + "SELECT vt.action_kind FROM slices_version sv " + "JOIN version_transaction vt ON vt.id = sv.transaction_id " + "WHERE sv.id = :id AND sv.operation_type = 1 " + "AND sv.slice_name = :name " + "AND sv.transaction_id > :boundary" + ), + { + "id": chart_id, + "name": "Girls next transaction", + "boundary": failed_metadata_tx_id, + }, + ).all() + assert len(next_rows) == 1 + assert next_rows[0].action_kind is None + finally: + chart = db.session.get(Slice, chart_id) + assert chart is not None + chart.slice_name = "Girls" + db.session.commit() diff --git a/tests/unit_tests/commands/chart/update_test.py b/tests/unit_tests/commands/chart/update_test.py index f1e50a7e907..440e98cf91f 100644 --- a/tests/unit_tests/commands/chart/update_test.py +++ b/tests/unit_tests/commands/chart/update_test.py @@ -17,10 +17,11 @@ import pytest from pytest_mock import MockerFixture -from superset.commands.chart.exceptions import ChartForbiddenError +from superset.commands.chart.exceptions import ChartForbiddenError, ChartInvalidError from superset.commands.chart.update import UpdateChartCommand from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import SupersetSecurityException +from superset.utils import json def _editorship_exc() -> SupersetSecurityException: @@ -149,3 +150,91 @@ def test_update_chart_editor_can_perform_regular_update( exceptions = compute_subjects.call_args.args[2] assert properties["editors"] == [2] assert exceptions == [] + + +def _query_context_payload(datasource: object) -> dict[str, object]: + """Build a query-context-only update payload targeting ``datasource``.""" + return { + "query_context": json.dumps({"datasource": datasource, "queries": []}), + "query_context_generation": True, + } + + +@pytest.mark.parametrize( + "datasource_type", + [ + "table", + "query", # non-table datasource types must also be accepted when matching + ], +) +def test_update_chart_query_context_matching_datasource_is_allowed( + mocker: MockerFixture, + datasource_type: str, +) -> None: + """A query context that targets the chart's own datasource is accepted.""" + find_by_id = mocker.patch("superset.commands.chart.update.ChartDAO.find_by_id") + find_by_id.return_value = mocker.MagicMock( + id=1, + tags=[], + dashboards=[], + datasource_id=42, + datasource_type=datasource_type, + ) + mocker.patch("superset.commands.chart.update.security_manager.raise_for_editorship") + mocker.patch("superset.commands.chart.update.security_manager.raise_for_access") + + UpdateChartCommand( + 1, _query_context_payload({"id": 42, "type": datasource_type}) + ).validate() + + +@pytest.mark.parametrize( + "datasource", + [ + {"id": 99, "type": "table"}, # different id + {"id": 42, "type": "query"}, # different type + {"id": "99", "type": "table"}, # different id as string + {"id": 42}, # matching id but missing type + {"id": "5f7b3c1a-...-uuid", "type": "table"}, # non-numeric id + ], +) +def test_update_chart_query_context_mismatched_datasource_is_rejected( + mocker: MockerFixture, + datasource: dict[str, object], +) -> None: + """A query context pointing at a different datasource is rejected with a 4xx.""" + find_by_id = mocker.patch("superset.commands.chart.update.ChartDAO.find_by_id") + find_by_id.return_value = mocker.MagicMock( + id=1, tags=[], dashboards=[], datasource_id=42, datasource_type="table" + ) + mocker.patch("superset.commands.chart.update.security_manager.raise_for_editorship") + mocker.patch("superset.commands.chart.update.security_manager.raise_for_access") + + with pytest.raises(ChartInvalidError): + UpdateChartCommand(1, _query_context_payload(datasource)).validate() + + +@pytest.mark.parametrize( + "query_context", + [ + "{}", # no datasource key + '{"datasource": null}', # null datasource + "not-json", # unparseable payload + ], +) +def test_update_chart_query_context_without_datasource_is_allowed( + mocker: MockerFixture, + query_context: str, +) -> None: + """Payloads with no verifiable datasource fall back to the chart's own.""" + find_by_id = mocker.patch("superset.commands.chart.update.ChartDAO.find_by_id") + find_by_id.return_value = mocker.MagicMock( + id=1, tags=[], dashboards=[], datasource_id=42, datasource_type="table" + ) + mocker.patch("superset.commands.chart.update.security_manager.raise_for_editorship") + mocker.patch("superset.commands.chart.update.security_manager.raise_for_access") + + UpdateChartCommand( + 1, + {"query_context": query_context, "query_context_generation": True}, + ).validate() diff --git a/tests/unit_tests/commands/report/alert_test.py b/tests/unit_tests/commands/report/alert_test.py index 1043dd25008..eaffb1da758 100644 --- a/tests/unit_tests/commands/report/alert_test.py +++ b/tests/unit_tests/commands/report/alert_test.py @@ -23,7 +23,10 @@ import pytest from pytest_mock import MockerFixture from superset.commands.report.alert import AlertCommand -from superset.commands.report.exceptions import AlertValidatorConfigError +from superset.commands.report.exceptions import ( + AlertValidatorConfigError, + ReportScheduleExecutorNotFoundError, +) from superset.reports.models import ReportScheduleValidatorType, ReportState @@ -494,3 +497,38 @@ def test_not_null_validator_with_empty_result_does_not_trigger( assert triggered is False, "NOT_NULL with empty result should not trigger" assert command._result is None + + +def test_execute_query_raises_when_executor_user_missing( + mocker: MockerFixture, +) -> None: + """ + When the configured executor user cannot be resolved + (``security_manager.find_user`` returns ``None``), the alert-query path + raises a dedicated ``ReportScheduleExecutorNotFoundError`` naming the + username, rather than swallowing it into an opaque ``AlertQueryError`` (or + surfacing a NoneType/AttributeError from the downstream auth flow). + """ + mocker.patch( + "superset.commands.report.alert.jinja_context.get_template_processor", + ) + mocker.patch( + "superset.commands.report.alert.get_executor", + return_value=("executor", "ghost_user"), + ) + mocker.patch( + "superset.commands.report.alert.security_manager.find_user", + return_value=None, + ) + + report_schedule_mock = mocker.Mock() + report_schedule_mock.id = 1 + report_schedule_mock.sql = "SELECT value FROM metrics" + + command = AlertCommand( + report_schedule=report_schedule_mock, + execution_id=uuid4(), + ) + + with pytest.raises(ReportScheduleExecutorNotFoundError, match="ghost_user"): + command._execute_query() diff --git a/tests/unit_tests/db_engine_specs/test_databricks.py b/tests/unit_tests/db_engine_specs/test_databricks.py index f7cfaa5c663..e8fd643a229 100644 --- a/tests/unit_tests/db_engine_specs/test_databricks.py +++ b/tests/unit_tests/db_engine_specs/test_databricks.py @@ -235,6 +235,80 @@ def test_extract_errors_with_context() -> None: ] +@pytest.mark.parametrize( + "msg", + [ + # tag only + "[INSUFFICIENT_PERMISSIONS] Insufficient privileges: User does not " + "have USE CATALOG on Catalog 'platform_production'.", + # SQLSTATE only + "Insufficient privileges: User does not have USE CATALOG on Catalog " + "'platform_production'. SQLSTATE: 42501.", + # SQLSTATE with irregular spacing + "Insufficient privileges on Catalog 'platform_production'. SQLSTATE:42501", + # both, as seen in real Databricks responses + "[INSUFFICIENT_PERMISSIONS] Insufficient privileges: User does not " + "have USE CATALOG on Catalog 'platform_production'. SQLSTATE: 42501.", + ], +) +def test_extract_errors_insufficient_permissions(msg: str) -> None: + """ + Test that Databricks catalog/schema permission errors are classified as + a client-side, warning-level error instead of a generic server error, + regardless of which half of the pattern is present in the raw message. + """ + result = DatabricksNativeEngineSpec.extract_errors(Exception(msg)) + + assert result == [ + SupersetError( + message=msg, + error_type=SupersetErrorType.CONNECTION_DATABASE_PERMISSIONS_ERROR, + level=ErrorLevel.WARNING, + extra={ + "engine_name": "Databricks (legacy)", + "issue_codes": [ + { + "code": 1017, + "message": "Issue 1017 - User doesn't have the proper permissions.", # noqa: E501 + } + ], + }, + ) + ] + + +def test_extract_errors_insufficient_permissions_covers_odbc() -> None: + """ + The insufficient-permissions classification lives on the shared + ``DatabricksBaseEngineSpec`` so that the ODBC ("SQL Endpoint") connector + benefits too, not just the native/Python-connector engines. + """ + from superset.db_engine_specs.databricks import DatabricksODBCEngineSpec + + msg = ( + "[INSUFFICIENT_PERMISSIONS] Insufficient privileges: User does not " + "have USE CATALOG on Catalog 'platform_production'. SQLSTATE: 42501." + ) + result = DatabricksODBCEngineSpec.extract_errors(Exception(msg)) + + assert result == [ + SupersetError( + message=msg, + error_type=SupersetErrorType.CONNECTION_DATABASE_PERMISSIONS_ERROR, + level=ErrorLevel.WARNING, + extra={ + "engine_name": "Databricks SQL Endpoint", + "issue_codes": [ + { + "code": 1017, + "message": "Issue 1017 - User doesn't have the proper permissions.", # noqa: E501 + } + ], + }, + ) + ] + + @pytest.mark.parametrize( "target_type,expected_result", [ diff --git a/tests/unit_tests/mcp_service/chart/test_chart_schemas.py b/tests/unit_tests/mcp_service/chart/test_chart_schemas.py index d0c640c6b62..5290d44042f 100644 --- a/tests/unit_tests/mcp_service/chart/test_chart_schemas.py +++ b/tests/unit_tests/mcp_service/chart/test_chart_schemas.py @@ -28,10 +28,16 @@ from superset.mcp_service.chart.schemas import ( FilterConfig, GenerateChartRequest, GenerateChartResponse, + GetChartDataRequest, + GetChartInfoRequest, + GetChartPreviewRequest, + GetChartSqlRequest, + ListChartsRequest, MixedTimeseriesChartConfig, PieChartConfig, PivotTableChartConfig, TableChartConfig, + UpdateChartRequest, XYChartConfig, ) @@ -1137,3 +1143,64 @@ class TestSqlMetricLlmContextWrapping: assert "" in metric["label"] assert metric["expressionType"] == "SQL" assert "" not in metric["optionName"] + + +class TestRequestSchemaAliasChoices: + """Test that LLM-friendly field name variants are accepted on the + chart MCP tool request schemas, so callers sending 'id'/'chart_id' + instead of 'identifier' (or 'columns' instead of 'select_columns') + don't silently have the field dropped.""" + + def test_get_chart_info_identifier_id_alias(self) -> None: + req = GetChartInfoRequest.model_validate({"id": 42}) + assert req.identifier == 42 + + def test_get_chart_info_identifier_chart_id_alias(self) -> None: + req = GetChartInfoRequest.model_validate({"chart_id": 42}) + assert req.identifier == 42 + + def test_get_chart_info_identifier_still_works(self) -> None: + req = GetChartInfoRequest.model_validate({"identifier": 42}) + assert req.identifier == 42 + + def test_get_chart_info_select_columns_columns_alias(self) -> None: + req = GetChartInfoRequest.model_validate( + {"id": 42, "columns": ["id", "slice_name"]} + ) + assert req.select_columns == ["id", "slice_name"] + + def test_get_chart_data_identifier_id_alias(self) -> None: + req = GetChartDataRequest.model_validate({"id": 7}) + assert req.identifier == 7 + + def test_get_chart_data_identifier_chart_id_alias(self) -> None: + req = GetChartDataRequest.model_validate({"chart_id": 7}) + assert req.identifier == 7 + + def test_get_chart_preview_identifier_id_alias(self) -> None: + req = GetChartPreviewRequest.model_validate({"id": 7}) + assert req.identifier == 7 + + def test_get_chart_preview_identifier_chart_id_alias(self) -> None: + req = GetChartPreviewRequest.model_validate({"chart_id": 7}) + assert req.identifier == 7 + + def test_get_chart_sql_identifier_id_alias(self) -> None: + req = GetChartSqlRequest.model_validate({"id": 7}) + assert req.identifier == 7 + + def test_get_chart_sql_identifier_chart_id_alias(self) -> None: + req = GetChartSqlRequest.model_validate({"chart_id": 7}) + assert req.identifier == 7 + + def test_update_chart_identifier_id_alias(self) -> None: + req = UpdateChartRequest.model_validate({"id": 7}) + assert req.identifier == 7 + + def test_update_chart_identifier_chart_id_alias(self) -> None: + req = UpdateChartRequest.model_validate({"chart_id": 7}) + assert req.identifier == 7 + + def test_list_charts_select_columns_columns_alias(self) -> None: + req = ListChartsRequest.model_validate({"columns": ["id", "slice_name"]}) + assert req.select_columns == ["id", "slice_name"] diff --git a/tests/unit_tests/mcp_service/dashboard/test_dashboard_schemas.py b/tests/unit_tests/mcp_service/dashboard/test_dashboard_schemas.py index 79f2e254ac5..867e9bba690 100644 --- a/tests/unit_tests/mcp_service/dashboard/test_dashboard_schemas.py +++ b/tests/unit_tests/mcp_service/dashboard/test_dashboard_schemas.py @@ -31,11 +31,14 @@ from superset.mcp_service.dashboard.schemas import ( _extract_cross_filters_enabled, _extract_native_filters, _safe_user_label, + AddChartToDashboardRequest, dashboard_serializer, DashboardInfo, DuplicateDashboardRequest, DuplicateDashboardResponse, GenerateDashboardRequest, + GetDashboardInfoRequest, + ListDashboardsRequest, serialize_chart_summary, serialize_dashboard_object, UpdateDashboardRequest, @@ -931,3 +934,46 @@ class TestDuplicateDashboardResponse: """A null error stays null rather than being wrapped.""" resp = DuplicateDashboardResponse(dashboard_url="http://host/d/1/") assert resp.error is None + + +class TestRequestSchemaAliasChoices: + """Test that LLM-friendly field name variants are accepted on the + dashboard MCP tool request schemas, so callers sending 'id'/'dashboard_id' + instead of 'identifier' (or 'columns' instead of 'select_columns') + don't silently have the field dropped.""" + + def test_get_dashboard_info_identifier_id_alias(self) -> None: + req = GetDashboardInfoRequest.model_validate({"id": 42}) + assert req.identifier == 42 + + def test_get_dashboard_info_identifier_dashboard_id_alias(self) -> None: + req = GetDashboardInfoRequest.model_validate({"dashboard_id": 42}) + assert req.identifier == 42 + + def test_get_dashboard_info_identifier_still_works(self) -> None: + req = GetDashboardInfoRequest.model_validate({"identifier": 42}) + assert req.identifier == 42 + + def test_get_dashboard_info_select_columns_columns_alias(self) -> None: + req = GetDashboardInfoRequest.model_validate( + {"id": 42, "columns": ["id", "dashboard_title"]} + ) + assert req.select_columns == ["id", "dashboard_title"] + + def test_list_dashboards_select_columns_columns_alias(self) -> None: + req = ListDashboardsRequest.model_validate( + {"columns": ["id", "dashboard_title"]} + ) + assert req.select_columns == ["id", "dashboard_title"] + + def test_add_chart_to_dashboard_dashboard_alias(self) -> None: + req = AddChartToDashboardRequest.model_validate({"dashboard": 1, "chart_id": 2}) + assert req.dashboard_id == 1 + + def test_add_chart_to_dashboard_id_alias(self) -> None: + req = AddChartToDashboardRequest.model_validate({"id": 1, "chart_id": 2}) + assert req.dashboard_id == 1 + + def test_add_chart_to_dashboard_chart_alias(self) -> None: + req = AddChartToDashboardRequest.model_validate({"dashboard_id": 1, "chart": 2}) + assert req.chart_id == 2 diff --git a/tests/unit_tests/mcp_service/dataset/test_dataset_schemas.py b/tests/unit_tests/mcp_service/dataset/test_dataset_schemas.py new file mode 100644 index 00000000000..630d5298c9b --- /dev/null +++ b/tests/unit_tests/mcp_service/dataset/test_dataset_schemas.py @@ -0,0 +1,54 @@ +# 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. + +""" +Unit tests for MCP dataset schema validation. +""" + +from superset.mcp_service.dataset.schemas import ( + GetDatasetInfoRequest, + ListDatasetsRequest, +) + + +class TestRequestSchemaAliasChoices: + """Test that LLM-friendly field name variants are accepted on the + dataset MCP tool request schemas, so callers sending 'id'/'dataset_id' + instead of 'identifier' (or 'columns' instead of 'select_columns') + don't silently have the field dropped.""" + + def test_get_dataset_info_identifier_id_alias(self) -> None: + req = GetDatasetInfoRequest.model_validate({"id": 42}) + assert req.identifier == 42 + + def test_get_dataset_info_identifier_dataset_id_alias(self) -> None: + req = GetDatasetInfoRequest.model_validate({"dataset_id": 42}) + assert req.identifier == 42 + + def test_get_dataset_info_identifier_still_works(self) -> None: + req = GetDatasetInfoRequest.model_validate({"identifier": 42}) + assert req.identifier == 42 + + def test_get_dataset_info_select_columns_columns_alias(self) -> None: + req = GetDatasetInfoRequest.model_validate( + {"id": 42, "columns": ["id", "table_name"]} + ) + assert req.select_columns == ["id", "table_name"] + + def test_list_datasets_select_columns_columns_alias(self) -> None: + req = ListDatasetsRequest.model_validate({"columns": ["id", "table_name"]}) + assert req.select_columns == ["id", "table_name"] diff --git a/tests/unit_tests/migrations/test_strip_metricsqlexpressions_from_ag_grid_params.py b/tests/unit_tests/migrations/test_strip_metricsqlexpressions_from_ag_grid_params.py new file mode 100644 index 00000000000..555d8cd3340 --- /dev/null +++ b/tests/unit_tests/migrations/test_strip_metricsqlexpressions_from_ag_grid_params.py @@ -0,0 +1,203 @@ +# 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. +"""Tests for migration ``d24e6b0a9c7f_strip_metricsqlexpressions_from_ag_grid_params``. + +Covers the two helper functions (_strip_params, _strip_query_context) and the +full upgrade() path that seeds an ag_grid_table slice with metricSqlExpressions +in both params and query_context, then asserts both fields are stripped. +""" + +from __future__ import annotations + +from importlib import import_module +from unittest.mock import patch + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from superset.utils import json + +migration = import_module( + "superset.migrations.versions." + "2026-06-30_00-00_d24e6b0a9c7f_strip_metricsqlexpressions_from_ag_grid_params" +) + +Slice = migration.Slice +_strip_params = migration._strip_params +_strip_query_context = migration._strip_query_context +_FIELD = migration._FIELD +_VIZ_TYPE = migration._VIZ_TYPE + +_SQL_EXPR = {"col1": "SELECT col1 FROM t", "col2": "SUM(revenue)"} + + +def _contaminated_params() -> str: + return json.dumps( + { + "viz_type": _VIZ_TYPE, + "extra_form_data": { + _FIELD: _SQL_EXPR, + "filters": [{"col": "col1", "op": "==", "val": "a"}], + }, + } + ) + + +def _contaminated_query_context() -> str: + return json.dumps( + { + "form_data": { + "extra_form_data": { + _FIELD: _SQL_EXPR, + }, + }, + "queries": [], + } + ) + + +@pytest.fixture +def engine(): + engine = create_engine("sqlite:///:memory:") + migration.Base.metadata.create_all(engine) + return engine + + +# --------------------------------------------------------------------------- +# Helper-function unit tests +# --------------------------------------------------------------------------- + + +def test_strip_params_removes_field() -> None: + slc = Slice(params=_contaminated_params()) + changed = _strip_params(slc) + assert changed + result = json.loads(slc.params) + assert _FIELD not in result["extra_form_data"] + assert result["extra_form_data"]["filters"] == [ + {"col": "col1", "op": "==", "val": "a"} + ] + + +def test_strip_params_noop_when_field_absent() -> None: + params = json.dumps({"extra_form_data": {"filters": []}}) + slc = Slice(params=params) + changed = _strip_params(slc) + assert not changed + assert json.loads(slc.params) == {"extra_form_data": {"filters": []}} + + +def test_strip_params_noop_when_params_empty() -> None: + assert not _strip_params(Slice(params=None)) + + +def test_strip_params_noop_on_invalid_json() -> None: + slc = Slice(params="not-json") + changed = _strip_params(slc) + assert not changed + assert slc.params == "not-json" + + +def test_strip_query_context_removes_field() -> None: + slc = Slice(query_context=_contaminated_query_context()) + changed = _strip_query_context(slc) + assert changed + result = json.loads(slc.query_context) + assert _FIELD not in result["form_data"]["extra_form_data"] + + +def test_strip_query_context_noop_when_field_absent() -> None: + qc = json.dumps({"form_data": {"extra_form_data": {}}}) + slc = Slice(query_context=qc) + assert not _strip_query_context(slc) + + +def test_strip_query_context_noop_when_query_context_empty() -> None: + assert not _strip_query_context(Slice(query_context=None)) + + +def test_strip_query_context_noop_on_invalid_json() -> None: + slc = Slice(query_context="not-json") + changed = _strip_query_context(slc) + assert not changed + assert slc.query_context == "not-json" + + +# --------------------------------------------------------------------------- +# Full upgrade() integration test +# --------------------------------------------------------------------------- + + +def test_upgrade_strips_both_fields(engine) -> None: + """upgrade() must strip _FIELD from params and query_context for ag_grid_table + slices, and must not touch slices of other viz types.""" + with Session(engine) as seed: + seed.add_all( + [ + Slice( + id=1, + viz_type=_VIZ_TYPE, + params=_contaminated_params(), + query_context=_contaminated_query_context(), + ), + # Different viz type — must be left unchanged. + Slice( + id=2, + viz_type="table", + params=_contaminated_params(), + query_context=_contaminated_query_context(), + ), + ] + ) + seed.commit() + + # Simulate the Alembic transaction: bind the migration session to an explicit + # connection opened via engine.begin() so the transaction auto-commits on + # context exit. The session.flush() inside upgrade() must emit the SQL + # UPDATEs before the connection is committed; without it the outer commit + # has nothing to write and the data is left unchanged. + with engine.begin() as conn: + upgrade_session = Session(bind=conn) + with ( + patch.object(migration, "op") as mock_op, + patch.object(migration, "db") as mock_db, + ): + mock_op.get_bind.return_value = conn + mock_db.Session.return_value = upgrade_session + migration.upgrade() + + with Session(engine) as verify: + slc1 = verify.get(Slice, 1) + params1 = json.loads(slc1.params) + assert _FIELD not in params1["extra_form_data"], ( + "upgrade() must strip _FIELD from params.extra_form_data" + ) + assert params1["extra_form_data"]["filters"] == [ + {"col": "col1", "op": "==", "val": "a"} + ] + + qc1 = json.loads(slc1.query_context) + assert _FIELD not in qc1["form_data"]["extra_form_data"], ( + "upgrade() must strip _FIELD from query_context.form_data.extra_form_data" + ) + + slc2 = verify.get(Slice, 2) + params2 = json.loads(slc2.params) + assert _FIELD in params2["extra_form_data"], ( + "upgrade() must not touch non-ag_grid_table slices" + ) diff --git a/tests/unit_tests/sqllab/test_sql_json_executer.py b/tests/unit_tests/sqllab/test_sql_json_executer.py new file mode 100644 index 00000000000..72275ce24f1 --- /dev/null +++ b/tests/unit_tests/sqllab/test_sql_json_executer.py @@ -0,0 +1,150 @@ +# 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 typing import Any +from unittest.mock import MagicMock + +import pytest + +from superset.exceptions import SupersetErrorsException +from superset.sqllab.sql_json_executer import SynchronousSqlJsonExecutor +from superset.utils.core import QueryStatus + + +def _make_executor(data: dict[str, Any]) -> SynchronousSqlJsonExecutor: + query_dao = MagicMock() + get_sql_results_task = MagicMock(return_value=data) + return SynchronousSqlJsonExecutor( + query_dao, + get_sql_results_task, + timeout_duration_in_seconds=60, + sqllab_backend_persistence_feature_enable=False, + ) + + +def _make_execution_context() -> MagicMock: + execution_context = MagicMock() + execution_context.query.id = 1 + execution_context.expand_data = False + execution_context.select_as_cta = False + return execution_context + + +def test_execute_raises_400_when_all_errors_are_warnings() -> None: + """ + All-WARNING rich errors (e.g. a Databricks insufficient-permissions + error) should surface as a 4xx client error, not a 500. + """ + data = { + "status": QueryStatus.FAILED, + "errors": [ + { + "message": "Insufficient privileges on Catalog 'foo'.", + "error_type": "CONNECTION_DATABASE_PERMISSIONS_ERROR", + "level": "warning", + "extra": {}, + } + ], + } + executor = _make_executor(data) + + with pytest.raises(SupersetErrorsException) as excinfo: + executor.execute(_make_execution_context(), "SELECT 1", None) + + assert excinfo.value.status == 400 + + +def test_execute_raises_400_when_errors_are_warning_and_info() -> None: + """ + A mix of WARNING and INFO errors (no ERROR-level entries) should still + surface as a 4xx, since nothing in the list indicates a server fault. + """ + data = { + "status": QueryStatus.FAILED, + "errors": [ + { + "message": "Insufficient privileges on Catalog 'foo'.", + "error_type": "CONNECTION_DATABASE_PERMISSIONS_ERROR", + "level": "warning", + "extra": {}, + }, + { + "message": "For your information.", + "error_type": "GENERIC_DB_ENGINE_ERROR", + "level": "info", + "extra": {}, + }, + ], + } + executor = _make_executor(data) + + with pytest.raises(SupersetErrorsException) as excinfo: + executor.execute(_make_execution_context(), "SELECT 1", None) + + assert excinfo.value.status == 400 + + +def test_execute_raises_500_when_any_error_is_error_level() -> None: + """ + Existing behavior is preserved: a single ERROR-level rich error still + surfaces as a 500. + """ + data = { + "status": QueryStatus.FAILED, + "errors": [ + { + "message": "Something went wrong.", + "error_type": "GENERIC_DB_ENGINE_ERROR", + "level": "error", + "extra": {}, + }, + ], + } + executor = _make_executor(data) + + with pytest.raises(SupersetErrorsException) as excinfo: + executor.execute(_make_execution_context(), "SELECT 1", None) + + assert excinfo.value.status == 500 + + +def test_execute_raises_500_when_errors_are_mixed_warning_and_error() -> None: + """ + A mix of WARNING and ERROR still surfaces as a 500: any ERROR wins. + """ + data = { + "status": QueryStatus.FAILED, + "errors": [ + { + "message": "Insufficient privileges on Catalog 'foo'.", + "error_type": "CONNECTION_DATABASE_PERMISSIONS_ERROR", + "level": "warning", + "extra": {}, + }, + { + "message": "Something went wrong.", + "error_type": "GENERIC_DB_ENGINE_ERROR", + "level": "error", + "extra": {}, + }, + ], + } + executor = _make_executor(data) + + with pytest.raises(SupersetErrorsException) as excinfo: + executor.execute(_make_execution_context(), "SELECT 1", None) + + assert excinfo.value.status == 500