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:
Maxim Mordasov 2024-01-08 13:50:19 +03:00 committed by GitHub
parent 2af23fb7c0
commit c57c0c4423
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1264 additions and 713 deletions

View file

@ -1,4 +1,3 @@
node_modules
frontend_enterprise
.DS_Store
playwright-report

View file

@ -13,7 +13,6 @@ yarn-error.log*
# This file is generated
grafana-plugin.yml
frontend_enterprise
# playwright
/playwright-report/

View file

@ -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';

View file

@ -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;
}

View file

@ -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;

View file

@ -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.',

View file

@ -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;

View file

@ -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>
);

View file

@ -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;

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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>;

View file

@ -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;

View file

@ -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;
}

View 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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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;
};

View file

@ -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>

View file

@ -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;
}

View file

@ -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;

View file

@ -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} />
</>

View file

@ -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>
);
});

View file

@ -0,0 +1,12 @@
.verification-code {
color: var(--primary-text-link);
}
.verification-code-text {
display: flex;
justify-content: space-between;
}
.heading {
margin-bottom: 24px;
}

View file

@ -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;

View 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;

View 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;

View 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();
}
}

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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;

View file

@ -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,

View file

@ -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>
)}
</>
);
};

View file

@ -0,0 +1,8 @@
.root {
display: block;
}
.header {
display: flex;
justify-content: space-between;
}

View file

@ -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);

View file

@ -5,4 +5,5 @@ export enum AppFeature {
CloudNotifications = 'grafana_cloud_notifications',
CloudConnection = 'grafana_cloud_connection',
Labels = 'labels',
MsTeams = 'msteams',
}

View file

@ -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() {

View file

@ -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,

View file

@ -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;