mirror of
https://github.com/apache/superset.git
synced 2024-09-06 22:07:34 -04:00
feat: alert/report execution log list view (#11937)
This commit is contained in:
parent
475f59cb1c
commit
df6efb6aa2
@ -16,17 +16,16 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
|
||||||
import thunk from 'redux-thunk';
|
|
||||||
import configureStore from 'redux-mock-store';
|
|
||||||
import fetchMock from 'fetch-mock';
|
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 { styledMount as mount } from 'spec/helpers/theming';
|
||||||
|
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||||
import AlertList from 'src/views/CRUD/alert/AlertList';
|
import { Switch } from 'src/common/components/Switch';
|
||||||
import ListView from 'src/components/ListView';
|
import ListView from 'src/components/ListView';
|
||||||
import SubMenu from 'src/components/Menu/SubMenu';
|
import SubMenu from 'src/components/Menu/SubMenu';
|
||||||
import { Switch } from 'src/common/components/Switch';
|
import AlertList from 'src/views/CRUD/alert/AlertList';
|
||||||
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
|
||||||
|
|
||||||
// store needed for withToasts(AlertList)
|
// store needed for withToasts(AlertList)
|
||||||
const mockStore = configureStore([thunk]);
|
const mockStore = configureStore([thunk]);
|
||||||
@ -35,6 +34,7 @@ const store = mockStore({});
|
|||||||
const alertsEndpoint = 'glob:*/api/v1/report/?*';
|
const alertsEndpoint = 'glob:*/api/v1/report/?*';
|
||||||
const alertEndpoint = 'glob:*/api/v1/report/*';
|
const alertEndpoint = 'glob:*/api/v1/report/*';
|
||||||
const alertsInfoEndpoint = 'glob:*/api/v1/report/_info*';
|
const alertsInfoEndpoint = 'glob:*/api/v1/report/_info*';
|
||||||
|
const alertsCreatedByEndpoint = 'glob:*/api/v1/report/related/created_by*';
|
||||||
|
|
||||||
const mockalerts = [...new Array(3)].map((_, i) => ({
|
const mockalerts = [...new Array(3)].map((_, i) => ({
|
||||||
active: true,
|
active: true,
|
||||||
@ -74,6 +74,7 @@ fetchMock.get(alertsEndpoint, {
|
|||||||
fetchMock.get(alertsInfoEndpoint, {
|
fetchMock.get(alertsInfoEndpoint, {
|
||||||
permissions: ['can_delete', 'can_edit'],
|
permissions: ['can_delete', 'can_edit'],
|
||||||
});
|
});
|
||||||
|
fetchMock.get(alertsCreatedByEndpoint, { result: [] });
|
||||||
fetchMock.put(alertEndpoint, { ...mockalerts[0], active: false });
|
fetchMock.put(alertEndpoint, { ...mockalerts[0], active: false });
|
||||||
fetchMock.put(alertsEndpoint, { ...mockalerts[0], active: false });
|
fetchMock.put(alertsEndpoint, { ...mockalerts[0], active: false });
|
||||||
|
|
||||||
|
@ -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)"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -29,6 +29,7 @@ import ErrorBoundary from 'src/components/ErrorBoundary';
|
|||||||
import Menu from 'src/components/Menu/Menu';
|
import Menu from 'src/components/Menu/Menu';
|
||||||
import FlashProvider from 'src/components/FlashProvider';
|
import FlashProvider from 'src/components/FlashProvider';
|
||||||
import AlertList from 'src/views/CRUD/alert/AlertList';
|
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 AnnotationLayersList from 'src/views/CRUD/annotationlayers/AnnotationLayersList';
|
||||||
import AnnotationList from 'src/views/CRUD/annotation/AnnotationList';
|
import AnnotationList from 'src/views/CRUD/annotation/AnnotationList';
|
||||||
import ChartList from 'src/views/CRUD/chart/ChartList';
|
import ChartList from 'src/views/CRUD/chart/ChartList';
|
||||||
@ -135,6 +136,16 @@ const App = () => (
|
|||||||
<AlertList user={user} isReportEnabled />
|
<AlertList user={user} isReportEnabled />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Route>
|
</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>
|
</Switch>
|
||||||
<ToastPresenter />
|
<ToastPresenter />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
|
@ -17,25 +17,25 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useEffect } from 'react';
|
import { t } from '@superset-ui/core';
|
||||||
import { t, styled } from '@superset-ui/core';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
|
import { useHistory } from 'react-router-dom';
|
||||||
import Button from 'src/components/Button';
|
|
||||||
import Icon, { IconName } from 'src/components/Icon';
|
|
||||||
import { Tooltip } from 'src/common/components/Tooltip';
|
|
||||||
import { Switch } from 'src/common/components/Switch';
|
import { Switch } from 'src/common/components/Switch';
|
||||||
|
import Button from 'src/components/Button';
|
||||||
import FacePile from 'src/components/FacePile';
|
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 SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
||||||
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
|
|
||||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
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 {
|
import {
|
||||||
useListViewResource,
|
useListViewResource,
|
||||||
useSingleViewResource,
|
useSingleViewResource,
|
||||||
} from 'src/views/CRUD/hooks';
|
} from 'src/views/CRUD/hooks';
|
||||||
|
import { createErrorHandler, createFetchRelated } from 'src/views/CRUD/utils';
|
||||||
import { AlertObject } from './types';
|
import { AlertObject, AlertState } from './types';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
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({
|
function AlertList({
|
||||||
addDangerToast,
|
addDangerToast,
|
||||||
isReportEnabled = false,
|
isReportEnabled = false,
|
||||||
user,
|
user,
|
||||||
}: AlertListProps) {
|
}: AlertListProps) {
|
||||||
const title = isReportEnabled ? t('report') : t('alert');
|
const title = isReportEnabled ? 'report' : 'alert';
|
||||||
|
const pathName = isReportEnabled ? 'Reports' : 'Alerts';
|
||||||
const initalFilters = useMemo(
|
const initalFilters = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -92,7 +78,7 @@ function AlertList({
|
|||||||
undefined,
|
undefined,
|
||||||
initalFilters,
|
initalFilters,
|
||||||
);
|
);
|
||||||
const pathName = isReportEnabled ? 'Reports' : 'Alerts';
|
|
||||||
const { updateResource } = useSingleViewResource<AlertObject>(
|
const { updateResource } = useSingleViewResource<AlertObject>(
|
||||||
'report',
|
'report',
|
||||||
t('reports'),
|
t('reports'),
|
||||||
@ -125,42 +111,7 @@ function AlertList({
|
|||||||
row: {
|
row: {
|
||||||
original: { last_state: lastState },
|
original: { last_state: lastState },
|
||||||
},
|
},
|
||||||
}: any) => {
|
}: any) => <AlertStatusIcon state={lastState} />,
|
||||||
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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
accessor: 'last_state',
|
accessor: 'last_state',
|
||||||
size: 'xs',
|
size: 'xs',
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
@ -176,7 +127,7 @@ function AlertList({
|
|||||||
},
|
},
|
||||||
}: any) =>
|
}: any) =>
|
||||||
recipients.map((r: any) => (
|
recipients.map((r: any) => (
|
||||||
<Icon key={r.id} name={r.type as IconName} />
|
<RecipientIcon key={r.id} type={r.type} />
|
||||||
)),
|
)),
|
||||||
accessor: 'recipients',
|
accessor: 'recipients',
|
||||||
Header: t('Notification Method'),
|
Header: t('Notification Method'),
|
||||||
@ -217,16 +168,20 @@ function AlertList({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Cell: ({ row: { original } }: any) => {
|
Cell: ({ row: { original } }: any) => {
|
||||||
|
const history = useHistory();
|
||||||
const handleEdit = () => {}; // handleAnnotationEdit(original);
|
const handleEdit = () => {}; // handleAnnotationEdit(original);
|
||||||
const handleDelete = () => {}; // setAlertCurrentlyDeleting(original);
|
const handleDelete = () => {}; // setAlertCurrentlyDeleting(original);
|
||||||
|
const handleGotoExecutionLog = () =>
|
||||||
|
history.push(`/${original.type.toLowerCase()}/${original.id}/log`);
|
||||||
|
|
||||||
const actions = [
|
const actions = [
|
||||||
canEdit
|
canEdit
|
||||||
? {
|
? {
|
||||||
label: 'preview-action',
|
label: 'execution-log-action',
|
||||||
tooltip: t('Execution Log'),
|
tooltip: t('Execution Log'),
|
||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
icon: 'note' as IconName,
|
icon: 'note' as IconName,
|
||||||
onClick: handleEdit,
|
onClick: handleGotoExecutionLog,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
canEdit
|
canEdit
|
||||||
@ -266,7 +221,7 @@ function AlertList({
|
|||||||
subMenuButtons.push({
|
subMenuButtons.push({
|
||||||
name: (
|
name: (
|
||||||
<>
|
<>
|
||||||
<i className="fa fa-plus" /> {title}
|
<i className="fa fa-plus" /> {t(`${title}`)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
buttonStyle: 'primary',
|
buttonStyle: 'primary',
|
||||||
@ -276,7 +231,7 @@ function AlertList({
|
|||||||
|
|
||||||
const EmptyStateButton = (
|
const EmptyStateButton = (
|
||||||
<Button buttonStyle="primary" onClick={() => {}}>
|
<Button buttonStyle="primary" onClick={() => {}}>
|
||||||
<i className="fa fa-plus" /> {title}
|
<i className="fa fa-plus" /> {t(`${title}`)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -310,9 +265,11 @@ function AlertList({
|
|||||||
operator: FilterOperators.equals,
|
operator: FilterOperators.equals,
|
||||||
unfilteredLabel: 'Any',
|
unfilteredLabel: 'Any',
|
||||||
selects: [
|
selects: [
|
||||||
{ label: t('Success'), value: 'Success' },
|
{ label: t(`${AlertState.success}`), value: AlertState.success },
|
||||||
{ label: t('Working'), value: 'Working' },
|
{ label: t(`${AlertState.working}`), value: AlertState.working },
|
||||||
{ label: t('Error'), value: 'Error' },
|
{ label: t(`${AlertState.error}`), value: AlertState.error },
|
||||||
|
{ label: t(`${AlertState.noop}`), value: AlertState.noop },
|
||||||
|
{ label: t(`${AlertState.grace}`), value: AlertState.grace },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
158
superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx
Normal file
158
superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx
Normal file
@ -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);
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -38,9 +38,32 @@ export type AlertObject = {
|
|||||||
created_on?: string;
|
created_on?: string;
|
||||||
id?: number;
|
id?: number;
|
||||||
last_eval_dttm?: number;
|
last_eval_dttm?: number;
|
||||||
last_state?: string;
|
last_state?: 'Success' | 'Working' | 'Error' | 'Not triggered' | 'On Grace';
|
||||||
name?: string;
|
name?: string;
|
||||||
owners?: Array<Owner>;
|
owners?: Array<Owner>;
|
||||||
recipients?: recipients;
|
recipients?: recipients;
|
||||||
type?: string;
|
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',
|
||||||
|
}
|
||||||
|
@ -50,6 +50,7 @@ class ReportExecutionLogRestApi(BaseSupersetModelRestApi):
|
|||||||
]
|
]
|
||||||
list_columns = [
|
list_columns = [
|
||||||
"id",
|
"id",
|
||||||
|
"scheduled_dttm",
|
||||||
"end_dttm",
|
"end_dttm",
|
||||||
"start_dttm",
|
"start_dttm",
|
||||||
"value",
|
"value",
|
||||||
|
@ -71,7 +71,7 @@ class AlertObservationModelView(
|
|||||||
class AlertReportModelView(SupersetModelView):
|
class AlertReportModelView(SupersetModelView):
|
||||||
datamodel = SQLAInterface(ReportSchedule)
|
datamodel = SQLAInterface(ReportSchedule)
|
||||||
route_base = "/report"
|
route_base = "/report"
|
||||||
include_route_methods = RouteMethod.CRUD_SET
|
include_route_methods = RouteMethod.CRUD_SET | {"log"}
|
||||||
|
|
||||||
@expose("/list/")
|
@expose("/list/")
|
||||||
@has_access
|
@has_access
|
||||||
@ -84,11 +84,22 @@ class AlertReportModelView(SupersetModelView):
|
|||||||
|
|
||||||
return super().render_app_template()
|
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
|
class AlertModelView(SupersetModelView): # pylint: disable=too-many-ancestors
|
||||||
datamodel = SQLAInterface(Alert)
|
datamodel = SQLAInterface(Alert)
|
||||||
route_base = "/alert"
|
route_base = "/alert"
|
||||||
include_route_methods = RouteMethod.CRUD_SET
|
include_route_methods = RouteMethod.CRUD_SET | {"log"}
|
||||||
|
|
||||||
list_columns = (
|
list_columns = (
|
||||||
"label",
|
"label",
|
||||||
@ -197,6 +208,17 @@ class AlertModelView(SupersetModelView): # pylint: disable=too-many-ancestors
|
|||||||
|
|
||||||
return super().render_app_template()
|
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:
|
def pre_add(self, item: "AlertModelView") -> None:
|
||||||
item.recipients = get_email_address_str(item.recipients)
|
item.recipients = get_email_address_str(item.recipients)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user