change(ui): set up msteams for share popup and note creation

This commit is contained in:
sylenien 2022-12-06 16:31:20 +01:00 committed by Delirium
parent 1687b5031a
commit 02027da02b
21 changed files with 328 additions and 116 deletions

View file

@ -195,6 +195,12 @@ class Router extends React.Component {
state: tenantId, state: tenantId,
}); });
break; break;
case '/integrations/msteams':
client.post('integrations/msteams/add', {
code: location.search.split('=')[1],
state: tenantId,
});
break;
} }
return <Redirect to={CLIENT_PATH} />; return <Redirect to={CLIENT_PATH} />;
}} }}

View file

@ -11,6 +11,7 @@ const siteIdRequiredPaths = [
'/metadata', '/metadata',
'/integrations/sentry/events', '/integrations/sentry/events',
'/integrations/slack/notify', '/integrations/slack/notify',
'/integrations/msteams/notify',
'/assignments', '/assignments',
'/integration/sources', '/integration/sources',
'/issue_types', '/issue_types',

View file

@ -1,6 +1,5 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Button, Form, Input, SegmentSelection, Checkbox, Message, Link, Icon } from 'UI'; import { Button, Form, Input, SegmentSelection, Checkbox, Icon } from 'UI';
import { alertMetrics as metrics } from 'App/constants';
import { alertConditions as conditions } from 'App/constants'; import { alertConditions as conditions } from 'App/constants';
import { client, CLIENT_TABS } from 'App/routes'; import { client, CLIENT_TABS } from 'App/routes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -47,12 +46,12 @@ const AlertForm = (props) => {
const { const {
instance, instance,
slackChannels, slackChannels,
msTeamsChannels,
webhooks, webhooks,
loading, loading,
onDelete, onDelete,
deleting, deleting,
triggerOptions, triggerOptions,
metricId,
style = { width: '580px', height: '100vh' }, style = { width: '580px', height: '100vh' },
} = props; } = props;
const write = ({ target: { value, name } }) => props.edit({ [name]: value }); const write = ({ target: { value, name } }) => props.edit({ [name]: value });
@ -241,6 +240,14 @@ const AlertForm = (props) => {
onClick={onChangeCheck} onClick={onChangeCheck}
label="Slack" label="Slack"
/> />
<Checkbox
name="msteams"
className="mr-8"
type="checkbox"
checked={instance.msteams}
onClick={onChangeCheck}
label="MS Teams"
/>
<Checkbox <Checkbox
name="email" name="email"
type="checkbox" type="checkbox"
@ -266,6 +273,20 @@ const AlertForm = (props) => {
</div> </div>
</div> </div>
)} )}
{instance.msteams && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'MS Teams'}</label>
<div className="w-4/6">
<DropdownChips
fluid
selected={instance.msTeamsInput}
options={msTeamsChannels}
placeholder="Select Channel"
onChange={(selected) => props.edit({ msTeamsInput: selected })}
/>
</div>
</div>
)}
{instance.email && ( {instance.email && (
<div className="flex items-start my-4"> <div className="flex items-start my-4">

View file

@ -17,6 +17,8 @@ const AlertItem = props => {
const getNotifyChannel = alert => { const getNotifyChannel = alert => {
let str = ''; let str = '';
if (alert.msteams)
str = 'MS Teams'
if (alert.slack) if (alert.slack)
str = 'Slack'; str = 'Slack';
if (alert.email) if (alert.email)

View file

@ -10,6 +10,7 @@ interface Props {
init: (inst: any) => void; init: (inst: any) => void;
update: (inst: any) => void; update: (inst: any) => void;
remove: (id: string) => void; remove: (id: string) => void;
onClose: () => void;
instance: any; instance: any;
saving: boolean; saving: boolean;
errors: any; errors: any;
@ -29,7 +30,7 @@ class TeamsAddForm extends React.PureComponent<Props> {
} }
}; };
remove = async (id) => { remove = async (id: string) => {
if ( if (
await confirm({ await confirm({
header: 'Confirm', header: 'Confirm',
@ -41,7 +42,7 @@ class TeamsAddForm extends React.PureComponent<Props> {
} }
}; };
write = ({ target: { name, value } }) => this.props.edit({ [name]: value }); write = ({ target: { name, value } }: { target: { name: string, value: string }}) => this.props.edit({ [name]: value });
render() { render() {
const { instance, saving, errors, onClose } = this.props; const { instance, saving, errors, onClose } = this.props;
@ -91,8 +92,8 @@ class TeamsAddForm extends React.PureComponent<Props> {
{errors && ( {errors && (
<div className="my-3"> <div className="my-3">
{errors.map((error) => ( {errors.map((error: any) => (
<Message visible={errors} size="mini" error key={error}> <Message visible={errors} key={error}>
{error} {error}
</Message> </Message>
))} ))}
@ -105,9 +106,9 @@ class TeamsAddForm extends React.PureComponent<Props> {
export default connect( export default connect(
(state: any) => ({ (state: any) => ({
instance: state.getIn(['slack', 'instance']), instance: state.getIn(['teams', 'instance']),
saving: state.getIn(['slack', 'saveRequest', 'loading']), saving: state.getIn(['teams', 'saveRequest', 'loading']),
errors: state.getIn(['slack', 'saveRequest', 'errors']), errors: state.getIn(['teams', 'saveRequest', 'errors']),
}), }),
{ edit, save, init, remove, update } { edit, save, init, remove, update }
)(TeamsAddForm); )(TeamsAddForm);

View file

@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import TeamsChannelList from './TeamsChannelList'; import TeamsChannelList from './TeamsChannelList';
import { fetchList, init } from 'Duck/integrations/teams'; import { fetchList, init } from 'Duck/integrations/teams';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import SlackAddForm from './SlackAddForm'; import TeamsAddForm from './TeamsAddForm';
import { Button } from 'UI'; import { Button } from 'UI';
interface Props { interface Props {
@ -31,7 +31,7 @@ const MSTeams = (props: Props) => {
<div className="bg-white h-screen overflow-y-auto flex items-start" style={{ width: active ? '700px' : '350px' }}> <div className="bg-white h-screen overflow-y-auto flex items-start" style={{ width: active ? '700px' : '350px' }}>
{active && ( {active && (
<div className="border-r h-full" style={{ width: '350px' }}> <div className="border-r h-full" style={{ width: '350px' }}>
<SlackAddForm onClose={() => setActive(false)} /> <TeamsAddForm onClose={() => setActive(false)} />
</div> </div>
)} )}
<div className="shrink-0" style={{ width: '350px' }}> <div className="shrink-0" style={{ width: '350px' }}>

View file

@ -16,7 +16,7 @@ function Webhooks(props) {
const { webhooks, loading } = props; const { webhooks, loading } = props;
const { showModal, hideModal } = useModal(); const { showModal, hideModal } = useModal();
const noSlackWebhooks = webhooks.filter((hook) => hook.type !== 'slack'); const noSlackWebhooks = webhooks.filter((hook) => hook.type === 'webhook');
useEffect(() => { useEffect(() => {
props.fetchList(); props.fetchList();
}, []); }, []);

View file

@ -6,6 +6,7 @@ interface INotifyHooks {
instance: Alert; instance: Alert;
onChangeCheck: (e: React.ChangeEvent<HTMLInputElement>) => void; onChangeCheck: (e: React.ChangeEvent<HTMLInputElement>) => void;
slackChannels: Array<any>; slackChannels: Array<any>;
msTeamsChannels: Array<any>;
validateEmail: (value: string) => boolean; validateEmail: (value: string) => boolean;
edit: (data: any) => void; edit: (data: any) => void;
hooks: Array<any>; hooks: Array<any>;
@ -16,6 +17,7 @@ function NotifyHooks({
onChangeCheck, onChangeCheck,
slackChannels, slackChannels,
validateEmail, validateEmail,
msTeamsChannels,
hooks, hooks,
edit, edit,
}: INotifyHooks) { }: INotifyHooks) {
@ -49,7 +51,7 @@ function NotifyHooks({
{instance.slack && ( {instance.slack && (
<div className="flex items-start my-4"> <div className="flex items-start my-4">
<label className="w-1/6 flex-shrink-0 font-normal pt-2">{'Slack'}</label> <label className="w-1/6 flex-shrink-0 font-normal pt-2">Slack</label>
<div className="w-2/6"> <div className="w-2/6">
<DropdownChips <DropdownChips
fluid fluid
@ -63,9 +65,25 @@ function NotifyHooks({
</div> </div>
)} )}
{instance.msteams && (
<div className="flex items-start my-4">
<label className="w-1/6 flex-shrink-0 font-normal pt-2">MS Teams</label>
<div className="w-2/6">
<DropdownChips
fluid
selected={instance.msteamsInput}
options={msTeamsChannels}
placeholder="Select Channel"
// @ts-ignore
onChange={(selected) => edit({ msteamsInput: selected })}
/>
</div>
</div>
)}
{instance.email && ( {instance.email && (
<div className="flex items-start my-4"> <div className="flex items-start my-4">
<label className="w-1/6 flex-shrink-0 font-normal pt-2">{'Email'}</label> <label className="w-1/6 flex-shrink-0 font-normal pt-2">Email</label>
<div className="w-2/6"> <div className="w-2/6">
<DropdownChips <DropdownChips
textFiled textFiled
@ -81,7 +99,7 @@ function NotifyHooks({
{instance.webhook && ( {instance.webhook && (
<div className="flex items-start my-4"> <div className="flex items-start my-4">
<label className="w-1/6 flex-shrink-0 font-normal pt-2">{'Webhook'}</label> <label className="w-1/6 flex-shrink-0 font-normal pt-2">Webhook</label>
<div className="w-2/6"> <div className="w-2/6">
<DropdownChips <DropdownChips
fluid fluid

View file

@ -41,6 +41,9 @@ const getNotifyChannel = (alert: Record<string, any>, webhooks: Array<any>) => {
str = 'Slack'; str = 'Slack';
str += alert.slackInput.length > 0 ? getSlackChannels() : ''; str += alert.slackInput.length > 0 ? getSlackChannels() : '';
} }
if (alert.msteams) {
str = 'MS Teams'
}
if (alert.email) { if (alert.email) {
str += (str === '' ? '' : ' and ') + (alert.emailInput.length > 1 ? 'Emails' : 'Email'); str += (str === '' ? '' : ' and ') + (alert.emailInput.length > 1 ? 'Emails' : 'Email');
str += alert.emailInput.length > 0 ? ' (' + alert.emailInput.join(', ') + ')' : ''; str += alert.emailInput.length > 0 ? ' (' + alert.emailInput.join(', ') + ')' : '';

View file

@ -8,8 +8,10 @@ import stl from './styles.module.css';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { fetchList as fetchSlack } from 'Duck/integrations/slack'; import { fetchList as fetchSlack } from 'Duck/integrations/slack';
import { fetchList as fetchTeams } from 'Duck/integrations/teams';
import Select from 'Shared/Select'; import Select from 'Shared/Select';
import { TeamBadge } from 'Shared/SessionListContainer/components/Notes' import { TeamBadge } from 'Shared/SessionListContainer/components/Notes';
import { List } from 'immutable'; import { List } from 'immutable';
interface Props { interface Props {
@ -22,7 +24,9 @@ interface Props {
isEdit: string; isEdit: string;
editNote: WriteNote; editNote: WriteNote;
slackChannels: List<Record<string, any>>; slackChannels: List<Record<string, any>>;
teamsChannels: List<Record<string, any>>;
fetchSlack: () => void; fetchSlack: () => void;
fetchTeams: () => void;
} }
function CreateNote({ function CreateNote({
@ -36,12 +40,18 @@ function CreateNote({
updateNote, updateNote,
slackChannels, slackChannels,
fetchSlack, fetchSlack,
teamsChannels,
fetchTeams,
}: Props) { }: Props) {
const [text, setText] = React.useState(''); const [text, setText] = React.useState('');
const [channel, setChannel] = React.useState(''); const [slackChannel, setSlackChannel] = React.useState('');
const [teamsChannel, setTeamsChannel] = React.useState('');
const [isPublic, setPublic] = React.useState(false); const [isPublic, setPublic] = React.useState(false);
const [tag, setTag] = React.useState<iTag>(TAGS[0]); const [tag, setTag] = React.useState<iTag>(TAGS[0]);
const [useTimestamp, setUseTs] = React.useState(true); const [useTimestamp, setUseTs] = React.useState(true);
const [useSlack, setSlack] = React.useState(false);
const [useTeams, setTeams] = React.useState(false);
const inputRef = React.createRef<HTMLTextAreaElement>(); const inputRef = React.createRef<HTMLTextAreaElement>();
const { notesStore } = useStore(); const { notesStore } = useStore();
@ -59,6 +69,7 @@ function CreateNote({
React.useEffect(() => { React.useEffect(() => {
if (inputRef.current && isVisible) { if (inputRef.current && isVisible) {
fetchSlack(); fetchSlack();
fetchTeams();
inputRef.current.focus(); inputRef.current.focus();
} }
}, [isVisible]); }, [isVisible]);
@ -75,17 +86,20 @@ function CreateNote({
isPublic, isPublic,
}; };
const onSuccess = (noteId: string) => { const onSuccess = (noteId: string) => {
if (channel) { if (slackChannel) {
notesStore.sendSlackNotification(noteId, channel) notesStore.sendSlackNotification(noteId, slackChannel);
} }
} if (teamsChannel) {
notesStore.sendMsTeamsNotification(noteId, teamsChannel);
}
};
if (isEdit) { if (isEdit) {
return notesStore return notesStore
.updateNote(editNote.noteId, note) .updateNote(editNote.noteId, note)
.then((r) => { .then((r) => {
toast.success('Note updated'); toast.success('Note updated');
notesStore.fetchSessionNotes(sessionId).then((notes) => { notesStore.fetchSessionNotes(sessionId).then((notes) => {
onSuccess(editNote.noteId) onSuccess(editNote.noteId);
updateNote(r); updateNote(r);
}); });
}) })
@ -103,7 +117,7 @@ function CreateNote({
return notesStore return notesStore
.addNote(sessionId, note) .addNote(sessionId, note)
.then((r) => { .then((r) => {
onSuccess(r.noteId as unknown as string) onSuccess(r.noteId as unknown as string);
toast.success('Note added'); toast.success('Note added');
notesStore.fetchSessionNotes(sessionId).then((notes) => { notesStore.fetchSessionNotes(sessionId).then((notes) => {
addNote(r); addNote(r);
@ -130,27 +144,41 @@ function CreateNote({
setTag(tag); setTag(tag);
}; };
const slackChannelsOptions = slackChannels.map(({ webhookId, name }) => ({ const slackChannelsOptions = slackChannels
value: webhookId, .map(({ webhookId, name }) => ({
label: name, value: webhookId,
})).toJS() as unknown as { value: string, label: string }[] label: name,
}))
.toJS() as unknown as { value: string; label: string }[];
const teamsChannelsOptions = teamsChannels
.map(({ webhookId, name }) => ({
value: webhookId,
label: name,
}))
.toJS() as unknown as { value: string; label: string }[];
slackChannelsOptions.unshift({ value: null, label: 'Share to slack?' }) slackChannelsOptions.unshift({ value: null, label: 'Pick a channel' });
teamsChannelsOptions.unshift({ value: null, label: 'Pick a channel' });
const changeChannel = ({ value, name }: { value: Record<string, string>; name: string }) => { const changeSlackChannel = ({ value, name }: { value: Record<string, string>; name: string }) => {
setChannel(value.value); setSlackChannel(value.value);
};
const changeTeamsChannel = ({ value, name }: { value: Record<string, string>; name: string }) => {
setTeamsChannel(value.value);
}; };
return ( return (
<div <div
className={stl.noteTooltip} className={stl.noteTooltip}
style={{ style={{
top: slackChannelsOptions.length > 0 ? -310 : 255,
width: 350, width: 350,
left: 'calc(50% - 175px)', left: 'calc(50% - 175px)',
display: isVisible ? 'flex' : 'none', display: isVisible ? 'flex' : 'none',
flexDirection: 'column', flexDirection: 'column',
gap: '1rem', gap: '1rem',
bottom: '15vh',
zIndex: 110,
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
@ -206,15 +234,44 @@ function CreateNote({
</div> </div>
{slackChannelsOptions.length > 0 ? ( {slackChannelsOptions.length > 0 ? (
<div> <div className="flex flex-col">
<Select <div className="flex items-center cursor-pointer" onClick={() => setSlack(!useSlack)}>
options={slackChannelsOptions} <Checkbox checked={useSlack} />
// @ts-ignore <span className="ml-1 mr-3"> Send to slack? </span>
defaultValue </div>
// @ts-ignore
onChange={changeChannel} {useSlack && (
className="mr-4" <div>
/> <Select
options={slackChannelsOptions}
// @ts-ignore
defaultValue
// @ts-ignore
onChange={changeSlackChannel}
/>
</div>
)}
</div>
) : null}
{teamsChannelsOptions.length > 0 ? (
<div className="flex flex-col">
<div className="flex items-center cursor-pointer" onClick={() => setTeams(!useTeams)}>
<Checkbox checked={useTeams} />
<span className="ml-1 mr-3"> Send to teams? </span>
</div>
{useTeams && (
<div>
<Select
options={teamsChannelsOptions}
// @ts-ignore
defaultValue
// @ts-ignore
onChange={changeTeamsChannel}
/>
</div>
)}
</div> </div>
) : null} ) : null}
@ -232,19 +289,17 @@ function CreateNote({
} }
export default connect( export default connect(
(state) => { (state: any) => {
const { const {
isVisible, isVisible,
time = 0, time = 0,
isEdit, isEdit,
note: editNote, note: editNote,
// @ts-ignore
} = state.getIn(['sessions', 'createNoteTooltip']); } = state.getIn(['sessions', 'createNoteTooltip']);
// @ts-ignore
const slackChannels = state.getIn(['slack', 'list']); const slackChannels = state.getIn(['slack', 'list']);
// @ts-ignore const teamsChannels = state.getIn(['teams', 'list']);
const sessionId = state.getIn(['sessions', 'current', 'sessionId']); const sessionId = state.getIn(['sessions', 'current', 'sessionId']);
return { isVisible, time, sessionId, isEdit, editNote, slackChannels }; return { isVisible, time, sessionId, isEdit, editNote, slackChannels, teamsChannels };
}, },
{ setCreateNoteTooltip, addNote, updateNote, fetchSlack } { setCreateNoteTooltip, addNote, updateNote, fetchSlack, fetchTeams }
)(CreateNote); )(CreateNote);

View file

@ -25,14 +25,13 @@
} }
.noteTooltip { .noteTooltip {
position: absolute; position: fixed;
padding: 1rem; padding: 1rem;
border-radius: 0.25rem; border-radius: 0.25rem;
transition-property: all; transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms; transition-duration: 150ms;
background: #F5F5F5; background: #F5F5F5;
top: -35px;
color: black; color: black;
cursor: default; cursor: default;
box-shadow: 0 4px 20px 4px rgb(0 20 60 / 10%), 0 4px 80px -8px rgb(0 20 60 / 20%); box-shadow: 0 4px 20px 4px rgb(0 20 60 / 10%), 0 4px 80px -8px rgb(0 20 60 / 20%);

View file

@ -1,26 +1,29 @@
import React from 'react' import React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { IconButton } from 'UI' import { Button, Icon } from 'UI'
import { CLIENT_TABS, client as clientRoute } from 'App/routes'; import { CLIENT_TABS, client as clientRoute } from 'App/routes';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
function IntegrateSlackButton({ history, tenantId }) { function IntegrateSlackTeamsButton({ history, tenantId }) {
const gotoPreferencesIntegrations = () => { const gotoPreferencesIntegrations = () => {
history.push(clientRoute(CLIENT_TABS.INTEGRATIONS)); history.push(clientRoute(CLIENT_TABS.INTEGRATIONS));
} }
return ( return (
<div> <div>
<IconButton <Button
className="my-auto mt-2 mb-2" className="my-auto mt-2 mb-2 flex items-center gap-2"
icon="integrations/slack"
label="Integrate Slack"
onClick={gotoPreferencesIntegrations} onClick={gotoPreferencesIntegrations}
/> >
<Icon name="integrations/slack" size={16} />
<Icon name="integrations/teams" size={18} className="mr-2" />
<span>Integrate Slack/MS Teams</span>
</Button>
</div> </div>
) )
} }
export default withRouter(connect(state => ({ export default withRouter(connect(state => ({
tenantId: state.getIn([ 'user', 'account', 'tenantId' ]), tenantId: state.getIn([ 'user', 'account', 'tenantId' ]),
}))(IntegrateSlackButton)) }))(IntegrateSlackTeamsButton))

View file

@ -1,45 +1,68 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import withRequest from 'HOCs/withRequest';
import { Icon, Button, Popover } from 'UI'; import { Icon, Button, Popover } from 'UI';
import styles from './sharePopup.module.css'; import styles from './sharePopup.module.css';
import IntegrateSlackButton from '../IntegrateSlackButton/IntegrateSlackButton'; import IntegrateSlackButton from '../IntegrateSlackButton/IntegrateSlackButton';
import SessionCopyLink from './SessionCopyLink'; import SessionCopyLink from './SessionCopyLink';
import Select from 'Shared/Select'; import Select from 'Shared/Select';
import cn from 'classnames'; import cn from 'classnames';
import { fetchList } from 'Duck/integrations/slack'; import { fetchList as fetchSlack, sendSlackMsg } from 'Duck/integrations/slack';
import { fetchList as fetchTeams, sendMsTeamsMsg } from 'Duck/integrations/teams';
@connect( @connect(
(state) => ({ (state) => ({
sessionId: state.getIn(['sessions', 'current', 'sessionId']),
channels: state.getIn(['slack', 'list']), channels: state.getIn(['slack', 'list']),
msTeamsChannels: state.getIn(['teams', 'list']),
tenantId: state.getIn(['user', 'account', 'tenantId']), tenantId: state.getIn(['user', 'account', 'tenantId']),
}), }),
{ fetchList } { fetchSlack, fetchTeams, sendSlackMsg, sendMsTeamsMsg }
) )
@withRequest({
endpoint: ({ id, entity }, integrationId) =>
`/integrations/slack/notify/${integrationId}/${entity}/${id}`,
method: 'POST',
})
export default class SharePopup extends React.PureComponent { export default class SharePopup extends React.PureComponent {
state = { state = {
comment: '', comment: '',
isOpen: false, isOpen: false,
channelId: this.props.channels.getIn([0, 'webhookId']), channelId: this.props.channels.getIn([0, 'webhookId']),
teamsChannel: this.props.msTeamsChannels.getIn([0, 'webhookId']),
loading: false,
}; };
componentDidMount() { componentDidMount() {
if (this.props.channels.size === 0) { if (this.props.channels.size === 0) {
this.props.fetchList(); this.props.fetchSlack();
}
if (this.props.msTeamsChannels.size === 0) {
this.props.fetchTeams();
} }
} }
editMessage = (e) => this.setState({ comment: e.target.value }); editMessage = (e) => this.setState({ comment: e.target.value });
share = () => shareToSlack = () => {
this.props this.setState({ loading: true }, () => {
.request({ comment: this.state.comment }, this.state.channelId) this.props
.then(this.handleSuccess); .sendSlackMsg({
integrationId: this.state.channelId,
entity: 'sessions',
entityId: this.props.sessionId,
data: { comment: this.state.comment },
})
.then(() => this.handleSuccess('Slack'));
});
};
shareToMSTeams = () => {
this.setState({ loading: true }, () => {
this.props
.sendMsTeamsMsg({
integrationId: this.state.teamsChannel,
entity: 'sessions',
entityId: this.props.sessionId,
data: { comment: this.state.comment },
})
.then(() => this.handleSuccess('MS Teams'));
});
};
handleOpen = () => { handleOpen = () => {
setTimeout(function () { setTimeout(function () {
@ -51,22 +74,28 @@ export default class SharePopup extends React.PureComponent {
this.setState({ comment: '' }); this.setState({ comment: '' });
}; };
handleSuccess = () => { handleSuccess = (endpoint) => {
this.setState({ isOpen: false, comment: '' }); this.setState({ isOpen: false, comment: '', loading: false });
toast.success('Sent to Slack.'); toast.success(`Sent to ${endpoint}.`);
}; };
changeChannel = ({ value }) => this.setState({ channelId: value.value }); changeSlackChannel = ({ value }) => this.setState({ channelId: value.value });
changeTeamsChannel = ({ value }) => this.setState({ teamsChannel: value.value });
onClickHandler = () => { onClickHandler = () => {
this.setState({ isOpen: true }); this.setState({ isOpen: true });
}; };
render() { render() {
const { trigger, loading, channels, showCopyLink = false } = this.props; const { trigger, channels, msTeamsChannels, showCopyLink = false } = this.props;
const { comment, channelId, isOpen } = this.state; const { comment, channelId, teamsChannel, loading } = this.state;
const options = channels const slackOptions = channels
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
.toJS();
const msTeamsOptions = msTeamsChannels
.map(({ webhookId, name }) => ({ value: webhookId, label: name })) .map(({ webhookId, name }) => ({ value: webhookId, label: name }))
.toJS(); .toJS();
@ -75,20 +104,11 @@ export default class SharePopup extends React.PureComponent {
render={() => ( render={() => (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.header}> <div className={styles.header}>
<div className={cn(styles.title, 'text-lg')}>Share this session link to Slack</div> <div className={cn(styles.title, 'text-lg')}>
Share this session link to Slack/MS Teams
</div>
</div> </div>
{options.length === 0 ? ( {slackOptions.length > 0 || msTeamsOptions.length > 0 ? (
<>
<div className={styles.body}>
<IntegrateSlackButton />
</div>
{showCopyLink && (
<div className={styles.footer}>
<SessionCopyLink />
</div>
)}
</>
) : (
<div> <div>
<div className={styles.body}> <div className={styles.body}>
<textarea <textarea
@ -100,30 +120,72 @@ export default class SharePopup extends React.PureComponent {
onChange={this.editMessage} onChange={this.editMessage}
value={comment} value={comment}
placeholder="Add Message (Optional)" placeholder="Add Message (Optional)"
className="p-4" className="p-4 text-figmaColors-text-primary text-base"
/> />
<div className="flex items-center justify-between"> {slackOptions.length > 0 && (
<Select <>
options={options} <span>Share to slack</span>
defaultValue={channelId} <div className="flex items-center justify-between mb-2">
onChange={this.changeChannel} <Select
className="mr-4" options={slackOptions}
/> defaultValue={channelId}
<div> onChange={this.changeSlackChannel}
<Button onClick={this.share} variant="primary"> className="mr-4"
<div className="flex items-center"> />
<Icon name="integrations/slack-bw" size="18" marginRight="10" /> {this.state.channelId && (
{loading ? 'Sending...' : 'Send'} <Button onClick={this.shareToSlack} variant="primary">
</div> <div className="flex items-center">
</Button> <Icon name="integrations/slack-bw" color="white" size="18" marginRight="10" />
</div> {loading ? 'Sending...' : 'Send'}
</div> </div>
</Button>
)}
</div>
</>
)}
{msTeamsOptions.length > 0 && (
<>
<span>Share to MS Teams</span>
<div className="flex items-center justify-between">
<Select
options={msTeamsOptions}
defaultValue={teamsChannel}
onChange={this.changeTeamsChannel}
className="mr-4"
/>
{this.state.teamsChannel && (
<Button onClick={this.shareToMSTeams} variant="primary">
<div className="flex items-center">
<Icon
name="integrations/teams-white"
color="white"
size="18"
marginRight="10"
/>
{loading ? 'Sending...' : 'Send'}
</div>
</Button>
)}
</div>
</>
)}
</div> </div>
<div className={styles.footer}> <div className={styles.footer}>
<SessionCopyLink /> <SessionCopyLink />
</div> </div>
</div> </div>
) : (
<>
<div className={styles.body}>
<IntegrateSlackButton />
</div>
{showCopyLink && (
<div className={styles.footer}>
<SessionCopyLink />
</div>
)}
</>
)} )}
</div> </div>
)} )}

View file

@ -39,14 +39,9 @@
} }
.footer { .footer {
/* display: flex; */
/* align-items: center; */
/* justify-content: space-between; */
/* padding: 10px 0; */
border-top: solid thin $gray-light; border-top: solid thin $gray-light;
margin: 0 -14px; margin: 0 -8px;
padding: 0 14px; padding: 0 14px;
/* border-bottom: solid thin $gray-light; */
} }
textarea { textarea {

View file

@ -12,7 +12,7 @@ const Message = ({
inline = false, inline = false,
success = false, success = false,
info = true, info = true,
text, text = undefined,
}) => }) =>
visible || !hidden ? ( visible || !hidden ? (
<div className={cn(styles.message, 'flex items-center')} data-inline={inline}> <div className={cn(styles.message, 'flex items-center')} data-inline={inline}>

File diff suppressed because one or more lines are too long

View file

@ -7,6 +7,7 @@ const SAVE = new RequestTypes('slack/SAVE');
const UPDATE = new RequestTypes('slack/UPDATE'); const UPDATE = new RequestTypes('slack/UPDATE');
const REMOVE = new RequestTypes('slack/REMOVE'); const REMOVE = new RequestTypes('slack/REMOVE');
const FETCH_LIST = new RequestTypes('slack/FETCH_LIST'); const FETCH_LIST = new RequestTypes('slack/FETCH_LIST');
const SEND_MSG = new RequestTypes('slack/SEND_MSG');
const EDIT = 'slack/EDIT'; const EDIT = 'slack/EDIT';
const INIT = 'slack/INIT'; const INIT = 'slack/INIT';
const idKey = 'webhookId'; const idKey = 'webhookId';
@ -61,7 +62,7 @@ export function save(instance) {
export function update(instance) { export function update(instance) {
return { return {
types: UPDATE.toArray(), types: UPDATE.toArray(),
call: (client) => client.put(`/integrations/slack/${instance.webhookId}`, instance.toData()), call: (client) => client.post(`/integrations/slack/${instance.webhookId}`, instance.toData()),
}; };
} }
@ -86,3 +87,12 @@ export function remove(id) {
id, id,
}; };
} }
// https://api.openreplay.com/5587/integrations/slack/notify/315/sessions/7856803626558104
//
export function sendSlackMsg({ integrationId, entity, entityId, data }) {
return {
types: SEND_MSG.toArray(),
call: (client) => client.post(`/integrations/slack/notify/${integrationId}/${entity}/${entityId}`, data)
}
}

View file

@ -7,6 +7,8 @@ const SAVE = new RequestTypes('msteams/SAVE');
const UPDATE = new RequestTypes('msteams/UPDATE'); const UPDATE = new RequestTypes('msteams/UPDATE');
const REMOVE = new RequestTypes('msteams/REMOVE'); const REMOVE = new RequestTypes('msteams/REMOVE');
const FETCH_LIST = new RequestTypes('msteams/FETCH_LIST'); const FETCH_LIST = new RequestTypes('msteams/FETCH_LIST');
const SEND_MSG = new RequestTypes('msteams/SEND_MSG');
const EDIT = 'msteams/EDIT'; const EDIT = 'msteams/EDIT';
const INIT = 'msteams/INIT'; const INIT = 'msteams/INIT';
const idKey = 'webhookId'; const idKey = 'webhookId';
@ -86,3 +88,12 @@ export function remove(id) {
id, id,
}; };
} }
// https://api.openreplay.com/5587/integrations/msteams/notify/315/sessions/7856803626558104
//
export function sendMsTeamsMsg({ integrationId, entity, entityId, data }) {
return {
types: SEND_MSG.toArray(),
call: (client) => client.post(`/integrations/msteams/notify/${integrationId}/${entity}/${entityId}`, data)
}
}

View file

@ -130,4 +130,13 @@ export default class NotesStore {
console.error(e) console.error(e)
} }
} }
async sendMsTeamsNotification(noteId: string, webhook: string) {
try {
const resp = await notesService.sendMsTeamsNotification(noteId, webhook)
return resp
} catch (e) {
console.error(e)
}
}
} }

View file

@ -119,4 +119,15 @@ export default class NotesService {
} }
}) })
} }
sendMsTeamsNotification(noteId: string, webhook: string) {
return this.client.get(`/notes/${noteId}/msteams/${webhook}`)
.then(r => {
if (r.ok) {
return r.json().then(r => r.data)
} else {
throw new Error('Error sending slack notif: ' + r.status)
}
})
}
} }

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M9.186 4.797a2.42 2.42 0 1 0-2.86-2.448h1.178c.929 0 1.682.753 1.682 1.682v.766Zm-4.295 7.738h2.613c.929 0 1.682-.753 1.682-1.682V5.58h2.783a.7.7 0 0 1 .682.716v4.294a4.197 4.197 0 0 1-4.093 4.293c-1.618-.04-3-.99-3.667-2.35Zm10.737-9.372a1.674 1.674 0 1 1-3.349 0 1.674 1.674 0 0 1 3.349 0Zm-2.238 9.488c-.04 0-.08 0-.12-.002a5.19 5.19 0 0 0 .381-2.07V6.306a1.692 1.692 0 0 0-.15-.725h1.792c.39 0 .707.317.707.707v3.765a2.598 2.598 0 0 1-2.598 2.598h-.013Z"/>
<path d="M.682 3.349h6.822c.377 0 .682.305.682.682v6.822a.682.682 0 0 1-.682.682H.682A.682.682 0 0 1 0 10.853V4.03c0-.377.305-.682.682-.682Zm5.206 2.596v-.72h-3.59v.72h1.357V9.66h.87V5.945h1.363Z"/>
</svg>

After

Width:  |  Height:  |  Size: 741 B