Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand All @@ -78,6 +78,6 @@ jobs:
# queries: security-extended,security-and-quality

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
category: "/language:${{matrix.language}}"
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"@typescript-eslint/parser": "^8.67.0",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.10.0",
"globals": "^17.11.0",
"oxfmt": "^0.63.0",
"typescript": "~6.0.3",
"typescript-eslint": "^8.67.0",
Expand Down
8 changes: 4 additions & 4 deletions docs/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9174,10 +9174,10 @@ globals@^14.0.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==

globals@^17.10.0:
version "17.10.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.10.0.tgz#f9dbd847ae99e236f98b13095e2426ac3b25a45c"
integrity sha512-V0kztuWST2k8A/VbxAY8+L+7+Rgo3fyA24IHRLrZp7HOzJjV0gHSaZUjK9lpP/IrBSNite2tZ1prhRkinRu1CA==
globals@^17.11.0:
version "17.11.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.11.0.tgz#d643485bb30220d7751e511cf4f68c73d3870d87"
integrity sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==

globalthis@^1.0.4:
version "1.0.4"
Expand Down
21 changes: 6 additions & 15 deletions superset-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion superset-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
"geostyler-openlayers-parser": "^5.7.1",
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^11.0.1",
"google-auth-library": "^11.0.2",
"immer": "^11.1.16",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
Expand Down
8 changes: 4 additions & 4 deletions superset-websocket/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion superset-websocket/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@typescript-eslint/parser": "^8.67.0",
"eslint": "^10.8.1",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.10.0",
"globals": "^17.11.0",
"oxfmt": "^0.63.0",
"tscw-config": "^1.1.2",
"typescript": "^6.0.3",
Expand Down
68 changes: 60 additions & 8 deletions superset/utils/screenshot_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,16 +159,49 @@ class TiledScreenshotBudgetExceededError(ScreenshotTaskBudgetExceededError):
f"{ALERT_SELECTOR}, {EMPTY_SELECTOR}, {MISSING_CHART_SELECTOR}"
)

# Shared body for holder readiness and timeout diagnostics. A holder is ready
# only after a terminal marker appears and its loading marker disappears.
UNREADY_CHART_HOLDERS_JS_BODY = f"""
const holders = document.querySelectorAll('{CHART_HOLDER_SELECTOR}');
const unready = [];
for (const holder of holders) {{
# Runtime contract with the dashboard frontend. Dispatching this window event
# forces every DashboardVirtualization row to render regardless of whether it
# intersects the headless viewport, mirroring the client-side "Download as
# Image/PDF" path (see FORCE_IN_VIEW_EVENT in
# superset-frontend/src/dashboard/constants.ts and forceLoadAllCharts in
# superset-frontend/src/utils/downloadUtils.ts). The non-tiled report capture
# takes a single full-page screenshot that includes below-the-fold holders, so
# those holders must be forced to render before the readiness wait -- otherwise
# a virtualized (or still-loading) off-screen holder is captured blank. A plain
# Event with no `detail.rowIds` means "force every row", matching the frontend's
# single-pass branch.
FORCE_ALL_CHART_HOLDERS_IN_VIEW_EVENT = "superset-force-all-in-view"
FORCE_ALL_CHART_HOLDERS_IN_VIEW_JS = (
f"() => window.dispatchEvent(new Event('{FORCE_ALL_CHART_HOLDERS_IN_VIEW_EVENT}'))"
)


def _unready_chart_holders_js_body(*, viewport_only: bool) -> str:
"""Return the shared holder-readiness scan body.

A holder is ready only after a terminal marker appears and its loading
marker disappears. When ``viewport_only`` is True the scan skips holders
that do not intersect the current viewport: correct for the tiled path,
which scrolls every region into view before capturing it, and for
thumbnails, which only ever capture the viewport. The non-tiled *report*
capture takes a single full-page screenshot that includes below-the-fold
holders, so it must scan every mounted holder (``viewport_only=False``) --
otherwise an off-screen holder that never rendered is captured blank and
silently delivered as a Success.
"""
viewport_skip = (
"""
const r = holder.getBoundingClientRect();
if (!(r.top < window.innerHeight && r.bottom > 0)) {{
if (!(r.top < window.innerHeight && r.bottom > 0)) {
continue;
}}
}"""
if viewport_only
else ""
)
return f"""
const holders = document.querySelectorAll('{CHART_HOLDER_SELECTOR}');
const unready = [];
for (const holder of holders) {{{viewport_skip}
const hasSliceContainer = holder.querySelector(
'{SLICE_CONTAINER_SELECTOR}'
) !== null;
Expand Down Expand Up @@ -206,6 +239,13 @@ class TiledScreenshotBudgetExceededError(ScreenshotTaskBudgetExceededError):
}}
"""


# Viewport-scoped scan (tiled path + thumbnails).
UNREADY_CHART_HOLDERS_JS_BODY = _unready_chart_holders_js_body(viewport_only=True)
# Full-dashboard scan (non-tiled report capture, which screenshots the whole
# element in one shot and therefore cannot ignore below-the-fold holders).
UNREADY_ALL_CHART_HOLDERS_JS_BODY = _unready_chart_holders_js_body(viewport_only=False)

# Diagnostic query for every chart holder, including terminal and virtualized
# states. It interpolates the same selector constants as the predicates.
FIND_CHART_HOLDER_STATES_JS = f"""
Expand Down Expand Up @@ -256,12 +296,24 @@ class TiledScreenshotBudgetExceededError(ScreenshotTaskBudgetExceededError):
f"() => {{ {UNREADY_CHART_HOLDERS_JS_BODY} "
"return holders.length > 0 && unready.length === 0; }"
)
# Report readiness for the non-tiled full-page capture: every mounted holder --
# including below-the-fold ones -- must be terminally rendered. Off-screen
# holders are forced to render first (FORCE_ALL_CHART_HOLDERS_IN_VIEW_JS); if any
# still fails to render within budget the wait times out and the report fails
# loudly rather than shipping a blank/partial screenshot as a Success.
REPORT_ALL_CHART_HOLDERS_READY_JS = (
f"() => {{ {UNREADY_ALL_CHART_HOLDERS_JS_BODY} "
"return holders.length > 0 && unready.length === 0; }"
)
CHART_HOLDERS_MOUNTED_JS = (
f"() => document.querySelectorAll('{CHART_HOLDER_SELECTOR}').length > 0"
)
FIND_UNREADY_CHART_HOLDERS_JS = (
f"() => {{ {UNREADY_CHART_HOLDERS_JS_BODY} return unready; }}"
)
FIND_ALL_UNREADY_CHART_HOLDERS_JS = (
f"() => {{ {UNREADY_ALL_CHART_HOLDERS_JS_BODY} return unready; }}"
)

# A chart capture has one target rather than dashboard holders, but needs the
# same positive terminal-state guarantee and loading exclusion.
Expand Down
48 changes: 43 additions & 5 deletions superset/utils/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
CHART_CONTAINER_READY_JS,
CHART_CONTAINER_STATE_JS,
CHART_HOLDERS_READY_JS,
FIND_ALL_UNREADY_CHART_HOLDERS_JS,
FIND_CHART_HOLDER_STATES_JS,
REPORT_CHART_HOLDERS_READY_JS,
FORCE_ALL_CHART_HOLDERS_IN_VIEW_JS,
REPORT_ALL_CHART_HOLDERS_READY_JS,
resolve_screenshot_task_budget_seconds,
ScreenshotTaskBudgetExceededError,
take_tiled_screenshot,
Expand Down Expand Up @@ -382,7 +384,19 @@ def _wait_for_charts_ready( # noqa: C901
if element_name == "chart-container":
readiness_predicate = CHART_CONTAINER_READY_JS
elif report_execution_context:
readiness_predicate = REPORT_CHART_HOLDERS_READY_JS
# This non-tiled path captures the whole element in one shot
# (`_get_screenshot` uses `full_page=True` / `element.screenshot()`),
# so below-the-fold holders end up in the image. Force every
# virtualized row to render up front -- mirroring the client-side
# "Download as Image/PDF" path -- and then require *all* mounted
# holders (not just the viewport-visible ones) to reach a terminal
# state. If an off-screen holder never renders, the wait times out
# and the report fails loudly instead of silently delivering a
# blank/partial screenshot as a Success. The tiled path keeps the
# viewport-scoped predicate because it scrolls each region into view
# before capturing it.
page.evaluate(FORCE_ALL_CHART_HOLDERS_IN_VIEW_JS)
readiness_predicate = REPORT_ALL_CHART_HOLDERS_READY_JS
else:
# Preserve the thumbnail behavior introduced by #42253. The
# stricter zero-holder gate is report-specific because an empty
Expand Down Expand Up @@ -430,6 +444,16 @@ def _wait_for_charts_ready( # noqa: C901
ready_holders = sum(
holder.get("state") in ready_states for holder in chart_holder_states
)
# `FIND_CHART_HOLDER_STATES_JS` short-circuits off-screen holders to
# "virtualized" (counted as ready above), so on the report path -- a
# full-page capture that includes below-the-fold holders -- the real
# culprits (off-screen holders that never rendered) would be hidden.
# Surface them explicitly using the non-viewport-scoped scan.
below_fold_unready = (
page.evaluate(FIND_ALL_UNREADY_CHART_HOLDERS_JS)
if report_execution_context
else unready_chart_holders
)
deadline_elapsed = deadline.elapsed_seconds if deadline else elapsed
deadline_remaining = (
deadline.remaining_seconds if deadline else remaining_budget
Expand All @@ -438,7 +462,8 @@ def _wait_for_charts_ready( # noqa: C901
"report_readiness_terminal url=%s expected_holders=%s "
"mounted_holders=%s ready_holders=%s elapsed_seconds=%.2f "
"remaining_seconds=%s effective_wait_seconds=%.2f%s "
"terminal_reason=readiness_timeout unready_holders=%s states=%s; "
"terminal_reason=readiness_timeout unready_holders=%s "
"all_unready_holders=%s states=%s; "
"aborting before capture or delivery",
url,
expected_holders,
Expand All @@ -453,6 +478,7 @@ def _wait_for_charts_ready( # noqa: C901
effective_load_wait,
context_suffix,
unready_chart_holders,
below_fold_unready,
chart_holder_states,
)
raise
Expand Down Expand Up @@ -747,9 +773,21 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n
context_suffix,
)

# Use tiled screenshots for large dashboards
# Use tiled screenshots for large dashboards. For scheduled
# reports a likely-large dashboard whose measured height is
# at or below a single tile is almost always mid-layout
# (charts still virtualized/collapsed at measurement time),
# not genuinely short -- a 52-chart dashboard is never really
# <one viewport tall. Routing it to the single-shot,
# full-page non-tiled capture risks shipping a windowed
# partial render. Prefer the tiled path, which scrolls every
# region into view and waits per tile; worst case it is a
# single tile. The tiled decision for thumbnails is
# unchanged.
use_tiled = likely_large_dashboard and (
height_unknown or dashboard_height > tile_height
height_unknown
or dashboard_height > tile_height
or report_execution_context is not None
)

if use_tiled:
Expand Down
Loading
Loading