diff --git a/superset-frontend/spec/javascripts/views/CRUD/alert/AlertList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/alert/AlertList_spec.jsx
index 9b9327ded5..dff305e867 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/alert/AlertList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/alert/AlertList_spec.jsx
@@ -16,17 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React from 'react';
-import thunk from 'redux-thunk';
-import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
+import React from 'react';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
import { styledMount as mount } from 'spec/helpers/theming';
-
-import AlertList from 'src/views/CRUD/alert/AlertList';
+import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+import { Switch } from 'src/common/components/Switch';
import ListView from 'src/components/ListView';
import SubMenu from 'src/components/Menu/SubMenu';
-import { Switch } from 'src/common/components/Switch';
-import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+import AlertList from 'src/views/CRUD/alert/AlertList';
// store needed for withToasts(AlertList)
const mockStore = configureStore([thunk]);
@@ -35,6 +34,7 @@ const store = mockStore({});
const alertsEndpoint = 'glob:*/api/v1/report/?*';
const alertEndpoint = 'glob:*/api/v1/report/*';
const alertsInfoEndpoint = 'glob:*/api/v1/report/_info*';
+const alertsCreatedByEndpoint = 'glob:*/api/v1/report/related/created_by*';
const mockalerts = [...new Array(3)].map((_, i) => ({
active: true,
@@ -74,6 +74,7 @@ fetchMock.get(alertsEndpoint, {
fetchMock.get(alertsInfoEndpoint, {
permissions: ['can_delete', 'can_edit'],
});
+fetchMock.get(alertsCreatedByEndpoint, { result: [] });
fetchMock.put(alertEndpoint, { ...mockalerts[0], active: false });
fetchMock.put(alertsEndpoint, { ...mockalerts[0], active: false });
diff --git a/superset-frontend/spec/javascripts/views/CRUD/alert/ExecutionLog_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/alert/ExecutionLog_spec.jsx
new file mode 100644
index 0000000000..f8f9b213c3
--- /dev/null
+++ b/superset-frontend/spec/javascripts/views/CRUD/alert/ExecutionLog_spec.jsx
@@ -0,0 +1,105 @@
+/**
+ * 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.
+ */
+import fetchMock from 'fetch-mock';
+import React from 'react';
+import { Provider } from 'react-redux';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import { styledMount as mount } from 'spec/helpers/theming';
+import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+import ListView from 'src/components/ListView';
+import ExecutionLog from 'src/views/CRUD/alert/ExecutionLog';
+
+// store needed for withToasts(ExecutionLog)
+const mockStore = configureStore([thunk]);
+const store = mockStore({});
+
+const executionLogsEndpoint = 'glob:*/api/v1/report/*/log*';
+const reportEndpoint = 'glob:*/api/v1/report/*';
+
+fetchMock.delete(executionLogsEndpoint, {});
+
+const mockannotations = [...new Array(3)].map((_, i) => ({
+ end_dttm: new Date().toISOString,
+ error_message: `report ${i} error message`,
+ id: i,
+ scheduled_dttm: new Date().toISOString,
+ start_dttm: new Date().toISOString,
+ state: 'Success',
+ value: `report ${i} value`,
+}));
+
+fetchMock.get(executionLogsEndpoint, {
+ ids: [2, 0, 1],
+ result: mockannotations,
+ count: 3,
+});
+
+fetchMock.get(reportEndpoint, {
+ id: 1,
+ result: { name: 'Test 0' },
+});
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
+ useParams: () => ({ alertId: '1' }),
+}));
+
+async function mountAndWait(props) {
+ const mounted = mount(
+
+
+ ,
+ );
+ await waitForComponentToPaint(mounted);
+
+ return mounted;
+}
+
+describe('ExecutionLog', () => {
+ let wrapper;
+
+ beforeAll(async () => {
+ wrapper = await mountAndWait();
+ });
+
+ it('renders', () => {
+ expect(wrapper.find(ExecutionLog)).toExist();
+ });
+
+ it('renders a ListView', () => {
+ expect(wrapper.find(ListView)).toExist();
+ });
+
+ it('fetches report/alert', () => {
+ const callsQ = fetchMock.calls(/report\/1/);
+ expect(callsQ).toHaveLength(2);
+ expect(callsQ[1][0]).toMatchInlineSnapshot(
+ `"http://localhost/api/v1/report/1"`,
+ );
+ });
+
+ it('fetches execution logs', () => {
+ const callsQ = fetchMock.calls(/report\/1\/log/);
+ expect(callsQ).toHaveLength(1);
+ expect(callsQ[0][0]).toMatchInlineSnapshot(
+ `"http://localhost/api/v1/report/1/log/?q=(order_column:start_dttm,order_direction:desc,page:0,page_size:25)"`,
+ );
+ });
+});
diff --git a/superset-frontend/src/views/App.tsx b/superset-frontend/src/views/App.tsx
index 57f416be31..ed9954087c 100644
--- a/superset-frontend/src/views/App.tsx
+++ b/superset-frontend/src/views/App.tsx
@@ -29,6 +29,7 @@ import ErrorBoundary from 'src/components/ErrorBoundary';
import Menu from 'src/components/Menu/Menu';
import FlashProvider from 'src/components/FlashProvider';
import AlertList from 'src/views/CRUD/alert/AlertList';
+import ExecutionLog from 'src/views/CRUD/alert/ExecutionLog';
import AnnotationLayersList from 'src/views/CRUD/annotationlayers/AnnotationLayersList';
import AnnotationList from 'src/views/CRUD/annotation/AnnotationList';
import ChartList from 'src/views/CRUD/chart/ChartList';
@@ -135,6 +136,16 @@ const App = () => (
+
+
+
+
+
+
+
+
+
+
diff --git a/superset-frontend/src/views/CRUD/alert/AlertList.tsx b/superset-frontend/src/views/CRUD/alert/AlertList.tsx
index cc15595a69..bed3742b01 100644
--- a/superset-frontend/src/views/CRUD/alert/AlertList.tsx
+++ b/superset-frontend/src/views/CRUD/alert/AlertList.tsx
@@ -17,25 +17,25 @@
* under the License.
*/
-import React, { useMemo, useEffect } from 'react';
-import { t, styled } from '@superset-ui/core';
-import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
-import Button from 'src/components/Button';
-import Icon, { IconName } from 'src/components/Icon';
-import { Tooltip } from 'src/common/components/Tooltip';
+import { t } from '@superset-ui/core';
+import React, { useEffect, useMemo } from 'react';
+import { useHistory } from 'react-router-dom';
import { Switch } from 'src/common/components/Switch';
+import Button from 'src/components/Button';
import FacePile from 'src/components/FacePile';
-import ListView, { Filters, FilterOperators } from 'src/components/ListView';
+import { IconName } from 'src/components/Icon';
+import ListView, { FilterOperators, Filters } from 'src/components/ListView';
+import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
-import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
import withToasts from 'src/messageToasts/enhancers/withToasts';
-
+import AlertStatusIcon from 'src/views/CRUD/alert/components/AlertStatusIcon';
+import RecipientIcon from 'src/views/CRUD/alert/components/RecipientIcon';
import {
useListViewResource,
useSingleViewResource,
} from 'src/views/CRUD/hooks';
-
-import { AlertObject } from './types';
+import { createErrorHandler, createFetchRelated } from 'src/views/CRUD/utils';
+import { AlertObject, AlertState } from './types';
const PAGE_SIZE = 25;
@@ -48,27 +48,13 @@ interface AlertListProps {
};
}
-const StatusIcon = styled(Icon)<{ status: string }>`
- color: ${({ status, theme }) => {
- switch (status) {
- case 'Working':
- return theme.colors.alert.base;
- case 'Error':
- return theme.colors.error.base;
- case 'Success':
- return theme.colors.success.base;
- default:
- return theme.colors.grayscale.base;
- }
- }};
-`;
-
function AlertList({
addDangerToast,
isReportEnabled = false,
user,
}: AlertListProps) {
- const title = isReportEnabled ? t('report') : t('alert');
+ const title = isReportEnabled ? 'report' : 'alert';
+ const pathName = isReportEnabled ? 'Reports' : 'Alerts';
const initalFilters = useMemo(
() => [
{
@@ -92,7 +78,7 @@ function AlertList({
undefined,
initalFilters,
);
- const pathName = isReportEnabled ? 'Reports' : 'Alerts';
+
const { updateResource } = useSingleViewResource(
'report',
t('reports'),
@@ -125,42 +111,7 @@ function AlertList({
row: {
original: { last_state: lastState },
},
- }: any) => {
- const lastStateConfig = {
- name: '',
- label: '',
- status: '',
- };
- switch (lastState) {
- case 'Success':
- lastStateConfig.name = 'check';
- lastStateConfig.label = t('Success');
- lastStateConfig.status = 'Success';
- break;
- case 'Working':
- lastStateConfig.name = 'exclamation';
- lastStateConfig.label = t('Working');
- lastStateConfig.status = 'Working';
- break;
- case 'Error':
- lastStateConfig.name = 'x-small';
- lastStateConfig.label = t('Error');
- lastStateConfig.status = 'Error';
- break;
- default:
- lastStateConfig.name = 'exclamation';
- lastStateConfig.label = t('Working');
- lastStateConfig.status = 'Working';
- }
- return (
-
-
-
- );
- },
+ }: any) => ,
accessor: 'last_state',
size: 'xs',
disableSortBy: true,
@@ -176,7 +127,7 @@ function AlertList({
},
}: any) =>
recipients.map((r: any) => (
-
+
)),
accessor: 'recipients',
Header: t('Notification Method'),
@@ -217,16 +168,20 @@ function AlertList({
},
{
Cell: ({ row: { original } }: any) => {
+ const history = useHistory();
const handleEdit = () => {}; // handleAnnotationEdit(original);
const handleDelete = () => {}; // setAlertCurrentlyDeleting(original);
+ const handleGotoExecutionLog = () =>
+ history.push(`/${original.type.toLowerCase()}/${original.id}/log`);
+
const actions = [
canEdit
? {
- label: 'preview-action',
+ label: 'execution-log-action',
tooltip: t('Execution Log'),
placement: 'bottom',
icon: 'note' as IconName,
- onClick: handleEdit,
+ onClick: handleGotoExecutionLog,
}
: null,
canEdit
@@ -266,7 +221,7 @@ function AlertList({
subMenuButtons.push({
name: (
<>
- {title}
+ {t(`${title}`)}
>
),
buttonStyle: 'primary',
@@ -276,7 +231,7 @@ function AlertList({
const EmptyStateButton = (
);
@@ -310,9 +265,11 @@ function AlertList({
operator: FilterOperators.equals,
unfilteredLabel: 'Any',
selects: [
- { label: t('Success'), value: 'Success' },
- { label: t('Working'), value: 'Working' },
- { label: t('Error'), value: 'Error' },
+ { label: t(`${AlertState.success}`), value: AlertState.success },
+ { label: t(`${AlertState.working}`), value: AlertState.working },
+ { label: t(`${AlertState.error}`), value: AlertState.error },
+ { label: t(`${AlertState.noop}`), value: AlertState.noop },
+ { label: t(`${AlertState.grace}`), value: AlertState.grace },
],
},
{
diff --git a/superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx b/superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx
new file mode 100644
index 0000000000..d5c950beff
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx
@@ -0,0 +1,158 @@
+/**
+ * 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.
+ */
+
+import { styled, t } from '@superset-ui/core';
+import moment from 'moment';
+import React, { useEffect, useMemo } from 'react';
+import { Link, useParams } from 'react-router-dom';
+import ListView from 'src/components/ListView';
+import SubMenu from 'src/components/Menu/SubMenu';
+import withToasts from 'src/messageToasts/enhancers/withToasts';
+import { fDuration } from 'src/modules/dates';
+import AlertStatusIcon from 'src/views/CRUD/alert/components/AlertStatusIcon';
+import {
+ useListViewResource,
+ useSingleViewResource,
+} from 'src/views/CRUD/hooks';
+import { AlertObject, LogObject } from './types';
+
+const PAGE_SIZE = 25;
+
+const StyledHeader = styled.div`
+ display: flex;
+ flex-direction: row;
+
+ a,
+ Link {
+ margin-left: 16px;
+ font-size: 12px;
+ font-weight: normal;
+ text-decoration: underline;
+ }
+`;
+
+interface ExecutionLogProps {
+ addDangerToast: (msg: string) => void;
+ addSuccessToast: (msg: string) => void;
+ isReportEnabled: boolean;
+}
+
+function ExecutionLog({ addDangerToast, isReportEnabled }: ExecutionLogProps) {
+ const { alertId }: any = useParams();
+ const {
+ state: { loading, resourceCount: logCount, resourceCollection: logs },
+ fetchData,
+ } = useListViewResource(
+ `report/${alertId}/log`,
+ t('log'),
+ addDangerToast,
+ false,
+ );
+ const {
+ state: { loading: alertLoading, resource: alertResource },
+ fetchResource,
+ } = useSingleViewResource(
+ 'report',
+ t('reports'),
+ addDangerToast,
+ );
+
+ useEffect(() => {
+ if (alertId !== null && !alertLoading) {
+ fetchResource(alertId);
+ }
+ }, [alertId]);
+
+ const initialSort = [{ id: 'start_dttm', desc: true }];
+ const columns = useMemo(
+ () => [
+ {
+ Cell: ({
+ row: {
+ original: { state },
+ },
+ }: any) => ,
+ accessor: 'state',
+ Header: t('State'),
+ size: 'xs',
+ disableSortBy: true,
+ },
+ {
+ accessor: 'scheduled_dttm',
+ Header: t('Scheduled at'),
+ },
+ {
+ Cell: ({
+ row: {
+ original: { start_dttm: startDttm },
+ },
+ }: any) => moment(new Date(startDttm)).format('ll'),
+ Header: t('Start At'),
+ accessor: 'start_dttm',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { start_dttm: startDttm, end_dttm: endDttm },
+ },
+ }: any) => fDuration(endDttm - startDttm),
+ Header: t('Duration'),
+ disableSortBy: true,
+ },
+ {
+ accessor: 'value',
+ Header: t('Value'),
+ },
+ {
+ accessor: 'error_message',
+ Header: t('Error Message'),
+ },
+ ],
+ [],
+ );
+ const path = `/${isReportEnabled ? 'report' : 'alert'}/list/`;
+ return (
+ <>
+
+
+ {t(`${alertResource?.type}`)} {alertResource?.name}
+
+
+ Back to all
+
+
+ }
+ />
+
+ className="execution-log-list-view"
+ columns={columns}
+ count={logCount}
+ data={logs}
+ fetchData={fetchData}
+ initialSort={initialSort}
+ loading={loading}
+ pageSize={PAGE_SIZE}
+ />
+ >
+ );
+}
+
+export default withToasts(ExecutionLog);
diff --git a/superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx b/superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx
new file mode 100644
index 0000000000..cb6d3a08fd
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx
@@ -0,0 +1,75 @@
+/**
+ * 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.
+ */
+import { styled, t } from '@superset-ui/core';
+import React from 'react';
+import { Tooltip } from 'src/common/components/Tooltip';
+import Icon, { IconName } from 'src/components/Icon';
+import { AlertState } from '../types';
+
+const StatusIcon = styled(Icon)<{ status: string }>`
+ color: ${({ status, theme }) => {
+ switch (status) {
+ case AlertState.working:
+ return theme.colors.alert.base;
+ case AlertState.error:
+ return theme.colors.error.base;
+ case AlertState.success:
+ return theme.colors.success.base;
+ default:
+ return theme.colors.grayscale.base;
+ }
+ }};
+`;
+
+export default function AlertStatusIcon({ state }: { state: string }) {
+ const lastStateConfig = {
+ name: '',
+ label: '',
+ status: '',
+ };
+ switch (state) {
+ case AlertState.success:
+ lastStateConfig.name = 'check';
+ lastStateConfig.label = t(`${AlertState.success}`);
+ lastStateConfig.status = AlertState.success;
+ break;
+ case AlertState.working:
+ lastStateConfig.name = 'exclamation';
+ lastStateConfig.label = t(`${AlertState.working}`);
+ lastStateConfig.status = AlertState.working;
+ break;
+ case AlertState.error:
+ lastStateConfig.name = 'x-small';
+ lastStateConfig.label = t(`${AlertState.error}`);
+ lastStateConfig.status = AlertState.error;
+ break;
+ default:
+ lastStateConfig.name = 'exclamation';
+ lastStateConfig.label = t(`${AlertState.working}`);
+ lastStateConfig.status = AlertState.working;
+ }
+ return (
+
+
+
+ );
+}
diff --git a/superset-frontend/src/views/CRUD/alert/components/RecipientIcon.tsx b/superset-frontend/src/views/CRUD/alert/components/RecipientIcon.tsx
new file mode 100644
index 0000000000..d437488e06
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/alert/components/RecipientIcon.tsx
@@ -0,0 +1,48 @@
+/**
+ * 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.
+ */
+import { t } from '@superset-ui/core';
+import React from 'react';
+import { Tooltip } from 'src/common/components/Tooltip';
+import Icon, { IconName } from 'src/components/Icon';
+import { RecipientIconName } from '../types';
+
+export default function RecipientIcon({ type }: { type: string }) {
+ const recipientIconConfig = {
+ name: '',
+ label: '',
+ };
+ switch (type) {
+ case RecipientIconName.email:
+ recipientIconConfig.name = 'email';
+ recipientIconConfig.label = t(`${RecipientIconName.email}`);
+ break;
+ case RecipientIconName.slack:
+ recipientIconConfig.name = 'slack';
+ recipientIconConfig.label = t(`${RecipientIconName.slack}`);
+ break;
+ default:
+ recipientIconConfig.name = '';
+ recipientIconConfig.label = '';
+ }
+ return recipientIconConfig.name.length ? (
+
+
+
+ ) : null;
+}
diff --git a/superset-frontend/src/views/CRUD/alert/types.ts b/superset-frontend/src/views/CRUD/alert/types.ts
index 311ff9ca8a..ca21fa4907 100644
--- a/superset-frontend/src/views/CRUD/alert/types.ts
+++ b/superset-frontend/src/views/CRUD/alert/types.ts
@@ -38,9 +38,32 @@ export type AlertObject = {
created_on?: string;
id?: number;
last_eval_dttm?: number;
- last_state?: string;
+ last_state?: 'Success' | 'Working' | 'Error' | 'Not triggered' | 'On Grace';
name?: string;
owners?: Array;
recipients?: recipients;
type?: string;
};
+
+export type LogObject = {
+ end_dttm: string;
+ error_message: string;
+ id: number;
+ scheduled_dttm: string;
+ start_dttm: string;
+ state: string;
+ value: string;
+};
+
+export enum AlertState {
+ success = 'Success',
+ working = 'Working',
+ error = 'Error',
+ noop = 'Not triggered',
+ grace = 'On Grace',
+}
+
+export enum RecipientIconName {
+ email = 'Email',
+ slack = 'Slack',
+}
diff --git a/superset/reports/logs/api.py b/superset/reports/logs/api.py
index 0b026c45a8..4c175b6635 100644
--- a/superset/reports/logs/api.py
+++ b/superset/reports/logs/api.py
@@ -50,6 +50,7 @@ class ReportExecutionLogRestApi(BaseSupersetModelRestApi):
]
list_columns = [
"id",
+ "scheduled_dttm",
"end_dttm",
"start_dttm",
"value",
diff --git a/superset/views/alerts.py b/superset/views/alerts.py
index fe894977ff..eca045fd21 100644
--- a/superset/views/alerts.py
+++ b/superset/views/alerts.py
@@ -71,7 +71,7 @@ class AlertObservationModelView(
class AlertReportModelView(SupersetModelView):
datamodel = SQLAInterface(ReportSchedule)
route_base = "/report"
- include_route_methods = RouteMethod.CRUD_SET
+ include_route_methods = RouteMethod.CRUD_SET | {"log"}
@expose("/list/")
@has_access
@@ -84,11 +84,22 @@ class AlertReportModelView(SupersetModelView):
return super().render_app_template()
+ @expose("//log/", methods=["GET"])
+ @has_access
+ def log(self, pk: int) -> FlaskResponse: # pylint: disable=unused-argument
+ if not (
+ is_feature_enabled("ENABLE_REACT_CRUD_VIEWS")
+ and is_feature_enabled("SIP_34_ALERTS_UI")
+ ):
+ return super().list()
+
+ return super().render_app_template()
+
class AlertModelView(SupersetModelView): # pylint: disable=too-many-ancestors
datamodel = SQLAInterface(Alert)
route_base = "/alert"
- include_route_methods = RouteMethod.CRUD_SET
+ include_route_methods = RouteMethod.CRUD_SET | {"log"}
list_columns = (
"label",
@@ -197,6 +208,17 @@ class AlertModelView(SupersetModelView): # pylint: disable=too-many-ancestors
return super().render_app_template()
+ @expose("//log/", methods=["GET"])
+ @has_access
+ def log(self, pk: int) -> FlaskResponse: # pylint: disable=unused-argument
+ if not (
+ is_feature_enabled("ENABLE_REACT_CRUD_VIEWS")
+ and is_feature_enabled("SIP_34_ALERTS_UI")
+ ):
+ return super().list()
+
+ return super().render_app_template()
+
def pre_add(self, item: "AlertModelView") -> None:
item.recipients = get_email_address_str(item.recipients)