From c9bfb047eaf847a4267cb9c18b16091d55b39127 Mon Sep 17 00:00:00 2001 From: Nick Fitzpatrick <nick@metabase.com> Date: Wed, 23 Oct 2024 18:03:21 -0300 Subject: [PATCH] Alert isValid check only looks at configured channels (#48485) * converting UpdateAlertModalContent and CreateAlertModalContent to functional * lint stuff * e2e adjustments: * type and e2e adjustments --- e2e/test/scenarios/question/saved.cy.spec.js | 7 +- .../scenarios/sharing/alert/alert.cy.spec.js | 19 ++- .../sharing/alert/email-alert.cy.spec.js | 1 + .../sharing/sharing-reproductions.cy.spec.js | 2 + frontend/src/metabase-lib/v1/Alert/Alert.ts | 8 +- .../components/ModalContent/ModalContent.tsx | 2 +- frontend/src/metabase/lib/alert.js | 12 +- frontend/src/metabase/lib/pulse.ts | 18 ++ frontend/src/metabase/pulse/selectors.js | 23 --- .../AlertListPopoverContent/AlertPopover.tsx | 12 +- .../AlertModals/CreateAlertModalContent.jsx | 159 ------------------ .../AlertModals/CreateAlertModalContent.tsx | 135 +++++++++++++++ .../AlertModals/UpdateAlertModalContent.jsx | 101 ----------- .../AlertModals/UpdateAlertModalContent.tsx | 93 ++++++++++ 14 files changed, 290 insertions(+), 302 deletions(-) delete mode 100644 frontend/src/metabase/query_builder/components/AlertModals/CreateAlertModalContent.jsx create mode 100644 frontend/src/metabase/query_builder/components/AlertModals/CreateAlertModalContent.tsx delete mode 100644 frontend/src/metabase/query_builder/components/AlertModals/UpdateAlertModalContent.jsx create mode 100644 frontend/src/metabase/query_builder/components/AlertModals/UpdateAlertModalContent.tsx diff --git a/e2e/test/scenarios/question/saved.cy.spec.js b/e2e/test/scenarios/question/saved.cy.spec.js index 304cbac69b1..3b4811187b8 100644 --- a/e2e/test/scenarios/question/saved.cy.spec.js +++ b/e2e/test/scenarios/question/saved.cy.spec.js @@ -31,6 +31,7 @@ import { sidesheet, summarize, tableHeaderClick, + toggleAlertChannel, visitQuestion, } from "e2e/support/helpers"; @@ -497,10 +498,8 @@ describe( popover().findByText("Create alert").click(); modal().button("Set up an alert").click(); modal().within(() => { - getAlertChannel(secondWebhookName).scrollIntoView(); - getAlertChannel(secondWebhookName) - .findByRole("checkbox") - .click({ force: true }); + toggleAlertChannel("Email"); + toggleAlertChannel(secondWebhookName); cy.button("Done").click(); }); cy.findByTestId("sharing-menu-button").click(); diff --git a/e2e/test/scenarios/sharing/alert/alert.cy.spec.js b/e2e/test/scenarios/sharing/alert/alert.cy.spec.js index 7e19af0a82c..2deb179e6c6 100644 --- a/e2e/test/scenarios/sharing/alert/alert.cy.spec.js +++ b/e2e/test/scenarios/sharing/alert/alert.cy.spec.js @@ -17,7 +17,18 @@ import { visitQuestion, } from "e2e/support/helpers"; -const channels = { slack: mockSlackConfigured, email: setupSMTP }; +const channels = { + slack: { + setup: mockSlackConfigured, + createAlert: () => { + toggleAlertChannel("Email"); + toggleAlertChannel("Slack"); + cy.findByPlaceholderText(/Pick a user or channel/).click(); + popover().findByText("#work").click(); + }, + }, + email: { setup: setupSMTP, createAlert: () => {} }, +}; describe("scenarios > alert", () => { beforeEach(() => { @@ -66,9 +77,9 @@ describe("scenarios > alert", () => { }); }); - Object.entries(channels).forEach(([channel, setup]) => { + Object.entries(channels).forEach(([channel, config]) => { describe(`with ${channel} set up`, { tags: "@external" }, () => { - beforeEach(setup); + beforeEach(config.setup); it("educational screen should show for the first alert, but not for the second", () => { cy.intercept("POST", "/api/alert").as("savedAlert"); @@ -94,6 +105,8 @@ describe("scenarios > alert", () => { // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage cy.findByText("Set up an alert").click(); + + config.createAlert(); // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage cy.findByText("Done").click(); diff --git a/e2e/test/scenarios/sharing/alert/email-alert.cy.spec.js b/e2e/test/scenarios/sharing/alert/email-alert.cy.spec.js index bf62504da7f..dd3b83d2680 100644 --- a/e2e/test/scenarios/sharing/alert/email-alert.cy.spec.js +++ b/e2e/test/scenarios/sharing/alert/email-alert.cy.spec.js @@ -58,6 +58,7 @@ describe("scenarios > alert > email_alert", { tags: "@external" }, () => { it("should respect email alerts toggled off (metabase#12349)", () => { updateSetting("report-timezone", "America/New_York"); + mockSlackConfigured(); //For this test, we need to pretend that slack is set up mockSlackConfigured(); diff --git a/e2e/test/scenarios/sharing/sharing-reproductions.cy.spec.js b/e2e/test/scenarios/sharing/sharing-reproductions.cy.spec.js index 44954979489..1a64a3346c8 100644 --- a/e2e/test/scenarios/sharing/sharing-reproductions.cy.spec.js +++ b/e2e/test/scenarios/sharing/sharing-reproductions.cy.spec.js @@ -17,6 +17,7 @@ import { getDashboardCard, getFullName, getIframeBody, + mockSlackConfigured, modal, openAndAddEmailsToSubscriptions, openNewPublicLinkDropdown, @@ -960,6 +961,7 @@ describe("issue 17547", () => { beforeEach(() => { restore(); cy.signInAsAdmin(); + mockSlackConfigured(); cy.createQuestion(questionDetails).then(({ body: { id: questionId } }) => { setUpAlert(questionId); diff --git a/frontend/src/metabase-lib/v1/Alert/Alert.ts b/frontend/src/metabase-lib/v1/Alert/Alert.ts index a556fe2814d..b674f5d6f4f 100644 --- a/frontend/src/metabase-lib/v1/Alert/Alert.ts +++ b/frontend/src/metabase-lib/v1/Alert/Alert.ts @@ -5,11 +5,11 @@ import type { User } from "metabase-types/api/user"; import { ALERT_TYPE_ROWS } from "./constants"; export const getDefaultAlert = ( - question: Question, - user: User, + question: Question | undefined, + user: User | null, visualizationSettings: VisualizationSettings, ) => { - const alertType = question.alertType(visualizationSettings); + const alertType = question?.alertType(visualizationSettings); const typeDependentAlertFields = alertType === ALERT_TYPE_ROWS ? { @@ -24,7 +24,7 @@ export const getDefaultAlert = ( return { card: { - id: question.id(), + id: question?.id(), include_csv: false, include_xls: false, }, diff --git a/frontend/src/metabase/components/ModalContent/ModalContent.tsx b/frontend/src/metabase/components/ModalContent/ModalContent.tsx index a1b883d526f..cbb23d949e1 100644 --- a/frontend/src/metabase/components/ModalContent/ModalContent.tsx +++ b/frontend/src/metabase/components/ModalContent/ModalContent.tsx @@ -11,7 +11,7 @@ import type { CommonModalProps } from "./types"; export interface ModalContentProps extends CommonModalProps { "data-testid"?: string; id?: string; - title: string; + title?: string; footer?: ReactNode; children: ReactNode; diff --git a/frontend/src/metabase/lib/alert.js b/frontend/src/metabase/lib/alert.js index 860ab019cda..b70bd2d947b 100644 --- a/frontend/src/metabase/lib/alert.js +++ b/frontend/src/metabase/lib/alert.js @@ -22,7 +22,15 @@ export function channelIsEnabled(channel) { return channel.enabled; } -export function alertIsValid(alert) { +export function alertIsValid(alert, channelSpec) { const enabledChannels = alert.channels.filter(channelIsEnabled); - return enabledChannels.length > 0 && enabledChannels.every(channelIsValid); + + return ( + channelSpec.channels && + enabledChannels.length > 0 && + enabledChannels.every(channel => channelIsValid(channel)) && + enabledChannels + .filter(c => c.enabled) + .every(c => channelSpec.channels[c.channel_type]?.configured) + ); } diff --git a/frontend/src/metabase/lib/pulse.ts b/frontend/src/metabase/lib/pulse.ts index 1db937155f2..86fc6981cbb 100644 --- a/frontend/src/metabase/lib/pulse.ts +++ b/frontend/src/metabase/lib/pulse.ts @@ -8,6 +8,7 @@ import { } from "metabase-lib/v1/parameters/utils/parameter-values"; import type { Channel, + ChannelApiResponse, ChannelSpec, Pulse, PulseParameter, @@ -214,3 +215,20 @@ export function getActivePulseParameters( (parameter: any) => parameter.value != null, ); } + +export const getHasConfiguredAnyChannel = ( + formInput: Partial<ChannelApiResponse>, +) => + (formInput.channels && + _.some(Object.values(formInput.channels), c => c.configured)) || + false; + +export const getHasConfiguredEmailChannel = ( + formInput: Partial<ChannelApiResponse>, +) => + (formInput.channels && + _.some( + Object.values(formInput.channels), + c => c.type === "email" && c.configured, + )) || + false; diff --git a/frontend/src/metabase/pulse/selectors.js b/frontend/src/metabase/pulse/selectors.js index d31fb454fc4..87e24c422c3 100644 --- a/frontend/src/metabase/pulse/selectors.js +++ b/frontend/src/metabase/pulse/selectors.js @@ -1,32 +1,9 @@ -import { createSelector } from "@reduxjs/toolkit"; import _ from "underscore"; export const getEditingPulse = state => state.pulse.editingPulse; export const getPulseFormInput = state => state.pulse?.formInput; -export const hasLoadedChannelInfoSelector = createSelector( - [getPulseFormInput], - formInput => !!formInput.channels, -); -export const hasConfiguredAnyChannelSelector = createSelector( - [getPulseFormInput], - formInput => - (formInput.channels && - _.some(Object.values(formInput.channels), c => c.configured)) || - false, -); -export const hasConfiguredEmailChannelSelector = createSelector( - [getPulseFormInput], - formInput => - (formInput.channels && - _.some( - Object.values(formInput.channels), - c => c.type === "email" && c.configured, - )) || - false, -); - export const getPulseCardPreviews = state => state.pulse.cardPreviews; export const getPulseId = (state, props) => diff --git a/frontend/src/metabase/query_builder/components/AlertListPopoverContent/AlertPopover.tsx b/frontend/src/metabase/query_builder/components/AlertListPopoverContent/AlertPopover.tsx index e6f5e56117c..4f9c752d846 100644 --- a/frontend/src/metabase/query_builder/components/AlertListPopoverContent/AlertPopover.tsx +++ b/frontend/src/metabase/query_builder/components/AlertListPopoverContent/AlertPopover.tsx @@ -85,11 +85,13 @@ export const AlertPopover = forwardRef(function _AlertPopover( isOpen={showingElement === "update-modal"} onClose={onClose} > - <UpdateAlertModalContent - alert={editingAlert} - onCancel={onClose} - onAlertUpdated={onClose} - /> + {editingAlert && ( + <UpdateAlertModalContent + alert={editingAlert} + onCancel={onClose} + onAlertUpdated={onClose} + /> + )} </Modal> </> ); diff --git a/frontend/src/metabase/query_builder/components/AlertModals/CreateAlertModalContent.jsx b/frontend/src/metabase/query_builder/components/AlertModals/CreateAlertModalContent.jsx deleted file mode 100644 index 05e2b0178f1..00000000000 --- a/frontend/src/metabase/query_builder/components/AlertModals/CreateAlertModalContent.jsx +++ /dev/null @@ -1,159 +0,0 @@ -/* eslint-disable react/prop-types */ -import cx from "classnames"; -import { Component } from "react"; -import { connect } from "react-redux"; -import { t } from "ttag"; - -import { createAlert } from "metabase/alert/alert"; -import ButtonWithStatus from "metabase/components/ButtonWithStatus"; -import ChannelSetupModal from "metabase/components/ChannelSetupModal"; -import ModalContent from "metabase/components/ModalContent"; -import Button from "metabase/core/components/Button"; -import CS from "metabase/css/core/index.css"; -import { alertIsValid } from "metabase/lib/alert"; -import MetabaseCookies from "metabase/lib/cookies"; -import { fetchPulseFormInput } from "metabase/pulse/actions"; -import { - hasConfiguredAnyChannelSelector, - hasConfiguredEmailChannelSelector, - hasLoadedChannelInfoSelector, -} from "metabase/pulse/selectors"; -import { apiUpdateQuestion, updateUrl } from "metabase/query_builder/actions"; -import { - getQuestion, - getVisualizationSettings, -} from "metabase/query_builder/selectors"; -import { getUser, getUserIsAdmin } from "metabase/selectors/user"; -import { getDefaultAlert } from "metabase-lib/v1/Alert"; - -import { AlertEditForm } from "./AlertEditForm"; -import { AlertEducationalScreen } from "./AlertEducationalScreen"; -import { AlertModalTitle } from "./AlertModalTitle"; -import { AlertModalFooter } from "./AlertModals.styled"; - -class CreateAlertModalContentInner extends Component { - constructor(props) { - super(); - - const { question, user, visualizationSettings } = props; - - this.state = { - hasSeenEducationalScreen: MetabaseCookies.getHasSeenAlertSplash(), - alert: getDefaultAlert(question, user, visualizationSettings), - }; - } - - UNSAFE_componentWillReceiveProps(newProps) { - // NOTE Atte Keinänen 11/6/17: Don't fill in the card information yet - // Because `onCreate` and `onSave` of QueryHeader mix Redux action dispatches and `setState` calls, - // we don't have up-to-date card information in the constructor yet - // TODO: Refactor QueryHeader so that `onCreate` and `onSave` only call Redux actions and don't modify the local state - if (this.props.question !== newProps.question) { - this.setState({ - alert: { - ...this.state.alert, - card: { ...this.state.alert.card, id: newProps.question.id() }, - }, - }); - } - } - - UNSAFE_componentWillMount() { - // loads the channel information - this.props.fetchPulseFormInput(); - } - - onAlertChange = alert => this.setState({ alert }); - - onCreateAlert = async () => { - const { question, createAlert, updateUrl, onAlertCreated } = this.props; - const { alert } = this.state; - - await createAlert(alert); - await updateUrl(question, { dirty: false }); - - onAlertCreated(); - }; - - proceedFromEducationalScreen = () => { - MetabaseCookies.setHasSeenAlertSplash(true); - this.setState({ hasSeenEducationalScreen: true }); - }; - - render() { - const { - question, - visualizationSettings, - onCancel, - hasConfiguredAnyChannel, - hasConfiguredEmailChannel, - isAdmin, - user, - hasLoadedChannelInfo, - } = this.props; - const { alert, hasSeenEducationalScreen } = this.state; - - const channelRequirementsMet = isAdmin - ? hasConfiguredAnyChannel - : hasConfiguredEmailChannel; - const isValid = alertIsValid(alert); - - if (hasLoadedChannelInfo && !channelRequirementsMet) { - return ( - <ChannelSetupModal - user={user} - onClose={onCancel} - entityNamePlural={t`alerts`} - channels={isAdmin ? ["email", "Slack", "Webhook"] : ["email"]} - /> - ); - } - if (!hasSeenEducationalScreen) { - return ( - <ModalContent onClose={onCancel} data-testid="alert-education-screen"> - <AlertEducationalScreen - onProceed={this.proceedFromEducationalScreen} - /> - </ModalContent> - ); - } - - // TODO: Remove PulseEdit css hack - return ( - <ModalContent data-testid="alert-create" onClose={onCancel}> - <div - className={cx(CS.mlAuto, CS.mrAuto, CS.mb4)} - style={{ maxWidth: "550px" }} - > - <AlertModalTitle text={t`Let's set up your alert`} /> - <AlertEditForm - alertType={question.alertType(visualizationSettings)} - alert={alert} - onAlertChange={this.onAlertChange} - /> - <AlertModalFooter> - <Button onClick={onCancel} className={CS.mr2}>{t`Cancel`}</Button> - <ButtonWithStatus - titleForState={{ default: t`Done` }} - disabled={!isValid} - onClickOperation={this.onCreateAlert} - /> - </AlertModalFooter> - </div> - </ModalContent> - ); - } -} - -export const CreateAlertModalContent = connect( - state => ({ - question: getQuestion(state), - visualizationSettings: getVisualizationSettings(state), - isAdmin: getUserIsAdmin(state), - user: getUser(state), - hasLoadedChannelInfo: hasLoadedChannelInfoSelector(state), - hasConfiguredAnyChannel: hasConfiguredAnyChannelSelector(state), - hasConfiguredEmailChannel: hasConfiguredEmailChannelSelector(state), - }), - { createAlert, fetchPulseFormInput, apiUpdateQuestion, updateUrl }, -)(CreateAlertModalContentInner); diff --git a/frontend/src/metabase/query_builder/components/AlertModals/CreateAlertModalContent.tsx b/frontend/src/metabase/query_builder/components/AlertModals/CreateAlertModalContent.tsx new file mode 100644 index 00000000000..17921384a26 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/AlertModals/CreateAlertModalContent.tsx @@ -0,0 +1,135 @@ +import cx from "classnames"; +import { useEffect, useState } from "react"; +import { t } from "ttag"; + +import { createAlert } from "metabase/alert/alert"; +import { useGetChannelInfoQuery } from "metabase/api"; +import ButtonWithStatus from "metabase/components/ButtonWithStatus"; +import ChannelSetupModal from "metabase/components/ChannelSetupModal"; +import ModalContent from "metabase/components/ModalContent"; +import Button from "metabase/core/components/Button"; +import CS from "metabase/css/core/index.css"; +import { alertIsValid } from "metabase/lib/alert"; +import MetabaseCookies from "metabase/lib/cookies"; +import { + getHasConfiguredAnyChannel, + getHasConfiguredEmailChannel, +} from "metabase/lib/pulse"; +import { useDispatch, useSelector } from "metabase/lib/redux"; +import { updateUrl } from "metabase/query_builder/actions"; +import { + getQuestion, + getVisualizationSettings, +} from "metabase/query_builder/selectors"; +import { getUser, getUserIsAdmin } from "metabase/selectors/user"; +import { getDefaultAlert } from "metabase-lib/v1/Alert"; +import type { Alert } from "metabase-types/api"; + +import { AlertEditForm } from "./AlertEditForm"; +import { AlertEducationalScreen } from "./AlertEducationalScreen"; +import { AlertModalTitle } from "./AlertModalTitle"; +import { AlertModalFooter } from "./AlertModals.styled"; + +interface CreateAlertModalContentProps { + onAlertCreated: () => void; + onCancel: () => void; +} + +export const CreateAlertModalContent = ({ + onAlertCreated, + onCancel, +}: CreateAlertModalContentProps) => { + const dispatch = useDispatch(); + const question = useSelector(getQuestion); + const visualizationSettings = useSelector(getVisualizationSettings); + const isAdmin = useSelector(getUserIsAdmin); + const user = useSelector(getUser); + + const { data: channelSpec = {}, isLoading: isLoadingChannelInfo } = + useGetChannelInfoQuery(); + + const hasConfiguredAnyChannel = getHasConfiguredAnyChannel(channelSpec); + const hasConfiguredEmailChannel = getHasConfiguredEmailChannel(channelSpec); + + const [alert, setAlert] = useState<any>( + getDefaultAlert(question, user, visualizationSettings), + ); + + const [hasSeenEducationalScreen, setHasSeenEducationalScreen] = useState( + MetabaseCookies.getHasSeenAlertSplash(), + ); + + useEffect(() => { + // NOTE Atte Keinänen 11/6/17: Don't fill in the card information yet + // Because `onCreate` and `onSave` of QueryHeader mix Redux action dispatches and `setState` calls, + // we don't have up-to-date card information in the constructor yet + // TODO: Refactor QueryHeader so that `onCreate` and `onSave` only call Redux actions and don't modify the local state + setAlert((currentAlert: any) => ({ + ...currentAlert, + card: { ...currentAlert.card, id: question?.id() }, + })); + }, [question]); + + const onAlertChange = (newAlert: Alert) => setAlert(newAlert); + + const onCreateAlert = async () => { + await dispatch(createAlert(alert)); + await dispatch(updateUrl(question, { dirty: false })); + + onAlertCreated(); + }; + + const proceedFromEducationalScreen = () => { + MetabaseCookies.setHasSeenAlertSplash(true); + setHasSeenEducationalScreen(true); + }; + + const channelRequirementsMet = isAdmin + ? hasConfiguredAnyChannel + : hasConfiguredEmailChannel; + + const isValid = alertIsValid(alert, channelSpec); + + if (!isLoadingChannelInfo && !channelRequirementsMet) { + return ( + <ChannelSetupModal + user={user} + onClose={onCancel} + entityNamePlural={t`alerts`} + channels={isAdmin ? ["email", "Slack", "Webhook"] : ["email"]} + /> + ); + } + if (!hasSeenEducationalScreen) { + return ( + <ModalContent onClose={onCancel} data-testid="alert-education-screen"> + <AlertEducationalScreen onProceed={proceedFromEducationalScreen} /> + </ModalContent> + ); + } + + // TODO: Remove PulseEdit css hack + return ( + <ModalContent data-testid="alert-create" onClose={onCancel}> + <div + className={cx(CS.mlAuto, CS.mrAuto, CS.mb4)} + style={{ maxWidth: "550px" }} + > + <AlertModalTitle text={t`Let's set up your alert`} /> + <AlertEditForm + alertType={question?.alertType(visualizationSettings)} + alert={alert} + onAlertChange={onAlertChange} + /> + <AlertModalFooter> + <Button onClick={onCancel} className={CS.mr2}>{t`Cancel`}</Button> + <ButtonWithStatus + titleForState={{ default: t`Done` }} + disabled={!isValid} + onClickOperation={onCreateAlert} + /> + </AlertModalFooter> + </div> + </ModalContent> + ); +}; diff --git a/frontend/src/metabase/query_builder/components/AlertModals/UpdateAlertModalContent.jsx b/frontend/src/metabase/query_builder/components/AlertModals/UpdateAlertModalContent.jsx deleted file mode 100644 index 2763dbb0d2d..00000000000 --- a/frontend/src/metabase/query_builder/components/AlertModals/UpdateAlertModalContent.jsx +++ /dev/null @@ -1,101 +0,0 @@ -/* eslint-disable react/prop-types */ -import cx from "classnames"; -import { Component } from "react"; -import { connect } from "react-redux"; -import { t } from "ttag"; - -import { deleteAlert, updateAlert } from "metabase/alert/alert"; -import ButtonWithStatus from "metabase/components/ButtonWithStatus"; -import ModalContent from "metabase/components/ModalContent"; -import Button from "metabase/core/components/Button"; -import CS from "metabase/css/core/index.css"; -import { alertIsValid } from "metabase/lib/alert"; -import { updateUrl } from "metabase/query_builder/actions"; -import { - getQuestion, - getVisualizationSettings, -} from "metabase/query_builder/selectors"; -import { getUser, getUserIsAdmin } from "metabase/selectors/user"; - -import { AlertEditForm } from "./AlertEditForm"; -import { AlertModalTitle } from "./AlertModalTitle"; -import { AlertModalFooter } from "./AlertModals.styled"; -import { DeleteAlertSection } from "./DeleteAlertSection"; - -class UpdateAlertModalContentInner extends Component { - constructor(props) { - super(); - this.state = { - modifiedAlert: props.alert, - }; - } - - onAlertChange = modifiedAlert => this.setState({ modifiedAlert }); - - onUpdateAlert = async () => { - const { question, updateAlert, updateUrl, onAlertUpdated } = this.props; - const { modifiedAlert } = this.state; - - await updateAlert(modifiedAlert); - await updateUrl(question, { dirty: false }); - onAlertUpdated(); - }; - - onDeleteAlert = async () => { - const { alert, deleteAlert, onAlertUpdated } = this.props; - await deleteAlert(alert.id); - onAlertUpdated(); - }; - - render() { - const { onCancel, question, visualizationSettings, alert, user, isAdmin } = - this.props; - const { modifiedAlert } = this.state; - - const isCurrentUser = alert.creator.id === user.id; - const title = isCurrentUser ? t`Edit your alert` : t`Edit alert`; - const isValid = alertIsValid(alert); - - // TODO: Remove PulseEdit css hack - return ( - <ModalContent onClose={onCancel} data-testid="alert-edit"> - <div - className={cx(CS.mlAuto, CS.mrAuto, CS.mb4)} - style={{ maxWidth: "550px" }} - > - <AlertModalTitle text={title} /> - <AlertEditForm - alertType={question.alertType(visualizationSettings)} - alert={modifiedAlert} - onAlertChange={this.onAlertChange} - /> - {isAdmin && ( - <DeleteAlertSection - alert={alert} - onDeleteAlert={this.onDeleteAlert} - /> - )} - - <AlertModalFooter> - <Button onClick={onCancel} className={CS.mr2}>{t`Cancel`}</Button> - <ButtonWithStatus - titleForState={{ default: t`Save changes` }} - disabled={!isValid} - onClickOperation={this.onUpdateAlert} - /> - </AlertModalFooter> - </div> - </ModalContent> - ); - } -} - -export const UpdateAlertModalContent = connect( - state => ({ - user: getUser(state), - isAdmin: getUserIsAdmin(state), - question: getQuestion(state), - visualizationSettings: getVisualizationSettings(state), - }), - { updateAlert, deleteAlert, updateUrl }, -)(UpdateAlertModalContentInner); diff --git a/frontend/src/metabase/query_builder/components/AlertModals/UpdateAlertModalContent.tsx b/frontend/src/metabase/query_builder/components/AlertModals/UpdateAlertModalContent.tsx new file mode 100644 index 00000000000..33442045340 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/AlertModals/UpdateAlertModalContent.tsx @@ -0,0 +1,93 @@ +import cx from "classnames"; +import { useState } from "react"; +import { t } from "ttag"; + +import { deleteAlert, updateAlert } from "metabase/alert/alert"; +import { useGetChannelInfoQuery } from "metabase/api"; +import ButtonWithStatus from "metabase/components/ButtonWithStatus"; +import ModalContent from "metabase/components/ModalContent"; +import Button from "metabase/core/components/Button"; +import CS from "metabase/css/core/index.css"; +import { alertIsValid } from "metabase/lib/alert"; +import { useDispatch, useSelector } from "metabase/lib/redux"; +import { updateUrl } from "metabase/query_builder/actions"; +import { + getQuestion, + getVisualizationSettings, +} from "metabase/query_builder/selectors"; +import { getUser, getUserIsAdmin } from "metabase/selectors/user"; +import type { Alert } from "metabase-types/api"; + +import { AlertEditForm } from "./AlertEditForm"; +import { AlertModalTitle } from "./AlertModalTitle"; +import { AlertModalFooter } from "./AlertModals.styled"; +import { DeleteAlertSection } from "./DeleteAlertSection"; + +interface UpdateAlertModalContentProps { + alert: Alert; + onAlertUpdated: () => void; + onCancel: () => void; +} + +export const UpdateAlertModalContent = ({ + alert, + onAlertUpdated, + onCancel, +}: UpdateAlertModalContentProps) => { + const dispatch = useDispatch(); + + const user = useSelector(getUser); + const isAdmin = useSelector(getUserIsAdmin); + const question = useSelector(getQuestion); + const visualizationSettings = useSelector(getVisualizationSettings); + + const [modifiedAlert, setModifiedAlert] = useState(alert); + const onAlertChange = (newModifiedAlert: Alert) => + setModifiedAlert(newModifiedAlert); + + const { data: channelSpec = {} } = useGetChannelInfoQuery(); + + const onUpdateAlert = async () => { + await dispatch(updateAlert(modifiedAlert)); + await dispatch(updateUrl(question, { dirty: false })); + onAlertUpdated(); + }; + + const onDeleteAlert = async () => { + await dispatch(deleteAlert(alert?.id)); + onAlertUpdated(); + }; + + const isCurrentUser = alert?.creator.id === user?.id; + const title = isCurrentUser ? t`Edit your alert` : t`Edit alert`; + const isValid = alertIsValid(modifiedAlert, channelSpec); + + // TODO: Remove PulseEdit css hack + return ( + <ModalContent onClose={onCancel} data-testid="alert-edit"> + <div + className={cx(CS.mlAuto, CS.mrAuto, CS.mb4)} + style={{ maxWidth: "550px" }} + > + <AlertModalTitle text={title} /> + <AlertEditForm + alertType={question?.alertType(visualizationSettings)} + alert={modifiedAlert} + onAlertChange={onAlertChange} + /> + {isAdmin && ( + <DeleteAlertSection alert={alert} onDeleteAlert={onDeleteAlert} /> + )} + + <AlertModalFooter> + <Button onClick={onCancel} className={CS.mr2}>{t`Cancel`}</Button> + <ButtonWithStatus + titleForState={{ default: t`Save changes` }} + disabled={!isValid} + onClickOperation={onUpdateAlert} + /> + </AlertModalFooter> + </div> + </ModalContent> + ); +}; -- GitLab