From 577a169508481a9214460db12b3e6274730ed9cc Mon Sep 17 00:00:00 2001 From: Shaheer Kochai Date: Mon, 11 Nov 2024 23:43:07 +0430 Subject: [PATCH] feat: alert rename interaction (#6208) * feat: alert rename interaction * feat: add support for enter and escape shortcuts to submit and cancel rename * chore: add missing alert field * chore: update the style similar to dashboard rename * refactor: remove buttonProps * chore: add missing alert property to fix the build --------- Co-authored-by: Srikanth Chekuri --- frontend/src/constants/reactQueryKeys.ts | 1 + .../src/container/CreateAlertRule/defaults.ts | 5 + .../ActionButtons/ActionButtons.tsx | 157 ++++++++++-------- .../ActionButtons/RenameModal.styles.scss | 138 +++++++++++++++ .../AlertHeader/ActionButtons/RenameModal.tsx | 95 +++++++++++ .../AlertDetails/AlertHeader/AlertHeader.tsx | 16 +- frontend/src/pages/AlertDetails/hooks.tsx | 38 +++++ frontend/src/types/api/alerts/def.ts | 2 +- 8 files changed, 377 insertions(+), 75 deletions(-) create mode 100644 frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/RenameModal.styles.scss create mode 100644 frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/RenameModal.tsx diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index ec2353abbf..1dbacde963 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -18,4 +18,5 @@ export const REACT_QUERY_KEY = { GET_ALL_ALLERTS: 'GET_ALL_ALLERTS', REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE', DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE', + UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE', }; diff --git a/frontend/src/container/CreateAlertRule/defaults.ts b/frontend/src/container/CreateAlertRule/defaults.ts index 34058a06f6..bac7c90865 100644 --- a/frontend/src/container/CreateAlertRule/defaults.ts +++ b/frontend/src/container/CreateAlertRule/defaults.ts @@ -57,6 +57,7 @@ export const alertDefaults: AlertDef = { }, annotations: defaultAnnotations, evalWindow: defaultEvalWindow, + alert: '', }; export const anamolyAlertDefaults: AlertDef = { @@ -101,6 +102,7 @@ export const anamolyAlertDefaults: AlertDef = { }, annotations: defaultAnnotations, evalWindow: defaultEvalWindow, + alert: '', }; export const logAlertDefaults: AlertDef = { @@ -132,6 +134,7 @@ export const logAlertDefaults: AlertDef = { }, annotations: defaultAnnotations, evalWindow: defaultEvalWindow, + alert: '', }; export const traceAlertDefaults: AlertDef = { @@ -163,6 +166,7 @@ export const traceAlertDefaults: AlertDef = { }, annotations: defaultAnnotations, evalWindow: defaultEvalWindow, + alert: '', }; export const exceptionAlertDefaults: AlertDef = { @@ -194,6 +198,7 @@ export const exceptionAlertDefaults: AlertDef = { }, annotations: defaultAnnotations, evalWindow: defaultEvalWindow, + alert: '', }; export const ALERTS_VALUES_MAP: Record = { diff --git a/frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/ActionButtons.tsx b/frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/ActionButtons.tsx index 2f37c4fc9d..00987a0a66 100644 --- a/frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/ActionButtons.tsx +++ b/frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/ActionButtons.tsx @@ -2,82 +2,90 @@ import './ActionButtons.styles.scss'; import { Color } from '@signozhq/design-tokens'; import { Divider, Dropdown, MenuProps, Switch, Tooltip } from 'antd'; -import { QueryParams } from 'constants/query'; -import ROUTES from 'constants/routes'; import { useIsDarkMode } from 'hooks/useDarkMode'; -import useUrlQuery from 'hooks/useUrlQuery'; -import history from 'lib/history'; import { Copy, Ellipsis, PenLine, Trash2 } from 'lucide-react'; import { useAlertRuleDelete, useAlertRuleDuplicate, useAlertRuleStatusToggle, + useAlertRuleUpdate, } from 'pages/AlertDetails/hooks'; import CopyToClipboard from 'periscope/components/CopyToClipboard'; import { useAlertRule } from 'providers/Alert'; -import React, { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { CSSProperties } from 'styled-components'; import { AlertDef } from 'types/api/alerts/def'; import { AlertHeaderProps } from '../AlertHeader'; +import RenameModal from './RenameModal'; const menuItemStyle: CSSProperties = { fontSize: '14px', letterSpacing: '0.14px', }; + function AlertActionButtons({ ruleId, alertDetails, + setUpdatedName, }: { ruleId: string; alertDetails: AlertHeaderProps['alertDetails']; + setUpdatedName: (name: string) => void; }): JSX.Element { const { alertRuleState, setAlertRuleState } = useAlertRule(); - const { handleAlertStateToggle } = useAlertRuleStatusToggle({ ruleId }); + const [intermediateName, setIntermediateName] = useState( + alertDetails.alert, + ); + const [isRenameAlertOpen, setIsRenameAlertOpen] = useState(false); + const isDarkMode = useIsDarkMode(); + const { handleAlertStateToggle } = useAlertRuleStatusToggle({ ruleId }); const { handleAlertDuplicate } = useAlertRuleDuplicate({ alertDetails: (alertDetails as unknown) as AlertDef, }); const { handleAlertDelete } = useAlertRuleDelete({ ruleId: Number(ruleId) }); + const { handleAlertUpdate, isLoading } = useAlertRuleUpdate({ + alertDetails: (alertDetails as unknown) as AlertDef, + setUpdatedName, + intermediateName, + }); - const params = useUrlQuery(); + const handleRename = useCallback(() => { + setIsRenameAlertOpen(true); + }, []); - const handleRename = React.useCallback(() => { - params.set(QueryParams.ruleId, String(ruleId)); - history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`); - }, [params, ruleId]); + const onNameChangeHandler = useCallback(() => { + handleAlertUpdate(); + setIsRenameAlertOpen(false); + }, [handleAlertUpdate]); - const menu: MenuProps['items'] = React.useMemo( - () => [ - { - key: 'rename-rule', - label: 'Rename', - icon: , - onClick: (): void => handleRename(), - style: menuItemStyle, + const menuItems: MenuProps['items'] = [ + { + key: 'rename-rule', + label: 'Rename', + icon: , + onClick: handleRename, + style: menuItemStyle, + }, + { + key: 'duplicate-rule', + label: 'Duplicate', + icon: , + onClick: handleAlertDuplicate, + style: menuItemStyle, + }, + { + key: 'delete-rule', + label: 'Delete', + icon: , + onClick: handleAlertDelete, + style: { + ...menuItemStyle, + color: Color.BG_CHERRY_400, }, - { - key: 'duplicate-rule', - label: 'Duplicate', - icon: , - onClick: (): void => handleAlertDuplicate(), - style: menuItemStyle, - }, - { type: 'divider' }, - { - key: 'delete-rule', - label: 'Delete', - icon: , - onClick: (): void => handleAlertDelete(), - style: { - ...menuItemStyle, - color: Color.BG_CHERRY_400, - }, - }, - ], - [handleAlertDelete, handleAlertDuplicate, handleRename], - ); - const isDarkMode = useIsDarkMode(); + }, + ]; // state for immediate UI feedback rather than waiting for onSuccess of handleAlertStateTiggle to updating the alertRuleState const [isAlertRuleDisabled, setIsAlertRuleDisabled] = useState< @@ -95,35 +103,48 @@ function AlertActionButtons({ // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => (): void => setAlertRuleState(undefined), []); + const toggleAlertRule = useCallback(() => { + setIsAlertRuleDisabled((prev) => !prev); + handleAlertStateToggle(); + }, [handleAlertStateToggle]); + return ( -
- - {isAlertRuleDisabled !== undefined && ( - { - setIsAlertRuleDisabled((prev) => !prev); - handleAlertStateToggle(); - }} - checked={!isAlertRuleDisabled} - /> - )} - - - - - - - - + <> +
+ + {isAlertRuleDisabled !== undefined && ( + + )} - -
+ + + + + + + + + +
+ + + ); } diff --git a/frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/RenameModal.styles.scss b/frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/RenameModal.styles.scss new file mode 100644 index 0000000000..d3552d8143 --- /dev/null +++ b/frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/RenameModal.styles.scss @@ -0,0 +1,138 @@ +.rename-alert { + .ant-modal-content { + width: 384px; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + padding: 0px; + + .ant-modal-header { + height: 52px; + padding: 16px; + background: var(--bg-ink-400); + border-bottom: 1px solid var(--bg-slate-500); + margin-bottom: 0px; + .ant-modal-title { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + width: 349px; + height: 20px; + } + } + + .ant-modal-body { + padding: 16px; + + .alert-content { + display: flex; + flex-direction: column; + gap: 8px; + + .name-text { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + } + + .alert-name-input { + display: flex; + padding: 6px 6px 6px 8px; + align-items: center; + gap: 4px; + align-self: stretch; + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + } + } + } + + .ant-modal-footer { + padding: 16px; + margin-top: 0px; + .alert-rename { + display: flex; + flex-direction: row-reverse; + gap: 12px; + + .cancel-btn { + display: flex; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 2px; + background: var(--bg-slate-500); + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + + .rename-btn { + display: flex; + align-items: center; + display: flex; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 2px; + background: var(--bg-robin-500); + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + } + } + } +} + +.lightMode { + .rename-alert { + .ant-modal-content { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .ant-modal-header { + background: var(--bg-vanilla-100); + border-bottom: 1px solid var(--bg-vanilla-300); + + .ant-modal-title { + color: var(--bg-ink-300); + } + } + + .ant-modal-body { + .alert-content { + .name-text { + color: var(--bg-ink-300); + } + + .alert-name-input { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } + } + } + + .ant-modal-footer { + .alert-rename { + .cancel-btn { + background: var(--bg-vanilla-300); + } + } + } + } + } +} diff --git a/frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/RenameModal.tsx b/frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/RenameModal.tsx new file mode 100644 index 0000000000..ce73260fb3 --- /dev/null +++ b/frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/RenameModal.tsx @@ -0,0 +1,95 @@ +import './RenameModal.styles.scss'; + +import { Button, Input, InputRef, Modal, Typography } from 'antd'; +import { Check, X } from 'lucide-react'; +import { useCallback, useEffect, useRef } from 'react'; + +type Props = { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + onNameChangeHandler: () => void; + isLoading: boolean; + intermediateName: string; + setIntermediateName: (name: string) => void; +}; + +function RenameModal({ + isOpen, + setIsOpen, + onNameChangeHandler, + isLoading, + intermediateName, + setIntermediateName, +}: Props): JSX.Element { + const inputRef = useRef(null); + + useEffect(() => { + if (isOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isOpen]); + + const handleClose = useCallback((): void => setIsOpen(false), [setIsOpen]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent): void => { + if (isOpen) { + if (e.key === 'Enter') { + onNameChangeHandler(); + } else if (e.key === 'Escape') { + handleClose(); + } + } + }; + + document.addEventListener('keydown', handleKeyDown); + + return (): void => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isOpen, onNameChangeHandler, handleClose]); + + return ( + + + + + } + > +
+ Enter a new name + setIntermediateName(e.target.value)} + /> +
+
+ ); +} + +export default RenameModal; diff --git a/frontend/src/pages/AlertDetails/AlertHeader/AlertHeader.tsx b/frontend/src/pages/AlertDetails/AlertHeader/AlertHeader.tsx index 04edd6a8b0..f617a6d78e 100644 --- a/frontend/src/pages/AlertDetails/AlertHeader/AlertHeader.tsx +++ b/frontend/src/pages/AlertDetails/AlertHeader/AlertHeader.tsx @@ -2,7 +2,7 @@ import './AlertHeader.styles.scss'; import LineClampedText from 'periscope/components/LineClampedText/LineClampedText'; import { useAlertRule } from 'providers/Alert'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import AlertActionButtons from './ActionButtons/ActionButtons'; import AlertLabels from './AlertLabels/AlertLabels'; @@ -19,7 +19,9 @@ export type AlertHeaderProps = { }; }; function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element { - const { state, alert, labels } = alertDetails; + const { state, alert: alertName, labels } = alertDetails; + const { alertRuleState } = useAlertRule(); + const [updatedName, setUpdatedName] = useState(alertName); const labelsWithoutSeverity = useMemo( () => @@ -29,8 +31,6 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element { [labels], ); - const { alertRuleState } = useAlertRule(); - return (
@@ -38,7 +38,7 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
- +
@@ -54,7 +54,11 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
- +
); diff --git a/frontend/src/pages/AlertDetails/hooks.tsx b/frontend/src/pages/AlertDetails/hooks.tsx index b4d7674c67..c159d2169b 100644 --- a/frontend/src/pages/AlertDetails/hooks.tsx +++ b/frontend/src/pages/AlertDetails/hooks.tsx @@ -467,6 +467,44 @@ export const useAlertRuleDuplicate = ({ return { handleAlertDuplicate }; }; +export const useAlertRuleUpdate = ({ + alertDetails, + setUpdatedName, + intermediateName, +}: { + alertDetails: AlertDef; + setUpdatedName: (name: string) => void; + intermediateName: string; +}): { + handleAlertUpdate: () => void; + isLoading: boolean; +} => { + const { notifications } = useNotifications(); + const handleError = useAxiosError(); + + const { mutate: updateAlertRule, isLoading } = useMutation( + [REACT_QUERY_KEY.UPDATE_ALERT_RULE, alertDetails.id], + save, + { + onMutate: () => setUpdatedName(intermediateName), + onSuccess: () => + notifications.success({ message: 'Alert renamed successfully' }), + onError: (error) => { + setUpdatedName(alertDetails.alert); + handleError(error); + }, + }, + ); + + const handleAlertUpdate = (): void => { + updateAlertRule({ + data: { ...alertDetails, alert: intermediateName }, + id: alertDetails.id, + }); + }; + + return { handleAlertUpdate, isLoading }; +}; export const useAlertRuleDelete = ({ ruleId, diff --git a/frontend/src/types/api/alerts/def.ts b/frontend/src/types/api/alerts/def.ts index 40a6036411..3891c1b123 100644 --- a/frontend/src/types/api/alerts/def.ts +++ b/frontend/src/types/api/alerts/def.ts @@ -19,7 +19,7 @@ export const defaultSeasonality = 'hourly'; export interface AlertDef { id?: number; alertType?: string; - alert?: string; + alert: string; ruleType?: string; frequency?: string; condition: RuleCondition;