Compare commits

...

9 Commits

Author SHA1 Message Date
Yongjie Zhao
968962a4ad fix: Pie chart not displayed in viz (#13987)
(cherry picked from commit 56dc74e09f)
2021-04-07 11:48:37 -07:00
Beto Dealmeida
81e1e6b726 fix: import dataset/dashboard empty keys (#13979)
(cherry picked from commit 3b11654c5a)
2021-04-07 11:48:08 -07:00
Erik Ritter
258923e91f Revert "fix: select table overlay (#13694)" (#13901)
This reverts commit b247279ffe.

(cherry picked from commit 5315d2cc2d)
2021-04-06 13:37:39 -07:00
AAfghahi
df04b66dae fix: Data table z index in sql Editor (#13972)
(cherry picked from commit 7fb138381f)
2021-04-06 12:12:38 -07:00
Lily Kuang
2347de09fc feat(alert/report): add ALERTS_ATTACH_REPORTS feature flags + feature (#13894)
* Add a feature flag ALERTS_ATTACH_REPORTS

* update test

* update feature flag

* add comment for feature flag

* add unit tests for alerts with attachments disabled

* fix lint

Co-authored-by: samtfm <sam@preset.io>
(cherry picked from commit 762101018b)
2021-04-06 12:11:08 -07:00
Hugh A. Miles II
52f7a0afeb fix: SQL -> Explore Overwrite flow (#13946)
(cherry picked from commit f291ba05c6)
2021-04-06 12:10:42 -07:00
Elizabeth Thompson
ea3d6905af catch collapse onchange (#13927)
(cherry picked from commit abd4051f7a)
2021-04-06 12:10:17 -07:00
AAfghahi
3eefca3a82 fix: Floating Menu in SQL Left Bar (#13858)
* floating table git issue

* made changes

* floating table git issue

* made changes

* long table names

* floating table git issue

* made changes

* floating table git issue

* made changes

* long table names

* floating table git issue

* made changes

* floating table git issue

* made changes

* long table names

* table values

* aligned

* active key

* changed to customStyle

* update dropdown styles

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>
(cherry picked from commit f19f2c3ac8)
2021-04-06 12:09:49 -07:00
AAfghahi
3396fe26fb fix: adjusted tab height (#13822)
(cherry picked from commit 4187d9e4c4)
2021-04-06 12:09:22 -07:00
25 changed files with 201 additions and 116 deletions

View File

@@ -61,7 +61,7 @@ describe('DatasourceEditor', () => {
}); });
it('renders Tabs', () => { it('renders Tabs', () => {
expect(wrapper.find(Tabs)).toExist(); expect(wrapper.find('#table-tabs')).toExist();
}); });
it('makes an async request', () => it('makes an async request', () =>

View File

@@ -20,7 +20,8 @@ import React from 'react';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { render, screen, act } from '@testing-library/react'; import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import '@testing-library/jest-dom/extend-expect'; import '@testing-library/jest-dom/extend-expect';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
@@ -56,6 +57,10 @@ describe('SqlEditorLeftBar', () => {
}); });
}); });
afterEach(() => {
wrapper.unmount();
});
it('is valid', () => { it('is valid', () => {
expect(React.isValidElement(<SqlEditorLeftBar {...mockedProps} />)).toBe( expect(React.isValidElement(<SqlEditorLeftBar {...mockedProps} />)).toBe(
true, true,
@@ -68,19 +73,14 @@ describe('SqlEditorLeftBar', () => {
}); });
describe('Left Panel Expansion', () => { describe('Left Panel Expansion', () => {
beforeEach(async () => { it('table should be visible when expanded is true', () => {
await act(async () => { const { container } = render(
render( <ThemeProvider theme={supersetTheme}>
<ThemeProvider theme={supersetTheme}> <Provider store={store}>
<Provider store={store}> <SqlEditorLeftBar {...mockedProps} />
<SqlEditorLeftBar {...mockedProps} /> </Provider>
</Provider> </ThemeProvider>,
</ThemeProvider>, );
);
});
});
it('table should be visible when expanded is true', async () => {
const dbSelect = screen.getByText(/select a database/i); const dbSelect = screen.getByText(/select a database/i);
const schemaSelect = screen.getByText(/select a schema \(0\)/i); const schemaSelect = screen.getByText(/select a schema \(0\)/i);
const dropdown = screen.getByText(/Select table/i); const dropdown = screen.getByText(/Select table/i);
@@ -89,5 +89,28 @@ describe('Left Panel Expansion', () => {
expect(schemaSelect).toBeInTheDocument(); expect(schemaSelect).toBeInTheDocument();
expect(dropdown).toBeInTheDocument(); expect(dropdown).toBeInTheDocument();
expect(abUser).toBeInTheDocument(); expect(abUser).toBeInTheDocument();
expect(
container.querySelector('.ant-collapse-content-active'),
).toBeInTheDocument();
});
it('should toggle the table when the header is clicked', async () => {
const collapseMock = jest.fn();
render(
<ThemeProvider theme={supersetTheme}>
<Provider store={store}>
<SqlEditorLeftBar
actions={{ ...mockedActions, collapseTable: collapseMock }}
tables={[table]}
queryEditor={defaultQueryEditor}
database={databases}
height={0}
/>
</Provider>
</ThemeProvider>,
);
const header = screen.getByText(/ab_user/);
userEvent.click(header);
expect(collapseMock).toHaveBeenCalled();
}); });
}); });

View File

@@ -118,22 +118,6 @@ describe('TableElement', () => {
'active', 'active',
); );
}); });
it('calls the collapseTable action', () => {
const wrapper = mount(
<Provider store={store}>
<TableElement {...mockedProps} />
</Provider>,
{
wrappingComponent: ThemeProvider,
wrappingComponentProps: {
theme: supersetTheme,
},
},
);
expect(mockedActions.collapseTable.called).toBe(false);
wrapper.find('[data-test="collapse"]').hostNodes().simulate('click');
expect(mockedActions.collapseTable.called).toBe(true);
});
it('removes the table', () => { it('removes the table', () => {
const wrapper = mount( const wrapper = mount(
<Provider store={store}> <Provider store={store}>

View File

@@ -33,7 +33,7 @@ import {
LOCALSTORAGE_MAX_QUERY_AGE_MS, LOCALSTORAGE_MAX_QUERY_AGE_MS,
} from '../../constants'; } from '../../constants';
const TAB_HEIGHT = 64; const TAB_HEIGHT = 90;
/* /*
editorQueries are queries executed by users passed from SqlEditor component editorQueries are queries executed by users passed from SqlEditor component
@@ -63,7 +63,6 @@ const StyledPane = styled.div`
flex-direction: column; flex-direction: column;
} }
.tab-content { .tab-content {
overflow: hidden;
.alert { .alert {
margin-top: ${({ theme }) => theme.gridUnit * 2}px; margin-top: ${({ theme }) => theme.gridUnit * 2}px;
} }

View File

@@ -59,6 +59,7 @@ export default class SqlEditorLeftBar extends React.PureComponent {
this.onDbChange = this.onDbChange.bind(this); this.onDbChange = this.onDbChange.bind(this);
this.getDbList = this.getDbList.bind(this); this.getDbList = this.getDbList.bind(this);
this.onTableChange = this.onTableChange.bind(this); this.onTableChange = this.onTableChange.bind(this);
this.onToggleTable = this.onToggleTable.bind(this);
} }
onSchemaChange(schema) { onSchemaChange(schema) {
@@ -91,6 +92,16 @@ export default class SqlEditorLeftBar extends React.PureComponent {
this.props.actions.addTable(this.props.queryEditor, tableName, schemaName); this.props.actions.addTable(this.props.queryEditor, tableName, schemaName);
} }
onToggleTable(tables) {
this.props.tables.forEach(table => {
if (!tables.includes(table.id.toString()) && table.expanded) {
this.props.actions.collapseTable(table);
} else if (tables.includes(table.id.toString()) && !table.expanded) {
this.props.actions.expandTable(table);
}
});
}
getDbList(dbs) { getDbList(dbs) {
this.props.actions.setDatabases(dbs); this.props.actions.setDatabases(dbs);
} }
@@ -172,13 +183,13 @@ export default class SqlEditorLeftBar extends React.PureComponent {
`} `}
expandIconPosition="right" expandIconPosition="right"
ghost ghost
onChange={this.onToggleTable}
> >
{this.props.tables.map(table => ( {this.props.tables.map(table => (
<TableElement <TableElement
table={table} table={table}
key={table.id} key={table.id}
actions={this.props.actions} actions={this.props.actions}
onClick={this.toggleTable}
/> />
))} ))}
</Collapse> </Collapse>

View File

@@ -79,15 +79,6 @@ class TableElement extends React.PureComponent {
this.props.actions.addQueryEditor(qe); this.props.actions.addQueryEditor(qe);
} }
toggleTable(e) {
e.preventDefault();
if (this.props.table.expanded) {
this.props.actions.collapseTable(this.props.table);
} else {
this.props.actions.expandTable(this.props.table);
}
}
removeTable() { removeTable() {
this.props.actions.removeDataPreview(this.props.table); this.props.actions.removeDataPreview(this.props.table);
this.props.actions.removeTable(this.props.table); this.props.actions.removeTable(this.props.table);
@@ -214,13 +205,7 @@ class TableElement extends React.PureComponent {
title={table.name} title={table.name}
trigger={['hover']} trigger={['hover']}
> >
<StyledSpan <StyledSpan data-test="collapse" className="table-name">
data-test="collapse"
className="table-name"
onClick={e => {
this.toggleTable(e);
}}
>
<strong>{table.name}</strong> <strong>{table.name}</strong>
</StyledSpan> </StyledSpan>
</Tooltip> </Tooltip>

View File

@@ -56,7 +56,7 @@ body {
position: relative; position: relative;
background-color: @lightest; background-color: @lightest;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: auto;
> .ant-tabs-tabpane { > .ant-tabs-tabpane {
position: absolute; position: absolute;
@@ -300,6 +300,7 @@ div.Workspace {
.schemaPane-enter-done, .schemaPane-enter-done,
.schemaPane-exit { .schemaPane-exit {
transform: translateX(0); transform: translateX(0);
z-index: 7;
} }
.schemaPane-exit-active { .schemaPane-exit-active {

View File

@@ -245,7 +245,6 @@ export default function DatabaseSelector({
placeholder={t('Select a database')} placeholder={t('Select a database')}
autoSelect autoSelect
isDisabled={!isDatabaseSelectEnabled || readOnly} isDisabled={!isDatabaseSelectEnabled || readOnly}
menuPosition="fixed"
/>, />,
null, null,
); );

View File

@@ -30,6 +30,10 @@ const ProgressBar = styled(({ striped, ...props }: ProgressBarProps) => (
<AntdProgress {...props} /> <AntdProgress {...props} />
))` ))`
line-height: 0; line-height: 0;
position: static;
.ant-progress-inner {
position: static;
}
.ant-progress-outer { .ant-progress-outer {
${({ percent }) => !percent && `display: none;`} ${({ percent }) => !percent && `display: none;`}
} }
@@ -37,6 +41,7 @@ const ProgressBar = styled(({ striped, ...props }: ProgressBarProps) => (
font-size: ${({ theme }) => theme.typography.sizes.s}px; font-size: ${({ theme }) => theme.typography.sizes.s}px;
} }
.ant-progress-bg { .ant-progress-bg {
position: static;
${({ striped }) => ${({ striped }) =>
striped && striped &&
` `

View File

@@ -326,7 +326,6 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
optionRenderer={renderTableOption} optionRenderer={renderTableOption}
valueRenderer={renderTableOption} valueRenderer={renderTableOption}
isDisabled={readOnly} isDisabled={readOnly}
menuPosition="fixed"
/> />
); );
} else if (formMode) { } else if (formMode) {

View File

@@ -79,6 +79,13 @@ const FlexRowContainer = styled.div`
} }
`; `;
const StyledTableTabs = styled(Tabs)`
overflow: visible;
.ant-tabs-content-holder {
overflow: visible;
}
`;
const EditLockContainer = styled.div` const EditLockContainer = styled.div`
font-size: ${supersetTheme.typography.sizes.s}px; font-size: ${supersetTheme.typography.sizes.s}px;
display: flex; display: flex;
@@ -995,7 +1002,7 @@ class DatasourceEditor extends React.PureComponent {
</> </>
} }
/> />
<Tabs <StyledTableTabs
fullWidth={false} fullWidth={false}
id="table-tabs" id="table-tabs"
data-test="edit-dataset-tabs" data-test="edit-dataset-tabs"
@@ -1086,7 +1093,7 @@ class DatasourceEditor extends React.PureComponent {
</Col> </Col>
</div> </div>
</Tabs.TabPane> </Tabs.TabPane>
</Tabs> </StyledTableTabs>
</DatasourceContainer> </DatasourceContainer>
); );
} }

View File

@@ -49,6 +49,10 @@ const StyledDatasourceModal = styled(Modal)`
.modal-footer { .modal-footer {
flex: 0 1 auto; flex: 0 1 auto;
} }
.ant-modal-body {
overflow: visible;
}
`; `;
interface DatasourceModalProps { interface DatasourceModalProps {

View File

@@ -107,6 +107,11 @@ function VizSupportValidation({ vizType }) {
); );
} }
const nativeFilterGate = behaviors =>
!behaviors.includes(Behavior.NATIVE_FILTER) ||
(isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) &&
behaviors.includes(Behavior.CROSS_FILTER));
const VizTypeControl = props => { const VizTypeControl = props => {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState('');
@@ -168,11 +173,7 @@ const VizTypeControl = props => {
const filteredTypes = DEFAULT_ORDER.filter(type => registry.has(type)) const filteredTypes = DEFAULT_ORDER.filter(type => registry.has(type))
.filter(type => { .filter(type => {
const behaviors = registry.get(type)?.behaviors || []; const behaviors = registry.get(type)?.behaviors || [];
return ( return nativeFilterGate(behaviors);
(isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) &&
behaviors.includes(Behavior.CROSS_FILTER)) ||
!behaviors.length
);
}) })
.map(type => ({ .map(type => ({
key: type, key: type,
@@ -183,11 +184,7 @@ const VizTypeControl = props => {
.entries() .entries()
.filter(entry => { .filter(entry => {
const behaviors = entry.value?.behaviors || []; const behaviors = entry.value?.behaviors || [];
return ( return nativeFilterGate(behaviors);
(isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) &&
behaviors.includes(Behavior.CROSS_FILTER)) ||
!behaviors.length
);
}) })
.filter(({ key }) => !typesWithDefaultOrder.has(key)), .filter(({ key }) => !typesWithDefaultOrder.has(key)),
) )

View File

@@ -359,6 +359,13 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
"OMNIBAR": False, "OMNIBAR": False,
"DASHBOARD_RBAC": False, "DASHBOARD_RBAC": False,
"ENABLE_EXPLORE_DRAG_AND_DROP": False, "ENABLE_EXPLORE_DRAG_AND_DROP": False,
# Enabling ALERTS_ATTACH_REPORTS, the system sends email and slack message
# with screenshot and link
# Disables ALERTS_ATTACH_REPORTS, the system DOES NOT generate screenshot
# for report with type 'alert' and sends email and slack message with only link;
# for report with type 'report' still send with email and slack message with
# screenshot and link
"ALERTS_ATTACH_REPORTS": True,
} }
# Set the default view to card/grid view if thumbnail support is enabled. # Set the default view to card/grid view if thumbnail support is enabled.

View File

@@ -115,7 +115,7 @@ def import_dashboard(
# TODO (betodealmeida): move this logic to import_from_dict # TODO (betodealmeida): move this logic to import_from_dict
config = config.copy() config = config.copy()
for key, new_name in JSON_KEYS.items(): for key, new_name in JSON_KEYS.items():
if config.get(key): if config.get(key) is not None:
value = config.pop(key) value = config.pop(key)
try: try:
config[new_name] = json.dumps(value) config[new_name] = json.dumps(value)

View File

@@ -314,6 +314,8 @@ class DatasetRestApi(BaseSupersetModelRestApi):
changed_model = UpdateDatasetCommand( changed_model = UpdateDatasetCommand(
g.user, pk, item, override_columns g.user, pk, item, override_columns
).run() ).run()
if override_columns:
RefreshDatasetCommand(g.user, pk).run()
response = self.response(200, id=changed_model.id, result=item) response = self.response(200, id=changed_model.id, result=item)
except DatasetNotFoundError: except DatasetNotFoundError:
response = self.response_404() response = self.response_404()

View File

@@ -92,7 +92,7 @@ def import_dataset(
# TODO (betodealmeida): move this logic to import_from_dict # TODO (betodealmeida): move this logic to import_from_dict
config = config.copy() config = config.copy()
for key in JSON_KEYS: for key in JSON_KEYS:
if config.get(key): if config.get(key) is not None:
try: try:
config[key] = json.dumps(config[key]) config[key] = json.dumps(config[key])
except TypeError: except TypeError:

View File

@@ -26,6 +26,7 @@ from sqlalchemy.orm import Session
from superset import app from superset import app
from superset.commands.base import BaseCommand from superset.commands.base import BaseCommand
from superset.commands.exceptions import CommandException from superset.commands.exceptions import CommandException
from superset.extensions import feature_flag_manager
from superset.models.reports import ( from superset.models.reports import (
ReportExecutionLog, ReportExecutionLog,
ReportSchedule, ReportSchedule,
@@ -52,7 +53,7 @@ from superset.reports.dao import (
ReportScheduleDAO, ReportScheduleDAO,
) )
from superset.reports.notifications import create_notification from superset.reports.notifications import create_notification
from superset.reports.notifications.base import NotificationContent, ScreenshotData from superset.reports.notifications.base import NotificationContent
from superset.reports.notifications.exceptions import NotificationError from superset.reports.notifications.exceptions import NotificationError
from superset.utils.celery import session_scope from superset.utils.celery import session_scope
from superset.utils.screenshots import ( from superset.utils.screenshots import (
@@ -153,7 +154,7 @@ class BaseReportState:
raise ReportScheduleSelleniumUserNotFoundError() raise ReportScheduleSelleniumUserNotFoundError()
return user return user
def _get_screenshot(self) -> ScreenshotData: def _get_screenshot(self) -> bytes:
""" """
Get a chart or dashboard screenshot Get a chart or dashboard screenshot
@@ -176,7 +177,6 @@ class BaseReportState:
window_size=app.config["WEBDRIVER_WINDOW"]["dashboard"], window_size=app.config["WEBDRIVER_WINDOW"]["dashboard"],
thumb_size=app.config["WEBDRIVER_WINDOW"]["dashboard"], thumb_size=app.config["WEBDRIVER_WINDOW"]["dashboard"],
) )
image_url = self._get_url(user_friendly=True)
user = self._get_screenshot_user() user = self._get_screenshot_user()
try: try:
image_data = screenshot.get_screenshot(user=user) image_data = screenshot.get_screenshot(user=user)
@@ -188,7 +188,7 @@ class BaseReportState:
) )
if not image_data: if not image_data:
raise ReportScheduleScreenshotFailedError() raise ReportScheduleScreenshotFailedError()
return ScreenshotData(url=image_url, image=image_data) return image_data
def _get_notification_content(self) -> NotificationContent: def _get_notification_content(self) -> NotificationContent:
""" """
@@ -196,7 +196,19 @@ class BaseReportState:
:raises: ReportScheduleScreenshotFailedError :raises: ReportScheduleScreenshotFailedError
""" """
screenshot_data = self._get_screenshot() screenshot_data = None
url = self._get_url(user_friendly=True)
if (
feature_flag_manager.is_feature_enabled("ALERTS_ATTACH_REPORTS")
or self._report_schedule.type == ReportScheduleType.REPORT
):
screenshot_data = self._get_screenshot()
if not screenshot_data:
return NotificationContent(
name=self._report_schedule.name,
text="Unexpected missing screenshot",
)
if self._report_schedule.chart: if self._report_schedule.chart:
name = ( name = (
f"{self._report_schedule.name}: " f"{self._report_schedule.name}: "
@@ -207,7 +219,7 @@ class BaseReportState:
f"{self._report_schedule.name}: " f"{self._report_schedule.name}: "
f"{self._report_schedule.dashboard.dashboard_title}" f"{self._report_schedule.dashboard.dashboard_title}"
) )
return NotificationContent(name=name, screenshot=screenshot_data) return NotificationContent(name=name, url=url, screenshot=screenshot_data)
def _send(self, notification_content: NotificationContent) -> None: def _send(self, notification_content: NotificationContent) -> None:
""" """

View File

@@ -21,16 +21,11 @@ from typing import Any, List, Optional, Type
from superset.models.reports import ReportRecipients, ReportRecipientType from superset.models.reports import ReportRecipients, ReportRecipientType
@dataclass
class ScreenshotData:
url: str # url to chart/dashboard for this screenshot
image: bytes # bytes for the screenshot
@dataclass @dataclass
class NotificationContent: class NotificationContent:
name: str name: str
screenshot: Optional[ScreenshotData] = None url: Optional[str] = None # url to chart/dashboard for this screenshot
screenshot: Optional[bytes] = None # bytes for the screenshot
text: Optional[str] = None text: Optional[str] = None

View File

@@ -63,21 +63,21 @@ class EmailNotification(BaseNotification): # pylint: disable=too-few-public-met
return EmailContent(body=self._error_template(self._content.text)) return EmailContent(body=self._error_template(self._content.text))
# Get the domain from the 'From' address .. # Get the domain from the 'From' address ..
# and make a message id without the < > in the end # and make a message id without the < > in the end
image = None
domain = self._get_smtp_domain()
msgid = make_msgid(domain)[1:-1]
body = __(
"""
<b><a href="%(url)s">Explore in Superset</a></b><p></p>
<img src="cid:%(msgid)s">
""",
url=self._content.url,
msgid=msgid,
)
if self._content.screenshot: if self._content.screenshot:
domain = self._get_smtp_domain() image = {msgid: self._content.screenshot}
msgid = make_msgid(domain)[1:-1]
image = {msgid: self._content.screenshot.image} return EmailContent(body=body, images=image)
body = __(
"""
<b><a href="%(url)s">Explore in Superset</a></b><p></p>
<img src="cid:%(msgid)s">
""",
url=self._content.screenshot.url,
msgid=msgid,
)
return EmailContent(body=body, images=image)
return EmailContent(body=self._error_template("Unexpected missing screenshot"))
def _get_subject(self) -> str: def _get_subject(self) -> str:
return __( return __(

View File

@@ -57,20 +57,18 @@ class SlackNotification(BaseNotification): # pylint: disable=too-few-public-met
def _get_body(self) -> str: def _get_body(self) -> str:
if self._content.text: if self._content.text:
return self._error_template(self._content.name, self._content.text) return self._error_template(self._content.name, self._content.text)
if self._content.screenshot: return __(
return __( """
""" *%(name)s*\n
*%(name)s*\n <%(url)s|Explore in Superset>
<%(url)s|Explore in Superset> """,
""", name=self._content.name,
name=self._content.name, url=self._content.url,
url=self._content.screenshot.url, )
)
return self._error_template(self._content.name, "Unexpected missing screenshot")
def _get_inline_screenshot(self) -> Optional[Union[str, IOBase, bytes]]: def _get_inline_screenshot(self) -> Optional[Union[str, IOBase, bytes]]:
if self._content.screenshot: if self._content.screenshot:
return self._content.screenshot.image return self._content.screenshot
return None return None
@retry(SlackApiError, delay=10, backoff=2, tries=5) @retry(SlackApiError, delay=10, backoff=2, tries=5)

View File

@@ -598,12 +598,7 @@ class TestDatasetApi(SupersetTestCase):
rv = self.put_assert_metric(uri, dataset_data, "put") rv = self.put_assert_metric(uri, dataset_data, "put")
assert rv.status_code == 200 assert rv.status_code == 200
columns = ( columns = db.session.query(TableColumn).filter_by(table_id=dataset.id).all()
db.session.query(TableColumn)
.filter_by(table_id=dataset.id)
.order_by("column_name")
.all()
)
assert columns[0].column_name == dataset_data["columns"][0]["column_name"] assert columns[0].column_name == dataset_data["columns"][0]["column_name"]
assert columns[0].description == dataset_data["columns"][0]["description"] assert columns[0].description == dataset_data["columns"][0]["description"]

View File

@@ -312,7 +312,7 @@ class TestImportDatasetsCommand(SupersetTestCase):
assert dataset.schema == "" assert dataset.schema == ""
assert dataset.sql == "" assert dataset.sql == ""
assert dataset.params is None assert dataset.params is None
assert dataset.template_params is None assert dataset.template_params == "{}"
assert dataset.filter_select_enabled assert dataset.filter_select_enabled
assert dataset.fetch_values_predicate is None assert dataset.fetch_values_predicate is None
assert dataset.extra is None assert dataset.extra is None

View File

@@ -368,7 +368,7 @@ dataset_config: Dict[str, Any] = {
"schema": "", "schema": "",
"sql": "", "sql": "",
"params": None, "params": None,
"template_params": None, "template_params": {},
"filter_select_enabled": True, "filter_select_enabled": True,
"fetch_values_predicate": None, "fetch_values_predicate": None,
"extra": None, "extra": None,

View File

@@ -750,6 +750,10 @@ def test_email_dashboard_report_fails(
) )
@patch("superset.reports.notifications.email.send_email_smtp") @patch("superset.reports.notifications.email.send_email_smtp")
@patch("superset.utils.screenshots.ChartScreenshot.get_screenshot") @patch("superset.utils.screenshots.ChartScreenshot.get_screenshot")
@patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
ALERTS_ATTACH_REPORTS=True,
)
def test_slack_chart_alert(screenshot_mock, email_mock, create_alert_email_chart): def test_slack_chart_alert(screenshot_mock, email_mock, create_alert_email_chart):
""" """
ExecuteReport Command: Test chart slack alert ExecuteReport Command: Test chart slack alert
@@ -773,6 +777,34 @@ def test_slack_chart_alert(screenshot_mock, email_mock, create_alert_email_chart
assert_log(ReportState.SUCCESS) assert_log(ReportState.SUCCESS)
@pytest.mark.usefixtures(
"load_birth_names_dashboard_with_slices", "create_alert_email_chart"
)
@patch("superset.reports.notifications.email.send_email_smtp")
@patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
ALERTS_ATTACH_REPORTS=False,
)
def test_slack_chart_alert_no_attachment(email_mock, create_alert_email_chart):
"""
ExecuteReport Command: Test chart slack alert
"""
# setup screenshot mock
with freeze_time("2020-01-01T00:00:00Z"):
AsyncExecuteReportScheduleCommand(
test_id, create_alert_email_chart.id, datetime.utcnow()
).run()
notification_targets = get_target_from_report_schedule(create_alert_email_chart)
# Assert the email smtp address
assert email_mock.call_args[0][0] == notification_targets[0]
# Assert the there is no attached image
assert email_mock.call_args[1]["images"] is None
# Assert logs are correct
assert_log(ReportState.SUCCESS)
@pytest.mark.usefixtures( @pytest.mark.usefixtures(
"load_birth_names_dashboard_with_slices", "create_report_slack_chart" "load_birth_names_dashboard_with_slices", "create_report_slack_chart"
) )
@@ -859,6 +891,10 @@ def test_soft_timeout_alert(email_mock, create_alert_email_chart):
) )
@patch("superset.reports.notifications.email.send_email_smtp") @patch("superset.reports.notifications.email.send_email_smtp")
@patch("superset.utils.screenshots.ChartScreenshot.get_screenshot") @patch("superset.utils.screenshots.ChartScreenshot.get_screenshot")
@patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
ALERTS_ATTACH_REPORTS=True,
)
def test_soft_timeout_screenshot(screenshot_mock, email_mock, create_alert_email_chart): def test_soft_timeout_screenshot(screenshot_mock, email_mock, create_alert_email_chart):
""" """
ExecuteReport Command: Test soft timeout on screenshot ExecuteReport Command: Test soft timeout on screenshot
@@ -882,11 +918,11 @@ def test_soft_timeout_screenshot(screenshot_mock, email_mock, create_alert_email
@pytest.mark.usefixtures( @pytest.mark.usefixtures(
"load_birth_names_dashboard_with_slices", "create_alert_email_chart" "load_birth_names_dashboard_with_slices", "create_report_email_chart"
) )
@patch("superset.reports.notifications.email.send_email_smtp") @patch("superset.reports.notifications.email.send_email_smtp")
@patch("superset.utils.screenshots.ChartScreenshot.get_screenshot") @patch("superset.utils.screenshots.ChartScreenshot.get_screenshot")
def test_fail_screenshot(screenshot_mock, email_mock, create_alert_email_chart): def test_fail_screenshot(screenshot_mock, email_mock, create_report_email_chart):
""" """
ExecuteReport Command: Test soft timeout on screenshot ExecuteReport Command: Test soft timeout on screenshot
""" """
@@ -896,10 +932,10 @@ def test_fail_screenshot(screenshot_mock, email_mock, create_alert_email_chart):
screenshot_mock.side_effect = Exception("Unexpected error") screenshot_mock.side_effect = Exception("Unexpected error")
with pytest.raises(ReportScheduleScreenshotFailedError): with pytest.raises(ReportScheduleScreenshotFailedError):
AsyncExecuteReportScheduleCommand( AsyncExecuteReportScheduleCommand(
test_id, create_alert_email_chart.id, datetime.utcnow() test_id, create_report_email_chart.id, datetime.utcnow()
).run() ).run()
notification_targets = get_target_from_report_schedule(create_alert_email_chart) notification_targets = get_target_from_report_schedule(create_report_email_chart)
# Assert the email smtp address, asserts a notification was sent with the error # Assert the email smtp address, asserts a notification was sent with the error
assert email_mock.call_args[0][0] == notification_targets[0] assert email_mock.call_args[0][0] == notification_targets[0]
@@ -908,6 +944,32 @@ def test_fail_screenshot(screenshot_mock, email_mock, create_alert_email_chart):
) )
@pytest.mark.usefixtures(
"load_birth_names_dashboard_with_slices", "create_alert_email_chart"
)
@patch("superset.reports.notifications.email.send_email_smtp")
@patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
ALERTS_ATTACH_REPORTS=False,
)
def test_email_disable_screenshot(email_mock, create_alert_email_chart):
"""
ExecuteReport Command: Test soft timeout on screenshot
"""
AsyncExecuteReportScheduleCommand(
test_id, create_alert_email_chart.id, datetime.utcnow()
).run()
notification_targets = get_target_from_report_schedule(create_alert_email_chart)
# Assert the email smtp address, asserts a notification was sent with the error
assert email_mock.call_args[0][0] == notification_targets[0]
# Assert the there is no attached image
assert email_mock.call_args[1]["images"] is None
assert_log(ReportState.SUCCESS)
@pytest.mark.usefixtures("create_invalid_sql_alert_email_chart") @pytest.mark.usefixtures("create_invalid_sql_alert_email_chart")
@patch("superset.reports.notifications.email.send_email_smtp") @patch("superset.reports.notifications.email.send_email_smtp")
def test_invalid_sql_alert(email_mock, create_invalid_sql_alert_email_chart): def test_invalid_sql_alert(email_mock, create_invalid_sql_alert_email_chart):