You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2023/06/29 21:33:06 UTC
[superset] branch master updated: feat: customize screenshot width for alerts/reports (#24547)
This is an automated email from the ASF dual-hosted git repository.
beto pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new be9eb0f3a3 feat: customize screenshot width for alerts/reports (#24547)
be9eb0f3a3 is described below
commit be9eb0f3a3c2d33ab6a1794ff36a4ee3f6b3a28b
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Thu Jun 29 14:32:58 2023 -0700
feat: customize screenshot width for alerts/reports (#24547)
---
.../src/components/ReportModal/index.tsx | 28 ++++++++++++
.../src/components/ReportModal/styles.tsx | 4 ++
.../src/features/alerts/AlertReportModal.tsx | 47 ++++++++++++++++---
superset-frontend/src/features/alerts/types.ts | 3 +-
superset-frontend/src/reports/types.ts | 1 +
superset/charts/api.py | 4 +-
superset/config.py | 3 ++
...5b0fb85b9a_add_custom_size_columns_to_report.py | 46 +++++++++++++++++++
superset/reports/api.py | 2 +
superset/reports/models.py | 3 ++
superset/reports/schemas.py | 52 +++++++++++++++++++++-
superset/utils/screenshots.py | 18 +++++---
12 files changed, 193 insertions(+), 18 deletions(-)
diff --git a/superset-frontend/src/components/ReportModal/index.tsx b/superset-frontend/src/components/ReportModal/index.tsx
index 5bf4c2c4d2..f98cddd66c 100644
--- a/superset-frontend/src/components/ReportModal/index.tsx
+++ b/superset-frontend/src/components/ReportModal/index.tsx
@@ -33,6 +33,7 @@ import LabeledErrorBoundInput from 'src/components/Form/LabeledErrorBoundInput';
import Icons from 'src/components/Icons';
import { CronError } from 'src/components/CronPicker';
import { RadioChangeEvent } from 'src/components';
+import { Input } from 'src/components/Input';
import withToasts from 'src/components/MessageToasts/withToasts';
import { ChartState } from 'src/explore/types';
import {
@@ -41,9 +42,14 @@ import {
NOTIFICATION_FORMATS,
} from 'src/reports/types';
import { reportSelector } from 'src/views/CRUD/hooks';
+import {
+ TRANSLATIONS,
+ StyledInputContainer,
+} from 'src/features/alerts/AlertReportModal';
import { CreationMethod } from './HeaderReportDropdown';
import {
antDErrorAlertStyles,
+ CustomWidthHeaderStyle,
StyledModal,
StyledTopSection,
StyledBottomSection,
@@ -170,6 +176,7 @@ function ReportModal({
type: 'Report',
active: true,
force_screenshot: false,
+ custom_width: currentReport.custom_width,
creation_method: creationMethod,
dashboard: dashboardId,
chart: chart?.id,
@@ -257,6 +264,26 @@ function ReportModal({
</div>
</>
);
+ const renderCustomWidthSection = (
+ <StyledInputContainer>
+ <div className="control-label" css={CustomWidthHeaderStyle}>
+ {TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_TEXT}
+ </div>
+ <div className="input-container">
+ <Input
+ type="number"
+ name="custom_width"
+ value={currentReport?.custom_width || ''}
+ placeholder={TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_PLACEHOLDER_TEXT}
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+ setCurrentReport({
+ custom_width: parseInt(event.target.value, 10) || null,
+ });
+ }}
+ />
+ </div>
+ </StyledInputContainer>
+ );
return (
<StyledModal
@@ -331,6 +358,7 @@ function ReportModal({
}}
/>
{isChart && renderMessageContentSection}
+ {(!isChart || !isTextBasedChart) && renderCustomWidthSection}
</StyledBottomSection>
{currentReport.error && (
<Alert
diff --git a/superset-frontend/src/components/ReportModal/styles.tsx b/superset-frontend/src/components/ReportModal/styles.tsx
index 960da9b10e..dd0a410ef5 100644
--- a/superset-frontend/src/components/ReportModal/styles.tsx
+++ b/superset-frontend/src/components/ReportModal/styles.tsx
@@ -90,6 +90,10 @@ export const TimezoneHeaderStyle = (theme: SupersetTheme) => css`
margin: ${theme.gridUnit * 3}px 0 ${theme.gridUnit * 2}px;
`;
+export const CustomWidthHeaderStyle = (theme: SupersetTheme) => css`
+ margin: ${theme.gridUnit * 3}px 0 ${theme.gridUnit * 2}px;
+`;
+
export const SectionHeaderStyle = (theme: SupersetTheme) => css`
margin: ${theme.gridUnit * 3}px 0;
`;
diff --git a/superset-frontend/src/features/alerts/AlertReportModal.tsx b/superset-frontend/src/features/alerts/AlertReportModal.tsx
index 8ff344c97e..ce46eb69f5 100644
--- a/superset-frontend/src/features/alerts/AlertReportModal.tsx
+++ b/superset-frontend/src/features/alerts/AlertReportModal.tsx
@@ -35,6 +35,7 @@ import rison from 'rison';
import { useSingleViewResource } from 'src/views/CRUD/hooks';
import Icons from 'src/components/Icons';
+import { Input } from 'src/components/Input';
import { Switch } from 'src/components/Switch';
import Modal from 'src/components/Modal';
import TimezoneSelector from 'src/components/TimezoneSelector';
@@ -46,6 +47,7 @@ import Owner from 'src/types/Owner';
import { AntdCheckbox, AsyncSelect, Select } from 'src/components';
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
import { useCommonConf } from 'src/features/databases/state';
+import { CustomWidthHeaderStyle } from 'src/components/ReportModal/styles';
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
import {
NotificationMethodOption,
@@ -370,7 +372,7 @@ interface NotificationMethodAddProps {
onClick: () => void;
}
-const TRANSLATIONS = {
+export const TRANSLATIONS = {
ADD_NOTIFICATION_METHOD_TEXT: t('Add notification method'),
ADD_DELIVERY_METHOD_TEXT: t('Add delivery method'),
SAVE_TEXT: t('Save'),
@@ -406,7 +408,9 @@ const TRANSLATIONS = {
SEND_AS_PNG_TEXT: t('Send as PNG'),
SEND_AS_CSV_TEXT: t('Send as CSV'),
SEND_AS_TEXT: t('Send as text'),
- IGNORE_CACHE_TEXT: t('Ignore cache when generating screenshot'),
+ IGNORE_CACHE_TEXT: t('Ignore cache when generating report'),
+ CUSTOM_SCREENSHOT_WIDTH_TEXT: t('Screenshot width'),
+ CUSTOM_SCREENSHOT_WIDTH_PLACEHOLDER_TEXT: t('Input custom width in pixels'),
NOTIFICATION_METHOD_TEXT: t('Notification method'),
};
@@ -466,6 +470,14 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
);
const [forceScreenshot, setForceScreenshot] = useState<boolean>(false);
+ const [isScreenshot, setIsScreenshot] = useState<boolean>(false);
+ useEffect(() => {
+ setIsScreenshot(
+ contentType === 'dashboard' ||
+ (contentType === 'chart' && reportFormat === 'PNG'),
+ );
+ }, [contentType, reportFormat]);
+
// Dropdown options
const [conditionNotNull, setConditionNotNull] = useState<boolean>(false);
const [sourceOptions, setSourceOptions] = useState<MetaObject[]>([]);
@@ -853,12 +865,15 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
}).then(response => setChartVizType(response.json.result.viz_type));
// Handle input/textarea updates
- const onTextChange = (
+ const onInputChange = (
event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
) => {
- const { target } = event;
+ const {
+ target: { type, value, name },
+ } = event;
+ const parsedValue = type === 'number' ? parseInt(value, 10) || null : value;
- updateAlertState(target.name, target.value);
+ updateAlertState(name, parsedValue);
};
const onTimeoutVerifyChange = (
@@ -1180,7 +1195,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
? TRANSLATIONS.REPORT_NAME_TEXT
: TRANSLATIONS.ALERT_NAME_TEXT
}
- onChange={onTextChange}
+ onChange={onInputChange}
css={inputSpacer}
/>
</div>
@@ -1216,7 +1231,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
name="description"
value={currentAlert ? currentAlert.description || '' : ''}
placeholder={TRANSLATIONS.DESCRIPTION_TEXT}
- onChange={onTextChange}
+ onChange={onInputChange}
css={inputSpacer}
/>
</div>
@@ -1471,6 +1486,24 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
</div>
</>
)}
+ {isScreenshot && (
+ <StyledInputContainer>
+ <div className="control-label" css={CustomWidthHeaderStyle}>
+ {TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_TEXT}
+ </div>
+ <div className="input-container">
+ <Input
+ type="number"
+ name="custom_width"
+ value={currentAlert?.custom_width || ''}
+ placeholder={
+ TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_PLACEHOLDER_TEXT
+ }
+ onChange={onInputChange}
+ />
+ </div>
+ </StyledInputContainer>
+ )}
{(isReport || contentType === 'dashboard') && (
<div className="inline-container">
<StyledCheckbox
diff --git a/superset-frontend/src/features/alerts/types.ts b/superset-frontend/src/features/alerts/types.ts
index 36d2b1d35a..34eb7fd261 100644
--- a/superset-frontend/src/features/alerts/types.ts
+++ b/superset-frontend/src/features/alerts/types.ts
@@ -68,10 +68,12 @@ export type AlertObject = {
created_by?: user;
created_on?: string;
crontab?: string;
+ custom_width?: number | null;
dashboard?: MetaObject;
dashboard_id?: number;
database?: MetaObject;
description?: string;
+ error?: string;
force_screenshot: boolean;
grace_period?: number;
id: number;
@@ -91,7 +93,6 @@ export type AlertObject = {
};
validator_type?: string;
working_timeout?: number;
- error?: string;
};
export type LogObject = {
diff --git a/superset-frontend/src/reports/types.ts b/superset-frontend/src/reports/types.ts
index 38cb3865cf..b67a7bac7b 100644
--- a/superset-frontend/src/reports/types.ts
+++ b/superset-frontend/src/reports/types.ts
@@ -56,5 +56,6 @@ export interface ReportObject {
working_timeout: number;
creation_method: string;
force_screenshot: boolean;
+ custom_width?: number | null;
error?: string;
}
diff --git a/superset/charts/api.py b/superset/charts/api.py
index c87b7bdda8..e8ccfa0fff 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -82,7 +82,7 @@ from superset.extensions import event_logger
from superset.models.slice import Slice
from superset.tasks.thumbnails import cache_chart_thumbnail
from superset.tasks.utils import get_current_user
-from superset.utils.screenshots import ChartScreenshot
+from superset.utils.screenshots import ChartScreenshot, DEFAULT_CHART_WINDOW_SIZE
from superset.utils.urls import get_url_path
from superset.views.base_api import (
BaseSupersetModelRestApi,
@@ -573,7 +573,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
rison_dict = kwargs["rison"]
- window_size = rison_dict.get("window_size") or (800, 600)
+ window_size = rison_dict.get("window_size") or DEFAULT_CHART_WINDOW_SIZE
# Don't shrink the image if thumb_size is not specified
thumb_size = rison_dict.get("thumb_size") or window_size
diff --git a/superset/config.py b/superset/config.py
index e781862395..0f9fafb68e 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -1273,6 +1273,9 @@ ALERT_REPORTS_NOTIFICATION_DRY_RUN = False
# Max tries to run queries to prevent false errors caused by transient errors
# being returned to users. Set to a value >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