Merge branch '318-slack-installation-ux' into 637-telegram-installation-redesign

This commit is contained in:
Yulia Shanyrova 2022-10-19 13:49:05 +02:00
commit fbc672b2d1
10 changed files with 332 additions and 69 deletions

View file

@ -501,8 +501,8 @@ class SlackEventApiEndpointView(APIView):
return
text = (
"Your Grafana account is not connected to your Slack account. :flushed:\n"
"That's very easy to fix. Please go to the *Grafana* -> *OnCall* -> *Users*, "
"The information in workspace is read-only. To be able to intercat with OnCall alert groups you need to connect a personal account.\n"
"Please go to the *Grafana* -> *OnCall* -> *Users*, "
"choose *your profile* and click the *connect* button.\n"
":rocket: :rocket: :rocket:"
)

View file

@ -0,0 +1,11 @@
.slack-infoblock {
width: 725px;
}
.slack-infoblock input {
color: var(--primary-text-link);
}
.slack-icon {
width: 60px;
}

View file

@ -0,0 +1,71 @@
import React, { useCallback, useState, FC } from 'react';
import { Button, VerticalGroup, Icon, Field, Input } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import { SlackNewIcon } from 'icons';
import Block from 'components/GBlock/Block';
import Text from 'components/Text/Text';
import styles from './SlackInstructions.module.css';
const cx = cn.bind(styles);
interface SlackInstructionsProps {}
/* This component will be used when we will work on moving ENV variables to chat-ops, but we need to do work on backend side first */
const SlackInstructions: FC<SlackInstructionsProps> = observer((props) => {
return (
<div>
<VerticalGroup spacing="lg">
<Text.Title level={2}>Connect Slack workspace</Text.Title>
<Block bordered withBackground className={cx('slack-infoblock')}>
<VerticalGroup align="center" spacing="lg">
<SlackNewIcon />
<Text>You can manage incidents in your Slack workspace. </Text>
<Text>Before start you need to connect your Slack bot to Grafana OnCall.</Text>
<Text type="secondary">
For bot creating instructions and additional information please read{' '}
<a href="https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup">
<Text type="link">our documentation</Text>
</a>
</Text>{' '}
</VerticalGroup>
</Block>
<Text>Setup environment</Text>
<Text>
Create OnCall Slack bot using{' '}
<a href="https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup">
<Text type="link">our instructions</Text>
</a>{' '}
and fill out app credentials below.
</Text>
<div className={cx('slack-infoblock')}>
<Field label="App ID">
<Input id="appId" onChange={() => {}} defaultValue={'appId'} />
</Field>
<Field label="Client secret">
<Input id="clientsecret" onChange={() => {}} defaultValue={'clientsecret'} />
</Field>
<Field label="Signing secret">
<Input id="signingsecret" onChange={() => {}} defaultValue={'signingsecret'} />
</Field>
<Field label="Redirect host">
<Input id="host" onChange={() => {}} defaultValue={'https://'} />
</Field>
</div>
<Block bordered withBackground className={cx('slack-infoblock')}>
<Text type="secondary">
<Icon name="info-circle" /> Your host to Slack must start with https://” and be publicly available (meaning
that it can be reached by Slack servers). If your host is private or local, you can use redirecting services
like Ngrok.
</Text>
</Block>
<Button onClick={() => {}}>Save environment</Button>
</VerticalGroup>
</div>
);
});
export default SlackInstructions;

View file

@ -64,7 +64,7 @@ const SlackIntegrationButton = observer((props: { className: string; disabled?:
disabled={disabled}
onClick={onInstallModalCallback}
>
Install Slack integration
Connect Slack
</Button>
</WithPermissionControl>
{showModal && <SlackModal onHide={onInstallModalHideCallback} onConfirm={onInstallClickCallback} />}
@ -81,8 +81,8 @@ const SlackModal = (props: SlackModalProps) => {
const { onHide, onConfirm } = props;
return (
<Modal title="One more thing..." closeOnEscape isOpen onDismiss={onHide}>
<div style={{ textAlign: 'center' }}>
<Modal title="Slack connection" closeOnEscape isOpen onDismiss={onHide}>
<div style={{ textAlign: 'left' }}>
You can view your Slack Workspace at the top-right corner after you are redirected. It should be a Workspace
with App Bot installed:
</div>

View file

@ -2,3 +2,13 @@
display: flex;
justify-content: flex-end;
}
.slack-infoblock {
text-align: center;
width: 725px;
}
.external-link-style {
margin-right: 4px;
align-self: baseline;
}

View file

@ -1,9 +1,11 @@
import React, { useCallback } from 'react';
import { Button, VerticalGroup } from '@grafana/ui';
import { Button, VerticalGroup, Icon } from '@grafana/ui';
import cn from 'classnames/bind';
import Text from 'components/Text/Text';
import Block from 'components/GBlock/Block';
import { SlackNewIcon } from 'icons';
import { useStore } from 'state/useStore';
import styles from './SlackTab.module.css';
@ -18,20 +20,32 @@ export const SlackTab = () => {
}, [slackStore]);
return (
<VerticalGroup>
<Text>
You can view your Slack Workspace at the top-right corner after you are redirected. It should be a Workspace
with App Bot installed:
</Text>
<img
style={{ height: '350px', display: 'block', margin: '0 auto' }}
src="public/plugins/grafana-oncall-app/img/slack_workspace_choose_attention.png"
/>
<div className={cx('footer')}>
<Button key="back" onClick={handleClickConnectSlackAccount}>
I'll check! Proceed to Slack...
</Button>
</div>
<VerticalGroup spacing="lg">
<Block bordered withBackground className={cx('slack-infoblock', 'personal-slack-infoblock')}>
<VerticalGroup align="center" spacing="lg">
<SlackNewIcon />
<Text>
Personal Slack connection will allow you to manage incidents in your connected team Internal Slack
workspace.
</Text>
<Text>To setup personal Slack click the button below, choose workspace and click Allow.</Text>
<Text type="secondary">
More details in{' '}
<a href="https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup">
<Text type="link">our documentation</Text>
</a>
</Text>
<img
style={{ height: '350px', display: 'block', margin: '0 auto' }}
src="public/plugins/grafana-oncall-app/img/slack_instructions.png"
/>
</VerticalGroup>
</Block>
<Button onClick={handleClickConnectSlackAccount}>
<Icon name="external-link-alt" className={cx('external-link-style')} /> Open Slack connection page
</Button>
</VerticalGroup>
);
};

View file

@ -293,3 +293,39 @@ export const TelegramColorIcon = () => {
</svg>
);
};
export const SlackNewIcon = (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px">
<path
fill="#33d375"
d="M33,8c0-2.209-1.791-4-4-4s-4,1.791-4,4c0,1.254,0,9.741,0,11c0,2.209,1.791,4,4,4s4-1.791,4-4 C33,17.741,33,9.254,33,8z"
/>
<path
fill="#33d375"
d="M43,19c0,2.209-1.791,4-4,4c-1.195,0-4,0-4,0s0-2.986,0-4c0-2.209,1.791-4,4-4S43,16.791,43,19z"
/>
<path
fill="#40c4ff"
d="M8,14c-2.209,0-4,1.791-4,4s1.791,4,4,4c1.254,0,9.741,0,11,0c2.209,0,4-1.791,4-4s-1.791-4-4-4 C17.741,14,9.254,14,8,14z"
/>
<path
fill="#40c4ff"
d="M19,4c2.209,0,4,1.791,4,4c0,1.195,0,4,0,4s-2.986,0-4,0c-2.209,0-4-1.791-4-4S16.791,4,19,4z"
/>
<path
fill="#e91e63"
d="M14,39.006C14,41.212,15.791,43,18,43s4-1.788,4-3.994c0-1.252,0-9.727,0-10.984 c0-2.206-1.791-3.994-4-3.994s-4,1.788-4,3.994C14,29.279,14,37.754,14,39.006z"
/>
<path
fill="#e91e63"
d="M4,28.022c0-2.206,1.791-3.994,4-3.994c1.195,0,4,0,4,0s0,2.981,0,3.994c0,2.206-1.791,3.994-4,3.994 S4,30.228,4,28.022z"
/>
<path
fill="#ffc107"
d="M39,33c2.209,0,4-1.791,4-4s-1.791-4-4-4c-1.254,0-9.741,0-11,0c-2.209,0-4,1.791-4,4s1.791,4,4,4 C29.258,33,37.746,33,39,33z"
/>
<path
fill="#ffc107"
d="M28,43c-2.209,0-4-1.791-4-4c0-1.195,0-4,0-4s2.986,0,4,0c2.209,0,4,1.791,4,4S30.209,43,28,43z"
/>
</svg>
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View file

@ -15,3 +15,17 @@
margin-bottom: 20px;
border-bottom: 1px solid rgba(204, 204, 220, 0.25);
}
.slack-infoblock {
text-align: center;
width: 725px;
}
.external-link-style {
margin-right: 4px;
align-self: baseline;
}
.team_workspace {
height: 30px;
}

View file

@ -1,16 +1,16 @@
import React, { Component } from 'react';
import { Field, HorizontalGroup, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
import { Field, HorizontalGroup, LoadingPlaceholder, VerticalGroup, Icon, Button } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import { SlackNewIcon } from 'icons';
import Block from 'components/GBlock/Block';
import PluginLink from 'components/PluginLink/PluginLink';
import Text from 'components/Text/Text';
import Tutorial from 'components/Tutorial/Tutorial';
import { TutorialStep } from 'components/Tutorial/Tutorial.types';
import WithConfirm from 'components/WithConfirm/WithConfirm';
import GSelect from 'containers/GSelect/GSelect';
import RemoteSelect from 'containers/RemoteSelect/RemoteSelect';
import SlackIntegrationButton from 'containers/SlackIntegrationButton/SlackIntegrationButton';
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config';
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
@ -25,16 +25,26 @@ const cx = cn.bind(styles);
interface SlackProps extends WithStoreProps {}
interface SlackState {}
interface SlackState {
showENVVariablesButton: boolean;
}
@observer
class SlackSettings extends Component<SlackProps, SlackState> {
state: SlackState = {};
state: SlackState = {
showENVVariablesButton: false,
};
componentDidMount() {
this.getSlackLiveSettings();
this.update();
}
handleOpenSlackInstructions = () => {
const { store } = this.props;
store.slackStore.installSlackIntegration();
};
update = () => {
const { store } = this.props;
@ -42,6 +52,32 @@ class SlackSettings extends Component<SlackProps, SlackState> {
store.slackStore.updateSlackSettings();
};
getSlackLiveSettings = async () => {
const { store } = this.props;
const slackClientOAUTH = await store.globalSettingStore.getGlobalSettingItemByName('SLACK_CLIENT_OAUTH_ID');
const slackClientOAUTHSecret = await store.globalSettingStore.getGlobalSettingItemByName(
'SLACK_CLIENT_OAUTH_SECRET'
);
const slackRedirectHost = await store.globalSettingStore.getGlobalSettingItemByName(
'SLACK_INSTALL_RETURN_REDIRECT_HOST'
);
const slackSigningSecret = await store.globalSettingStore.getGlobalSettingItemByName('SLACK_SIGNING_SECRET');
console.log('slackClientOAUTH', slackClientOAUTH?.error);
console.log('slackClientOAUTHSecret', slackClientOAUTHSecret?.error);
console.log('slackRedirectHost', slackRedirectHost?.error);
console.log('slackSigningSecret', slackSigningSecret?.error);
if (
slackClientOAUTH?.error ||
slackClientOAUTHSecret?.error ||
slackRedirectHost?.error ||
slackSigningSecret?.error
) {
console.log('BLA BLA');
this.setState({ showENVVariablesButton: true });
}
};
render() {
const { store } = this.props;
const { teamStore } = store;
@ -59,34 +95,47 @@ class SlackSettings extends Component<SlackProps, SlackState> {
return (
<div className={cx('root')}>
<Text.Title level={4} className={cx('title')}>
Slack
</Text.Title>
<div className={cx('slack-settings')}>
<Field label="Default channel for Slack notifications">
<WithPermissionControl userAction={UserAction.UpdateGeneralLogChannelId}>
<GSelect
showSearch
className={cx('select', 'control')}
modelName="slackChannelStore"
displayField="display_name"
valueField="id"
placeholder="Select Slack Channel"
value={teamStore.currentTeam?.slack_channel?.id}
onChange={this.handleSlackChannelChange}
nullItemName={PRIVATE_CHANNEL_NAME}
/>
</WithPermissionControl>
</Field>
<div className={cx('title')}>
<Text.Title level={3}>Slack</Text.Title>
</div>
<div className={cx('slack-settings')}>
<Text.Title level={4} className={cx('title')}>
<HorizontalGroup justify="space-between">
<HorizontalGroup align="center">
<Field label="Slack Workspace">
<div className={cx('select', 'control', 'team_workspace')}>
<Text>{store.teamStore.currentTeam.slack_team_identity?.cached_name}</Text>
</div>
</Field>
<Field label="Default channel for Slack notifications">
<WithPermissionControl userAction={UserAction.UpdateGeneralLogChannelId}>
<GSelect
showSearch
className={cx('select', 'control')}
modelName="slackChannelStore"
displayField="display_name"
valueField="id"
placeholder="Select Slack Channel"
value={teamStore.currentTeam?.slack_channel?.id}
onChange={this.handleSlackChannelChange}
nullItemName={PRIVATE_CHANNEL_NAME}
/>
</WithPermissionControl>
</Field>
</HorizontalGroup>
<WithPermissionControl userAction={UserAction.UpdateIntegrations}>
<WithConfirm title="Are you sure to delete this Slack Integration?">
<Button variant="destructive" size="sm" onClick={() => this.removeSlackIntegration()}>
Disconnect
</Button>
</WithConfirm>
</WithPermissionControl>
</HorizontalGroup>
</div>
<div className={cx('slack-settings')}>
<Text.Title level={5} className={cx('title')}>
Additional settings
</Text.Title>
<Field
label="Timeout for acknowledged alerts"
description="Set up a reminder and timeout for acknowledged alert to never forget about them"
>
<Field label="Timeout for acknowledged alerts">
<HorizontalGroup>
<WithPermissionControl userAction={UserAction.UpdateGeneralLogChannelId}>
<RemoteSelect
@ -110,14 +159,51 @@ class SlackSettings extends Component<SlackProps, SlackState> {
</HorizontalGroup>
</Field>
</div>
<Text.Title level={4} className={cx('title')}>
Remove integration
</Text.Title>
<SlackIntegrationButton className={cx('slack-button')} />
</div>
);
};
renderSlackWorkspace = () => {
const { store } = this.props;
return <Text>{store.teamStore.currentTeam.slack_team_identity?.cached_name}</Text>;
};
renderSlackChannels = () => {
const { store } = this.props;
return (
<WithPermissionControl userAction={UserAction.UpdateGeneralLogChannelId}>
<GSelect
showSearch
className={cx('select', 'control')}
modelName="slackChannelStore"
displayField="display_name"
valueField="id"
placeholder="Select Slack Channel"
value={store.teamStore.currentTeam?.slack_channel?.id}
onChange={this.handleSlackChannelChange}
nullItemName={PRIVATE_CHANNEL_NAME}
/>
</WithPermissionControl>
);
};
renderActionButtons = () => {
<WithPermissionControl userAction={UserAction.UpdateIntegrations}>
<WithConfirm title="Are you sure to delete this Slack Integration?">
<Button variant="destructive" size="sm" onClick={() => this.removeSlackIntegration()}>
Disconnect
</Button>
</WithConfirm>
</WithPermissionControl>;
};
removeSlackIntegration = () => {
const { store } = this.props;
store.slackStore.removeSlackIntegration().then(() => {
store.teamStore.loadCurrentTeam();
});
};
getSlackSettingsChangeHandler = (field: string) => {
const { store } = this.props;
const { slackStore } = store;
@ -138,28 +224,49 @@ class SlackSettings extends Component<SlackProps, SlackState> {
renderSlackStub = () => {
const { store } = this.props;
const { showENVVariablesButton } = this.state;
const isLiveSettingAvailable = store.hasFeature(AppFeature.LiveSettings) && showENVVariablesButton;
return (
<Tutorial
step={TutorialStep.Slack}
title={
<VerticalGroup spacing="lg">
<Text.Title level={2}>Connect Slack workspace</Text.Title>
<Block bordered withBackground className={cx('slack-infoblock')}>
<VerticalGroup align="center" spacing="lg">
<Text type="secondary">
Bring the whole incident lifecycle to Slack, from alerts, monitoring, escalations to resolution notes and
reports.
<SlackNewIcon />
<Text>
Slack connection will allow you to manage incidents in your team Slack workspace.
<br />
After a basic workspace connection, your team members need to connect their personal Slack accounts in
order to be allowed to manage incidents.
</Text>
<SlackIntegrationButton className={cx('slack-button')} />
{store.hasFeature(AppFeature.LiveSettings) && (
{isLiveSettingAvailable && (
<Text type="secondary">
Before installing <PluginLink query={{ page: 'live-settings' }}>check ENV variables</PluginLink> related
to Slack please
For bot creating instructions and additional information please read{' '}
<a href="https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup">
<Text type="link">our documentation</Text>
</a>
</Text>
)}
<img
style={{ height: '350px', display: 'block', margin: '0 auto' }}
src="public/plugins/grafana-oncall-app/img/slack_instructions.png"
/>
</VerticalGroup>
}
/>
</Block>
{isLiveSettingAvailable ? (
<PluginLink query={{ page: 'live-settings' }}>
<Button variant="primary">Setup ENV Variables</Button>
</PluginLink>
) : (
<HorizontalGroup>
<Button onClick={this.handleOpenSlackInstructions}>
<Icon name="external-link-alt" className={cx('external-link-style')} /> Open Slack connection page
</Button>
<PluginLink query={{ page: 'live-settings' }}>
<Button variant="secondary">See ENV Variables</Button>
</PluginLink>
</HorizontalGroup>
)}
</VerticalGroup>
);
};
}