= ({
>
)}
+ {isScreenshot && (
+
+
+ {TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_TEXT}
+
+
+
+
+
+ )}
{(isReport || contentType === 'dashboard') && (
1 to enable retries.
ALERT_REPORTS_QUERY_EXECUTION_MAX_TRIES = 1
+# Custom width for screenshots
+ALERT_REPORTS_MIN_CUSTOM_SCREENSHOT_WIDTH = 600
+ALERT_REPORTS_MAX_CUSTOM_SCREENSHOT_WIDTH = 2400
# A custom prefix to use on all Alerts & Reports emails
EMAIL_REPORTS_SUBJECT_PREFIX = "[Report] "
diff --git a/superset/migrations/versions/2023-06-27_16-54_8e5b0fb85b9a_add_custom_size_columns_to_report.py b/superset/migrations/versions/2023-06-27_16-54_8e5b0fb85b9a_add_custom_size_columns_to_report.py
new file mode 100644
index 0000000000..16b46254d4
--- /dev/null
+++ b/superset/migrations/versions/2023-06-27_16-54_8e5b0fb85b9a_add_custom_size_columns_to_report.py
@@ -0,0 +1,46 @@
+# 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.
+"""Add custom size columns to report schedule
+
+Revision ID: 8e5b0fb85b9a
+Revises: 6fbe660cac39
+Create Date: 2023-06-27 16:54:57.161475
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "8e5b0fb85b9a"
+down_revision = "6fbe660cac39"
+
+
+def upgrade():
+ op.add_column(
+ "report_schedule",
+ sa.Column("custom_width", sa.Integer(), nullable=True),
+ )
+ op.add_column(
+ "report_schedule",
+ sa.Column("custom_height", sa.Integer(), nullable=True),
+ )
+
+
+def downgrade():
+ op.drop_column("report_schedule", "custom_width")
+ op.drop_column("report_schedule", "custom_height")
diff --git a/superset/reports/api.py b/superset/reports/api.py
index 125a3e6763..3686ab74bd 100644
--- a/superset/reports/api.py
+++ b/superset/reports/api.py
@@ -93,6 +93,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
"context_markdown",
"creation_method",
"crontab",
+ "custom_width",
"dashboard.dashboard_title",
"dashboard.id",
"database.database_name",
@@ -159,6 +160,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
"context_markdown",
"creation_method",
"crontab",
+ "custom_width",
"dashboard",
"database",
"description",
diff --git a/superset/reports/models.py b/superset/reports/models.py
index 24d4657b7d..2cbcbe0daa 100644
--- a/superset/reports/models.py
+++ b/superset/reports/models.py
@@ -154,6 +154,9 @@ class ReportSchedule(Model, AuditMixinNullable, ExtraJSONMixin):
# (Reports) When generating a screenshot, bypass the cache?
force_screenshot = Column(Boolean, default=False)
+ custom_width = Column(Integer, nullable=True)
+ custom_height = Column(Integer, nullable=True)
+
extra: ReportScheduleExtra # type: ignore
def __repr__(self) -> str:
diff --git a/superset/reports/schemas.py b/superset/reports/schemas.py
index fbe681be36..7bdbf34f12 100644
--- a/superset/reports/schemas.py
+++ b/superset/reports/schemas.py
@@ -17,8 +17,9 @@
from typing import Any, Union
from croniter import croniter
+from flask import current_app
from flask_babel import gettext as _
-from marshmallow import fields, Schema, validate, validates_schema
+from marshmallow import fields, Schema, validate, validates, validates_schema
from marshmallow.validate import Length, Range, ValidationError
from pytz import all_timezones
@@ -208,10 +209,34 @@ class ReportSchedulePostSchema(Schema):
dump_default=None,
)
force_screenshot = fields.Boolean(dump_default=False)
+ custom_width = fields.Integer(
+ metadata={
+ "description": _("Custom width of the screenshot in pixels"),
+ "example": 1000,
+ },
+ allow_none=True,
+ required=False,
+ default=None,
+ )
+
+ @validates("custom_width")
+ def validate_custom_width(self, value: int) -> None: # pylint: disable=no-self-use
+ min_width = current_app.config["ALERT_REPORTS_MIN_CUSTOM_SCREENSHOT_WIDTH"]
+ max_width = current_app.config["ALERT_REPORTS_MAX_CUSTOM_SCREENSHOT_WIDTH"]
+ if not min_width <= value <= max_width:
+ raise ValidationError(
+ _(
+ "Screenshot width must be between %(min)spx and %(max)spx",
+ min=min_width,
+ max=max_width,
+ )
+ )
@validates_schema
def validate_report_references( # pylint: disable=unused-argument,no-self-use
- self, data: dict[str, Any], **kwargs: Any
+ self,
+ data: dict[str, Any],
+ **kwargs: Any,
) -> None:
if data["type"] == ReportScheduleType.REPORT:
if "database" in data:
@@ -307,3 +332,26 @@ class ReportSchedulePutSchema(Schema):
)
extra = fields.Dict(dump_default=None)
force_screenshot = fields.Boolean(dump_default=False)
+
+ custom_width = fields.Integer(
+ metadata={
+ "description": _("Custom width of the screenshot in pixels"),
+ "example": 1000,
+ },
+ allow_none=True,
+ required=False,
+ default=None,
+ )
+
+ @validates("custom_width")
+ def validate_custom_width(self, value: int) -> None: # pylint: disable=no-self-use
+ min_width = current_app.config["ALERT_REPORTS_MIN_CUSTOM_SCREENSHOT_WIDTH"]
+ max_width = current_app.config["ALERT_REPORTS_MAX_CUSTOM_SCREENSHOT_WIDTH"]
+ if not min_width <= value <= max_width:
+ raise ValidationError(
+ _(
+ "Screenshot width must be between %(min)spx and %(max)spx",
+ min=min_width,
+ max=max_width,
+ )
+ )
diff --git a/superset/utils/screenshots.py b/superset/utils/screenshots.py
index 5c699e9e19..2743f85195 100644
--- a/superset/utils/screenshots.py
+++ b/superset/utils/screenshots.py
@@ -33,6 +33,12 @@ from superset.utils.webdriver import (
logger = logging.getLogger(__name__)
+DEFAULT_SCREENSHOT_WINDOW_SIZE = 800, 600
+DEFAULT_SCREENSHOT_THUMBNAIL_SIZE = 400, 300
+DEFAULT_CHART_WINDOW_SIZE = DEFAULT_CHART_THUMBNAIL_SIZE = 800, 600
+DEFAULT_DASHBOARD_WINDOW_SIZE = 1600, 1200
+DEFAULT_DASHBOARD_THUMBNAIL_SIZE = 800, 600
+
try:
from PIL import Image
except ModuleNotFoundError:
@@ -47,8 +53,8 @@ class BaseScreenshot:
driver_type = current_app.config["WEBDRIVER_TYPE"]
thumbnail_type: str = ""
element: str = ""
- window_size: WindowSize = (800, 600)
- thumb_size: WindowSize = (400, 300)
+ window_size: WindowSize = DEFAULT_SCREENSHOT_WINDOW_SIZE
+ thumb_size: WindowSize = DEFAULT_SCREENSHOT_THUMBNAIL_SIZE
def __init__(self, url: str, digest: str):
self.digest: str = digest
@@ -216,8 +222,8 @@ class ChartScreenshot(BaseScreenshot):
standalone=ChartStandaloneMode.HIDE_NAV.value,
)
super().__init__(url, digest)
- self.window_size = window_size or (800, 600)
- self.thumb_size = thumb_size or (800, 600)
+ self.window_size = window_size or DEFAULT_CHART_WINDOW_SIZE
+ self.thumb_size = thumb_size or DEFAULT_CHART_THUMBNAIL_SIZE
class DashboardScreenshot(BaseScreenshot):
@@ -239,5 +245,5 @@ class DashboardScreenshot(BaseScreenshot):
)
super().__init__(url, digest)
- self.window_size = window_size or (1600, 1200)
- self.thumb_size = thumb_size or (800, 600)
+ self.window_size = window_size or DEFAULT_DASHBOARD_WINDOW_SIZE
+ self.thumb_size = thumb_size or DEFAULT_DASHBOARD_THUMBNAIL_SIZE