Skip to content

Commit c7374b6

Browse files
authored
Alerting: Add Synthetic and IRM link cards (#107538)
1 parent 15e1aa8 commit c7374b6

File tree

11 files changed

+336
-29
lines changed

11 files changed

+336
-29
lines changed

pkg/services/user/model.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const (
1616
HelpFlagGettingStartedPanelDismissed HelpFlags1 = 1 << iota
1717
HelpFlagDashboardHelp1
1818
HelpFlagEnterpriseAuth1
19+
HelpFlagSyntheticMonitoring1
20+
HelpFlagIRM1
1921
)
2022

2123
type UpdateEmailActionType string
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { t } from '@grafana/i18n';
2+
3+
import { OrangeBadge } from './OrangeBadge';
4+
5+
export function CloudBadge() {
6+
return <OrangeBadge text={t('cloud-feature-badge', 'Cloud')} />;
7+
}
Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,7 @@
1-
import { css } from '@emotion/css';
1+
import { t } from '@grafana/i18n';
22

3-
import { GrafanaTheme2 } from '@grafana/data';
4-
import { Trans } from '@grafana/i18n';
5-
import { Icon, useStyles2 } from '@grafana/ui';
3+
import { OrangeBadge } from './OrangeBadge';
64

75
export function CloudEnterpriseBadge() {
8-
const styles = useStyles2(getStyles);
9-
return (
10-
<div className={styles.wrapper}>
11-
<Icon name="cloud" size="sm" />
12-
<Trans i18nKey="cloud-enterprise-feature-badge">Cloud & Enterprise </Trans>
13-
</div>
14-
);
6+
return <OrangeBadge text={t('cloud-enterprise-feature-badge', 'Cloud & Enterprise')} />;
157
}
16-
17-
const getStyles = (theme: GrafanaTheme2) => {
18-
return {
19-
wrapper: css({
20-
display: 'inline-flex',
21-
padding: theme.spacing(0.5, 1),
22-
borderRadius: theme.shape.radius.pill,
23-
background: theme.colors.gradients.brandHorizontal,
24-
color: theme.colors.primary.contrastText,
25-
fontWeight: theme.typography.fontWeightMedium,
26-
gap: theme.spacing(0.5),
27-
fontSize: theme.typography.bodySmall.fontSize,
28-
lineHeight: theme.typography.bodySmall.lineHeight,
29-
alignItems: 'center',
30-
}),
31-
};
32-
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { css } from '@emotion/css';
2+
3+
import { GrafanaTheme2 } from '@grafana/data';
4+
import { Icon, useStyles2 } from '@grafana/ui';
5+
6+
export function OrangeBadge({ text }: { text: string }) {
7+
const styles = useStyles2(getStyles);
8+
return (
9+
<div className={styles.wrapper}>
10+
<Icon name="cloud" size="sm" />
11+
{text}
12+
</div>
13+
);
14+
}
15+
16+
const getStyles = (theme: GrafanaTheme2) => {
17+
return {
18+
wrapper: css({
19+
display: 'inline-flex',
20+
padding: theme.spacing(0.5, 1),
21+
borderRadius: theme.shape.radius.pill,
22+
background: theme.colors.gradients.brandHorizontal,
23+
color: theme.colors.primary.contrastText,
24+
fontWeight: theme.typography.fontWeightMedium,
25+
gap: theme.spacing(0.5),
26+
fontSize: theme.typography.bodySmall.fontSize,
27+
lineHeight: theme.typography.bodySmall.lineHeight,
28+
alignItems: 'center',
29+
}),
30+
};
31+
};
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { css } from '@emotion/css';
2+
import { useState } from 'react';
3+
4+
import { GrafanaTheme2 } from '@grafana/data';
5+
import { Trans, t } from '@grafana/i18n';
6+
import { Button, Divider, Icon, IconButton, useStyles2 } from '@grafana/ui';
7+
import { CloudBadge } from 'app/core/components/Branding/CloudBadge';
8+
import { backendSrv } from 'app/core/services/backend_srv';
9+
import { contextSrv } from 'app/core/services/context_srv';
10+
import { isOpenSourceBuildOrUnlicenced } from 'app/features/admin/EnterpriseAuthFeaturesCard';
11+
12+
type AdCardProps = {
13+
title: string;
14+
description: string;
15+
href: string;
16+
logoUrl: string;
17+
items: string[];
18+
helpFlag: number;
19+
};
20+
21+
export default function AdCard({ title, description, href, logoUrl, items, helpFlag }: AdCardProps) {
22+
const styles = useStyles2(getAddCardStyles);
23+
24+
const helpFlags = contextSrv.user.helpFlags1;
25+
const [isDismissed, setDismissed] = useState<boolean>(Boolean(helpFlags & helpFlag));
26+
27+
const onDismiss = () => {
28+
backendSrv.put(`/api/user/helpflags/${helpFlag}`, undefined, { showSuccessAlert: false }).then((res) => {
29+
contextSrv.user.helpFlags1 = res.helpFlags1;
30+
setDismissed(true);
31+
});
32+
};
33+
34+
if (isDismissed || !isOpenSourceBuildOrUnlicenced()) {
35+
return null;
36+
}
37+
38+
return (
39+
<div className={styles.cardBody} title={title}>
40+
<div className={styles.preHeader}>
41+
<CloudBadge />
42+
<IconButton name="times" size="sm" onClick={onDismiss} aria-label={t('alerting.ad.close', 'Close')} />
43+
</div>
44+
<header className={styles.header}>
45+
<img src={logoUrl} alt={title.concat(' logo')} className={styles.logo} />
46+
<div className={styles.contentColumn}>
47+
<h3 className={styles.title}>{title}</h3>
48+
<p className={styles.description}>{description}</p>
49+
</div>
50+
</header>
51+
<Divider />
52+
<div className={styles.itemsList}>
53+
{items.map((item) => (
54+
<div key={item} className={styles.listItem}>
55+
<Icon className={styles.icon} name="check" />
56+
{item}
57+
</div>
58+
))}
59+
</div>
60+
<Divider />
61+
<Button fill="solid" variant="secondary" onClick={() => window.open(href, '_blank')} className={styles.button}>
62+
<Trans i18nKey="alerting.ad.learn-more">Learn more</Trans>
63+
<Icon name="external-link-alt" className={styles.buttonIcon} />
64+
</Button>
65+
</div>
66+
);
67+
}
68+
69+
const getAddCardStyles = (theme: GrafanaTheme2) => ({
70+
logo: css({
71+
objectFit: 'contain',
72+
width: '47px',
73+
height: '47px',
74+
}),
75+
76+
header: css({
77+
display: 'flex',
78+
alignItems: 'flex-start',
79+
gap: theme.spacing(2),
80+
paddingTop: theme.spacing(2),
81+
height: theme.spacing(8),
82+
}),
83+
84+
contentColumn: css({
85+
flex: 1,
86+
}),
87+
88+
title: css({
89+
marginBottom: theme.spacing(1),
90+
fontSize: theme.typography.h4.fontSize,
91+
fontWeight: theme.typography.h4.fontWeight,
92+
color: theme.colors.text.primary,
93+
}),
94+
95+
description: css({
96+
fontSize: theme.typography.bodySmall.fontSize,
97+
color: theme.colors.text.secondary,
98+
lineHeight: theme.typography.bodySmall.lineHeight,
99+
}),
100+
101+
itemsList: css({
102+
display: 'grid',
103+
gridTemplateColumns: '1fr',
104+
gap: theme.spacing(0.5),
105+
[theme.breakpoints.up('xl')]: {
106+
gridTemplateColumns: '1fr 1fr',
107+
gap: theme.spacing(1),
108+
},
109+
}),
110+
111+
listItem: css({
112+
display: 'flex',
113+
alignItems: 'flex-start',
114+
fontSize: theme.typography.bodySmall.fontSize,
115+
color: theme.colors.text.secondary,
116+
lineHeight: theme.typography.bodySmall.lineHeight,
117+
marginBottom: theme.spacing(0.5),
118+
}),
119+
120+
icon: css({
121+
marginRight: theme.spacing(1),
122+
color: theme.colors.success.main,
123+
}),
124+
125+
button: css({
126+
padding: `0 ${theme.spacing(2)}`,
127+
}),
128+
129+
buttonIcon: css({
130+
marginLeft: theme.spacing(1),
131+
}),
132+
133+
cardBody: css({
134+
padding: `${theme.spacing(3)} ${theme.spacing(4)} ${theme.spacing(2.25)} ${theme.spacing(4)}`,
135+
backgroundColor: theme.colors.background.secondary,
136+
borderRadius: theme.shape.radius.default,
137+
border: `1px solid ${theme.colors.border.weak}`,
138+
flex: 1,
139+
}),
140+
141+
preHeader: css({
142+
display: 'flex',
143+
justifyContent: 'space-between',
144+
alignItems: 'center',
145+
}),
146+
});

public/app/features/alerting/unified/home/Home.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { isLocalDevEnv } from '../utils/misc';
88
import { withPageErrorBoundary } from '../withPageErrorBoundary';
99

1010
import GettingStarted, { WelcomeHeader } from './GettingStarted';
11+
import IRMCard from './IRMCard';
1112
import { getInsightsScenes, insightsIsAvailable } from './Insights';
1213
import { PluginIntegrations } from './PluginIntegrations';
14+
import SyntheticMonitoringCard from './SyntheticMonitoringCard';
1315

1416
function Home() {
1517
const insightsEnabled = insightsIsAvailable() || isLocalDevEnv();
@@ -27,6 +29,12 @@ function Home() {
2729
<WelcomeHeader />
2830
<PluginIntegrations />
2931
</Stack>
32+
<Box marginTop={{ lg: 2, md: 2, xs: 2 }}>
33+
<Stack direction="row" gap={2}>
34+
<SyntheticMonitoringCard />
35+
<IRMCard />
36+
</Stack>
37+
</Box>
3038
<Box marginTop={{ lg: 2, md: 0, xs: 0 }}>
3139
<TabsBar>
3240
{insightsEnabled && (
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { t } from '@grafana/i18n';
2+
import irmSvg from 'img/irm_logo.svg';
3+
4+
import AdCard from './AdCard';
5+
6+
const LINK = 'https://grafana.com/auth/sign-up/create-user?redirectPath=irm&src=oss-grafana&cnt=alerting-irm';
7+
const HELP_FLAG_IRM = 0x0010;
8+
9+
export default function IRMCard() {
10+
return (
11+
<AdCard
12+
title={t('alerting.home.irm-card-title', 'Incident response and management')}
13+
description={t(
14+
'alerting.home.irm-card-description',
15+
'Unify on-call, alerting, and incident response with Grafana Cloud IRM.'
16+
)}
17+
href={LINK}
18+
logoUrl={irmSvg}
19+
items={[
20+
t('alerting.home.irm-card-item-1', 'Manage on-call schedules with your calendar or Terraform.'),
21+
t('alerting.home.irm-card-item-2', 'Respond to incidents via web, app, Slack, or other channels.'),
22+
t('alerting.home.irm-card-item-3', 'Pinpoint root causes with AI-powered Grafana SIFT.'),
23+
t('alerting.home.irm-card-item-4', 'Analyze past incidents to improve response and resilience.'),
24+
]}
25+
helpFlag={HELP_FLAG_IRM}
26+
/>
27+
);
28+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { t } from '@grafana/i18n';
2+
import syntheticMonitoringSvg from 'img/synthetic_monitoring_logo.svg';
3+
4+
import AdCard from './AdCard';
5+
6+
const LINK =
7+
'https://grafana.com/auth/sign-up/create-user?redirectPath=synthetic-monitoring&src=oss-grafana&cnt=alerting-synthetic-monitoring';
8+
const HELP_FLAG_SYNTHETIC_MONITORING = 0x0008;
9+
10+
export default function SyntheticMonitoringCard() {
11+
return (
12+
<AdCard
13+
title={t('alerting.home.synthetic-monitoring-card-title', 'Synthetic Monitoring')}
14+
description={t(
15+
'alerting.home.synthetic-monitoring-card-description',
16+
'Monitor critical user flows, websites, and APIs externally, from global locations.'
17+
)}
18+
href={LINK}
19+
logoUrl={syntheticMonitoringSvg}
20+
items={[
21+
t('alerting.home.synthetic-monitoring-card-item-1', 'Simulate end-to-end user journeys with browser checks.'),
22+
t(
23+
'alerting.home.synthetic-monitoring-card-item-2',
24+
'Run ping, DNS, HTTP/S, and TCP checks at every network layer.'
25+
),
26+
t(
27+
'alerting.home.synthetic-monitoring-card-item-3',
28+
'Use 20+ global probes or private probes behind your firewall.'
29+
),
30+
t(
31+
'alerting.home.synthetic-monitoring-card-item-4',
32+
'Track SLOs with built-in Prometheus-style alerts — right from the UI.'
33+
),
34+
]}
35+
helpFlag={HELP_FLAG_SYNTHETIC_MONITORING}
36+
/>
37+
);
38+
}

public/img/irm_logo.svg

Lines changed: 45 additions & 0 deletions
Loading
Lines changed: 10 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)