diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index ea45df6a535..c1e67662f8e 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -484,6 +484,15 @@ func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) hasAccess := ac.HasAccess(s.accessControl, c) var alertChildNavs []*navtree.NavLink + if !s.features.IsEnabled(featuremgmt.FlagTopnav) { + alertChildNavs = append(alertChildNavs, &navtree.NavLink{ + Text: "Home", + Id: "alert-home", + Url: s.cfg.AppSubURL + "/alerting/home", + Icon: "home", + }) + } + if hasAccess(ac.ReqViewer, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) { alertChildNavs = append(alertChildNavs, &navtree.NavLink{ Text: "Alert rules", SubTitle: "Rules that determine whether an alert will fire", Id: "alert-list", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul", @@ -539,7 +548,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) if s.features.IsEnabled(featuremgmt.FlagTopnav) { alertNav.Url = s.cfg.AppSubURL + "/alerting" } else { - alertNav.Url = s.cfg.AppSubURL + "/alerting/list" + alertNav.Url = s.cfg.AppSubURL + "/alerting/home" } return &alertNav diff --git a/public/app/core/components/NavBar/navBarItem-translations.ts b/public/app/core/components/NavBar/navBarItem-translations.ts index 4cbebe51972..886393984c9 100644 --- a/public/app/core/components/NavBar/navBarItem-translations.ts +++ b/public/app/core/components/NavBar/navBarItem-translations.ts @@ -51,6 +51,8 @@ export function getNavTitle(navId: string | undefined) { return t('nav.alerting.title', 'Alerting'); case 'alerting-legacy': return t('nav.alerting-legacy.title', 'Alerting (legacy)'); + case 'alert-home': + return t('nav.alerting-home.title', 'Home'); case 'alert-list': return t('nav.alerting-list.title', 'Alert rules'); case 'receivers': diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 082f631879b..95b2bda7ed5 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -11,16 +11,15 @@ import { AccessControlAction } from 'app/types'; import { evaluateAccess } from './unified/utils/access-control'; -const commonRoutes: RouteDescriptor[] = [ +const commonRoutes: RouteDescriptor[] = []; + +const legacyRoutes: RouteDescriptor[] = [ + ...commonRoutes, { path: '/alerting', component: () => config.featureToggles.topnav ? : , }, -]; - -const legacyRoutes: RouteDescriptor[] = [ - ...commonRoutes, { path: '/alerting/list', component: SafeDynamicImport( @@ -91,6 +90,19 @@ const legacyRoutes: RouteDescriptor[] = [ const unifiedRoutes: RouteDescriptor[] = [ ...commonRoutes, + config.featureToggles.topnav + ? { + path: '/alerting', + component: SafeDynamicImport( + () => import(/* webpackChunkName: "AlertingHome" */ 'app/features/alerting/unified/Home') + ), + } + : { + path: '/alerting/home', + component: SafeDynamicImport( + () => import(/* webpackChunkName: "AlertingHome" */ 'app/features/alerting/unified/Home') + ), + }, { path: '/alerting/list', roles: evaluateAccess( diff --git a/public/app/features/alerting/unified/Home.tsx b/public/app/features/alerting/unified/Home.tsx new file mode 100644 index 00000000000..da7684870a4 --- /dev/null +++ b/public/app/features/alerting/unified/Home.tsx @@ -0,0 +1,307 @@ +import { css, cx } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Stack } from '@grafana/experimental'; +import { config } from '@grafana/runtime'; +import { Icon, LinkButton, useStyles2, useTheme2, Tooltip } from '@grafana/ui'; + +import { AlertingPageWrapper } from './components/AlertingPageWrapper'; + +export default function Home() { + const theme = useTheme2(); + const styles = useStyles2(getWelcomePageStyles); + + return ( + +
+ + + Alerting flow chart +
    +
  • + Grafana alerting periodically queries your data sources and evaluates the alerting + condition you define +
  • +
  • + If the condition is breached, the alert rule fires and produces alert instances{' '} + + + +
  • +
  • + Firing instances are sent to the Alertmanager{' '} + + + +
  • +
  • + Alertmanager routes firing alert instances to notification policies based on whether the + labels match +
  • +
  • + Notifications are sent out to the contact point defined in the matching notification + policy +
  • +
+
+ + +
    +
  • + Create an alert rule by adding queries and expressions from multiple data sources. +
  • +
  • + Add labels to your alert rules{' '} + to connect them to notification policies +
  • +
  • + Configure contact points to define where to send your notifications to. +
  • +
  • + Configure notification policies to route your alert instances to contact points. +
  • +
+
+ + +
+
+
+ + + +
+
+ ); +} + +const getWelcomePageStyles = (theme: GrafanaTheme2) => ({ + grid: css` + display: grid; + grid-template-rows: min-content auto auto; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr; + gap: ${theme.spacing(2)}; + `, + ctaContainer: css` + grid-column: 1 / span 5; + `, + flowBlock: css` + width: 100%; + grid-column: 1 / span 5; + + img { + display: block; + margin: 0 auto; + height: auto; + width: 100%; + } + `, + videoBlock: css` + grid-column: 3 / span 3; + grid-row: 3 / span 1; + + // Video required + position: relative; + padding: 56.25% 0 0 0; /* 16:9 */ + + iframe { + position: absolute; + top: ${theme.spacing(2)}; + left: ${theme.spacing(2)}; + width: calc(100% - ${theme.spacing(4)}); + height: calc(100% - ${theme.spacing(4)}); + border: none; + } + `, + gettingStartedBlock: css` + grid-column: 1 / span 2; + justify-content: space-between; + + ul { + margin-left: ${theme.spacing(2)}; + } + `, + howItWorks: css` + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: ${theme.spacing(2)}; + list-style: none inside none; + list-style-type: disclosure-closed; + + > li { + flex: 1; + min-width: 150px; + } + `, +}); + +function WelcomeHeader({ className }: { className?: string }) { + const styles = useStyles2(getWelcomeHeaderStyles); + + return ( +
+
+

Welcome to Grafana Alerting

+
Grafana Alerting helps you manage your alert rules.
+
+
+ + + +
+
+ ); +} + +const getWelcomeHeaderStyles = (theme: GrafanaTheme2) => ({ + container: css` + display: flex; + flex-direction: column; + padding: ${theme.spacing(4)}; + background-image: url(public/img/alerting/welcome_cta_bg_${theme.name.toLowerCase()}.svg); + background-size: cover; + background-clip: padding-box; + + outline: 1px solid hsla(6deg, 60%, 80%, 0.14); + outline-offset: -1px; + border-radius: 3px; + `, + ctaContainer: css` + padding: ${theme.spacing(4)}; + display: flex; + gap: ${theme.spacing(4)}; + justify-content: space-between; + flex-wrap: wrap; + `, +}); + +interface WelcomeCTABoxProps { + title: string; + description: string; + icon: React.ComponentProps['name']; + href: string; + hrefText: string; +} + +function WelcomeCTABox({ title, description, icon, href, hrefText }: WelcomeCTABoxProps) { + const styles = useStyles2(getWelcomeCTAButtonStyles); + + return ( +
+
+ +
+

{title}

+
{description}
+ + {hrefText} + +
+ ); +} + +const getWelcomeCTAButtonStyles = (theme: GrafanaTheme2) => ({ + container: css` + flex: 1; + min-width: 240px; + display: grid; + gap: ${theme.spacing(1)}; + grid-template-columns: min-content 1fr 1fr 1fr; + grid-template-rows: min-content auto min-content; + `, + + title: css` + grid-column: 2 / span 3; + grid-row: 1; + `, + + desc: css` + grid-column: 2 / span 3; + grid-row: 2; + `, + + actionButton: css` + grid-column: 2 / span 3; + grid-row: 3; + max-width: 240px; + `, + + icon: css` + grid-column: 1; + grid-row: 1 / span 2; + margin: auto; + color: #ff8833; + `, +}); + +function ContentBox({ children, title, className }: React.PropsWithChildren<{ title?: string; className?: string }>) { + const styles = useStyles2(getContentBoxStyles); + + return ( +
+ {title &&

{title}

} + {children} +
+ ); +} + +const getContentBoxStyles = (theme: GrafanaTheme2) => ({ + box: css` + padding: ${theme.spacing(2)}; + background-color: ${theme.colors.background.secondary}; + border-radius: 3px; + outline: 1px solid ${theme.colors.border.strong}; + `, +}); + +function ArrowLink({ href, title }: { href: string; title: string }) { + const styles = useStyles2(getArrowLinkStyles); + + return ( + + {title} + + ); +} + +const getArrowLinkStyles = (theme: GrafanaTheme2) => ({ + link: css` + display: block; + color: ${theme.colors.text.link}; + `, +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx index fc1ff41babb..d985d37012a 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx @@ -1,9 +1,9 @@ import { css } from '@emotion/css'; -import React, { useState } from 'react'; +import React from 'react'; import { useFormContext } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; -import { Card, Link, useStyles2, useTheme2 } from '@grafana/ui'; +import { Card, Link, useStyles2 } from '@grafana/ui'; import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; @@ -12,14 +12,13 @@ import LabelsField from './LabelsField'; import { RuleEditorSection } from './RuleEditorSection'; export const NotificationsStep = () => { - const [hideFlowChart, setHideFlowChart] = useState(false); const styles = useStyles2(getStyles); - const theme = useTheme2(); - const { watch } = useFormContext(); + const { watch, getValues } = useFormContext(); const type = watch('type'); const dataSourceName = watch('dataSourceName') ?? GRAFANA_RULES_SOURCE_NAME; + const hasLabelsDefined = getNonEmptyLabels(getValues('labels')).length > 0; return ( { title="Notifications" description="Grafana handles the notifications for alerts by assigning labels to alerts. These labels connect alerts to contact points and silence alert instances that have matching labels." > -
-
setHideFlowChart(!hideFlowChart)}> - {`${!hideFlowChart ? 'Hide' : 'Show'} flow chart`} -
-
- {!hideFlowChart && ( - notification policy flow chart - )}
+ {!hasLabelsDefined && ( + + Root route – default for all alerts + + Without custom labels, your alert will be routed through the root route. To view and edit the root + route, go to notification policies or contact your admin in case + you are using non-Grafana alert management. + + + )} - - Root route – default for all alerts - - Without custom labels, your alert will be routed through the root route. To view and edit the root route, - go to notification policies or contact your admin in case you are - using non-Grafana alert management. - -
); }; +interface Label { + key: string; + value: string; +} + +function getNonEmptyLabels(labels: Label[]) { + return labels.filter((label) => label.key && label.value); +} + const getStyles = (theme: GrafanaTheme2) => ({ contentWrapper: css` display: flex; diff --git a/public/img/alerting/at_a_glance_dark.svg b/public/img/alerting/at_a_glance_dark.svg new file mode 100644 index 00000000000..b36ddedaafe --- /dev/null +++ b/public/img/alerting/at_a_glance_dark.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/alerting/at_a_glance_light.svg b/public/img/alerting/at_a_glance_light.svg new file mode 100644 index 00000000000..2053cadb250 --- /dev/null +++ b/public/img/alerting/at_a_glance_light.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/alerting/notification_policy_dark.svg b/public/img/alerting/notification_policy_dark.svg deleted file mode 100644 index 25d743758dc..00000000000 --- a/public/img/alerting/notification_policy_dark.svg +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/img/alerting/notification_policy_light.svg b/public/img/alerting/notification_policy_light.svg deleted file mode 100644 index c48fbe93b00..00000000000 --- a/public/img/alerting/notification_policy_light.svg +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/img/alerting/welcome_cta_bg_dark.svg b/public/img/alerting/welcome_cta_bg_dark.svg new file mode 100644 index 00000000000..844f2640066 --- /dev/null +++ b/public/img/alerting/welcome_cta_bg_dark.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/alerting/welcome_cta_bg_light.svg b/public/img/alerting/welcome_cta_bg_light.svg new file mode 100644 index 00000000000..2b46f1cb732 --- /dev/null +++ b/public/img/alerting/welcome_cta_bg_light.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +