From a788230e703fc01750b3db6a6b19485f6f459fd3 Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Sat, 15 Jun 2024 12:56:37 +0530 Subject: [PATCH] feat: added alert rule empty state and product edu (#5185) * feat: added alert rule empty state and product edu * feat: added lightMode styles * chore: update docs and card links * feat: code refactor and added loadingState for newAlert btn * chore: update alert links --------- Co-authored-by: makeavish --- frontend/public/Icons/alert_emoji.svg | 10 + .../CreateAlertRule/SelectAlertType/index.tsx | 2 +- .../AlertsEmptyState/AlertInfoCard.tsx | 37 +++ .../AlertsEmptyState.styles.scss | 251 ++++++++++++++++++ .../AlertsEmptyState/AlertsEmptyState.tsx | 127 +++++++++ .../AlertsEmptyState/InfoLinkText.tsx | 31 +++ .../AlertsEmptyState/alertLinks.ts | 50 ++++ .../src/container/ListAlertRules/index.tsx | 5 + 8 files changed, 512 insertions(+), 1 deletion(-) create mode 100644 frontend/public/Icons/alert_emoji.svg create mode 100644 frontend/src/container/ListAlertRules/AlertsEmptyState/AlertInfoCard.tsx create mode 100644 frontend/src/container/ListAlertRules/AlertsEmptyState/AlertsEmptyState.styles.scss create mode 100644 frontend/src/container/ListAlertRules/AlertsEmptyState/AlertsEmptyState.tsx create mode 100644 frontend/src/container/ListAlertRules/AlertsEmptyState/InfoLinkText.tsx create mode 100644 frontend/src/container/ListAlertRules/AlertsEmptyState/alertLinks.ts diff --git a/frontend/public/Icons/alert_emoji.svg b/frontend/public/Icons/alert_emoji.svg new file mode 100644 index 0000000000..70f9091283 --- /dev/null +++ b/frontend/public/Icons/alert_emoji.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx b/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx index 916c9341a6..cd837b666b 100644 --- a/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx +++ b/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx @@ -29,7 +29,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element { break; case AlertTypes.EXCEPTIONS_BASED_ALERT: url = - 'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#example'; + 'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples'; break; default: break; diff --git a/frontend/src/container/ListAlertRules/AlertsEmptyState/AlertInfoCard.tsx b/frontend/src/container/ListAlertRules/AlertsEmptyState/AlertInfoCard.tsx new file mode 100644 index 0000000000..76403a1884 --- /dev/null +++ b/frontend/src/container/ListAlertRules/AlertsEmptyState/AlertInfoCard.tsx @@ -0,0 +1,37 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import { ArrowRightOutlined } from '@ant-design/icons'; +import { Typography } from 'antd'; + +interface AlertInfoCardProps { + header: string; + subheader: string; + link: string; +} + +function AlertInfoCard({ + header, + subheader, + link, +}: AlertInfoCardProps): JSX.Element { + return ( +
{ + window.open(link, '_blank'); + }} + > +
+ + {header} + + + {subheader} + +
+ +
+ ); +} + +export default AlertInfoCard; diff --git a/frontend/src/container/ListAlertRules/AlertsEmptyState/AlertsEmptyState.styles.scss b/frontend/src/container/ListAlertRules/AlertsEmptyState/AlertsEmptyState.styles.scss new file mode 100644 index 0000000000..c852b5833b --- /dev/null +++ b/frontend/src/container/ListAlertRules/AlertsEmptyState/AlertsEmptyState.styles.scss @@ -0,0 +1,251 @@ +.alert-list-container { + margin-top: 104px; + margin-bottom: 30px; + display: flex; + justify-content: center; + width: 100%; + + .alert-list-view-content { + width: calc(100% - 30px); + max-width: 836px; + + .alert-list-title-container { + .title { + color: var(--bg-vanilla-100); + font-size: var(--font-size-lg); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 28px; /* 155.556% */ + letter-spacing: -0.09px; + } + + .subtitle { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + + .empty-alert-info-container { + display: flex; + padding: 71px 193.5px; + justify-content: center; + align-items: center; + border-radius: 6px; + border: 1px dashed var(--bg-slate-500); + margin-top: 16px; + + .alert-content { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + + .heading { + display: flex; + flex-direction: column; + gap: 4px; + + .icons { + color: white; + } + + .empty-alert-action { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ + letter-spacing: -0.07px; + } + + .empty-info { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.07px; + } + } + + .action-container { + display: flex; + gap: 24px; + align-items: center; + padding-top: 24px; + padding-bottom: 24px; + width: 100%; + } + } + } + + .get-started-text { + display: flex; + justify-content: center; + align-items: center; + gap: 16px; + margin-top: 24px; + margin-bottom: 24px; + width: 100%; + + .ant-divider::before, + .ant-divider::after { + border-bottom: 2px dotted var(--bg-slate-300); + border-top: 2px dotted var(--bg-slate-300); + height: 8px; + } + + .ant-typography { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 166.667% */ + letter-spacing: 0.48px; + text-transform: uppercase; + padding-top: 8px; + } + } + + .alert-info-card { + display: flex; + padding: 16px; + justify-content: space-between; + align-items: center; + + border-radius: 6px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + margin-bottom: 16px; + + &:hover { + cursor: pointer; + } + + .alert-card-text { + display: flex; + gap: 2px; + flex-direction: column; + + .alert-card-text-header { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .alert-card-text-subheader { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + } + } + } + } +} + +.info-text { + color: var(--bg-robin-400) !important; + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; /* 133.333% */ + letter-spacing: -0.06px; +} + +.info-link-container { + .anticon { + color: var(--bg-robin-400); + } + + :hover { + cursor: pointer; + } +} + +.lightMode { + .alert-list-container { + .alert-list-view-content { + .alert-list-title-container { + .title { + color: var(--bg-slate-400); + } + + .subtitle { + color: var(--bg-slate-100); + } + } + + .empty-alert-info-container { + border: 1px dashed var(--bg-vanilla-400); + + .alert-content { + .heading { + .icons { + color: white; + } + + .empty-alert-action { + color: var(--bg-slate-100); + } + + .empty-info { + color: var(--bg-slate-400); + } + } + } + } + + .get-started-text { + .ant-divider::before, + .ant-divider::after { + border-bottom: 2px dotted var(--bg-vanilla-400); + border-top: 2px dotted var(--bg-vanilla-400); + } + + .ant-typography { + color: var(--bg-slate-100); + } + } + + .alert-info-card { + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-100); + + .alert-card-text { + .alert-card-text-header { + color: var(--bg-slate-400); + } + + .alert-card-text-subheader { + color: var(--bg-slate-100); + } + } + } + } + } + + .info-text { + color: var(--bg-robin-600) !important; + } + + .info-link-container { + .anticon { + color: var(--bg-robin-400); + } + } +} diff --git a/frontend/src/container/ListAlertRules/AlertsEmptyState/AlertsEmptyState.tsx b/frontend/src/container/ListAlertRules/AlertsEmptyState/AlertsEmptyState.tsx new file mode 100644 index 0000000000..0f388053c0 --- /dev/null +++ b/frontend/src/container/ListAlertRules/AlertsEmptyState/AlertsEmptyState.tsx @@ -0,0 +1,127 @@ +import './AlertsEmptyState.styles.scss'; + +import { PlusOutlined } from '@ant-design/icons'; +import { Button, Divider, Typography } from 'antd'; +import ROUTES from 'constants/routes'; +import useComponentPermission from 'hooks/useComponentPermission'; +import { useNotifications } from 'hooks/useNotifications'; +import history from 'lib/history'; +import { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import AppReducer from 'types/reducer/app'; + +import AlertInfoCard from './AlertInfoCard'; +import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks'; +import InfoLinkText from './InfoLinkText'; + +export function AlertsEmptyState(): JSX.Element { + const { t } = useTranslation('common'); + const { role, featureResponse } = useSelector( + (state) => state.app, + ); + const [addNewAlert] = useComponentPermission( + ['add_new_alert', 'action'], + role, + ); + + const { notifications: notificationsApi } = useNotifications(); + + const handleError = useCallback((): void => { + notificationsApi.error({ + message: t('something_went_wrong'), + }); + }, [notificationsApi, t]); + + const [loading, setLoading] = useState(false); + + const onClickNewAlertHandler = useCallback(() => { + setLoading(true); + featureResponse + .refetch() + .then(() => { + setLoading(false); + history.push(ROUTES.ALERTS_NEW); + }) + .catch(handleError) + .finally(() => setLoading(false)); + }, [featureResponse, handleError]); + + return ( +
+
+
+ Alert Rules + + Create and manage alert rules for your resources. + +
+
+
+
+ alert-header +
+ + No Alert rules yet.{' '} + + + Create an Alert Rule to get started + +
+
+
+ + +
+ + {ALERT_INFO_LINKS.map((info) => ( + + ))} +
+
+
+ + + Or get started with these sample alerts + + +
+ + {ALERT_CARDS.map((card) => ( + + ))} +
+
+ ); +} diff --git a/frontend/src/container/ListAlertRules/AlertsEmptyState/InfoLinkText.tsx b/frontend/src/container/ListAlertRules/AlertsEmptyState/InfoLinkText.tsx new file mode 100644 index 0000000000..1f17cd3969 --- /dev/null +++ b/frontend/src/container/ListAlertRules/AlertsEmptyState/InfoLinkText.tsx @@ -0,0 +1,31 @@ +import { ArrowRightOutlined, PlayCircleFilled } from '@ant-design/icons'; +import { Flex, Typography } from 'antd'; + +interface InfoLinkTextProps { + infoText: string; + link: string; + leftIconVisible: boolean; + rightIconVisible: boolean; +} + +function InfoLinkText({ + infoText, + link, + leftIconVisible, + rightIconVisible, +}: InfoLinkTextProps): JSX.Element { + return ( + { + window.open(link, '_blank'); + }} + className="info-link-container" + > + {leftIconVisible && } + {infoText} + {rightIconVisible && } + + ); +} + +export default InfoLinkText; diff --git a/frontend/src/container/ListAlertRules/AlertsEmptyState/alertLinks.ts b/frontend/src/container/ListAlertRules/AlertsEmptyState/alertLinks.ts new file mode 100644 index 0000000000..fac0ad6b12 --- /dev/null +++ b/frontend/src/container/ListAlertRules/AlertsEmptyState/alertLinks.ts @@ -0,0 +1,50 @@ +export const ALERT_INFO_LINKS = [ + { + infoText: 'How to create Metrics-based alerts', + link: + 'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page', + leftIconVisible: false, + rightIconVisible: true, + }, + { + infoText: 'How to create Log-based alerts', + link: + 'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-empty-page', + leftIconVisible: false, + rightIconVisible: true, + }, + { + infoText: 'How to create Trace-based alerts', + link: + 'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-empty-page', + leftIconVisible: false, + rightIconVisible: true, + }, +]; + +export const ALERT_CARDS = [ + { + header: 'Alert on high memory usage', + subheader: "Monitor your host's memory usage", + link: + 'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-memory-usage-for-host-goes-above-400-mb-or-any-fixed-memory', + }, + { + header: 'Alert on slow external API calls', + subheader: 'Monitor your external API calls', + link: + 'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-external-api-latency-p90-is-over-1-second-for-last-5-mins', + }, + { + header: 'Alert on high percentage of timeout errors in logs', + subheader: 'Monitor your logs for errors', + link: + 'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-percentage-of-redis-timeout-error-logs-greater-than-7-in-last-5-mins', + }, + { + header: 'Alert on high error percentage of an endpoint', + subheader: 'Monitor your API endpoint', + link: + 'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page#3-alert-when-the-error-percentage-for-an-endpoint-exceeds-5', + }, +]; diff --git a/frontend/src/container/ListAlertRules/index.tsx b/frontend/src/container/ListAlertRules/index.tsx index 3880a7c2e6..810cd0eb9b 100644 --- a/frontend/src/container/ListAlertRules/index.tsx +++ b/frontend/src/container/ListAlertRules/index.tsx @@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'; import { useQuery } from 'react-query'; import { useLocation } from 'react-router-dom'; +import { AlertsEmptyState } from './AlertsEmptyState/AlertsEmptyState'; import ListAlert from './ListAlert'; function ListAlertRules(): JSX.Element { @@ -45,6 +46,10 @@ function ListAlertRules(): JSX.Element { ); } + if (status === 'success' && !data.payload?.length) { + return ; + } + // in case of loading if (isLoading || !data?.payload) { return ;