change(ui): set up msteams for share popup and note creation
This commit is contained in:
parent
1687b5031a
commit
02027da02b
21 changed files with 328 additions and 116 deletions
|
|
@ -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} />;
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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' }}>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(', ') + ')' : '';
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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%);
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
frontend/app/svg/icons/integrations/teams-white.svg
Normal file
4
frontend/app/svg/icons/integrations/teams-white.svg
Normal 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 |
Loading…
Add table
Reference in a new issue