move ms teams related models, containers, components etc to oncall (#3594)
# What this PR does Move ms teams related models, containers, components etc to oncall ## Which issue(s) this PR fixes https://github.com/grafana/oncall-private/issues/2144 ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
This commit is contained in:
parent
2af23fb7c0
commit
c57c0c4423
40 changed files with 1264 additions and 713 deletions
|
|
@ -1,4 +1,3 @@
|
|||
node_modules
|
||||
frontend_enterprise
|
||||
.DS_Store
|
||||
playwright-report
|
||||
|
|
|
|||
1
grafana-plugin/.gitignore
vendored
1
grafana-plugin/.gitignore
vendored
|
|
@ -13,7 +13,6 @@ yarn-error.log*
|
|||
|
||||
# This file is generated
|
||||
grafana-plugin.yml
|
||||
frontend_enterprise
|
||||
|
||||
# playwright
|
||||
/playwright-report/
|
||||
|
|
|
|||
|
|
@ -1,80 +1,45 @@
|
|||
import { AppFeature } from 'state/features';
|
||||
|
||||
import { TemplateForEdit, commonTemplateForEdit } from './CommonAlertTemplatesForm.config';
|
||||
export interface Template {
|
||||
name: string;
|
||||
group: string;
|
||||
}
|
||||
|
||||
export const templateForEdit: { [id: string]: TemplateForEdit } = commonTemplateForEdit;
|
||||
export const getTemplatesForEdit = (features: Record<string, boolean>) => {
|
||||
if (features[AppFeature.MsTeams]) {
|
||||
return { ...commonTemplateForEdit, ...additionalTemplateForEdit };
|
||||
}
|
||||
return commonTemplateForEdit;
|
||||
};
|
||||
|
||||
export const templatesToRender: Template[] = [
|
||||
{
|
||||
name: 'web_title_template',
|
||||
group: 'web',
|
||||
const additionalTemplateForEdit: { [id: string]: TemplateForEdit } = {
|
||||
msteams_title_template: {
|
||||
name: 'msteams_title_template',
|
||||
displayName: 'MS Teams title',
|
||||
description: '',
|
||||
additionalData: {
|
||||
chatOpsName: 'msteams',
|
||||
chatOpsDisplayName: 'MS Teams',
|
||||
},
|
||||
{
|
||||
name: 'slack_title_template',
|
||||
group: 'slack',
|
||||
type: 'plain',
|
||||
},
|
||||
{
|
||||
name: 'sms_title_template',
|
||||
group: 'sms',
|
||||
msteams_message_template: {
|
||||
name: 'msteams_message_template',
|
||||
displayName: 'MS Teams message',
|
||||
description: '',
|
||||
additionalData: {
|
||||
chatOpsName: 'msteams',
|
||||
chatOpsDisplayName: 'MS Teams',
|
||||
},
|
||||
{
|
||||
name: 'phone_call_title_template',
|
||||
group: 'phone',
|
||||
type: 'plain',
|
||||
},
|
||||
{
|
||||
name: 'email_title_template',
|
||||
group: 'email',
|
||||
msteams_image_url_template: {
|
||||
name: 'msteams_image_url_template',
|
||||
displayName: 'MS Teams image url',
|
||||
description: '',
|
||||
additionalData: {
|
||||
chatOpsName: 'msteams',
|
||||
chatOpsDisplayName: 'MS Teams',
|
||||
},
|
||||
{
|
||||
name: 'telegram_title_template',
|
||||
group: 'telegram',
|
||||
type: 'plain',
|
||||
},
|
||||
{
|
||||
name: 'slack_message_template',
|
||||
group: 'slack',
|
||||
},
|
||||
{
|
||||
name: 'web_message_template',
|
||||
group: 'web',
|
||||
},
|
||||
{
|
||||
name: 'email_message_template',
|
||||
group: 'email',
|
||||
},
|
||||
{
|
||||
name: 'telegram_message_template',
|
||||
group: 'telegram',
|
||||
},
|
||||
{
|
||||
name: 'slack_image_url_template',
|
||||
group: 'slack',
|
||||
},
|
||||
{
|
||||
name: 'web_image_url_template',
|
||||
group: 'web',
|
||||
},
|
||||
{
|
||||
name: 'telegram_image_url_template',
|
||||
group: 'telegram',
|
||||
},
|
||||
{
|
||||
name: 'grouping_id_template',
|
||||
group: 'alert behaviour',
|
||||
},
|
||||
{
|
||||
name: 'acknowledge_condition_template',
|
||||
group: 'alert behaviour',
|
||||
},
|
||||
{
|
||||
name: 'resolve_condition_template',
|
||||
group: 'alert behaviour',
|
||||
},
|
||||
{
|
||||
name: 'source_link_template',
|
||||
group: 'alert behaviour',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const FORM_NAME = 'AlertTemplates';
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
.select {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.templates {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.borderLeftRightBottom {
|
||||
border-left: 1px solid rgba(204, 204, 220, 0.15);
|
||||
border-bottom: 1px solid rgba(204, 204, 220, 0.15);
|
||||
border-right: 1px solid rgba(204, 204, 220, 0.15);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.borderRightBottom {
|
||||
border-bottom: 1px solid rgba(204, 204, 220, 0.15);
|
||||
border-right: 1px solid rgba(204, 204, 220, 0.15);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.template-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.templatesInfo {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.templates-editors {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.select-template {
|
||||
width: 50% !important;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.typographyText {
|
||||
margin-bottom: 24px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.autoresolve-condition section {
|
||||
border: 1px solid var(--primary-text-link);
|
||||
}
|
||||
|
||||
.autoresolve-label {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.web-title-message {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
|
@ -1,283 +0,0 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Label, Button, HorizontalGroup, VerticalGroup, Select, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { capitalCase } from 'change-case';
|
||||
import cn from 'classnames/bind';
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
import { templatesToRender, Template } from 'components/AlertTemplates/AlertTemplatesForm.config';
|
||||
import { getLabelFromTemplateName } from 'components/AlertTemplates/AlertTemplatesForm.helper';
|
||||
import Block from 'components/GBlock/Block';
|
||||
import MonacoEditor from 'components/MonacoEditor/MonacoEditor';
|
||||
import SourceCode from 'components/SourceCode/SourceCode';
|
||||
import Text from 'components/Text/Text';
|
||||
import TemplatePreview, { TEMPLATE_PAGE } from 'containers/TemplatePreview/TemplatePreview';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { makeRequest } from 'network';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { UserActions, isUserActionAllowed } from 'utils/authorization';
|
||||
|
||||
import styles from './AlertTemplatesForm.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface AlertTemplatesFormProps {
|
||||
templates: any;
|
||||
onUpdateTemplates: (values: any) => void;
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
alertGroupId?: Alert['pk'];
|
||||
demoAlertEnabled: boolean;
|
||||
handleSendDemoAlertClick: () => void;
|
||||
templatesRefreshing: boolean;
|
||||
selectedTemplateName?: string;
|
||||
}
|
||||
|
||||
const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
|
||||
const {
|
||||
onUpdateTemplates,
|
||||
templates,
|
||||
alertReceiveChannelId,
|
||||
alertGroupId,
|
||||
demoAlertEnabled,
|
||||
handleSendDemoAlertClick,
|
||||
templatesRefreshing,
|
||||
selectedTemplateName,
|
||||
} = props;
|
||||
|
||||
const [tempValues, setTempValues] = useState<{
|
||||
[key: string]: string | null;
|
||||
}>({});
|
||||
const [activeGroup, setActiveGroup] = useState<string>();
|
||||
const [activeTemplate, setActiveTemplate] = useState<Template>();
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest('/preview_template_options/', {});
|
||||
}, []);
|
||||
|
||||
const getChangeHandler = (templateName: string) => {
|
||||
return (value: string) => {
|
||||
setTempValues((oldTempValues) => ({
|
||||
...oldTempValues, // erase another edited templates
|
||||
[templateName]: value,
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const data = Object.keys(tempValues).reduce((acc: { [key: string]: string }, key: string) => {
|
||||
if (templates[key] !== tempValues[key]) {
|
||||
acc = { ...acc, [key]: tempValues[key] };
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
onUpdateTemplates(data);
|
||||
}, [onUpdateTemplates, tempValues]);
|
||||
|
||||
const handleReset = () => {
|
||||
const temValuesCopy = omit(
|
||||
tempValues,
|
||||
groups[activeGroup].map((group) => group.name)
|
||||
);
|
||||
setTempValues(temValuesCopy);
|
||||
};
|
||||
|
||||
const filteredTemplatesToRender = useMemo(() => {
|
||||
return templates
|
||||
? templatesToRender.filter((template) => {
|
||||
return template.name in templates;
|
||||
})
|
||||
: [];
|
||||
}, [templates]);
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const groups: { [key: string]: Template[] } = {};
|
||||
|
||||
filteredTemplatesToRender.forEach((templateToRender) => {
|
||||
if (!groups[templateToRender.group]) {
|
||||
groups[templateToRender.group] = [];
|
||||
}
|
||||
groups[templateToRender.group].push(templateToRender);
|
||||
});
|
||||
return groups;
|
||||
}, [filteredTemplatesToRender]);
|
||||
|
||||
const getGroupByTemplateName = (templateName: string) => {
|
||||
Object.values(groups).find((group) => {
|
||||
const foundTemplate = group.find((obj) => obj.name === templateName);
|
||||
setActiveGroup(foundTemplate?.group);
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeActiveGroup = useCallback((group: SelectableValue) => {
|
||||
setActiveGroup(group.value);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const groupsArr = Object.keys(groups);
|
||||
if (selectedTemplateName) {
|
||||
getGroupByTemplateName(selectedTemplateName);
|
||||
} else {
|
||||
if (!activeGroup && groupsArr.length) {
|
||||
setActiveGroup(groupsArr[0]);
|
||||
}
|
||||
}
|
||||
}, [groups, activeGroup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeGroup && groups[activeGroup]) {
|
||||
setActiveTemplate(groups[activeGroup][0]);
|
||||
}
|
||||
}, [activeGroup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeTemplate && filteredTemplatesToRender.length) {
|
||||
setActiveTemplate(filteredTemplatesToRender[0]);
|
||||
}
|
||||
}, [activeTemplate, filteredTemplatesToRender]);
|
||||
|
||||
if (!activeTemplate) {
|
||||
return <LoadingPlaceholder text="Loading..." />;
|
||||
}
|
||||
|
||||
const sendDemoAlertBlock = (
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">There are no alerts from this monitoring yet.</Text>
|
||||
{demoAlertEnabled ? (
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsTest}>
|
||||
<Button className={cx('button')} variant="primary" onClick={handleSendDemoAlertClick} size="sm">
|
||||
Send demo alert
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
) : null}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
const handleGoToTemplateSettingsCllick = () => LocationHelper.update({ tab: 'Autoresolve' }, 'partial');
|
||||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<Block bordered>
|
||||
<VerticalGroup>
|
||||
<Label>Edit template for</Label>
|
||||
<Select
|
||||
options={Object.keys(groups).map((group: string) => ({
|
||||
value: group,
|
||||
label: capitalCase(group),
|
||||
}))}
|
||||
value={activeGroup}
|
||||
onChange={handleChangeActiveGroup}
|
||||
className={cx('select', 'select-template')}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</Block>
|
||||
<div className={cx('templatesInfo')}>
|
||||
<Block className={cx('templates', 'borderLeftRightBottom')}>
|
||||
<VerticalGroup>
|
||||
<Text type="secondary">
|
||||
<p>
|
||||
<a href="https://jinja.palletsprojects.com/en/3.0.x/" target="_blank" rel="noreferrer">
|
||||
Jinja2
|
||||
</a>
|
||||
{activeGroup === 'slack' && ', Slack markdown'}
|
||||
{activeGroup === 'web' && ', Markdown'}
|
||||
{activeGroup === 'telegram' && ', html'}
|
||||
{' supported. '}
|
||||
Reserved variables available: <Text keyboard>payload</Text>, <Text keyboard>grafana_oncall_link</Text>,{' '}
|
||||
<Text keyboard>grafana_oncall_incident_id</Text>, <Text keyboard>integration_name</Text>,
|
||||
<Text keyboard>source_link</Text>. Press <Text keyboard>Ctrl</Text>+<Text keyboard>Space</Text> to get
|
||||
suggestions
|
||||
</p>
|
||||
</Text>
|
||||
{groups[activeGroup].map((activeTemplate) => (
|
||||
<div
|
||||
key={activeTemplate.name}
|
||||
className={cx('template-form', {
|
||||
'template-form-full': true,
|
||||
'autoresolve-condition': selectedTemplateName && activeTemplate.name === 'resolve_condition_template',
|
||||
})}
|
||||
>
|
||||
<Label className={cx({ 'autoresolve-label': activeTemplate.name === 'resolve_condition_template' })}>
|
||||
{getLabelFromTemplateName(activeTemplate.name, activeGroup)}
|
||||
</Label>
|
||||
{activeTemplate.name === 'resolve_condition_template' && (
|
||||
<Text type="secondary" size="small">
|
||||
To activate autoresolving change integration
|
||||
<Button fill="text" size="sm" onClick={handleGoToTemplateSettingsCllick}>
|
||||
settings
|
||||
</Button>
|
||||
</Text>
|
||||
)}
|
||||
<MonacoEditor
|
||||
value={tempValues[activeTemplate.name] ?? (templates[activeTemplate.name] || '')}
|
||||
disabled={false}
|
||||
data={templates}
|
||||
onChange={getChangeHandler(activeTemplate.name)}
|
||||
loading={templatesRefreshing}
|
||||
/>
|
||||
<div className={cx('typographyText')}>
|
||||
<Text type="secondary">
|
||||
Press <Text keyboard>Ctrl</Text>+<Text keyboard>Space</Text> to get suggestions
|
||||
</Text>
|
||||
{activeGroup === 'web' && activeTemplate.name === 'web_title_template' && (
|
||||
<div className={cx('web-title-message')}>
|
||||
<Text type="secondary" size="small">
|
||||
Please note that after changing the web title template new alert groups will be searchable by
|
||||
new title. Alert Groups created before the template was changed will be still searchable by old
|
||||
title only.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<HorizontalGroup spacing="sm">
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Button variant="primary" onClick={handleSubmit}>
|
||||
Save Templates
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
<Button variant="destructive" onClick={handleReset}>
|
||||
Reset Template
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Block>
|
||||
<Block className={cx('templates', 'borderRightBottom')}>
|
||||
<VerticalGroup>
|
||||
{templates?.payload_example ? (
|
||||
<VerticalGroup spacing="md">
|
||||
{isUserActionAllowed(UserActions.IntegrationsTest) && (
|
||||
<VerticalGroup>
|
||||
<Label>{`${capitalCase(activeGroup)} Preview`}</Label>
|
||||
<VerticalGroup style={{ width: '100%' }}>
|
||||
{groups[activeGroup].map((template) => (
|
||||
<TemplatePreview
|
||||
templatePage={TEMPLATE_PAGE.Integrations}
|
||||
key={template.name}
|
||||
templateName={template.name}
|
||||
templateBody={tempValues[template.name] ?? templates[template.name]}
|
||||
alertReceiveChannelId={alertReceiveChannelId}
|
||||
alertGroupId={alertGroupId}
|
||||
/>
|
||||
))}
|
||||
</VerticalGroup>
|
||||
</VerticalGroup>
|
||||
)}
|
||||
<VerticalGroup>
|
||||
<Label>Payload Example</Label>
|
||||
<SourceCode>{JSON.stringify(templates?.payload_example, null, 4)}</SourceCode>
|
||||
</VerticalGroup>
|
||||
</VerticalGroup>
|
||||
) : (
|
||||
sendDemoAlertBlock
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Block>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertTemplatesForm;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { TemplateOptions } from 'pages/integration/Integration.config';
|
||||
import { BaseTemplateOptions } from 'pages/integration/IntegrationCommon.config';
|
||||
|
||||
export interface Template {
|
||||
name: string;
|
||||
|
|
@ -22,19 +22,19 @@ export interface TemplateForEdit {
|
|||
export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
|
||||
web_title_template: {
|
||||
displayName: 'Web title',
|
||||
name: TemplateOptions.WebTitle.key,
|
||||
name: BaseTemplateOptions.WebTitle.key,
|
||||
description: '',
|
||||
type: 'html',
|
||||
},
|
||||
web_message_template: {
|
||||
displayName: 'Web message',
|
||||
name: TemplateOptions.WebMessage.key,
|
||||
name: BaseTemplateOptions.WebMessage.key,
|
||||
description: '',
|
||||
type: 'html',
|
||||
},
|
||||
slack_title_template: {
|
||||
displayName: 'Slack title',
|
||||
name: TemplateOptions.SlackTitle.key,
|
||||
name: BaseTemplateOptions.SlackTitle.key,
|
||||
description: '',
|
||||
additionalData: {
|
||||
chatOpsName: 'slack',
|
||||
|
|
@ -44,26 +44,26 @@ export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
|
|||
type: 'plain',
|
||||
},
|
||||
sms_title_template: {
|
||||
name: TemplateOptions.SMS.key,
|
||||
name: BaseTemplateOptions.SMS.key,
|
||||
displayName: 'Sms title',
|
||||
description:
|
||||
"Result of this template will be used as title of SMS message. Please don't include any urls, or phone numbers, to avoid SMS message being blocked by carriers.",
|
||||
type: 'plain',
|
||||
},
|
||||
phone_call_title_template: {
|
||||
name: TemplateOptions.Phone.key,
|
||||
name: BaseTemplateOptions.Phone.key,
|
||||
displayName: 'Phone Call title',
|
||||
description: '',
|
||||
type: 'plain',
|
||||
},
|
||||
email_title_template: {
|
||||
name: TemplateOptions.EmailTitle.key,
|
||||
name: BaseTemplateOptions.EmailTitle.key,
|
||||
displayName: 'Email title',
|
||||
description: '',
|
||||
type: 'plain',
|
||||
},
|
||||
telegram_title_template: {
|
||||
name: TemplateOptions.TelegramTitle.key,
|
||||
name: BaseTemplateOptions.TelegramTitle.key,
|
||||
displayName: 'Telegram title',
|
||||
description: '',
|
||||
additionalData: {
|
||||
|
|
@ -73,7 +73,7 @@ export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
|
|||
type: 'plain',
|
||||
},
|
||||
slack_message_template: {
|
||||
name: TemplateOptions.SlackMessage.key,
|
||||
name: BaseTemplateOptions.SlackMessage.key,
|
||||
displayName: 'Slack message',
|
||||
description: '',
|
||||
additionalData: {
|
||||
|
|
@ -84,13 +84,13 @@ export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
|
|||
type: 'plain',
|
||||
},
|
||||
email_message_template: {
|
||||
name: TemplateOptions.EmailMessage.key,
|
||||
name: BaseTemplateOptions.EmailMessage.key,
|
||||
displayName: 'Email message',
|
||||
description: '',
|
||||
type: 'plain',
|
||||
},
|
||||
telegram_message_template: {
|
||||
name: TemplateOptions.TelegramMessage.key,
|
||||
name: BaseTemplateOptions.TelegramMessage.key,
|
||||
displayName: 'Telegram message',
|
||||
description: '',
|
||||
additionalData: {
|
||||
|
|
@ -100,7 +100,7 @@ export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
|
|||
type: 'plain',
|
||||
},
|
||||
slack_image_url_template: {
|
||||
name: TemplateOptions.SlackImage.key,
|
||||
name: BaseTemplateOptions.SlackImage.key,
|
||||
displayName: 'Slack image url',
|
||||
description: '',
|
||||
additionalData: {
|
||||
|
|
@ -111,13 +111,13 @@ export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
|
|||
type: 'plain',
|
||||
},
|
||||
web_image_url_template: {
|
||||
name: TemplateOptions.WebImage.key,
|
||||
name: BaseTemplateOptions.WebImage.key,
|
||||
displayName: 'Web image url',
|
||||
description: '',
|
||||
type: 'image',
|
||||
},
|
||||
telegram_image_url_template: {
|
||||
name: TemplateOptions.TelegramImage.key,
|
||||
name: BaseTemplateOptions.TelegramImage.key,
|
||||
displayName: 'Telegram image url',
|
||||
description: '',
|
||||
additionalData: {
|
||||
|
|
@ -127,33 +127,33 @@ export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
|
|||
type: 'image',
|
||||
},
|
||||
grouping_id_template: {
|
||||
name: TemplateOptions.Grouping.key,
|
||||
name: BaseTemplateOptions.Grouping.key,
|
||||
displayName: 'Grouping',
|
||||
description:
|
||||
'Reduce noise, minimize duplication with Alert Grouping, based on time, alert content, and even multiple features at the same time. Check the cheasheet to customize your template.',
|
||||
type: 'plain',
|
||||
},
|
||||
acknowledge_condition_template: {
|
||||
name: TemplateOptions.Autoacknowledge.key,
|
||||
name: BaseTemplateOptions.Autoacknowledge.key,
|
||||
displayName: 'Acknowledge condition',
|
||||
description: '',
|
||||
type: 'boolean',
|
||||
},
|
||||
resolve_condition_template: {
|
||||
name: TemplateOptions.Resolve.key,
|
||||
name: BaseTemplateOptions.Resolve.key,
|
||||
displayName: 'Resolve condition',
|
||||
description:
|
||||
'When monitoring systems return to normal, they can send "resolve" alerts. OnCall can use these signals to resolve alert groups accordingly.',
|
||||
type: 'boolean',
|
||||
},
|
||||
source_link_template: {
|
||||
name: TemplateOptions.SourceLink.key,
|
||||
name: BaseTemplateOptions.SourceLink.key,
|
||||
displayName: 'Source link',
|
||||
description: '',
|
||||
type: 'plain',
|
||||
},
|
||||
route_template: {
|
||||
name: TemplateOptions.Routing.key,
|
||||
name: BaseTemplateOptions.Routing.key,
|
||||
displayName: 'Routing',
|
||||
description:
|
||||
'Routes direct alerts to different escalation chains based on the content, such as severity or region.',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { HorizontalGroup, InlineSwitch } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import GSelect from 'containers/GSelect/GSelect';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { MSTeamsChannel } from 'models/msteams_channel/msteams_channel.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
import styles from 'containers/AlertRules/parts/connectors/index.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface MSTeamsConnectorProps {
|
||||
channelFilterId: ChannelFilter['id'];
|
||||
}
|
||||
|
||||
const MSTeamsConnector = (props: MSTeamsConnectorProps) => {
|
||||
const { channelFilterId } = props;
|
||||
|
||||
const store = useStore();
|
||||
const { alertReceiveChannelStore } = store;
|
||||
|
||||
const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId];
|
||||
|
||||
const handleMSTeamsChannelChange = useCallback((_value: MSTeamsChannel['id'], msteamsChannel: MSTeamsChannel) => {
|
||||
alertReceiveChannelStore.saveChannelFilter(channelFilterId, {
|
||||
notification_backends: {
|
||||
MSTEAMS: { channel: msteamsChannel?.id || null },
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleChannelFilterNotifyInMSTeamsChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
alertReceiveChannelStore.saveChannelFilter(channelFilterId, {
|
||||
notification_backends: { MSTEAMS: { enabled: event.target.checked } },
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<HorizontalGroup wrap spacing="sm">
|
||||
<div className={cx('slack-channel-switch')}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<InlineSwitch
|
||||
value={channelFilter.notification_backends?.MSTEAMS?.enabled}
|
||||
onChange={handleChannelFilterNotifyInMSTeamsChange}
|
||||
transparent
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</div>
|
||||
Post to Microsoft Teams channel
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<GSelect
|
||||
showSearch
|
||||
allowClear
|
||||
className={cx('select', 'control')}
|
||||
modelName="msteamsChannelStore"
|
||||
displayField="display_name"
|
||||
valueField="id"
|
||||
placeholder="Select Microsoft Teams Channel"
|
||||
value={channelFilter.notification_backends?.MSTEAMS?.channel}
|
||||
onChange={handleMSTeamsChannelChange}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MSTeamsConnector;
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { VerticalGroup } from '@grafana/ui';
|
||||
|
||||
import Timeline from 'components/Timeline/Timeline';
|
||||
import MSTeamsConnector from 'containers/AlertRules/parts/connectors/MSTeamsConnector';
|
||||
import SlackConnector from 'containers/AlertRules/parts/connectors/SlackConnector';
|
||||
import TelegramConnector from 'containers/AlertRules/parts/connectors/TelegramConnector';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
|
|
@ -19,13 +20,19 @@ export const ChatOpsConnectors = (props: ChatOpsConnectorsProps) => {
|
|||
const { channelFilterId, showLineNumber = true } = props;
|
||||
|
||||
const store = useStore();
|
||||
const { telegramChannelStore, organizationStore } = store;
|
||||
const { organizationStore, telegramChannelStore, msteamsChannelStore } = store;
|
||||
|
||||
const isSlackInstalled = Boolean(organizationStore.currentOrganization?.slack_team_identity);
|
||||
const isTelegramInstalled =
|
||||
store.hasFeature(AppFeature.Telegram) && telegramChannelStore.currentTeamToTelegramChannel?.length > 0;
|
||||
|
||||
if (!isSlackInstalled && !isTelegramInstalled) {
|
||||
useEffect(() => {
|
||||
msteamsChannelStore.updateMSTeamsChannels();
|
||||
}, []);
|
||||
|
||||
const isMSTeamsInstalled = msteamsChannelStore.currentTeamToMSTeamsChannel?.length > 0;
|
||||
|
||||
if (!isSlackInstalled && !isTelegramInstalled && !isMSTeamsInstalled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +41,7 @@ export const ChatOpsConnectors = (props: ChatOpsConnectorsProps) => {
|
|||
<VerticalGroup>
|
||||
{isSlackInstalled && <SlackConnector channelFilterId={channelFilterId} />}
|
||||
{isTelegramInstalled && <TelegramConnector channelFilterId={channelFilterId} />}
|
||||
{isMSTeamsInstalled && <MSTeamsConnector channelFilterId={channelFilterId} />}
|
||||
</VerticalGroup>
|
||||
</Timeline.Item>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import AlertTemplatesForm from 'components/AlertTemplates/AlertTemplatesForm';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openErrorNotification, openNotification } from 'utils';
|
||||
|
||||
interface TeamEditContainerProps {
|
||||
onHide: () => void;
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
alertGroupId?: Alert['pk'];
|
||||
onUpdate?: () => void;
|
||||
onUpdateTemplates?: () => void;
|
||||
visible?: boolean;
|
||||
selectedTemplateName?: string;
|
||||
}
|
||||
|
||||
const AlertTemplatesFormContainer = observer((props: TeamEditContainerProps) => {
|
||||
const { alertReceiveChannelId, alertGroupId, onUpdateTemplates, selectedTemplateName } = props;
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const [templatesRefreshing, setTemplatesRefreshing] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
store.alertReceiveChannelStore.updateItem(alertReceiveChannelId);
|
||||
store.alertReceiveChannelStore.updateTemplates(alertReceiveChannelId, alertGroupId);
|
||||
}, [alertGroupId, alertReceiveChannelId, store]);
|
||||
|
||||
const onUpdateTemplatesCallback = useCallback(
|
||||
(data) => {
|
||||
store.alertReceiveChannelStore
|
||||
.saveTemplates(alertReceiveChannelId, data)
|
||||
.then(() => {
|
||||
openNotification('Alert templates are successfully updated');
|
||||
if (onUpdateTemplates) {
|
||||
onUpdateTemplates();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response?.data?.length > 0) {
|
||||
openErrorNotification(err.response.data);
|
||||
} else {
|
||||
openErrorNotification(err.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
[alertReceiveChannelId, onUpdateTemplates, store.alertReceiveChannelStore]
|
||||
);
|
||||
|
||||
const handleSendDemoAlertClickCallback = useCallback(() => {
|
||||
store.alertReceiveChannelStore.sendDemoAlert(alertReceiveChannelId).then(() => {
|
||||
setTemplatesRefreshing(true);
|
||||
store.alertReceiveChannelStore.updateTemplates(alertReceiveChannelId).then(() => {
|
||||
setTemplatesRefreshing(false);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const templates = store.alertReceiveChannelStore.templates[alertReceiveChannelId];
|
||||
const alertReceiveChannel = store.alertReceiveChannelStore.items[alertReceiveChannelId];
|
||||
|
||||
return (
|
||||
<AlertTemplatesForm
|
||||
alertReceiveChannelId={alertReceiveChannelId}
|
||||
alertGroupId={alertGroupId}
|
||||
templates={templates}
|
||||
onUpdateTemplates={onUpdateTemplatesCallback}
|
||||
demoAlertEnabled={alertReceiveChannel?.demo_alert_enabled}
|
||||
handleSendDemoAlertClick={handleSendDemoAlertClickCallback}
|
||||
templatesRefreshing={templatesRefreshing}
|
||||
selectedTemplateName={selectedTemplateName}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default AlertTemplatesFormContainer;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { User } from 'models/user/user.types';
|
||||
|
||||
export const getIfChatOpsConnected = (user: User) => {
|
||||
return user?.slack_user_identity || user?.telegram_configuration;
|
||||
return user?.slack_user_identity || user?.telegram_configuration || user?.messaging_backends?.MSTEAMS;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,35 @@
|
|||
import { MONACO_INPUT_HEIGHT_SMALL, MONACO_INPUT_HEIGHT_TALL } from 'pages/integration/IntegrationCommon.config';
|
||||
import { AppFeature } from 'state/features';
|
||||
|
||||
import { TemplateBlock, commonTemplatesToRender } from './IntegrationCommonTemplatesList.config';
|
||||
|
||||
export const templatesToRender: TemplateBlock[] = commonTemplatesToRender;
|
||||
const additionalTemplatesToRender: TemplateBlock[] = [
|
||||
{
|
||||
name: 'MS Teams',
|
||||
contents: [
|
||||
{
|
||||
name: 'msteams_title_template',
|
||||
label: 'Title',
|
||||
height: MONACO_INPUT_HEIGHT_SMALL,
|
||||
},
|
||||
{
|
||||
name: 'msteams_message_template',
|
||||
label: 'Message',
|
||||
height: MONACO_INPUT_HEIGHT_TALL,
|
||||
},
|
||||
{
|
||||
name: 'msteams_image_url_template',
|
||||
label: 'Image',
|
||||
height: MONACO_INPUT_HEIGHT_SMALL,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const getTemplatesToRender = (features: Record<string, boolean>) => {
|
||||
if (features[AppFeature.MsTeams]) {
|
||||
return commonTemplatesToRender.concat(additionalTemplatesToRender);
|
||||
}
|
||||
|
||||
return commonTemplatesToRender;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ import React, { useState, useCallback } from 'react';
|
|||
|
||||
import { ConfirmModal, InlineSwitch, Tooltip } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import IntegrationBlockItem from 'components/Integrations/IntegrationBlockItem';
|
||||
import IntegrationTemplateBlock from 'components/Integrations/IntegrationTemplateBlock';
|
||||
import MonacoEditor from 'components/MonacoEditor/MonacoEditor';
|
||||
import { MONACO_READONLY_CONFIG } from 'components/MonacoEditor/MonacoEditor.config';
|
||||
import Text from 'components/Text/Text';
|
||||
import { templatesToRender } from 'containers/IntegrationContainers/IntegrationTemplatesList.config';
|
||||
import { getTemplatesToRender } from 'containers/IntegrationContainers/IntegrationTemplatesList.config';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
|
||||
import IntegrationHelper from 'pages/integration/Integration.helper';
|
||||
|
|
@ -27,14 +28,15 @@ interface IntegrationTemplateListProps {
|
|||
alertReceiveChannelAllowSourceBasedResolving: boolean;
|
||||
}
|
||||
|
||||
const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
|
||||
const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = observer(
|
||||
({
|
||||
templates,
|
||||
openEditTemplateModal,
|
||||
alertReceiveChannelId,
|
||||
alertReceiveChannelIsBasedOnAlertManager,
|
||||
alertReceiveChannelAllowSourceBasedResolving,
|
||||
}) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
}) => {
|
||||
const { alertReceiveChannelStore, features } = useStore();
|
||||
const [isRestoringTemplate, setIsRestoringTemplate] = useState<boolean>(false);
|
||||
const [templateRestoreName, setTemplateRestoreName] = useState<string>(undefined);
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
|
|
@ -51,6 +53,8 @@ const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
|
|||
});
|
||||
}, []);
|
||||
|
||||
const templatesToRender = getTemplatesToRender(features);
|
||||
|
||||
return (
|
||||
<div className={cx('integration__templates')}>
|
||||
{showConfirmModal && (
|
||||
|
|
@ -90,7 +94,9 @@ const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
|
|||
</Tooltip>
|
||||
)}
|
||||
{isResolveConditionTemplateEditable(contents.name) && (
|
||||
<div className={cx('input', { 'input-with-toggle': isResolveConditionTemplate(contents.name) })}>
|
||||
<div
|
||||
className={cx('input', { 'input-with-toggle': isResolveConditionTemplate(contents.name) })}
|
||||
>
|
||||
<MonacoEditor
|
||||
value={IntegrationHelper.getFilteredTemplate(
|
||||
templates[contents.name] || '',
|
||||
|
|
@ -157,7 +163,8 @@ const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
|
|||
setShowConfirmModal(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const VerticalBlock: React.FC<{ children: any[] }> = ({ children }) => {
|
||||
return <div className={cx('vertical-block')}>{children}</div>;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import cn from 'classnames/bind';
|
|||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { templateForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config';
|
||||
import { getTemplatesForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config';
|
||||
import { TemplateForEdit } from 'components/AlertTemplates/CommonAlertTemplatesForm.config';
|
||||
import CheatSheet from 'components/CheatSheet/CheatSheet';
|
||||
import {
|
||||
|
|
@ -22,7 +22,8 @@ import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_
|
|||
import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { TemplateOptions } from 'pages/integration/Integration.config';
|
||||
import { BaseTemplateOptions } from 'pages/integration/IntegrationCommon.config';
|
||||
import { useStore } from 'state/useStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
|
|
@ -51,7 +52,11 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
const [resultError, setResultError] = useState<string>(undefined);
|
||||
const [isRecentAlertGroupExisting, setIsRecentAlertGroupExisting] = useState<boolean>(false);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
const templateForEdit = getTemplatesForEdit(store.features);
|
||||
|
||||
if (templateForEdit[template.name]) {
|
||||
const locationParams: any = { template: template.name };
|
||||
if (template.isRoute) {
|
||||
|
|
@ -124,25 +129,25 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
|
||||
const getCheatSheet = (templateKey: string) => {
|
||||
switch (templateKey) {
|
||||
case TemplateOptions.Grouping.key:
|
||||
case TemplateOptions.Resolve.key:
|
||||
case BaseTemplateOptions.Grouping.key:
|
||||
case BaseTemplateOptions.Resolve.key:
|
||||
return groupingTemplateCheatSheet;
|
||||
case TemplateOptions.WebTitle.key:
|
||||
case TemplateOptions.WebMessage.key:
|
||||
case TemplateOptions.WebImage.key:
|
||||
case BaseTemplateOptions.WebTitle.key:
|
||||
case BaseTemplateOptions.WebMessage.key:
|
||||
case BaseTemplateOptions.WebImage.key:
|
||||
return genericTemplateCheatSheet;
|
||||
case TemplateOptions.Autoacknowledge.key:
|
||||
case TemplateOptions.SourceLink.key:
|
||||
case TemplateOptions.Phone.key:
|
||||
case TemplateOptions.SMS.key:
|
||||
case TemplateOptions.SlackTitle.key:
|
||||
case TemplateOptions.SlackMessage.key:
|
||||
case TemplateOptions.SlackImage.key:
|
||||
case TemplateOptions.TelegramTitle.key:
|
||||
case TemplateOptions.TelegramMessage.key:
|
||||
case TemplateOptions.TelegramImage.key:
|
||||
case TemplateOptions.EmailTitle.key:
|
||||
case TemplateOptions.EmailMessage.key:
|
||||
case BaseTemplateOptions.Autoacknowledge.key:
|
||||
case BaseTemplateOptions.SourceLink.key:
|
||||
case BaseTemplateOptions.Phone.key:
|
||||
case BaseTemplateOptions.SMS.key:
|
||||
case BaseTemplateOptions.SlackTitle.key:
|
||||
case BaseTemplateOptions.SlackMessage.key:
|
||||
case BaseTemplateOptions.SlackImage.key:
|
||||
case BaseTemplateOptions.TelegramTitle.key:
|
||||
case BaseTemplateOptions.TelegramMessage.key:
|
||||
case BaseTemplateOptions.TelegramImage.key:
|
||||
case BaseTemplateOptions.EmailTitle.key:
|
||||
case BaseTemplateOptions.EmailMessage.key:
|
||||
return slackMessageTemplateCheatSheet;
|
||||
default:
|
||||
return genericTemplateCheatSheet;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
.info-block {
|
||||
width: 752px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.field-command {
|
||||
margin-top: 8px;
|
||||
width: 752px;
|
||||
}
|
||||
|
||||
.field-command input {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--primary-text-link);
|
||||
}
|
||||
|
||||
.infoblock-text {
|
||||
margin-left: 48px;
|
||||
margin-right: 48px;
|
||||
}
|
||||
|
||||
.done-button {
|
||||
width: 752px;
|
||||
direction: rtl;
|
||||
}
|
||||
133
grafana-plugin/src/containers/MSTeams/MSTeamsInstructions.tsx
Normal file
133
grafana-plugin/src/containers/MSTeams/MSTeamsInstructions.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Button, Icon, VerticalGroup, Field, Input } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
|
||||
import Block from 'components/GBlock/Block';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import MSTeamsLogo from 'icons/MSTeamsLogo';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openNotification, openWarningNotification } from 'utils';
|
||||
|
||||
import styles from './MSTeamsInstructions.module.css';
|
||||
|
||||
interface MSTeamsInstructionsProps {
|
||||
onCallisAdded?: boolean;
|
||||
showInfoBox?: boolean;
|
||||
personalSettings?: boolean;
|
||||
verificationCode: string;
|
||||
onHide?: () => void;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props) => {
|
||||
const { onCallisAdded, showInfoBox, personalSettings, onHide = () => {}, verificationCode } = props;
|
||||
const { msteamsChannelStore } = useStore();
|
||||
|
||||
const handleMSTeamsGetChannels = () => {
|
||||
msteamsChannelStore.updateItems().then(() => {
|
||||
const connectedChannels = msteamsChannelStore.getSearchResult();
|
||||
if (!connectedChannels?.length) {
|
||||
openWarningNotification('No MS Teams channels found');
|
||||
}
|
||||
});
|
||||
|
||||
onHide();
|
||||
};
|
||||
|
||||
return (
|
||||
<VerticalGroup align="flex-start" spacing="lg">
|
||||
{!personalSettings && <Text.Title level={2}>Connect MS Teams workspace</Text.Title>}
|
||||
{showInfoBox && (
|
||||
<Block bordered withBackground className={cx('info-block')}>
|
||||
<VerticalGroup align="center">
|
||||
<div style={{ width: '60px', marginTop: '24px' }}>
|
||||
<MSTeamsLogo />
|
||||
</div>
|
||||
<Text>You can manage alert groups in your Microsoft Teams workspace.</Text>
|
||||
<br />
|
||||
{personalSettings ? (
|
||||
<VerticalGroup align="center">
|
||||
<Text>This setup is for direct profile connection with bot. </Text>
|
||||
<br />
|
||||
<Text className={cx('infoblock-text')}>
|
||||
To manage alert groups in Team channel, setup{' '}
|
||||
<PluginLink query={{ page: 'chat-ops', tab: 'MSTeams' }}>Team ChatOps</PluginLink>
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
) : (
|
||||
<VerticalGroup align="center">
|
||||
<Text>This setup is for Team channel connection with bot. </Text>
|
||||
<br />
|
||||
<Text className={cx('infoblock-text')}>
|
||||
To manage alert groups in Direct Messages and verify users who are allowed to operate with MS Teams,
|
||||
setup <PluginLink query={{ page: 'users', id: 'me' }}>personal MS Teams connection</PluginLink>
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
{!onCallisAdded && (
|
||||
<Text type="secondary">
|
||||
1. Go to{' '}
|
||||
<a href="https://appsource.microsoft.com/en-us/product/office/WA200004307" target="_blank" rel="noreferrer">
|
||||
<Text type="link">MS Teams marketplace</Text>
|
||||
</a>{' '}
|
||||
and add <Text type="primary">Grafana OnCall app</Text> to your MS Teams org workspace.{' '}
|
||||
</Text>
|
||||
)}
|
||||
<Text type="secondary">
|
||||
{!onCallisAdded ? 2 : 1}.{' '}
|
||||
{personalSettings ? (
|
||||
<Text type="secondary">
|
||||
Send a direct message to the Grafana OnCall bot using <Text type="primary">linkUser</Text> command with
|
||||
following code:
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="secondary">
|
||||
Add OnCall bot to your team channel and send this code by{' '}
|
||||
<Text type="primary">@Grafana OnCall linkTeam</Text> command
|
||||
</Text>
|
||||
)}
|
||||
<Field className={cx('field-command')}>
|
||||
<Input
|
||||
id="msTeamsCommand"
|
||||
value={verificationCode}
|
||||
suffix={
|
||||
<CopyToClipboard
|
||||
text={verificationCode}
|
||||
onCopy={() => {
|
||||
openNotification('Code is copied');
|
||||
}}
|
||||
>
|
||||
<Icon name="copy" />
|
||||
</CopyToClipboard>
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</Text>
|
||||
<Block bordered withBackground className={cx('info-block')}>
|
||||
<Text type="secondary">
|
||||
For more information please read{' '}
|
||||
<a href="https://grafana.com/docs/oncall/latest/notify/ms-teams/" target="_blank" rel="noreferrer">
|
||||
<Text type="link">OnCall documentation</Text>
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</Block>
|
||||
{!personalSettings && (
|
||||
<div className={cx('done-button')}>
|
||||
<Button onClick={handleMSTeamsGetChannels}>Done</Button>
|
||||
</div>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
);
|
||||
});
|
||||
|
||||
export default MSTeamsInstructions;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
.msteams-bot {
|
||||
color: var(--primary-text-link);
|
||||
}
|
||||
|
||||
.msteams-instruction-container {
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.verification-code {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
color: var(--primary-text-link);
|
||||
}
|
||||
|
||||
.msteams-instruction-cancel {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.msTeams-modal {
|
||||
min-width: 800px;
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { Button, Modal } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import MSTeamsInstructions from 'containers/MSTeams/MSTeamsInstructions';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
import styles from './MSTeamsIntegrationButton.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface MSTeamsIntegrationProps {
|
||||
disabled?: boolean;
|
||||
size?: 'md' | 'lg';
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
const MSTeamsIntegrationButton = observer((props: MSTeamsIntegrationProps) => {
|
||||
const { disabled, size = 'md', onUpdate } = props;
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const onInstallModalHideCallback = useCallback(() => {
|
||||
setShowModal(false);
|
||||
}, []);
|
||||
|
||||
const onInstallModalCallback = useCallback(() => {
|
||||
setShowModal(true);
|
||||
}, []);
|
||||
|
||||
const onModalUpdateCallback = useCallback(() => {
|
||||
setShowModal(false);
|
||||
|
||||
onUpdate();
|
||||
}, [onUpdate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Button size={size} variant="primary" icon="plus" disabled={disabled} onClick={onInstallModalCallback}>
|
||||
Add MS Teams channel
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
{showModal && <MSTeamsModal onHide={onInstallModalHideCallback} onUpdate={onModalUpdateCallback} />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
interface MSTeamsModalProps {
|
||||
onHide: () => void;
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
const MSTeamsModal = (props: MSTeamsModalProps) => {
|
||||
const { onHide, onUpdate } = props;
|
||||
const [verificationCode, setVerificationCode] = useState<string>();
|
||||
const store = useStore();
|
||||
useEffect(() => {
|
||||
store.msteamsChannelStore.getMSTeamsChannelVerificationCode().then((res) => {
|
||||
setVerificationCode(res);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal className={cx('msTeams-modal')} title="Connect MS Teams workspace" closeOnEscape isOpen onDismiss={onUpdate}>
|
||||
<MSTeamsInstructions onHide={onHide} verificationCode={verificationCode} onCallisAdded />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MSTeamsIntegrationButton;
|
||||
|
|
@ -24,7 +24,7 @@ import { logoCoors } from 'components/IntegrationLogo/IntegrationLogo.config';
|
|||
import RenderConditionally from 'components/RenderConditionally/RenderConditionally';
|
||||
import Text from 'components/Text/Text';
|
||||
import Labels, { LabelsProps } from 'containers/Labels/Labels';
|
||||
import { webhookPresetIcons } from 'containers/OutgoingWebhookForm/WebhookPresetIcons.config';
|
||||
import { getWebhookPresetIcons } from 'containers/OutgoingWebhookForm/WebhookPresetIcons.config';
|
||||
import OutgoingWebhookStatus from 'containers/OutgoingWebhookStatus/OutgoingWebhookStatus';
|
||||
import WebhooksTemplateEditor from 'containers/WebhooksTemplateEditor/WebhooksTemplateEditor';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
|
|
@ -452,7 +452,11 @@ const WebhookTabsContent: React.FC<WebhookTabsProps> = ({
|
|||
const WebhookPresetBlocks: React.FC<{
|
||||
presets: OutgoingWebhookPreset[];
|
||||
onBlockClick: (preset: OutgoingWebhookPreset) => void;
|
||||
}> = ({ presets, onBlockClick }) => {
|
||||
}> = observer(({ presets, onBlockClick }) => {
|
||||
const store = useStore();
|
||||
|
||||
const webhookPresetIcons = getWebhookPresetIcons(store.features);
|
||||
|
||||
return (
|
||||
<div className={cx('cards')} data-testid="create-outgoing-webhook-modal">
|
||||
{presets.length ? (
|
||||
|
|
@ -486,6 +490,6 @@ const WebhookPresetBlocks: React.FC<{
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default OutgoingWebhookForm;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,18 @@
|
|||
import { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import MachineLearningLogo from 'icons/MachineLearningLogo';
|
||||
import { AppFeature } from 'state/features';
|
||||
|
||||
import { commonWebhookPresetIconsConfig } from './CommonWebhookPresetIcons.config';
|
||||
|
||||
export const webhookPresetIcons: { [id: string]: () => ReactElement } = commonWebhookPresetIconsConfig;
|
||||
export const additionalWebhookPresetIcons: { [id: string]: () => React.ReactElement } = {
|
||||
machine_learning: () => <MachineLearningLogo />,
|
||||
};
|
||||
|
||||
export const getWebhookPresetIcons = (features: Record<string, boolean>) => {
|
||||
if (features[AppFeature.MsTeams]) {
|
||||
return { ...commonWebhookPresetIconsConfig, ...additionalWebhookPresetIcons };
|
||||
}
|
||||
|
||||
return commonWebhookPresetIconsConfig;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -49,11 +49,18 @@ const UserSettings = observer(({ id, onHide, tab = UserSettingsTab.UserInfo }: U
|
|||
setActiveTab(tab);
|
||||
}, []);
|
||||
|
||||
const [showNotificationSettingsTab, showSlackConnectionTab, showTelegramConnectionTab, showMobileAppConnectionTab] = [
|
||||
const [
|
||||
showNotificationSettingsTab,
|
||||
showSlackConnectionTab,
|
||||
showTelegramConnectionTab,
|
||||
showMobileAppConnectionTab,
|
||||
showMsTeamsConnectionTab,
|
||||
] = [
|
||||
!isDesktopOrLaptop,
|
||||
isCurrent && organizationStore.currentOrganization?.slack_team_identity && !storeUser.slack_user_identity,
|
||||
isCurrent && store.hasFeature(AppFeature.Telegram) && !storeUser.telegram_configuration,
|
||||
isCurrent,
|
||||
store.hasFeature(AppFeature.MsTeams),
|
||||
];
|
||||
|
||||
const title = (
|
||||
|
|
@ -73,6 +80,7 @@ const UserSettings = observer(({ id, onHide, tab = UserSettingsTab.UserInfo }: U
|
|||
showSlackConnectionTab={showSlackConnectionTab}
|
||||
showTelegramConnectionTab={showTelegramConnectionTab}
|
||||
showMobileAppConnectionTab={showMobileAppConnectionTab}
|
||||
showMsTeamsConnectionTab={showMsTeamsConnectionTab}
|
||||
/>
|
||||
<TabsContent id={id} activeTab={activeTab} onTabChange={onTabChange} isDesktopOrLaptop={isDesktopOrLaptop} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
import { User } from 'models/user/user.types';
|
||||
|
||||
export enum UserSettingsTab {
|
||||
UserInfo,
|
||||
NotificationSettings,
|
||||
PhoneVerification,
|
||||
SlackInfo,
|
||||
TelegramInfo,
|
||||
MSTeamsInfo,
|
||||
MobileAppConnection,
|
||||
}
|
||||
|
||||
export interface UserFormData extends Partial<User> {
|
||||
slack_user_identity_name?: string;
|
||||
telegram_configuration_telegram_nick_name?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, InlineField, Input } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from 'containers/UserSettings/parts/connectors/index.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface MSTeamsConnectorProps {
|
||||
id: User['pk'];
|
||||
onTabChange: (tab: UserSettingsTab) => void;
|
||||
}
|
||||
|
||||
const MSTeamsConnector = observer((props: MSTeamsConnectorProps) => {
|
||||
const { id, onTabChange } = props;
|
||||
|
||||
const store = useStore();
|
||||
const { userStore } = store;
|
||||
|
||||
const storeUser = userStore.items[id];
|
||||
|
||||
const isCurrentUser = id === store.userStore.currentUserPk;
|
||||
|
||||
const handleConnectButtonClick = useCallback(() => {
|
||||
onTabChange(UserSettingsTab.MSTeamsInfo);
|
||||
}, []);
|
||||
|
||||
const handleUnlinkMSTeamsAccount = useCallback(() => {
|
||||
userStore.unlinkBackend(id, 'MSTEAMS');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cx('user-item')}>
|
||||
{storeUser.messaging_backends.MSTEAMS ? (
|
||||
<InlineField label="MS Teams" labelWidth={12}>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Input disabled={true} value={storeUser.messaging_backends.MSTEAMS?.name || '—'} />
|
||||
<WithConfirm title="Are you sure to disconnect your Microsoft Teams account?" confirmText="Disconnect">
|
||||
<Button
|
||||
disabled={!isCurrentUser}
|
||||
variant="destructive"
|
||||
icon="times"
|
||||
onClick={handleUnlinkMSTeamsAccount}
|
||||
tooltip={'Unlink MS Teams Account'}
|
||||
/>
|
||||
</WithConfirm>
|
||||
</HorizontalGroup>
|
||||
</InlineField>
|
||||
) : (
|
||||
<div>
|
||||
<InlineField label="MS Teams" labelWidth={12} disabled={!isCurrentUser}>
|
||||
<Button onClick={handleConnectButtonClick}>Connect account</Button>
|
||||
</InlineField>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default MSTeamsConnector;
|
||||
|
|
@ -8,6 +8,7 @@ import { AppFeature } from 'state/features';
|
|||
import { useStore } from 'state/useStore';
|
||||
|
||||
import ICalConnector from './ICalConnector';
|
||||
import MSTeamsConnector from './MSTeamsConnector';
|
||||
import MobileAppConnector from './MobileAppConnector';
|
||||
import PhoneConnector from './PhoneConnector';
|
||||
import SlackConnector from './SlackConnector';
|
||||
|
|
@ -26,6 +27,7 @@ export const Connectors: FC<ConnectorsProps> = (props) => {
|
|||
<MobileAppConnector {...props} />
|
||||
<SlackConnector {...props} />
|
||||
{store.hasFeature(AppFeature.Telegram) && <TelegramConnector {...props} />}
|
||||
{store.hasFeature(AppFeature.MsTeams) && <MSTeamsConnector {...props} />}
|
||||
<Legend>Calendar export</Legend>
|
||||
<ICalConnector {...props} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import Block from 'components/GBlock/Block';
|
|||
import MobileAppConnection from 'containers/MobileAppConnection/MobileAppConnection';
|
||||
import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
|
||||
import { SlackTab } from 'containers/UserSettings/parts/tabs//SlackTab/SlackTab';
|
||||
import CloudPhoneSettings from 'containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings';
|
||||
import MSTeamsInfo from 'containers/UserSettings/parts/tabs/MSTeamsInfo/MSTeamsInfo';
|
||||
import { NotificationSettingsTab } from 'containers/UserSettings/parts/tabs/NotificationSettingsTab';
|
||||
import PhoneVerification from 'containers/UserSettings/parts/tabs/PhoneVerification/PhoneVerification';
|
||||
import TelegramInfo from 'containers/UserSettings/parts/tabs/TelegramInfo/TelegramInfo';
|
||||
|
|
@ -16,8 +18,6 @@ import { User } from 'models/user/user.types';
|
|||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import CloudPhoneSettings from './tabs/CloudPhoneSettings/CloudPhoneSettings';
|
||||
|
||||
import styles from 'containers/UserSettings/parts/index.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
|
@ -29,6 +29,7 @@ interface TabsProps {
|
|||
showMobileAppConnectionTab: boolean;
|
||||
showSlackConnectionTab: boolean;
|
||||
showTelegramConnectionTab: boolean;
|
||||
showMsTeamsConnectionTab: boolean;
|
||||
}
|
||||
|
||||
export const Tabs = ({
|
||||
|
|
@ -38,6 +39,7 @@ export const Tabs = ({
|
|||
showMobileAppConnectionTab,
|
||||
showSlackConnectionTab,
|
||||
showTelegramConnectionTab,
|
||||
showMsTeamsConnectionTab,
|
||||
}: TabsProps) => {
|
||||
const getTabClickHandler = useCallback(
|
||||
(tab: UserSettingsTab) => {
|
||||
|
|
@ -100,6 +102,15 @@ export const Tabs = ({
|
|||
data-testid="tab-telegram"
|
||||
/>
|
||||
)}
|
||||
{showMsTeamsConnectionTab && (
|
||||
<Tab
|
||||
active={activeTab === UserSettingsTab.MSTeamsInfo}
|
||||
label="Ms Teams Connection"
|
||||
key={UserSettingsTab.MSTeamsInfo}
|
||||
onChangeTab={getTabClickHandler(UserSettingsTab.MSTeamsInfo)}
|
||||
data-testid="tab-msteams"
|
||||
/>
|
||||
)}
|
||||
</TabsBar>
|
||||
);
|
||||
};
|
||||
|
|
@ -143,6 +154,7 @@ export const TabsContent = observer(({ id, activeTab, onTabChange, isDesktopOrLa
|
|||
{activeTab === UserSettingsTab.MobileAppConnection && <MobileAppConnection userPk={id} />}
|
||||
{activeTab === UserSettingsTab.SlackInfo && <SlackTab />}
|
||||
{activeTab === UserSettingsTab.TelegramInfo && <TelegramInfo />}
|
||||
{activeTab === UserSettingsTab.MSTeamsInfo && <MSTeamsInfo />}
|
||||
</TabContent>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
.verification-code {
|
||||
color: var(--primary-text-link);
|
||||
}
|
||||
|
||||
.verification-code-text {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Text from 'components/Text/Text';
|
||||
import MSTeamsInstructions from 'containers/MSTeams/MSTeamsInstructions';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from 'containers/UserSettings/parts/tabs/MSTeamsInfo/MSTeamsInfo.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const MSTeamsInfo = observer(() => {
|
||||
const { userStore, msteamsChannelStore } = useStore();
|
||||
|
||||
const [verificationCode, setVerificationCode] = useState<string>();
|
||||
const [onCallisAdded, setOnCallisAdded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
userStore.sendBackendConfirmationCode(userStore.currentUserPk, 'MSTEAMS').then(setVerificationCode);
|
||||
msteamsChannelStore.updateItems().then(() => {
|
||||
const connectedChannels = msteamsChannelStore.getSearchResult();
|
||||
if (connectedChannels?.length) {
|
||||
setOnCallisAdded(true);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
userStore.loadCurrentUser();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text.Title level={2} className={cx('heading')}>
|
||||
Connect MS Teams workspace
|
||||
</Text.Title>
|
||||
<MSTeamsInstructions
|
||||
personalSettings
|
||||
onCallisAdded={onCallisAdded}
|
||||
showInfoBox
|
||||
verificationCode={verificationCode}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default MSTeamsInfo;
|
||||
68
grafana-plugin/src/icons/MSTeamsLogo.tsx
Normal file
68
grafana-plugin/src/icons/MSTeamsLogo.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import React from 'react';
|
||||
|
||||
const MSTeamsLogo = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2228.833 2073.333">
|
||||
<path
|
||||
fill="#5059C9"
|
||||
d="M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z"
|
||||
/>
|
||||
<circle fill="#5059C9" cx="1943.75" cy="440.583" r="233.25" />
|
||||
<circle fill="#7B83EB" cx="1218.083" cy="336.917" r="336.917" />
|
||||
<path
|
||||
fill="#7B83EB"
|
||||
d="M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z"
|
||||
/>
|
||||
<path
|
||||
opacity=".1"
|
||||
d="M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
d="M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
d="M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
d="M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z"
|
||||
/>
|
||||
<path
|
||||
opacity=".1"
|
||||
d="M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
d="M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z"
|
||||
/>
|
||||
<linearGradient
|
||||
id="a"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="198.099"
|
||||
y1="1683.0726"
|
||||
x2="942.2344"
|
||||
y2="394.2607"
|
||||
gradientTransform="matrix(1 0 0 -1 0 2075.3333)"
|
||||
>
|
||||
<stop offset="0" stopColor="#5a62c3" />
|
||||
<stop offset=".5" stopColor="#4d55bd" />
|
||||
<stop offset="1" stopColor="#3940ab" />
|
||||
</linearGradient>
|
||||
<path
|
||||
fill="url(#a)"
|
||||
d="M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z"
|
||||
/>
|
||||
<path fill="#FFF" d="M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default MSTeamsLogo;
|
||||
29
grafana-plugin/src/icons/MachineLearningLogo.tsx
Normal file
29
grafana-plugin/src/icons/MachineLearningLogo.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
|
||||
const MachineLearningLogo = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 135.46 118.24"
|
||||
width="40px"
|
||||
height="40px"
|
||||
>
|
||||
<defs>
|
||||
<style>{'.cls-1{fill:url(#linear-gradient);}'}</style>
|
||||
<linearGradient id="linear-gradient" x1="67.73" y1="137.67" x2="67.73" y2="-0.38" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stopColor="#f9ea1c" />
|
||||
<stop offset="1" stopColor="#ed5a29" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<path
|
||||
className="cls-1"
|
||||
d="M135.37,64.86l-5.6-26.43a4.05,4.05,0,0,0-.9-1.82L113.19,18.24a4.11,4.11,0,0,0-1-.87L83.71.57A4.18,4.18,0,0,0,81.62,0H58.54a4.19,4.19,0,0,0-1,.12L26,8a4.16,4.16,0,0,0-2,1.17L1.12,33.32A4.1,4.1,0,0,0,0,36.14v24.2A4.1,4.1,0,0,0,2.46,64.1l22.85,10L39,95.07a4.13,4.13,0,0,0,3.45,1.86H68.66L81.59,116.4A4.09,4.09,0,0,0,85,118.24h0l6,0a4.11,4.11,0,0,0,4.09-4.11V90.21H117a4.09,4.09,0,0,0,3.36-1.75l14.34-20.38A4.1,4.1,0,0,0,135.37,64.86ZM86.12,34.1a3.29,3.29,0,0,0-.67.77l0,.05H59.83L57,30.53A3.38,3.38,0,0,0,54.24,29a3.45,3.45,0,0,0-2.86,1.45l-3.15,4.51H10.92L21.64,23.58H98.3Zm-3,4.52L72.77,55.23,62.19,38.62ZM37.75,50H8.22V38.62H45.66ZM29.12,15.65,59.05,8.22H80.49l19.74,11.66H25.13Zm-20.9,38h27L26.73,65.78,8.22,57.65ZM114.87,82h-4V46.21a2.3,2.3,0,0,0-2.3-2.3h-1.39a2.3,2.3,0,0,0-2.3,2.3V82h-12V63.92a2.31,2.31,0,0,0-2.3-2.31H89.21a2.31,2.31,0,0,0-2.3,2.31v45.64L74.48,90.84V78.36a2.3,2.3,0,0,0-2.3-2.3H70.79a2.3,2.3,0,0,0-2.3,2.3V88.71H56.23V58.54a2.32,2.32,0,0,0-2.31-2.31H52.54a2.32,2.32,0,0,0-2.31,2.31V88.71H44.64l-12.39-19L53.93,38.62h.21l15.8,24.81A3.32,3.32,0,0,0,72.82,65a3.39,3.39,0,0,0,2.87-1.6L91,38.9l16.73-14.45,14.26,16.7,5,23.66Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default MachineLearningLogo;
|
||||
126
grafana-plugin/src/models/msteams_channel/msteams_channel.ts
Normal file
126
grafana-plugin/src/models/msteams_channel/msteams_channel.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { action, computed, observable, makeObservable, runInAction } from 'mobx';
|
||||
|
||||
import BaseStore from 'models/base_store';
|
||||
import { makeRequest } from 'network';
|
||||
import { RootStore } from 'state';
|
||||
|
||||
import { MSTeamsChannel } from './msteams_channel.types';
|
||||
|
||||
export class MSTeamsChannelStore extends BaseStore {
|
||||
@observable.shallow
|
||||
items: { [id: string]: MSTeamsChannel } = {};
|
||||
|
||||
@observable
|
||||
currentTeamToMSTeamsChannel?: Array<MSTeamsChannel['id']>;
|
||||
|
||||
@observable.shallow
|
||||
searchResult: { [key: string]: Array<MSTeamsChannel['id']> } = {};
|
||||
|
||||
private autoUpdateTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore);
|
||||
|
||||
makeObservable(this);
|
||||
|
||||
this.path = '/msteams/channels/';
|
||||
}
|
||||
|
||||
@action
|
||||
async updateMSTeamsChannels() {
|
||||
const response = await makeRequest(this.path, {});
|
||||
|
||||
const items = response.reduce(
|
||||
(acc: any, msteamsChannel: MSTeamsChannel) => ({
|
||||
...acc,
|
||||
[msteamsChannel.id]: msteamsChannel,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.items = {
|
||||
...this.items,
|
||||
...items,
|
||||
};
|
||||
|
||||
this.currentTeamToMSTeamsChannel = response.map((msteamsChannel: MSTeamsChannel) => msteamsChannel.id);
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async updateById(id: MSTeamsChannel['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
runInAction(() => {
|
||||
this.items = {
|
||||
...this.items,
|
||||
[id]: response,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async updateItems(query = '') {
|
||||
const result = await this.getAll();
|
||||
|
||||
runInAction(() => {
|
||||
this.items = {
|
||||
...this.items,
|
||||
...result.reduce(
|
||||
(acc: { [key: number]: MSTeamsChannel }, item: MSTeamsChannel) => ({
|
||||
...acc,
|
||||
[item.id]: item,
|
||||
}),
|
||||
{}
|
||||
),
|
||||
};
|
||||
|
||||
this.searchResult = {
|
||||
...this.searchResult,
|
||||
[query]: result.map((item: MSTeamsChannel) => item.id),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getSearchResult(query = '') {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
}
|
||||
return this.searchResult[query].map((msteamsChannelId: MSTeamsChannel['id']) => this.items[msteamsChannelId]);
|
||||
}
|
||||
|
||||
@computed
|
||||
get hasItems() {
|
||||
return Boolean(this.getSearchResult('')?.length);
|
||||
}
|
||||
|
||||
async startAutoUpdate() {
|
||||
this.autoUpdateTimer = setInterval(this.updateMSTeamsChannels.bind(this), 3000);
|
||||
}
|
||||
|
||||
async stopAutoUpdate() {
|
||||
if (this.autoUpdateTimer) {
|
||||
clearInterval(this.autoUpdateTimer);
|
||||
}
|
||||
}
|
||||
|
||||
async getMSTeamsChannelVerificationCode() {
|
||||
return await makeRequest(`/current_team/get_channel_verification_code/?backend=MSTEAMS`, {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
async makeMSTeamsChannelDefault(id: MSTeamsChannel['id']) {
|
||||
return makeRequest(`/msteams/channels/${id}/set_default/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
async deleteMSTeamsChannel(id: MSTeamsChannel['id']) {
|
||||
return super.delete(id);
|
||||
}
|
||||
|
||||
async getMSTeamsChannels() {
|
||||
return super.getAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// TODO check backend response to verify data shape
|
||||
|
||||
export interface MSTeamsChannel {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
team: string;
|
||||
is_default: false;
|
||||
}
|
||||
|
|
@ -1,14 +1,50 @@
|
|||
/*
|
||||
[oncall-private]
|
||||
Any change to this file needs to be done in the oncall-private also
|
||||
*/
|
||||
import { AppFeature } from 'state/features';
|
||||
import { KeyValuePair } from 'utils';
|
||||
|
||||
import { BASE_INTEGRATION_TEMPLATES_LIST, BaseTemplateOptions } from './IntegrationCommon.config';
|
||||
|
||||
export const TemplateOptions = {
|
||||
...BaseTemplateOptions,
|
||||
export const MsTeamsTemplateOptions = {
|
||||
MSTeams: new KeyValuePair('Microsoft Teams', 'Microsoft Teams'),
|
||||
MSTeamsTitle: new KeyValuePair('MSTeams Title', 'Title'),
|
||||
MSTeamsMessage: new KeyValuePair('MSTeams Message', 'Message'),
|
||||
MSTeamsImage: new KeyValuePair('MSTeams Image', 'Image'),
|
||||
};
|
||||
|
||||
export const INTEGRATION_TEMPLATES_LIST = {
|
||||
...BASE_INTEGRATION_TEMPLATES_LIST,
|
||||
export const getTemplateOptions = (features: Record<string, boolean>) => {
|
||||
if (features[AppFeature.MsTeams]) {
|
||||
return {
|
||||
...BaseTemplateOptions,
|
||||
...MsTeamsTemplateOptions,
|
||||
};
|
||||
}
|
||||
return BaseTemplateOptions;
|
||||
};
|
||||
|
||||
export const getIntegrationTemplatesList = (features: Record<string, boolean>) => {
|
||||
if (features[AppFeature.MsTeams]) {
|
||||
return [
|
||||
...BASE_INTEGRATION_TEMPLATES_LIST,
|
||||
|
||||
{
|
||||
label: MsTeamsTemplateOptions.MSTeams.value,
|
||||
value: MsTeamsTemplateOptions.MSTeams.key,
|
||||
children: [
|
||||
{
|
||||
label: MsTeamsTemplateOptions.MSTeamsTitle.value,
|
||||
value: MsTeamsTemplateOptions.MSTeamsTitle.key,
|
||||
},
|
||||
{
|
||||
label: MsTeamsTemplateOptions.MSTeamsMessage.value,
|
||||
value: MsTeamsTemplateOptions.MSTeamsMessage.key,
|
||||
},
|
||||
{
|
||||
label: MsTeamsTemplateOptions.MSTeamsImage.value,
|
||||
value: MsTeamsTemplateOptions.MSTeamsImage.key,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return BASE_INTEGRATION_TEMPLATES_LIST;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
/*
|
||||
[oncall-private]
|
||||
Any change to this file needs to be done in the oncall-private also
|
||||
*/
|
||||
|
||||
import { IconName } from '@grafana/ui';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
|
|
@ -66,24 +61,28 @@ const IntegrationHelper = {
|
|||
return totalDiffString;
|
||||
},
|
||||
|
||||
fetchChatOps(_store: RootStore): Promise<void> {
|
||||
// in oncall-private this fetches MSTeams data
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
hasChatopsInstalled(store: RootStore) {
|
||||
const hasSlack = Boolean(store.organizationStore.currentOrganization?.slack_team_identity);
|
||||
const hasTelegram =
|
||||
store.hasFeature(AppFeature.Telegram) && store.telegramChannelStore.currentTeamToTelegramChannel?.length > 0;
|
||||
return hasSlack || hasTelegram;
|
||||
const isMSTeamsInstalled = Boolean(store.msteamsChannelStore.currentTeamToMSTeamsChannel?.length > 0);
|
||||
|
||||
return hasSlack || hasTelegram || isMSTeamsInstalled;
|
||||
},
|
||||
|
||||
fetchChatOps(store: RootStore): Promise<void> {
|
||||
return store.msteamsChannelStore.updateMSTeamsChannels();
|
||||
},
|
||||
|
||||
getChatOpsChannels(channelFilter: ChannelFilter, store: RootStore): Array<{ name: string; icon: IconName }> {
|
||||
const channels: Array<{ name: string; icon: IconName }> = [];
|
||||
const telegram = Object.keys(store.telegramChannelStore.items).map((k) => store.telegramChannelStore.items[k]);
|
||||
|
||||
if (store.hasFeature(AppFeature.Slack) && channelFilter.notify_in_slack) {
|
||||
const matchingSlackChannel = store.organizationStore.currentOrganization?.slack_channel?.id
|
||||
? store.slackChannelStore.items[store.organizationStore.currentOrganization.slack_channel?.id]
|
||||
const { currentOrganization } = store.organizationStore;
|
||||
|
||||
const matchingSlackChannel = currentOrganization?.slack_channel?.id
|
||||
? store.slackChannelStore.items[currentOrganization.slack_channel?.id]
|
||||
: undefined;
|
||||
if (channelFilter.slack_channel?.display_name || matchingSlackChannel?.display_name) {
|
||||
channels.push({
|
||||
|
|
@ -93,12 +92,32 @@ const IntegrationHelper = {
|
|||
}
|
||||
}
|
||||
|
||||
const matchingTelegram = telegram.find((t) => t.id === channelFilter.telegram_channel);
|
||||
|
||||
if (
|
||||
store.hasFeature(AppFeature.Telegram) &&
|
||||
channelFilter.telegram_channel_details &&
|
||||
channelFilter.notify_in_telegram
|
||||
channelFilter.telegram_channel &&
|
||||
channelFilter.notify_in_telegram &&
|
||||
matchingTelegram?.channel_name
|
||||
) {
|
||||
channels.push({ name: channelFilter.telegram_channel_details.display_name, icon: 'telegram-alt' });
|
||||
channels.push({
|
||||
name: matchingTelegram.channel_name,
|
||||
icon: 'telegram-alt',
|
||||
});
|
||||
}
|
||||
|
||||
const { notification_backends } = channelFilter;
|
||||
const msteamsChannels = store.msteamsChannelStore.items;
|
||||
|
||||
if (
|
||||
notification_backends?.MSTEAMS &&
|
||||
notification_backends?.MSTEAMS.enabled &&
|
||||
msteamsChannels[notification_backends.MSTEAMS.channel]
|
||||
) {
|
||||
channels.push({
|
||||
name: msteamsChannels[notification_backends.MSTEAMS.channel].display_name,
|
||||
icon: 'microsoft',
|
||||
});
|
||||
}
|
||||
|
||||
return channels;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
VerticalGroup,
|
||||
Icon,
|
||||
LoadingPlaceholder,
|
||||
CascaderOption,
|
||||
IconButton,
|
||||
ConfirmModal,
|
||||
Drawer,
|
||||
|
|
@ -20,7 +19,7 @@ import CopyToClipboard from 'react-copy-to-clipboard';
|
|||
import Emoji from 'react-emoji-render';
|
||||
import { RouteComponentProps, useHistory, withRouter } from 'react-router-dom';
|
||||
|
||||
import { templateForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config';
|
||||
import { getTemplatesForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config';
|
||||
import { TemplateForEdit } from 'components/AlertTemplates/CommonAlertTemplatesForm.config';
|
||||
import HamburgerMenu from 'components/HamburgerMenu/HamburgerMenu';
|
||||
import IntegrationCollapsibleTreeView, {
|
||||
|
|
@ -58,7 +57,6 @@ import {
|
|||
} from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { INTEGRATION_TEMPLATES_LIST } from 'pages/integration/Integration.config';
|
||||
import IntegrationHelper from 'pages/integration/Integration.helper';
|
||||
import styles from 'pages/integration/Integration.module.scss';
|
||||
import { AppFeature } from 'state/features';
|
||||
|
|
@ -648,9 +646,11 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
});
|
||||
};
|
||||
|
||||
getTemplatesList = (): CascaderOption[] => INTEGRATION_TEMPLATES_LIST;
|
||||
|
||||
openEditTemplateModal = (templateName, channelFilterId?: ChannelFilter['id']) => {
|
||||
const { store } = this.props;
|
||||
|
||||
const templateForEdit = getTemplatesForEdit(store.features);
|
||||
|
||||
if (templateForEdit[templateName]) {
|
||||
this.setState({
|
||||
isEditTemplateModalOpen: true,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import cn from 'classnames/bind';
|
|||
import { observer } from 'mobx-react';
|
||||
|
||||
import VerticalTabsBar, { VerticalTab } from 'components/VerticalTabsBar/VerticalTabsBar';
|
||||
import MSTeamsSettings from 'pages/settings/tabs/ChatOps/tabs/MSTeamsSettings/MSTeamsSettings';
|
||||
import SlackSettings from 'pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings';
|
||||
import TelegramSettings from 'pages/settings/tabs/ChatOps/tabs/TelegramSettings/TelegramSettings';
|
||||
import { AppFeature } from 'state/features';
|
||||
|
|
@ -21,6 +22,7 @@ const cx = cn.bind(styles);
|
|||
export enum ChatOpsTab {
|
||||
Slack = 'Slack',
|
||||
Telegram = 'Telegram',
|
||||
MSTeams = 'MSTeams',
|
||||
}
|
||||
interface ChatOpsProps extends AppRootProps, WithStoreProps {}
|
||||
interface ChatOpsState {
|
||||
|
|
@ -83,7 +85,11 @@ class ChatOpsPage extends React.Component<ChatOpsProps, ChatOpsState> {
|
|||
|
||||
isChatOpsConfigured(): boolean {
|
||||
const { store } = this.props;
|
||||
return store.hasFeature(AppFeature.Slack) || store.hasFeature(AppFeature.Telegram);
|
||||
return (
|
||||
store.hasFeature(AppFeature.Slack) ||
|
||||
store.hasFeature(AppFeature.Telegram) ||
|
||||
store.hasFeature(AppFeature.MsTeams)
|
||||
);
|
||||
}
|
||||
|
||||
handleChatopsTabChange(tab: ChatOpsTab) {
|
||||
|
|
@ -122,6 +128,14 @@ const Tabs = (props: TabsProps) => {
|
|||
</HorizontalGroup>
|
||||
</VerticalTab>
|
||||
)}
|
||||
{store.hasFeature(AppFeature.MsTeams) && (
|
||||
<VerticalTab id={ChatOpsTab.MSTeams}>
|
||||
<HorizontalGroup>
|
||||
<Icon name="microsoft" />
|
||||
Microsoft Teams
|
||||
</HorizontalGroup>
|
||||
</VerticalTab>
|
||||
)}
|
||||
</VerticalTabsBar>
|
||||
);
|
||||
};
|
||||
|
|
@ -146,6 +160,11 @@ const TabsContent = (props: TabsContentProps) => {
|
|||
<TelegramSettings />
|
||||
</div>
|
||||
)}
|
||||
{store.hasFeature(AppFeature.MsTeams) && activeTab === ChatOpsTab.MSTeams && (
|
||||
<div className={cx('messenger-settings')}>
|
||||
<MSTeamsSettings />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
.root {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, LoadingPlaceholder, Badge } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import GTable from 'components/GTable/GTable';
|
||||
import Text from 'components/Text/Text';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import MSTeamsInstructions from 'containers/MSTeams/MSTeamsInstructions';
|
||||
import MSTeamsIntegrationButton from 'containers/MSTeamsIntegrationButton/MSTeamsIntegrationButton';
|
||||
import { MSTeamsChannel } from 'models/msteams_channel/msteams_channel.types';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
||||
import styles from 'pages/settings/tabs/ChatOps/tabs/MSTeamsSettings/MSTeamsSettings.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface MSTeamsProps extends WithStoreProps {}
|
||||
|
||||
interface MSTeamsState {
|
||||
verificationCode: string;
|
||||
}
|
||||
|
||||
@observer
|
||||
class MSTeamsSettings extends Component<MSTeamsProps, MSTeamsState> {
|
||||
state: MSTeamsState = {
|
||||
verificationCode: '',
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.update();
|
||||
}
|
||||
|
||||
update = () => {
|
||||
const { store } = this.props;
|
||||
store.msteamsChannelStore.getMSTeamsChannelVerificationCode().then((data) => {
|
||||
this.setState({ verificationCode: data });
|
||||
});
|
||||
store.msteamsChannelStore.updateItems();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { store } = this.props;
|
||||
const { msteamsChannelStore } = store;
|
||||
const { verificationCode } = this.state;
|
||||
|
||||
const connectedChannels = msteamsChannelStore.getSearchResult();
|
||||
|
||||
if (!connectedChannels) {
|
||||
return <LoadingPlaceholder text="Loading..." />;
|
||||
}
|
||||
|
||||
if (!connectedChannels.length) {
|
||||
return (
|
||||
<>
|
||||
<MSTeamsInstructions showInfoBox verificationCode={verificationCode} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
width: '40%',
|
||||
title: 'Team',
|
||||
dataIndex: 'team',
|
||||
},
|
||||
{
|
||||
width: '30%',
|
||||
title: 'Channel name',
|
||||
render: this.renderDefaultBadge,
|
||||
},
|
||||
{
|
||||
width: '30%',
|
||||
key: 'action',
|
||||
render: this.renderActionButtons,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div>
|
||||
{connectedChannels && (
|
||||
<div className={cx('root')}>
|
||||
<GTable
|
||||
title={() => (
|
||||
<div className={cx('header')}>
|
||||
<Text.Title level={3}>Microsoft Teams Channels</Text.Title>
|
||||
<MSTeamsIntegrationButton onUpdate={this.update} />
|
||||
</div>
|
||||
)}
|
||||
emptyText={connectedChannels ? 'No Microsoft Teams channels connected' : 'Loading...'}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
data={connectedChannels}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderActionButtons = (record: MSTeamsChannel) => {
|
||||
return (
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Button
|
||||
onClick={() => this.setChannelAsDefault(record.id)}
|
||||
disabled={record.is_default}
|
||||
fill="outline"
|
||||
size="sm"
|
||||
>
|
||||
Make default
|
||||
</Button>
|
||||
<WithConfirm title="Are you sure to disconnect?">
|
||||
<Button onClick={() => this.unsetChannelAsDefault(record.id)} fill="outline" variant="destructive" size="sm">
|
||||
Disconnect
|
||||
</Button>
|
||||
</WithConfirm>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
};
|
||||
|
||||
renderDefaultBadge = (record: MSTeamsChannel) => {
|
||||
return (
|
||||
<>
|
||||
{record.name} {record.is_default && <Badge text="Default" color="green" />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
setChannelAsDefault = async (id: MSTeamsChannel['id']) => {
|
||||
const { store } = this.props;
|
||||
const { msteamsChannelStore } = store;
|
||||
|
||||
await msteamsChannelStore.makeMSTeamsChannelDefault(id);
|
||||
msteamsChannelStore.updateItems();
|
||||
};
|
||||
|
||||
unsetChannelAsDefault = async (id: MSTeamsChannel['id']) => {
|
||||
const { store } = this.props;
|
||||
const { msteamsChannelStore } = store;
|
||||
|
||||
await msteamsChannelStore.deleteMSTeamsChannel(id);
|
||||
msteamsChannelStore.updateItems();
|
||||
};
|
||||
}
|
||||
|
||||
export default withMobXProviderContext(MSTeamsSettings);
|
||||
|
|
@ -5,4 +5,5 @@ export enum AppFeature {
|
|||
CloudNotifications = 'grafana_cloud_notifications',
|
||||
CloudConnection = 'grafana_cloud_connection',
|
||||
Labels = 'labels',
|
||||
MsTeams = 'msteams',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { GrafanaTeamStore } from 'models/grafana_team/grafana_team';
|
|||
import { HeartbeatStore } from 'models/heartbeat/heartbeat';
|
||||
import { LabelStore } from 'models/label/label';
|
||||
import { LoaderStore } from 'models/loader/loader';
|
||||
import { MSTeamsChannelStore } from 'models/msteams_channel/msteams_channel';
|
||||
import { OrganizationStore } from 'models/organization/organization';
|
||||
import { OutgoingWebhookStore } from 'models/outgoing_webhook/outgoing_webhook';
|
||||
import { ResolutionNotesStore } from 'models/resolution_note/resolution_note';
|
||||
|
|
@ -114,6 +115,7 @@ export class RootBaseStore {
|
|||
globalSettingStore = new GlobalSettingStore(this);
|
||||
filtersStore = new FiltersStore(this);
|
||||
labelsStore = new LabelStore(this);
|
||||
msteamsChannelStore: MSTeamsChannelStore = new MSTeamsChannelStore(this);
|
||||
loaderStore = LoaderStore;
|
||||
|
||||
constructor() {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"extends": "@grafana/toolkit/src/config/tsconfig.plugin.json",
|
||||
"include": ["src", "frontend_enterprise/src", "e2e-tests", "playwright.config.ts"],
|
||||
"include": ["src", "e2e-tests", "playwright.config.ts"],
|
||||
"types": ["node", "@emotion/core"],
|
||||
"compilerOptions": {
|
||||
"rootDirs": ["src", "frontend_enterprise/src"],
|
||||
"rootDirs": ["src"],
|
||||
"baseUrl": "src",
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"noUnusedLocals": true,
|
||||
|
|
|
|||
|
|
@ -150,12 +150,6 @@ module.exports.getWebpackConfig = (config, options) => {
|
|||
'process.env': JSON.stringify(dotenv.config().parsed),
|
||||
}),
|
||||
],
|
||||
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
symlinks: false,
|
||||
modules: [path.resolve(__dirname, './frontend_enterprise/src'), ...config.resolve.modules],
|
||||
},
|
||||
};
|
||||
|
||||
return newConfig;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue