mirror of
https://github.com/apache/superset.git
synced 2024-09-12 08:39:45 -04:00
feat: customize screenshot width for alerts/reports (#24547)
This commit is contained in:
parent
8ba0b81957
commit
be9eb0f3a3
@ -33,6 +33,7 @@ import LabeledErrorBoundInput from 'src/components/Form/LabeledErrorBoundInput';
|
|||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import { CronError } from 'src/components/CronPicker';
|
import { CronError } from 'src/components/CronPicker';
|
||||||
import { RadioChangeEvent } from 'src/components';
|
import { RadioChangeEvent } from 'src/components';
|
||||||
|
import { Input } from 'src/components/Input';
|
||||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||||
import { ChartState } from 'src/explore/types';
|
import { ChartState } from 'src/explore/types';
|
||||||
import {
|
import {
|
||||||
@ -41,9 +42,14 @@ import {
|
|||||||
NOTIFICATION_FORMATS,
|
NOTIFICATION_FORMATS,
|
||||||
} from 'src/reports/types';
|
} from 'src/reports/types';
|
||||||
import { reportSelector } from 'src/views/CRUD/hooks';
|
import { reportSelector } from 'src/views/CRUD/hooks';
|
||||||
|
import {
|
||||||
|
TRANSLATIONS,
|
||||||
|
StyledInputContainer,
|
||||||
|
} from 'src/features/alerts/AlertReportModal';
|
||||||
import { CreationMethod } from './HeaderReportDropdown';
|
import { CreationMethod } from './HeaderReportDropdown';
|
||||||
import {
|
import {
|
||||||
antDErrorAlertStyles,
|
antDErrorAlertStyles,
|
||||||
|
CustomWidthHeaderStyle,
|
||||||
StyledModal,
|
StyledModal,
|
||||||
StyledTopSection,
|
StyledTopSection,
|
||||||
StyledBottomSection,
|
StyledBottomSection,
|
||||||
@ -170,6 +176,7 @@ function ReportModal({
|
|||||||
type: 'Report',
|
type: 'Report',
|
||||||
active: true,
|
active: true,
|
||||||
force_screenshot: false,
|
force_screenshot: false,
|
||||||
|
custom_width: currentReport.custom_width,
|
||||||
creation_method: creationMethod,
|
creation_method: creationMethod,
|
||||||
dashboard: dashboardId,
|
dashboard: dashboardId,
|
||||||
chart: chart?.id,
|
chart: chart?.id,
|
||||||
@ -257,6 +264,26 @@ function ReportModal({
|
|||||||
</div>
|
</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 (
|
return (
|
||||||
<StyledModal
|
<StyledModal
|
||||||
@ -331,6 +358,7 @@ function ReportModal({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{isChart && renderMessageContentSection}
|
{isChart && renderMessageContentSection}
|
||||||
|
{(!isChart || !isTextBasedChart) && renderCustomWidthSection}
|
||||||
</StyledBottomSection>
|
</StyledBottomSection>
|
||||||
{currentReport.error && (
|
{currentReport.error && (
|
||||||
<Alert
|
<Alert
|
||||||
|
@ -90,6 +90,10 @@ export const TimezoneHeaderStyle = (theme: SupersetTheme) => css`
|
|||||||
margin: ${theme.gridUnit * 3}px 0 ${theme.gridUnit * 2}px;
|
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`
|
export const SectionHeaderStyle = (theme: SupersetTheme) => css`
|
||||||
margin: ${theme.gridUnit * 3}px 0;
|
margin: ${theme.gridUnit * 3}px 0;
|
||||||
`;
|
`;
|
||||||
|
@ -35,6 +35,7 @@ import rison from 'rison';
|
|||||||
import { useSingleViewResource } from 'src/views/CRUD/hooks';
|
import { useSingleViewResource } from 'src/views/CRUD/hooks';
|
||||||
|
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
|
import { Input } from 'src/components/Input';
|
||||||
import { Switch } from 'src/components/Switch';
|
import { Switch } from 'src/components/Switch';
|
||||||
import Modal from 'src/components/Modal';
|
import Modal from 'src/components/Modal';
|
||||||
import TimezoneSelector from 'src/components/TimezoneSelector';
|
import TimezoneSelector from 'src/components/TimezoneSelector';
|
||||||
@ -46,6 +47,7 @@ import Owner from 'src/types/Owner';
|
|||||||
import { AntdCheckbox, AsyncSelect, Select } from 'src/components';
|
import { AntdCheckbox, AsyncSelect, Select } from 'src/components';
|
||||||
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
|
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
|
||||||
import { useCommonConf } from 'src/features/databases/state';
|
import { useCommonConf } from 'src/features/databases/state';
|
||||||
|
import { CustomWidthHeaderStyle } from 'src/components/ReportModal/styles';
|
||||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||||
import {
|
import {
|
||||||
NotificationMethodOption,
|
NotificationMethodOption,
|
||||||
@ -370,7 +372,7 @@ interface NotificationMethodAddProps {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRANSLATIONS = {
|
export const TRANSLATIONS = {
|
||||||
ADD_NOTIFICATION_METHOD_TEXT: t('Add notification method'),
|
ADD_NOTIFICATION_METHOD_TEXT: t('Add notification method'),
|
||||||
ADD_DELIVERY_METHOD_TEXT: t('Add delivery method'),
|
ADD_DELIVERY_METHOD_TEXT: t('Add delivery method'),
|
||||||
SAVE_TEXT: t('Save'),
|
SAVE_TEXT: t('Save'),
|
||||||
@ -406,7 +408,9 @@ const TRANSLATIONS = {
|
|||||||
SEND_AS_PNG_TEXT: t('Send as PNG'),
|
SEND_AS_PNG_TEXT: t('Send as PNG'),
|
||||||
SEND_AS_CSV_TEXT: t('Send as CSV'),
|
SEND_AS_CSV_TEXT: t('Send as CSV'),
|
||||||
SEND_AS_TEXT: t('Send as text'),
|
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'),
|
NOTIFICATION_METHOD_TEXT: t('Notification method'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -466,6 +470,14 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||||||
);
|
);
|
||||||
const [forceScreenshot, setForceScreenshot] = useState<boolean>(false);
|
const [forceScreenshot, setForceScreenshot] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [isScreenshot, setIsScreenshot] = useState<boolean>(false);
|
||||||
|
useEffect(() => {
|
||||||
|
setIsScreenshot(
|
||||||
|
contentType === 'dashboard' ||
|
||||||
|
(contentType === 'chart' && reportFormat === 'PNG'),
|
||||||
|
);
|
||||||
|
}, [contentType, reportFormat]);
|
||||||
|
|
||||||
// Dropdown options
|
// Dropdown options
|
||||||
const [conditionNotNull, setConditionNotNull] = useState<boolean>(false);
|
const [conditionNotNull, setConditionNotNull] = useState<boolean>(false);
|
||||||
const [sourceOptions, setSourceOptions] = useState<MetaObject[]>([]);
|
const [sourceOptions, setSourceOptions] = useState<MetaObject[]>([]);
|
||||||
@ -853,12 +865,15 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||||||
}).then(response => setChartVizType(response.json.result.viz_type));
|
}).then(response => setChartVizType(response.json.result.viz_type));
|
||||||
|
|
||||||
// Handle input/textarea updates
|
// Handle input/textarea updates
|
||||||
const onTextChange = (
|
const onInputChange = (
|
||||||
event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
|
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 = (
|
const onTimeoutVerifyChange = (
|
||||||
@ -1180,7 +1195,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||||||
? TRANSLATIONS.REPORT_NAME_TEXT
|
? TRANSLATIONS.REPORT_NAME_TEXT
|
||||||
: TRANSLATIONS.ALERT_NAME_TEXT
|
: TRANSLATIONS.ALERT_NAME_TEXT
|
||||||
}
|
}
|
||||||
onChange={onTextChange}
|
onChange={onInputChange}
|
||||||
css={inputSpacer}
|
css={inputSpacer}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1216,7 +1231,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||||||
name="description"
|
name="description"
|
||||||
value={currentAlert ? currentAlert.description || '' : ''}
|
value={currentAlert ? currentAlert.description || '' : ''}
|
||||||
placeholder={TRANSLATIONS.DESCRIPTION_TEXT}
|
placeholder={TRANSLATIONS.DESCRIPTION_TEXT}
|
||||||
onChange={onTextChange}
|
onChange={onInputChange}
|
||||||
css={inputSpacer}
|
css={inputSpacer}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1471,6 +1486,24 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||||||
</div>
|
</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') && (
|
{(isReport || contentType === 'dashboard') && (
|
||||||
<div className="inline-container">
|
<div className="inline-container">
|
||||||
<StyledCheckbox
|
<StyledCheckbox
|
||||||
|
@ -68,10 +68,12 @@ export type AlertObject = {
|
|||||||
created_by?: user;
|
created_by?: user;
|
||||||
created_on?: string;
|
created_on?: string;
|
||||||
crontab?: string;
|
crontab?: string;
|
||||||
|
custom_width?: number | null;
|
||||||
dashboard?: MetaObject;
|
dashboard?: MetaObject;
|
||||||
dashboard_id?: number;
|
dashboard_id?: number;
|
||||||
database?: MetaObject;
|
database?: MetaObject;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
error?: string;
|
||||||
force_screenshot: boolean;
|
force_screenshot: boolean;
|
||||||
grace_period?: number;
|
grace_period?: number;
|
||||||
id: number;
|
id: number;
|
||||||
@ -91,7 +93,6 @@ export type AlertObject = {
|
|||||||
};
|
};
|
||||||
validator_type?: string;
|
validator_type?: string;
|
||||||
working_timeout?: number;
|
working_timeout?: number;
|
||||||
error?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LogObject = {
|
export type LogObject = {
|
||||||
|
@ -56,5 +56,6 @@ export interface ReportObject {
|
|||||||
working_timeout: number;
|
working_timeout: number;
|
||||||
creation_method: string;
|
creation_method: string;
|
||||||
force_screenshot: boolean;
|
force_screenshot: boolean;
|
||||||
|
custom_width?: number | null;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ from superset.extensions import event_logger
|
|||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
from superset.tasks.thumbnails import cache_chart_thumbnail
|
from superset.tasks.thumbnails import cache_chart_thumbnail
|
||||||
from superset.tasks.utils import get_current_user
|
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.utils.urls import get_url_path
|
||||||
from superset.views.base_api import (
|
from superset.views.base_api import (
|
||||||
BaseSupersetModelRestApi,
|
BaseSupersetModelRestApi,
|
||||||
@ -573,7 +573,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||||||
$ref: '#/components/responses/500'
|
$ref: '#/components/responses/500'
|
||||||
"""
|
"""
|
||||||
rison_dict = kwargs["rison"]
|
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
|
# Don't shrink the image if thumb_size is not specified
|
||||||
thumb_size = rison_dict.get("thumb_size") or window_size
|
thumb_size = rison_dict.get("thumb_size") or window_size
|
||||||
|
@ -1273,6 +1273,9 @@ ALERT_REPORTS_NOTIFICATION_DRY_RUN = False
|
|||||||
# Max tries to run queries to prevent false errors caused by transient errors
|
# 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.
|
# being returned to users. Set to a value >1 to enable retries.
|
||||||
ALERT_REPORTS_QUERY_EXECUTION_MAX_TRIES = 1
|
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
|
# A custom prefix to use on all Alerts & Reports emails
|
||||||
EMAIL_REPORTS_SUBJECT_PREFIX = "[Report] "
|
EMAIL_REPORTS_SUBJECT_PREFIX = "[Report] "
|
||||||
|
@ -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")
|
@ -93,6 +93,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
|
|||||||
"context_markdown",
|
"context_markdown",
|
||||||
"creation_method",
|
"creation_method",
|
||||||
"crontab",
|
"crontab",
|
||||||
|
"custom_width",
|
||||||
"dashboard.dashboard_title",
|
"dashboard.dashboard_title",
|
||||||
"dashboard.id",
|
"dashboard.id",
|
||||||
"database.database_name",
|
"database.database_name",
|
||||||
@ -159,6 +160,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
|
|||||||
"context_markdown",
|
"context_markdown",
|
||||||
"creation_method",
|
"creation_method",
|
||||||
"crontab",
|
"crontab",
|
||||||
|
"custom_width",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"database",
|
"database",
|
||||||
"description",
|
"description",
|
||||||
|
@ -154,6 +154,9 @@ class ReportSchedule(Model, AuditMixinNullable, ExtraJSONMixin):
|
|||||||
# (Reports) When generating a screenshot, bypass the cache?
|
# (Reports) When generating a screenshot, bypass the cache?
|
||||||
force_screenshot = Column(Boolean, default=False)
|
force_screenshot = Column(Boolean, default=False)
|
||||||
|
|
||||||
|
custom_width = Column(Integer, nullable=True)
|
||||||
|
custom_height = Column(Integer, nullable=True)
|
||||||
|
|
||||||
extra: ReportScheduleExtra # type: ignore
|
extra: ReportScheduleExtra # type: ignore
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
@ -17,8 +17,9 @@
|
|||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
|
|
||||||
from croniter import croniter
|
from croniter import croniter
|
||||||
|
from flask import current_app
|
||||||
from flask_babel import gettext as _
|
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 marshmallow.validate import Length, Range, ValidationError
|
||||||
from pytz import all_timezones
|
from pytz import all_timezones
|
||||||
|
|
||||||
@ -208,10 +209,34 @@ class ReportSchedulePostSchema(Schema):
|
|||||||
dump_default=None,
|
dump_default=None,
|
||||||
)
|
)
|
||||||
force_screenshot = fields.Boolean(dump_default=False)
|
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
|
@validates_schema
|
||||||
def validate_report_references( # pylint: disable=unused-argument,no-self-use
|
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:
|
) -> None:
|
||||||
if data["type"] == ReportScheduleType.REPORT:
|
if data["type"] == ReportScheduleType.REPORT:
|
||||||
if "database" in data:
|
if "database" in data:
|
||||||
@ -307,3 +332,26 @@ class ReportSchedulePutSchema(Schema):
|
|||||||
)
|
)
|
||||||
extra = fields.Dict(dump_default=None)
|
extra = fields.Dict(dump_default=None)
|
||||||
force_screenshot = fields.Boolean(dump_default=False)
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ -33,6 +33,12 @@ from superset.utils.webdriver import (
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
try:
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
@ -47,8 +53,8 @@ class BaseScreenshot:
|
|||||||
driver_type = current_app.config["WEBDRIVER_TYPE"]
|
driver_type = current_app.config["WEBDRIVER_TYPE"]
|
||||||
thumbnail_type: str = ""
|
thumbnail_type: str = ""
|
||||||
element: str = ""
|
element: str = ""
|
||||||
window_size: WindowSize = (800, 600)
|
window_size: WindowSize = DEFAULT_SCREENSHOT_WINDOW_SIZE
|
||||||
thumb_size: WindowSize = (400, 300)
|
thumb_size: WindowSize = DEFAULT_SCREENSHOT_THUMBNAIL_SIZE
|
||||||
|
|
||||||
def __init__(self, url: str, digest: str):
|
def __init__(self, url: str, digest: str):
|
||||||
self.digest: str = digest
|
self.digest: str = digest
|
||||||
@ -216,8 +222,8 @@ class ChartScreenshot(BaseScreenshot):
|
|||||||
standalone=ChartStandaloneMode.HIDE_NAV.value,
|
standalone=ChartStandaloneMode.HIDE_NAV.value,
|
||||||
)
|
)
|
||||||
super().__init__(url, digest)
|
super().__init__(url, digest)
|
||||||
self.window_size = window_size or (800, 600)
|
self.window_size = window_size or DEFAULT_CHART_WINDOW_SIZE
|
||||||
self.thumb_size = thumb_size or (800, 600)
|
self.thumb_size = thumb_size or DEFAULT_CHART_THUMBNAIL_SIZE
|
||||||
|
|
||||||
|
|
||||||
class DashboardScreenshot(BaseScreenshot):
|
class DashboardScreenshot(BaseScreenshot):
|
||||||
@ -239,5 +245,5 @@ class DashboardScreenshot(BaseScreenshot):
|
|||||||
)
|
)
|
||||||
|
|
||||||
super().__init__(url, digest)
|
super().__init__(url, digest)
|
||||||
self.window_size = window_size or (1600, 1200)
|
self.window_size = window_size or DEFAULT_DASHBOARD_WINDOW_SIZE
|
||||||
self.thumb_size = thumb_size or (800, 600)
|
self.thumb_size = thumb_size or DEFAULT_DASHBOARD_THUMBNAIL_SIZE
|
||||||
|
Loading…
Reference in New Issue
Block a user