mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Contact points v2 – part 1 (#70643)
This commit is contained in:
12
public/app/features/alerting/components/ConditionalWrap.tsx
Normal file
12
public/app/features/alerting/components/ConditionalWrap.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ConditionalWrapProps {
|
||||
shouldWrap: boolean;
|
||||
children: JSX.Element;
|
||||
wrap: (children: JSX.Element) => JSX.Element;
|
||||
}
|
||||
|
||||
export const ConditionalWrap = ({ shouldWrap, children, wrap }: ConditionalWrapProps): JSX.Element =>
|
||||
shouldWrap ? React.cloneElement(wrap(children)) : children;
|
||||
|
||||
export default ConditionalWrap;
|
||||
@@ -1,5 +1,267 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
const ContactPoints = () => <>Hello, contact points v2!</>;
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Badge, Button, Dropdown, Icon, Menu, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Span } from '@grafana/ui/src/unstable';
|
||||
import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap';
|
||||
import { GrafanaNotifierType } from 'app/types/alerting';
|
||||
|
||||
import { INTEGRATION_ICONS } from '../../types/contact-points';
|
||||
import { MetaText } from '../MetaText';
|
||||
import { Spacer } from '../Spacer';
|
||||
import { Strong } from '../Strong';
|
||||
|
||||
const ContactPoints = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<Stack direction="column">
|
||||
<div className={styles.contactPointWrapper}>
|
||||
<Stack direction="column" gap={0}>
|
||||
<ContactPointHeader name={'grafana-default-email'} policies={['', '']} />
|
||||
<div className={styles.receiversWrapper}>
|
||||
<ContactPointReceiver type={'email'} description="gilles.demey@grafana.com" />
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div className={styles.contactPointWrapper}>
|
||||
<Stack direction="column" gap={0}>
|
||||
<ContactPointHeader name={'New school'} provenance={'api'} />
|
||||
<div className={styles.receiversWrapper}>
|
||||
<Stack direction="column" gap={0}>
|
||||
<ContactPointReceiver type={'slack'} description="#test-alerts" sendingResolved={false} />
|
||||
<ContactPointReceiver type={'discord'} />
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div className={styles.contactPointWrapper}>
|
||||
<Stack direction="column" gap={0}>
|
||||
<ContactPointHeader name={'Japan 🇯🇵'} />
|
||||
<div className={styles.receiversWrapper}>
|
||||
<ContactPointReceiver type={'line'} />
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div className={styles.contactPointWrapper}>
|
||||
<Stack direction="column" gap={0}>
|
||||
<ContactPointHeader name={'Google Stuff'} />
|
||||
<div className={styles.receiversWrapper}>
|
||||
<ContactPointReceiver type={'googlechat'} />
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div className={styles.contactPointWrapper}>
|
||||
<Stack direction="column" gap={0}>
|
||||
<ContactPointHeader name={'Chinese Contact Points'} />
|
||||
<div className={styles.receiversWrapper}>
|
||||
<Stack direction="column" gap={0}>
|
||||
<ContactPointReceiver type={'dingding'} />
|
||||
<ContactPointReceiver type={'wecom'} error="403 unauthorized" />
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div className={styles.contactPointWrapper}>
|
||||
<Stack direction="column" gap={0}>
|
||||
<ContactPointHeader
|
||||
name={
|
||||
"This is a very long title to check if we are dealing with it appropriately, it shouldn't cause any layout issues"
|
||||
}
|
||||
/>
|
||||
<div className={styles.receiversWrapper}>
|
||||
<Stack direction="column" gap={0}>
|
||||
<ContactPointReceiver type={'dingding'} />
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
interface ContactPointHeaderProps {
|
||||
name: string;
|
||||
provenance?: string;
|
||||
policies?: string[]; // some array of policies that refer to this contact point
|
||||
}
|
||||
|
||||
const ContactPointHeader = (props: ContactPointHeaderProps) => {
|
||||
const { name, provenance, policies = [] } = props;
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
const isProvisioned = Boolean(provenance);
|
||||
|
||||
return (
|
||||
<div className={styles.headerWrapper}>
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<Stack alignItems="center" gap={1}>
|
||||
<Span variant="body">{name}</Span>
|
||||
</Stack>
|
||||
{policies.length > 0 ? (
|
||||
<MetaText>
|
||||
{/* TODO make this a link to the notification policies page with the filter applied */}
|
||||
is used by <Strong>{policies.length}</Strong> notification policies
|
||||
</MetaText>
|
||||
) : (
|
||||
<MetaText>is not used</MetaText>
|
||||
)}
|
||||
{isProvisioned && <Badge color="purple" text="Provisioned" />}
|
||||
<Spacer />
|
||||
<ConditionalWrap
|
||||
shouldWrap={isProvisioned}
|
||||
wrap={(children) => (
|
||||
<Tooltip content="Provisioned items cannot be edited in the UI" placement="top">
|
||||
{children}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon="edit"
|
||||
type="button"
|
||||
disabled={isProvisioned}
|
||||
aria-label="edit-action"
|
||||
data-testid="edit-action"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</ConditionalWrap>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item label="Export" icon="download-alt" />
|
||||
<Menu.Divider />
|
||||
<Menu.Item label="Delete" icon="trash-alt" destructive disabled={isProvisioned} />
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon="ellipsis-h"
|
||||
type="button"
|
||||
aria-label="more-actions"
|
||||
data-testid="more-actions"
|
||||
/>
|
||||
</Dropdown>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ContactPointReceiverProps {
|
||||
type: GrafanaNotifierType | string;
|
||||
description?: string;
|
||||
error?: string;
|
||||
sendingResolved?: boolean;
|
||||
}
|
||||
|
||||
const ContactPointReceiver = (props: ContactPointReceiverProps) => {
|
||||
const { type, description, error, sendingResolved = true } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const iconName = INTEGRATION_ICONS[type];
|
||||
|
||||
return (
|
||||
<div className={styles.integrationWrapper}>
|
||||
<Stack direction="column" gap={0}>
|
||||
<div className={styles.receiverDescriptionRow}>
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
{iconName && <Icon name={iconName} />}
|
||||
<Span variant="body" color="primary">
|
||||
{type}
|
||||
</Span>
|
||||
</Stack>
|
||||
{description && (
|
||||
<Span variant="bodySmall" color="secondary">
|
||||
{description}
|
||||
</Span>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
<div className={styles.metadataRow}>
|
||||
<Stack direction="row" gap={1}>
|
||||
{error ? (
|
||||
<>
|
||||
{/* TODO we might need an error variant for MetaText, dito for success */}
|
||||
{/* TODO show error details on hover or elsewhere */}
|
||||
<Span color="error" variant="bodySmall" weight="bold">
|
||||
<Stack direction="row" alignItems={'center'} gap={0.5}>
|
||||
<Tooltip
|
||||
content={
|
||||
'failed to send notification to email addresses: gilles.demey@grafana.com: dial tcp 192.168.1.21:1025: connect: connection refused'
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Icon name="exclamation-circle" /> Last delivery attempt failed
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MetaText icon="clock-nine">
|
||||
Last delivery attempt <Strong>25 minutes ago</Strong>
|
||||
</MetaText>
|
||||
<MetaText icon="stopwatch">
|
||||
took <Strong>2s</Strong>
|
||||
</MetaText>
|
||||
</>
|
||||
)}
|
||||
{!sendingResolved && (
|
||||
<MetaText icon="info-circle">
|
||||
Delivering <Strong>only firing</Strong> notifications
|
||||
</MetaText>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
contactPointWrapper: css`
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
border: solid 1px ${theme.colors.border.weak};
|
||||
border-bottom: none;
|
||||
`,
|
||||
integrationWrapper: css`
|
||||
position: relative;
|
||||
background: ${theme.colors.background.primary};
|
||||
|
||||
border-bottom: solid 1px ${theme.colors.border.weak};
|
||||
`,
|
||||
headerWrapper: css`
|
||||
padding: ${theme.spacing(1)} ${theme.spacing(1.5)};
|
||||
|
||||
background: ${theme.colors.background.secondary};
|
||||
|
||||
border-bottom: solid 1px ${theme.colors.border.weak};
|
||||
border-top-left-radius: ${theme.shape.borderRadius()};
|
||||
border-top-right-radius: ${theme.shape.borderRadius()};
|
||||
`,
|
||||
receiverDescriptionRow: css`
|
||||
padding: ${theme.spacing(1)} ${theme.spacing(1.5)};
|
||||
`,
|
||||
metadataRow: css`
|
||||
padding: 0 ${theme.spacing(1.5)} ${theme.spacing(1.5)} ${theme.spacing(1.5)};
|
||||
|
||||
border-bottom-left-radius: ${theme.shape.borderRadius()};
|
||||
border-bottom-right-radius: ${theme.shape.borderRadius()};
|
||||
`,
|
||||
receiversWrapper: css``,
|
||||
});
|
||||
|
||||
export default ContactPoints;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* This hook will combine data from both the Alertmanager config
|
||||
* and (if available) it will also fetch the status from the Grafana Managed status endpoint
|
||||
*/
|
||||
|
||||
import { NotifierType, NotifierStatus } from 'app/types';
|
||||
|
||||
// A Contact Point has 1 or more integrations
|
||||
// each integration can have additional metadata assigned to it
|
||||
export interface ContactPoint<T extends Notifier> {
|
||||
notifiers: T[];
|
||||
}
|
||||
|
||||
interface Notifier {
|
||||
type: NotifierType;
|
||||
}
|
||||
|
||||
// Grafana Managed contact points have receivers with additional diagnostics
|
||||
export interface NotifierWithDiagnostics extends Notifier {
|
||||
status: NotifierStatus;
|
||||
}
|
||||
|
||||
export function useContactPoints(AlertManagerSourceName: string) {}
|
||||
@@ -4,7 +4,7 @@ import pluralize from 'pluralize';
|
||||
import React, { FC, Fragment, ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { GrafanaTheme2, IconName } from '@grafana/data';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Badge, Button, Dropdown, getTagColorsFromName, Icon, Menu, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Span } from '@grafana/ui/src/unstable';
|
||||
@@ -12,6 +12,7 @@ import { contextSrv } from 'app/core/core';
|
||||
import { RouteWithID, Receiver, ObjectMatcher, AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { ReceiversState } from 'app/types';
|
||||
|
||||
import { INTEGRATION_ICONS } from '../../types/contact-points';
|
||||
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||
import { normalizeMatchers } from '../../utils/matchers';
|
||||
import { createContactPointLink, createMuteTimingLink } from '../../utils/misc';
|
||||
@@ -130,7 +131,7 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
<div className={styles.policyItemWrapper}>
|
||||
<Stack direction="column" gap={1}>
|
||||
{/* Matchers and actions */}
|
||||
<div className={styles.matchersRow}>
|
||||
<div>
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
{isDefaultPolicy ? (
|
||||
<DefaultPolicyIndicator />
|
||||
@@ -429,18 +430,6 @@ interface ContactPointDetailsProps {
|
||||
receivers: Receiver[];
|
||||
}
|
||||
|
||||
const INTEGRATION_ICONS: Record<string, IconName> = {
|
||||
discord: 'discord',
|
||||
email: 'envelope',
|
||||
googlechat: 'google-hangouts-alt',
|
||||
hipchat: 'hipchat',
|
||||
line: 'line',
|
||||
pagerduty: 'pagerduty',
|
||||
slack: 'slack',
|
||||
teams: 'microsoft',
|
||||
telegram: 'telegram-alt',
|
||||
};
|
||||
|
||||
// @TODO make this work for cloud AMs too
|
||||
const ContactPointsHoverDetails: FC<ContactPointDetailsProps> = ({
|
||||
alertManagerSourceName,
|
||||
@@ -601,10 +590,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
metadataRow: css`
|
||||
background: ${theme.colors.background.secondary};
|
||||
|
||||
border-bottom-left-radius: ${theme.shape.borderRadius(1)};
|
||||
border-bottom-right-radius: ${theme.shape.borderRadius(1)};
|
||||
border-bottom-left-radius: ${theme.shape.borderRadius(2)};
|
||||
border-bottom-right-radius: ${theme.shape.borderRadius(2)};
|
||||
`,
|
||||
matchersRow: css``,
|
||||
policyWrapper: (hasFocus = false) => css`
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
13
public/app/features/alerting/unified/types/contact-points.ts
Normal file
13
public/app/features/alerting/unified/types/contact-points.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IconName } from '@grafana/ui';
|
||||
|
||||
export const INTEGRATION_ICONS: Record<string, IconName> = {
|
||||
discord: 'discord',
|
||||
email: 'envelope',
|
||||
googlechat: 'google-hangouts-alt',
|
||||
hipchat: 'hipchat',
|
||||
line: 'line',
|
||||
pagerduty: 'pagerduty',
|
||||
slack: 'slack',
|
||||
teams: 'microsoft',
|
||||
telegram: 'telegram-alt',
|
||||
};
|
||||
Reference in New Issue
Block a user