This is an automated email from the ASF dual-hosted git repository.

tai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new df6efb6  feat: alert/report execution log list view (#11937)
df6efb6 is described below

commit df6efb6aa2b73aea3f1a438240cd8de358714f77
Author: Lily Kuang <[email protected]>
AuthorDate: Thu Dec 10 15:15:13 2020 -0800

    feat: alert/report execution log list view (#11937)
---
 .../views/CRUD/alert/AlertList_spec.jsx            |  13 +-
 .../views/CRUD/alert/ExecutionLog_spec.jsx         | 105 ++++++++++++++
 superset-frontend/src/views/App.tsx                |  11 ++
 .../src/views/CRUD/alert/AlertList.tsx             | 101 ++++---------
 .../src/views/CRUD/alert/ExecutionLog.tsx          | 158 +++++++++++++++++++++
 .../CRUD/alert/components/AlertStatusIcon.tsx      |  75 ++++++++++
 .../views/CRUD/alert/components/RecipientIcon.tsx  |  48 +++++++
 superset-frontend/src/views/CRUD/alert/types.ts    |  25 +++-
 superset/reports/logs/api.py                       |   1 +
 superset/views/alerts.py                           |  26 +++-
 10 files changed, 482 insertions(+), 81 deletions(-)

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 9b9327d..dff305e 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 fetchMock from 'fetch-mock';
 import React from 'react';
-import thunk from 'redux-thunk';
 import configureStore from 'redux-mock-store';
-import fetchMock from 'fetch-mock';
+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 0000000..f8f9b21
--- /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(
+    <Provider store={store}>
+      <ExecutionLog {...props} />
+    </Provider>,
+  );
+  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 57f416b..ed99540 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 = () => (
                   <AlertList user={user} isReportEnabled />
                 </ErrorBoundary>
               </Route>
+              <Route path="/alert/:alertId/log">
+                <ErrorBoundary>
+                  <ExecutionLog user={user} />
+                </ErrorBoundary>
+              </Route>
+              <Route path="/report/:alertId/log">
+                <ErrorBoundary>
+                  <ExecutionLog user={user} isReportEnabled />
+                </ErrorBoundary>
+              </Route>
             </Switch>
             <ToastPresenter />
           </QueryParamProvider>
diff --git a/superset-frontend/src/views/CRUD/alert/AlertList.tsx 
b/superset-frontend/src/views/CRUD/alert/AlertList.tsx
index cc15595..bed3742 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<AlertObject>(
     '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 (
-            <Tooltip title={lastStateConfig.label} placement="bottom">
-              <StatusIcon
-                name={lastStateConfig.name as IconName}
-                status={lastStateConfig.status}
-              />
-            </Tooltip>
-          );
-        },
+        }: any) => <AlertStatusIcon state={lastState} />,
         accessor: 'last_state',
         size: 'xs',
         disableSortBy: true,
@@ -176,7 +127,7 @@ function AlertList({
           },
         }: any) =>
           recipients.map((r: any) => (
-            <Icon key={r.id} name={r.type as IconName} />
+            <RecipientIcon key={r.id} type={r.type} />
           )),
         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: (
         <>
-          <i className="fa fa-plus" /> {title}
+          <i className="fa fa-plus" /> {t(`${title}`)}
         </>
       ),
       buttonStyle: 'primary',
@@ -276,7 +231,7 @@ function AlertList({
 
   const EmptyStateButton = (
     <Button buttonStyle="primary" onClick={() => {}}>
-      <i className="fa fa-plus" /> {title}
+      <i className="fa fa-plus" /> {t(`${title}`)}
     </Button>
   );
 
@@ -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 0000000..d5c950b
--- /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<LogObject>(
+    `report/${alertId}/log`,
+    t('log'),
+    addDangerToast,
+    false,
+  );
+  const {
+    state: { loading: alertLoading, resource: alertResource },
+    fetchResource,
+  } = useSingleViewResource<AlertObject>(
+    '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) => <AlertStatusIcon state={state} />,
+        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 (
+    <>
+      <SubMenu
+        name={
+          <StyledHeader>
+            <span>
+              {t(`${alertResource?.type}`)} {alertResource?.name}
+            </span>
+            <span>
+              <Link to={path}>Back to all</Link>
+            </span>
+          </StyledHeader>
+        }
+      />
+      <ListView<LogObject>
+        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 0000000..cb6d3a0
--- /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 (
+    <Tooltip title={lastStateConfig.label} placement="bottom">
+      <StatusIcon
+        name={lastStateConfig.name as IconName}
+        status={lastStateConfig.status}
+      />
+    </Tooltip>
+  );
+}
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 0000000..d437488
--- /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 ? (
+    <Tooltip title={recipientIconConfig.label} placement="bottom">
+      <Icon name={recipientIconConfig.name as IconName} />
+    </Tooltip>
+  ) : null;
+}
diff --git a/superset-frontend/src/views/CRUD/alert/types.ts 
b/superset-frontend/src/views/CRUD/alert/types.ts
index 311ff9c..ca21fa4 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<Owner>;
   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 0b026c4..4c175b6 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 fe89497..eca045f 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("/<pk>/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("/<pk>/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)
 

Reply via email to