diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index 1c880e22791..e0daffde12b 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -199,6 +199,7 @@ export const availableIconsIndex = { 'square-shape': true, star: true, 'step-backward': true, + stopwatch: true, 'stopwatch-slash': true, sync: true, 'sync-slash': true, diff --git a/packages/grafana-ui/src/components/Icon/cached.json b/packages/grafana-ui/src/components/Icon/cached.json index 781f0d9c845..c86d3a2532c 100644 --- a/packages/grafana-ui/src/components/Icon/cached.json +++ b/packages/grafana-ui/src/components/Icon/cached.json @@ -120,6 +120,7 @@ "unicons/star", "unicons/step-backward", "unicons/sync", + "unicons/stopwatch", "unicons/table", "unicons/tag-alt", "unicons/times", diff --git a/packages/grafana-ui/src/components/Icon/iconBundle.ts b/packages/grafana-ui/src/components/Icon/iconBundle.ts index 675871f8201..553e0d11977 100644 --- a/packages/grafana-ui/src/components/Icon/iconBundle.ts +++ b/packages/grafana-ui/src/components/Icon/iconBundle.ts @@ -128,52 +128,53 @@ import u1117 from '../../../../../public/img/icons/unicons/square-shape.svg'; import u1118 from '../../../../../public/img/icons/unicons/star.svg'; import u1119 from '../../../../../public/img/icons/unicons/step-backward.svg'; import u1120 from '../../../../../public/img/icons/unicons/sync.svg'; -import u1121 from '../../../../../public/img/icons/unicons/table.svg'; -import u1122 from '../../../../../public/img/icons/unicons/tag-alt.svg'; -import u1123 from '../../../../../public/img/icons/unicons/times.svg'; -import u1124 from '../../../../../public/img/icons/unicons/trash-alt.svg'; -import u1125 from '../../../../../public/img/icons/unicons/unlock.svg'; -import u1126 from '../../../../../public/img/icons/unicons/upload.svg'; -import u1127 from '../../../../../public/img/icons/unicons/user.svg'; -import u1128 from '../../../../../public/img/icons/unicons/users-alt.svg'; -import u1129 from '../../../../../public/img/icons/unicons/wrap-text.svg'; -import u1130 from '../../../../../public/img/icons/unicons/cloud-upload.svg'; -import u1131 from '../../../../../public/img/icons/unicons/credit-card.svg'; -import u1132 from '../../../../../public/img/icons/unicons/file-copy-alt.svg'; -import u1133 from '../../../../../public/img/icons/unicons/fire.svg'; -import u1134 from '../../../../../public/img/icons/unicons/hourglass.svg'; -import u1135 from '../../../../../public/img/icons/unicons/layer-group.svg'; -import u1136 from '../../../../../public/img/icons/unicons/layers-alt.svg'; -import u1137 from '../../../../../public/img/icons/unicons/line-alt.svg'; -import u1138 from '../../../../../public/img/icons/unicons/list-ui-alt.svg'; -import u1139 from '../../../../../public/img/icons/unicons/message.svg'; -import u1140 from '../../../../../public/img/icons/unicons/palette.svg'; -import u1141 from '../../../../../public/img/icons/unicons/percentage.svg'; -import u1142 from '../../../../../public/img/icons/unicons/shield-exclamation.svg'; -import u1143 from '../../../../../public/img/icons/unicons/plus-square.svg'; -import u1144 from '../../../../../public/img/icons/unicons/x.svg'; -import u1145 from '../../../../../public/img/icons/unicons/capture.svg'; -import u1146 from '../../../../../public/img/icons/custom/gf-grid.svg'; -import u1147 from '../../../../../public/img/icons/custom/gf-landscape.svg'; -import u1148 from '../../../../../public/img/icons/custom/gf-layout-simple.svg'; -import u1149 from '../../../../../public/img/icons/custom/gf-portrait.svg'; -import u1150 from '../../../../../public/img/icons/custom/gf-bar-alignment-after.svg'; -import u1151 from '../../../../../public/img/icons/custom/gf-bar-alignment-before.svg'; -import u1152 from '../../../../../public/img/icons/custom/gf-bar-alignment-center.svg'; -import u1153 from '../../../../../public/img/icons/custom/gf-interpolation-linear.svg'; -import u1154 from '../../../../../public/img/icons/custom/gf-interpolation-smooth.svg'; -import u1155 from '../../../../../public/img/icons/custom/gf-interpolation-step-after.svg'; -import u1156 from '../../../../../public/img/icons/custom/gf-interpolation-step-before.svg'; -import u1157 from '../../../../../public/img/icons/custom/gf-logs.svg'; -import u1158 from '../../../../../public/img/icons/custom/gf-movepane-left.svg'; -import u1159 from '../../../../../public/img/icons/custom/gf-movepane-right.svg'; -import u1160 from '../../../../../public/img/icons/mono/favorite.svg'; -import u1161 from '../../../../../public/img/icons/mono/grafana.svg'; -import u1162 from '../../../../../public/img/icons/mono/heart.svg'; -import u1163 from '../../../../../public/img/icons/mono/heart-break.svg'; -import u1164 from '../../../../../public/img/icons/mono/panel-add.svg'; -import u1165 from '../../../../../public/img/icons/mono/library-panel.svg'; -import u1166 from '../../../../../public/img/icons/unicons/record-audio.svg'; +import u1121 from '../../../../../public/img/icons/unicons/stopwatch.svg'; +import u1122 from '../../../../../public/img/icons/unicons/table.svg'; +import u1123 from '../../../../../public/img/icons/unicons/tag-alt.svg'; +import u1124 from '../../../../../public/img/icons/unicons/times.svg'; +import u1125 from '../../../../../public/img/icons/unicons/trash-alt.svg'; +import u1126 from '../../../../../public/img/icons/unicons/unlock.svg'; +import u1127 from '../../../../../public/img/icons/unicons/upload.svg'; +import u1128 from '../../../../../public/img/icons/unicons/user.svg'; +import u1129 from '../../../../../public/img/icons/unicons/users-alt.svg'; +import u1130 from '../../../../../public/img/icons/unicons/wrap-text.svg'; +import u1131 from '../../../../../public/img/icons/unicons/cloud-upload.svg'; +import u1132 from '../../../../../public/img/icons/unicons/credit-card.svg'; +import u1133 from '../../../../../public/img/icons/unicons/file-copy-alt.svg'; +import u1134 from '../../../../../public/img/icons/unicons/fire.svg'; +import u1135 from '../../../../../public/img/icons/unicons/hourglass.svg'; +import u1136 from '../../../../../public/img/icons/unicons/layer-group.svg'; +import u1137 from '../../../../../public/img/icons/unicons/layers-alt.svg'; +import u1138 from '../../../../../public/img/icons/unicons/line-alt.svg'; +import u1139 from '../../../../../public/img/icons/unicons/list-ui-alt.svg'; +import u1140 from '../../../../../public/img/icons/unicons/message.svg'; +import u1141 from '../../../../../public/img/icons/unicons/palette.svg'; +import u1142 from '../../../../../public/img/icons/unicons/percentage.svg'; +import u1143 from '../../../../../public/img/icons/unicons/shield-exclamation.svg'; +import u1144 from '../../../../../public/img/icons/unicons/plus-square.svg'; +import u1145 from '../../../../../public/img/icons/unicons/x.svg'; +import u1146 from '../../../../../public/img/icons/unicons/capture.svg'; +import u1147 from '../../../../../public/img/icons/custom/gf-grid.svg'; +import u1148 from '../../../../../public/img/icons/custom/gf-landscape.svg'; +import u1149 from '../../../../../public/img/icons/custom/gf-layout-simple.svg'; +import u1150 from '../../../../../public/img/icons/custom/gf-portrait.svg'; +import u1151 from '../../../../../public/img/icons/custom/gf-bar-alignment-after.svg'; +import u1152 from '../../../../../public/img/icons/custom/gf-bar-alignment-before.svg'; +import u1153 from '../../../../../public/img/icons/custom/gf-bar-alignment-center.svg'; +import u1154 from '../../../../../public/img/icons/custom/gf-interpolation-linear.svg'; +import u1155 from '../../../../../public/img/icons/custom/gf-interpolation-smooth.svg'; +import u1156 from '../../../../../public/img/icons/custom/gf-interpolation-step-after.svg'; +import u1157 from '../../../../../public/img/icons/custom/gf-interpolation-step-before.svg'; +import u1158 from '../../../../../public/img/icons/custom/gf-logs.svg'; +import u1159 from '../../../../../public/img/icons/custom/gf-movepane-left.svg'; +import u1160 from '../../../../../public/img/icons/custom/gf-movepane-right.svg'; +import u1161 from '../../../../../public/img/icons/mono/favorite.svg'; +import u1162 from '../../../../../public/img/icons/mono/grafana.svg'; +import u1163 from '../../../../../public/img/icons/mono/heart.svg'; +import u1164 from '../../../../../public/img/icons/mono/heart-break.svg'; +import u1165 from '../../../../../public/img/icons/mono/panel-add.svg'; +import u1166 from '../../../../../public/img/icons/mono/library-panel.svg'; +import u1167 from '../../../../../public/img/icons/unicons/record-audio.svg'; // do not edit this list directly // the list of icons live here: @grafana/ui/components/Icon/cached.json @@ -317,52 +318,53 @@ export function initIconCache() { cacheItem(u1118, 'unicons/star.svg'); cacheItem(u1119, 'unicons/step-backward.svg'); cacheItem(u1120, 'unicons/sync.svg'); - cacheItem(u1121, 'unicons/table.svg'); - cacheItem(u1122, 'unicons/tag-alt.svg'); - cacheItem(u1123, 'unicons/times.svg'); - cacheItem(u1124, 'unicons/trash-alt.svg'); - cacheItem(u1125, 'unicons/unlock.svg'); - cacheItem(u1126, 'unicons/upload.svg'); - cacheItem(u1127, 'unicons/user.svg'); - cacheItem(u1128, 'unicons/users-alt.svg'); - cacheItem(u1129, 'unicons/wrap-text.svg'); - cacheItem(u1130, 'unicons/cloud-upload.svg'); - cacheItem(u1131, 'unicons/credit-card.svg'); - cacheItem(u1132, 'unicons/file-copy-alt.svg'); - cacheItem(u1133, 'unicons/fire.svg'); - cacheItem(u1134, 'unicons/hourglass.svg'); - cacheItem(u1135, 'unicons/layer-group.svg'); - cacheItem(u1136, 'unicons/layers-alt.svg'); - cacheItem(u1137, 'unicons/line-alt.svg'); - cacheItem(u1138, 'unicons/list-ui-alt.svg'); - cacheItem(u1139, 'unicons/message.svg'); - cacheItem(u1140, 'unicons/palette.svg'); - cacheItem(u1141, 'unicons/percentage.svg'); - cacheItem(u1142, 'unicons/shield-exclamation.svg'); - cacheItem(u1143, 'unicons/plus-square.svg'); - cacheItem(u1144, 'unicons/x.svg'); - cacheItem(u1145, 'unicons/capture.svg'); - cacheItem(u1146, 'custom/gf-grid.svg'); - cacheItem(u1147, 'custom/gf-landscape.svg'); - cacheItem(u1148, 'custom/gf-layout-simple.svg'); - cacheItem(u1149, 'custom/gf-portrait.svg'); - cacheItem(u1150, 'custom/gf-bar-alignment-after.svg'); - cacheItem(u1151, 'custom/gf-bar-alignment-before.svg'); - cacheItem(u1152, 'custom/gf-bar-alignment-center.svg'); - cacheItem(u1153, 'custom/gf-interpolation-linear.svg'); - cacheItem(u1154, 'custom/gf-interpolation-smooth.svg'); - cacheItem(u1155, 'custom/gf-interpolation-step-after.svg'); - cacheItem(u1156, 'custom/gf-interpolation-step-before.svg'); - cacheItem(u1157, 'custom/gf-logs.svg'); - cacheItem(u1158, 'custom/gf-movepane-left.svg'); - cacheItem(u1159, 'custom/gf-movepane-right.svg'); - cacheItem(u1160, 'mono/favorite.svg'); - cacheItem(u1161, 'mono/grafana.svg'); - cacheItem(u1162, 'mono/heart.svg'); - cacheItem(u1163, 'mono/heart-break.svg'); - cacheItem(u1164, 'mono/panel-add.svg'); - cacheItem(u1165, 'mono/library-panel.svg'); - cacheItem(u1166, 'unicons/record-audio.svg'); + cacheItem(u1121, 'unicons/stopwatch.svg'); + cacheItem(u1122, 'unicons/table.svg'); + cacheItem(u1123, 'unicons/tag-alt.svg'); + cacheItem(u1124, 'unicons/times.svg'); + cacheItem(u1125, 'unicons/trash-alt.svg'); + cacheItem(u1126, 'unicons/unlock.svg'); + cacheItem(u1127, 'unicons/upload.svg'); + cacheItem(u1128, 'unicons/user.svg'); + cacheItem(u1129, 'unicons/users-alt.svg'); + cacheItem(u1130, 'unicons/wrap-text.svg'); + cacheItem(u1131, 'unicons/cloud-upload.svg'); + cacheItem(u1132, 'unicons/credit-card.svg'); + cacheItem(u1133, 'unicons/file-copy-alt.svg'); + cacheItem(u1134, 'unicons/fire.svg'); + cacheItem(u1135, 'unicons/hourglass.svg'); + cacheItem(u1136, 'unicons/layer-group.svg'); + cacheItem(u1137, 'unicons/layers-alt.svg'); + cacheItem(u1138, 'unicons/line-alt.svg'); + cacheItem(u1139, 'unicons/list-ui-alt.svg'); + cacheItem(u1140, 'unicons/message.svg'); + cacheItem(u1141, 'unicons/palette.svg'); + cacheItem(u1142, 'unicons/percentage.svg'); + cacheItem(u1143, 'unicons/shield-exclamation.svg'); + cacheItem(u1144, 'unicons/plus-square.svg'); + cacheItem(u1145, 'unicons/x.svg'); + cacheItem(u1146, 'unicons/capture.svg'); + cacheItem(u1147, 'custom/gf-grid.svg'); + cacheItem(u1148, 'custom/gf-landscape.svg'); + cacheItem(u1149, 'custom/gf-layout-simple.svg'); + cacheItem(u1150, 'custom/gf-portrait.svg'); + cacheItem(u1151, 'custom/gf-bar-alignment-after.svg'); + cacheItem(u1152, 'custom/gf-bar-alignment-before.svg'); + cacheItem(u1153, 'custom/gf-bar-alignment-center.svg'); + cacheItem(u1154, 'custom/gf-interpolation-linear.svg'); + cacheItem(u1155, 'custom/gf-interpolation-smooth.svg'); + cacheItem(u1156, 'custom/gf-interpolation-step-after.svg'); + cacheItem(u1157, 'custom/gf-interpolation-step-before.svg'); + cacheItem(u1158, 'custom/gf-logs.svg'); + cacheItem(u1159, 'custom/gf-movepane-left.svg'); + cacheItem(u1160, 'custom/gf-movepane-right.svg'); + cacheItem(u1161, 'mono/favorite.svg'); + cacheItem(u1162, 'mono/grafana.svg'); + cacheItem(u1163, 'mono/heart.svg'); + cacheItem(u1164, 'mono/heart-break.svg'); + cacheItem(u1165, 'mono/panel-add.svg'); + cacheItem(u1166, 'mono/library-panel.svg'); + cacheItem(u1167, 'unicons/record-audio.svg'); // do not edit this list directly // the list of icons live here: @grafana/ui/components/Icon/cached.json } diff --git a/public/app/features/alerting/components/ConditionalWrap.tsx b/public/app/features/alerting/components/ConditionalWrap.tsx new file mode 100644 index 00000000000..7d6c495be2f --- /dev/null +++ b/public/app/features/alerting/components/ConditionalWrap.tsx @@ -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; diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx index 016efeb9330..2ea0c2deac2 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx @@ -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 ( + +
+ + +
+ +
+
+
+ +
+ + +
+ + + + +
+
+
+ +
+ + +
+ +
+
+
+ +
+ + +
+ +
+
+
+ +
+ + +
+ + + + +
+
+
+ +
+ + +
+ + + +
+
+
+
+ ); +}; + +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 ( +
+ + + {name} + + {policies.length > 0 ? ( + + {/* TODO make this a link to the notification policies page with the filter applied */} + is used by {policies.length} notification policies + + ) : ( + is not used + )} + {isProvisioned && } + + ( + + {children} + + )} + > + + + + + + + + } + > +
+ ); +}; + +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 ( +
+ +
+ + + {iconName && } + + {type} + + + {description && ( + + {description} + + )} + +
+
+ + {error ? ( + <> + {/* TODO we might need an error variant for MetaText, dito for success */} + {/* TODO show error details on hover or elsewhere */} + + + + + Last delivery attempt failed + + + + + + ) : ( + <> + + Last delivery attempt 25 minutes ago + + + took 2s + + + )} + {!sendingResolved && ( + + Delivering only firing notifications + + )} + +
+
+
+ ); +}; + +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; diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx new file mode 100644 index 00000000000..32244114cd9 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx @@ -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 { + 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) {} diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index bb9339d2dad..8ff296708b5 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -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 = ({
{/* Matchers and actions */} -
+
{isDefaultPolicy ? ( @@ -429,18 +430,6 @@ interface ContactPointDetailsProps { receivers: Receiver[]; } -const INTEGRATION_ICONS: Record = { - 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 = ({ 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; diff --git a/public/app/features/alerting/unified/types/contact-points.ts b/public/app/features/alerting/unified/types/contact-points.ts new file mode 100644 index 00000000000..b0fe57edf7f --- /dev/null +++ b/public/app/features/alerting/unified/types/contact-points.ts @@ -0,0 +1,13 @@ +import { IconName } from '@grafana/ui'; + +export const INTEGRATION_ICONS: Record = { + discord: 'discord', + email: 'envelope', + googlechat: 'google-hangouts-alt', + hipchat: 'hipchat', + line: 'line', + pagerduty: 'pagerduty', + slack: 'slack', + teams: 'microsoft', + telegram: 'telegram-alt', +};