From 671ee22c428f48a41d54d55b59cda541d6b7d21f Mon Sep 17 00:00:00 2001 From: riahk Date: Mon, 16 Nov 2020 15:56:54 -0800 Subject: [PATCH 01/19] alert list modal --- .../src/views/CRUD/alert/AlertList.tsx | 31 ++- .../src/views/CRUD/alert/AlertReportModal.tsx | 188 ++++++++++++++++++ superset/config.py | 2 +- 3 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx diff --git a/superset-frontend/src/views/CRUD/alert/AlertList.tsx b/superset-frontend/src/views/CRUD/alert/AlertList.tsx index bed3742b01e4..362dab472cfc 100644 --- a/superset-frontend/src/views/CRUD/alert/AlertList.tsx +++ b/superset-frontend/src/views/CRUD/alert/AlertList.tsx @@ -17,19 +17,21 @@ * under the License. */ -import { t } from '@superset-ui/core'; -import React, { useEffect, useMemo } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; -import { Switch } from 'src/common/components/Switch'; +import { t } from '@superset-ui/core'; +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 ListView, { FilterOperators, Filters } from 'src/components/ListView'; -import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar'; import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu'; +import { Switch } from 'src/common/components/Switch'; import withToasts from 'src/messageToasts/enhancers/withToasts'; +import AlertReportModal from './AlertReportModal'; import AlertStatusIcon from 'src/views/CRUD/alert/components/AlertStatusIcon'; import RecipientIcon from 'src/views/CRUD/alert/components/RecipientIcon'; + import { useListViewResource, useSingleViewResource, @@ -85,6 +87,16 @@ function AlertList({ addDangerToast, ); + const [alertModalOpen, setAlertModalOpen] = useState(false); + const [currentAlert, setCurrentAlert] = useState(null); + + // Actions + function handleAlertEdit(alert: AlertObject | null) { + setCurrentAlert(alert); + setAlertModalOpen(true); + } + + const canEdit = hasPerm('can_edit'); const canDelete = hasPerm('can_delete'); const canCreate = hasPerm('can_add'); @@ -217,6 +229,7 @@ function AlertList({ ); const subMenuButtons: SubMenuProps['buttons'] = []; + if (canCreate) { subMenuButtons.push({ name: ( @@ -225,7 +238,9 @@ function AlertList({ ), buttonStyle: 'primary', - onClick: () => {}, + onClick: () => { + handleAlertEdit(null); + }, }); } @@ -303,6 +318,12 @@ function AlertList({ ]} buttons={subMenuButtons} /> + setAlertModalOpen(false)} + show={alertModalOpen} + /> className="alerts-list-view" columns={columns} diff --git a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx new file mode 100644 index 000000000000..08f250e1373c --- /dev/null +++ b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx @@ -0,0 +1,188 @@ +/** + * 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, { FunctionComponent, useState, useEffect } from 'react'; +import { styled, t } from '@superset-ui/core'; +// import { useSingleViewResource } from 'src/views/CRUD/hooks'; + +import Icon from 'src/components/Icon'; +import Modal from 'src/common/components/Modal'; +import withToasts from 'src/messageToasts/enhancers/withToasts'; + +import { AlertObject } from './types'; + +interface AlertReportModalProps { + addDangerToast: (msg: string) => void; + alert?: AlertObject | null; + isReport?: boolean; + onAdd?: (alert?: AlertObject) => void; + onHide: () => void; + show: boolean; +} + +const StyledIcon = styled(Icon)` + margin: auto ${({ theme }) => theme.gridUnit * 2}px auto 0; +`; + +const AlertReportModal: FunctionComponent = ({ + addDangerToast, + onAdd, + onHide, + show, + alert = null, + isReport = false, +}) => { + const [disableSave, setDisableSave] = useState(true); + const [currentAlert, setCurrentAlert] = useState(); + const [isHidden, setIsHidden] = useState(true); + const isEditMode = alert !== null; + + // TODO: Alert fetch logic + /* const { + state: { loading, resource }, + fetchResource, + createResource, + updateResource, + } = useSingleViewResource( + 'alert', + t('alert'), + addDangerToast, + ); */ + + // Functions + const hide = () => { + setIsHidden(true); + onHide(); + }; + + const onSave = () => { + if (isEditMode) { + // Edit + if (currentAlert && currentAlert.id) { + /* const update_id = currentAlert.id; + delete currentAlert.id; + delete currentAlert.created_by; + + updateResource(update_id, currentAlert).then(() => { + if (onAdd) { + onAdd(); + } + + hide(); + }); */ + hide(); + } + } else if (currentAlert) { + // Create + /* createResource(currentAlert).then(response => { + if (onAdd) { + onAdd(response); + } + + hide(); + }); */ + hide(); + } + }; + + // Handle input/textarea updates + /*const onTextChange = ( + event: + | React.ChangeEvent + | React.ChangeEvent, + ) => { + const { target } = event; + const data = { + ...currentAlert, + name: currentAlert ? currentAlert.name : '', + }; + + data[target.name] = target.value; + setCurrentAlert(data); + };*/ + + const validate = () => { + if (currentAlert && currentAlert.name.length) { + setDisableSave(false); + } else { + setDisableSave(true); + } + }; + + // Initialize + if ( + isEditMode && + (!currentAlert || + !currentAlert.id || + (alert && alert.id !== currentAlert.id) || + (isHidden && show)) + ) { + if (alert && alert.id !== null /*&& !loading*/) { + /* const id = alert.id || 0; + + fetchResource(id).then(() => { + setCurrentAlert(resource); + }); */ + } + } else if ( + !isEditMode && + (!currentAlert || currentAlert.id || (isHidden && show)) + ) { + // TODO: update to match expected type variables + setCurrentAlert({ + name: '', + }); + } + + // Validation + useEffect(() => { + validate(); + }, [currentAlert ? currentAlert.name : '']); + + // Show/hide + if (isHidden && show) { + setIsHidden(false); + } + + return ( + + {isEditMode ? ( + + ) : ( + + )} + {isEditMode + ? t(`Edit ${isReport ? 'Report' : 'Alert'}`) + : t(`Add ${isReport ? 'Report' : 'Alert'}`)} + + } + > +
Content Here
+
+ ); +}; + +export default withToasts(AlertReportModal); diff --git a/superset/config.py b/superset/config.py index f20cbff716d9..8fb3fd5f33ed 100644 --- a/superset/config.py +++ b/superset/config.py @@ -801,7 +801,7 @@ class CeleryConfig: # pylint: disable=too-few-public-methods # Enable / disable Alerts, where users can define custom SQL that # will send emails with screenshots of charts or dashboards periodically # if it meets the criteria -ENABLE_ALERTS = False +ENABLE_ALERTS = True # Slack API token for the superset reports SLACK_API_TOKEN = None From 5ba092db44767049ca344a607f9c7042ed482d3f Mon Sep 17 00:00:00 2001 From: riahk Date: Mon, 16 Nov 2020 17:31:42 -0800 Subject: [PATCH 02/19] basic layout w/ placeholder inputs --- .../src/common/components/Modal/Modal.tsx | 4 + .../src/views/CRUD/alert/AlertReportModal.tsx | 281 +++++++++++++++++- .../src/views/CRUD/alert/types.ts | 2 + 3 files changed, 283 insertions(+), 4 deletions(-) diff --git a/superset-frontend/src/common/components/Modal/Modal.tsx b/superset-frontend/src/common/components/Modal/Modal.tsx index 4431fd32c85d..dc8b28ac3267 100644 --- a/superset-frontend/src/common/components/Modal/Modal.tsx +++ b/superset-frontend/src/common/components/Modal/Modal.tsx @@ -105,6 +105,10 @@ const StyledModal = styled(BaseModal)` .ant-tabs { margin-top: -${({ theme }) => theme.gridUnit * 4}px; } + + &.no-content-padding .ant-modal-body { + padding: 0; + } `; const CustomModal = ({ diff --git a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx index 08f250e1373c..7235f8334e1b 100644 --- a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx +++ b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx @@ -39,6 +39,110 @@ const StyledIcon = styled(Icon)` margin: auto ${({ theme }) => theme.gridUnit * 2}px auto 0; `; +const StyledSectionContainer = styled.div` + display: flex; + flex-direction: column; + + .header-section { + display: flex; + flex: 0 0 auto; + width: 100%; + padding: ${({ theme }) => theme.gridUnit * 4}px; + border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; + } + + .column-section { + display: flex; + flex: 1 1 auto; + + .column { + flex: 1 1 auto; + min-width: 33.33%; + padding: ${({ theme }) => theme.gridUnit * 4}px; + + &.condition { + border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; + } + + &.message { + border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; + } + } + } + + .inline-container { + display: flex; + flex-direction: row; + + > div { + flex: 1 1 auto; + } + } +`; + +const StyledSectionTitle = styled.div` + margin: ${({ theme }) => theme.gridUnit * 2}px auto + ${({ theme }) => theme.gridUnit * 4}px auto; +`; + +const StyledInputContainer = styled.div` + flex: 1 1 auto; + margin: ${({ theme }) => theme.gridUnit * 2}px; + margin-top: 0; + + .required { + margin-left: ${({ theme }) => theme.gridUnit / 2}px; + color: ${({ theme }) => theme.colors.error.base}; + } + + .input-container { + display: flex; + align-items: center; + + label { + display: flex; + margin-right: ${({ theme }) => theme.gridUnit * 2}px; + } + + i { + margin: 0 ${({ theme }) => theme.gridUnit}px; + } + } + + input, + textarea { + flex: 1 1 auto; + } + + textarea { + height: 160px; + resize: none; + } + + input::placeholder, + textarea::placeholder { + color: ${({ theme }) => theme.colors.grayscale.light1}; + } + + textarea, + input[type='text'], + input[type='number'] { + padding: ${({ theme }) => theme.gridUnit * 1.5}px + ${({ theme }) => theme.gridUnit * 2}px; + border-style: none; + border: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; + border-radius: ${({ theme }) => theme.gridUnit}px; + + &[name='description'] { + flex: 1 1 auto; + } + } + + .input-label { + margin-left: 10px; + } +`; + const AlertReportModal: FunctionComponent = ({ addDangerToast, onAdd, @@ -101,7 +205,7 @@ const AlertReportModal: FunctionComponent = ({ }; // Handle input/textarea updates - /*const onTextChange = ( + const onTextChange = ( event: | React.ChangeEvent | React.ChangeEvent, @@ -114,7 +218,7 @@ const AlertReportModal: FunctionComponent = ({ data[target.name] = target.value; setCurrentAlert(data); - };*/ + }; const validate = () => { if (currentAlert && currentAlert.name.length) { @@ -132,7 +236,7 @@ const AlertReportModal: FunctionComponent = ({ (alert && alert.id !== currentAlert.id) || (isHidden && show)) ) { - if (alert && alert.id !== null /*&& !loading*/) { + if (alert && alert.id !== null /* && !loading */) { /* const id = alert.id || 0; fetchResource(id).then(() => { @@ -161,6 +265,7 @@ const AlertReportModal: FunctionComponent = ({ return ( = ({ } > -
Content Here
+ +
+ +
+ {t('Alert Name')} + * +
+
+ +
+
+ +
+ {t('Owners')} + * +
+
+ +
+
+ +
{t('Description')}
+
+ +
+
+
+
+ {!isReport && ( +
+ +

{t('Alert Condition')}

+
+ +
+ {t('Source')} + * +
+
+ +
+
+ +
+ {t('SQL Query')} + * +
+
+