| /** |
| * 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 React, { useState, useMemo } from 'react'; |
| import { useHistory } from 'react-router-dom'; |
| import { t, SupersetClient, makeApi, styled } from '@superset-ui/core'; |
| import moment from 'moment'; |
| import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar'; |
| import Button from 'src/components/Button'; |
| import FacePile from 'src/components/FacePile'; |
| import { IconName } from 'src/components/Icon'; |
| import { Tooltip } from 'src/common/components/Tooltip'; |
| import ListView, { |
| FilterOperators, |
| Filters, |
| ListViewProps, |
| } from 'src/components/ListView'; |
| import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu'; |
| import { Switch } from 'src/common/components/Switch'; |
| import { DATETIME_WITH_TIME_ZONE } from 'src/constants'; |
| 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 ConfirmStatusChange from 'src/components/ConfirmStatusChange'; |
| import DeleteModal from 'src/components/DeleteModal'; |
| import LastUpdated from 'src/components/LastUpdated'; |
| |
| import { |
| useListViewResource, |
| useSingleViewResource, |
| } from 'src/views/CRUD/hooks'; |
| import { createErrorHandler, createFetchRelated } from 'src/views/CRUD/utils'; |
| import AlertReportModal from './AlertReportModal'; |
| import { AlertObject, AlertState } from './types'; |
| |
| const PAGE_SIZE = 25; |
| |
| interface AlertListProps { |
| addDangerToast: (msg: string) => void; |
| addSuccessToast: (msg: string) => void; |
| isReportEnabled: boolean; |
| user: { |
| userId: string | number; |
| }; |
| } |
| const deleteAlerts = makeApi<number[], { message: string }>({ |
| requestType: 'rison', |
| method: 'DELETE', |
| endpoint: '/api/v1/report/', |
| }); |
| |
| const RefreshContainer = styled.div` |
| width: 100%; |
| padding: 0 ${({ theme }) => theme.gridUnit * 4}px |
| ${({ theme }) => theme.gridUnit * 3}px; |
| background-color: ${({ theme }) => theme.colors.grayscale.light5}; |
| `; |
| |
| function AlertList({ |
| addDangerToast, |
| isReportEnabled = false, |
| user, |
| addSuccessToast, |
| }: AlertListProps) { |
| const title = isReportEnabled ? t('report') : t('alert'); |
| const titlePlural = isReportEnabled ? t('reports') : t('alerts'); |
| const pathName = isReportEnabled ? 'Reports' : 'Alerts'; |
| const initalFilters = useMemo( |
| () => [ |
| { |
| id: 'type', |
| operator: FilterOperators.equals, |
| value: isReportEnabled ? 'Report' : 'Alert', |
| }, |
| ], |
| [isReportEnabled], |
| ); |
| const { |
| state: { |
| loading, |
| resourceCount: alertsCount, |
| resourceCollection: alerts, |
| bulkSelectEnabled, |
| lastFetched, |
| }, |
| hasPerm, |
| fetchData, |
| refreshData, |
| toggleBulkSelect, |
| } = useListViewResource<AlertObject>( |
| 'report', |
| t('reports'), |
| addDangerToast, |
| true, |
| undefined, |
| initalFilters, |
| ); |
| |
| const { updateResource } = useSingleViewResource<Partial<AlertObject>>( |
| 'report', |
| t('reports'), |
| addDangerToast, |
| ); |
| |
| const [alertModalOpen, setAlertModalOpen] = useState<boolean>(false); |
| const [currentAlert, setCurrentAlert] = useState<Partial<AlertObject> | null>( |
| null, |
| ); |
| const [ |
| currentAlertDeleting, |
| setCurrentAlertDeleting, |
| ] = useState<AlertObject | null>(null); |
| |
| // Actions |
| function handleAlertEdit(alert: AlertObject | null) { |
| setCurrentAlert(alert); |
| setAlertModalOpen(true); |
| } |
| |
| const canEdit = hasPerm('can_write'); |
| const canDelete = hasPerm('can_write'); |
| const canCreate = hasPerm('can_write'); |
| |
| const handleAlertDelete = ({ id, name }: AlertObject) => { |
| SupersetClient.delete({ |
| endpoint: `/api/v1/report/${id}`, |
| }).then( |
| () => { |
| refreshData(); |
| setCurrentAlertDeleting(null); |
| addSuccessToast(t('Deleted: %s', name)); |
| }, |
| createErrorHandler(errMsg => |
| addDangerToast(t('There was an issue deleting %s: %s', name, errMsg)), |
| ), |
| ); |
| }; |
| |
| const handleBulkAlertDelete = async (alertsToDelete: AlertObject[]) => { |
| try { |
| const { message } = await deleteAlerts( |
| alertsToDelete.map(({ id }) => id), |
| ); |
| refreshData(); |
| addSuccessToast(message); |
| } catch (e) { |
| createErrorHandler(errMsg => |
| addDangerToast( |
| t( |
| 'There was an issue deleting the selected %s: %s', |
| titlePlural, |
| errMsg, |
| ), |
| ), |
| )(e); |
| } |
| }; |
| |
| const initialSort = [{ id: 'name', desc: true }]; |
| |
| const toggleActive = (data: AlertObject, checked: boolean) => { |
| if (data && data.id) { |
| const update_id = data.id; |
| updateResource(update_id, { active: checked }).then(() => { |
| refreshData(); |
| }); |
| } |
| }; |
| |
| const columns = useMemo( |
| () => [ |
| { |
| Cell: ({ |
| row: { |
| original: { last_state: lastState }, |
| }, |
| }: any) => ( |
| <AlertStatusIcon |
| state={lastState} |
| isReportEnabled={isReportEnabled} |
| /> |
| ), |
| accessor: 'last_state', |
| size: 'xs', |
| disableSortBy: true, |
| }, |
| { |
| Cell: ({ |
| row: { |
| original: { last_eval_dttm: lastEvalDttm }, |
| }, |
| }: any) => |
| lastEvalDttm |
| ? moment.utc(lastEvalDttm).local().format(DATETIME_WITH_TIME_ZONE) |
| : '', |
| accessor: 'last_eval_dttm', |
| Header: t('Last run'), |
| size: 'lg', |
| }, |
| { |
| accessor: 'name', |
| Header: t('Name'), |
| size: 'xl', |
| }, |
| { |
| Header: t('Schedule'), |
| accessor: 'crontab_humanized', |
| size: 'xl', |
| Cell: ({ |
| row: { |
| original: { crontab_humanized = '' }, |
| }, |
| }: any) => ( |
| <Tooltip title={crontab_humanized} placement="topLeft"> |
| <span>{crontab_humanized}</span> |
| </Tooltip> |
| ), |
| }, |
| { |
| Cell: ({ |
| row: { |
| original: { recipients }, |
| }, |
| }: any) => |
| recipients.map((r: any) => ( |
| <RecipientIcon key={r.id} type={r.type} /> |
| )), |
| accessor: 'recipients', |
| Header: t('Notification method'), |
| disableSortBy: true, |
| size: 'xl', |
| }, |
| { |
| accessor: 'created_by', |
| disableSortBy: true, |
| hidden: true, |
| size: 'xl', |
| }, |
| { |
| Cell: ({ |
| row: { |
| original: { owners = [] }, |
| }, |
| }: any) => <FacePile users={owners} />, |
| Header: t('Owners'), |
| id: 'owners', |
| disableSortBy: true, |
| size: 'xl', |
| }, |
| { |
| Cell: ({ row: { original } }: any) => ( |
| <Switch |
| data-test="toggle-active" |
| checked={original.active} |
| onClick={(checked: boolean) => toggleActive(original, checked)} |
| size="small" |
| /> |
| ), |
| Header: t('Active'), |
| accessor: 'active', |
| id: 'active', |
| size: 'xl', |
| }, |
| { |
| Cell: ({ row: { original } }: any) => { |
| const history = useHistory(); |
| const handleEdit = () => handleAlertEdit(original); |
| const handleDelete = () => setCurrentAlertDeleting(original); |
| const handleGotoExecutionLog = () => |
| history.push(`/${original.type.toLowerCase()}/${original.id}/log`); |
| |
| const actions = [ |
| canEdit |
| ? { |
| label: 'execution-log-action', |
| tooltip: t('Execution log'), |
| placement: 'bottom', |
| icon: 'note' as IconName, |
| onClick: handleGotoExecutionLog, |
| } |
| : null, |
| canEdit |
| ? { |
| label: 'edit-action', |
| tooltip: t('Edit'), |
| placement: 'bottom', |
| icon: 'edit' as IconName, |
| onClick: handleEdit, |
| } |
| : null, |
| canDelete |
| ? { |
| label: 'delete-action', |
| tooltip: t('Delete'), |
| placement: 'bottom', |
| icon: 'trash' as IconName, |
| onClick: handleDelete, |
| } |
| : null, |
| ].filter(item => item !== null); |
| |
| return <ActionsBar actions={actions as ActionProps[]} />; |
| }, |
| Header: t('Actions'), |
| id: 'actions', |
| hidden: !canEdit && !canDelete, |
| disableSortBy: true, |
| size: 'xl', |
| }, |
| ], |
| [canDelete, canEdit, isReportEnabled], |
| ); |
| |
| const subMenuButtons: SubMenuProps['buttons'] = []; |
| |
| if (canCreate) { |
| subMenuButtons.push({ |
| name: ( |
| <> |
| <i className="fa fa-plus" /> {title} |
| </> |
| ), |
| buttonStyle: 'primary', |
| onClick: () => { |
| handleAlertEdit(null); |
| }, |
| }); |
| } |
| if (canDelete) { |
| subMenuButtons.push({ |
| name: t('Bulk select'), |
| onClick: toggleBulkSelect, |
| buttonStyle: 'secondary', |
| 'data-test': 'bulk-select-toggle', |
| }); |
| } |
| |
| const EmptyStateButton = ( |
| <Button buttonStyle="primary" onClick={() => handleAlertEdit(null)}> |
| <i className="fa fa-plus" /> {title} |
| </Button> |
| ); |
| |
| const emptyState = { |
| message: t('No %s yet', titlePlural), |
| slot: canCreate ? EmptyStateButton : null, |
| }; |
| |
| const filters: Filters = useMemo( |
| () => [ |
| { |
| Header: t('Created by'), |
| id: 'created_by', |
| input: 'select', |
| operator: FilterOperators.relationOneMany, |
| unfilteredLabel: 'All', |
| fetchSelects: createFetchRelated( |
| 'report', |
| 'created_by', |
| createErrorHandler(errMsg => |
| t('An error occurred while fetching created by values: %s', errMsg), |
| ), |
| user.userId, |
| ), |
| paginate: true, |
| }, |
| { |
| Header: t('Status'), |
| id: 'last_state', |
| input: 'select', |
| operator: FilterOperators.equals, |
| unfilteredLabel: 'Any', |
| selects: [ |
| { 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 }, |
| ], |
| }, |
| { |
| Header: t('Search'), |
| id: 'name', |
| input: 'search', |
| operator: FilterOperators.contains, |
| }, |
| ], |
| [], |
| ); |
| |
| return ( |
| <> |
| <SubMenu |
| activeChild={pathName} |
| name={t('Alerts & reports')} |
| tabs={[ |
| { |
| name: 'Alerts', |
| label: t('Alerts'), |
| url: '/alert/list/', |
| usesRouter: true, |
| }, |
| { |
| name: 'Reports', |
| label: t('Reports'), |
| url: '/report/list/', |
| usesRouter: true, |
| }, |
| ]} |
| buttons={subMenuButtons} |
| > |
| <RefreshContainer> |
| <LastUpdated updatedAt={lastFetched} update={() => refreshData()} /> |
| </RefreshContainer> |
| </SubMenu> |
| <AlertReportModal |
| alert={currentAlert} |
| addDangerToast={addDangerToast} |
| layer={currentAlert} |
| onHide={() => { |
| setAlertModalOpen(false); |
| refreshData(); |
| }} |
| show={alertModalOpen} |
| isReport={isReportEnabled} |
| /> |
| {currentAlertDeleting && ( |
| <DeleteModal |
| description={t( |
| 'This action will permanently delete %s.', |
| currentAlertDeleting.name, |
| )} |
| onConfirm={() => { |
| if (currentAlertDeleting) { |
| handleAlertDelete(currentAlertDeleting); |
| } |
| }} |
| onHide={() => setCurrentAlertDeleting(null)} |
| open |
| title={t('Delete %s?', title)} |
| /> |
| )} |
| <ConfirmStatusChange |
| title={t('Please confirm')} |
| description={t( |
| 'Are you sure you want to delete the selected %s?', |
| titlePlural, |
| )} |
| onConfirm={handleBulkAlertDelete} |
| > |
| {confirmDelete => { |
| const bulkActions: ListViewProps['bulkActions'] = canDelete |
| ? [ |
| { |
| key: 'delete', |
| name: t('Delete'), |
| onSelect: confirmDelete, |
| type: 'danger', |
| }, |
| ] |
| : []; |
| return ( |
| <ListView<AlertObject> |
| className="alerts-list-view" |
| columns={columns} |
| count={alertsCount} |
| data={alerts} |
| emptyState={emptyState} |
| fetchData={fetchData} |
| filters={filters} |
| initialSort={initialSort} |
| loading={loading} |
| bulkActions={bulkActions} |
| bulkSelectEnabled={bulkSelectEnabled} |
| disableBulkSelect={toggleBulkSelect} |
| pageSize={PAGE_SIZE} |
| /> |
| ); |
| }} |
| </ConfirmStatusChange> |
| </> |
| ); |
| } |
| |
| export default withToasts(AlertList); |