Merge pull request #1 from jambonz/microsoft_teams_integration

Microsoft teams integration
This commit is contained in:
Dave Horton
2020-05-31 14:18:32 -04:00
committed by GitHub
15 changed files with 1351 additions and 157 deletions
+46 -27
View File
@@ -12,14 +12,18 @@ import AccountsList from './components/pages/internal/AccountsList';
import ApplicationsList from './components/pages/internal/ApplicationsList';
import SipTrunksList from './components/pages/internal/SipTrunksList';
import PhoneNumbersList from './components/pages/internal/PhoneNumbersList';
import MsTeamsTenantsList from './components/pages/internal/MsTeamsTenantsList';
import AccountsAddEdit from './components/pages/internal/AccountsAddEdit';
import ApplicationsAddEdit from './components/pages/internal/ApplicationsAddEdit';
import SipTrunksAddEdit from './components/pages/internal/SipTrunksAddEdit';
import PhoneNumbersAddEdit from './components/pages/internal/PhoneNumbersAddEdit';
import MsTeamsTenantsAddEdit from './components/pages/internal/MsTeamsTenantsAddEdit';
import Settings from './components/pages/internal/Settings';
import InvalidRoute from './components/pages/InvalidRoute';
import Notification from './components/blocks/Notification';
import Nav from './components/blocks/Nav';
import SideMenu from './components/blocks/SideMenu';
function App() {
const notifications = useContext(NotificationStateContext);
@@ -35,37 +39,52 @@ function App() {
<Route exact path="/configure-sip-trunk"><ConfigureSipTrunk /></Route>
<Route exact path="/setup-complete"><SetupComplete /></Route>
<Route exact path="/internal/accounts"><AccountsList /></Route>
<Route exact path="/internal/applications"><ApplicationsList /></Route>
<Route exact path="/internal/sip-trunks"><SipTrunksList /></Route>
<Route exact path="/internal/phone-numbers"><PhoneNumbersList /></Route>
<Route path="/internal">
<div style={{ display: "flex" }}>
<SideMenu />
<Route exact path="/internal/accounts"><AccountsList /></Route>
<Route exact path="/internal/applications"><ApplicationsList /></Route>
<Route exact path="/internal/sip-trunks"><SipTrunksList /></Route>
<Route exact path="/internal/phone-numbers"><PhoneNumbersList /></Route>
<Route exact path="/internal/ms-teams-tenants"><MsTeamsTenantsList /></Route>
<Route exact path={[
"/internal/accounts/add",
"/internal/accounts/:account_sid/edit"
]}>
<AccountsAddEdit />
</Route>
<Route exact path={[
"/internal/accounts/add",
"/internal/accounts/:account_sid/edit"
]}>
<AccountsAddEdit />
</Route>
<Route exact path={[
"/internal/applications/add",
"/internal/applications/:application_sid/edit"
]}>
<ApplicationsAddEdit />
</Route>
<Route exact path={[
"/internal/applications/add",
"/internal/applications/:application_sid/edit"
]}>
<ApplicationsAddEdit />
</Route>
<Route exact path={[
"/internal/sip-trunks/add",
"/internal/sip-trunks/:voip_carrier_sid/edit"
]}>
<SipTrunksAddEdit />
</Route>
<Route exact path={[
"/internal/sip-trunks/add",
"/internal/sip-trunks/:voip_carrier_sid/edit"
]}>
<SipTrunksAddEdit />
</Route>
<Route exact path={[
"/internal/phone-numbers/add",
"/internal/phone-numbers/:phone_number_sid/edit"
]}>
<PhoneNumbersAddEdit />
<Route exact path={[
"/internal/phone-numbers/add",
"/internal/phone-numbers/:phone_number_sid/edit"
]}>
<PhoneNumbersAddEdit />
</Route>
<Route exact path={[
"/internal/ms-teams-tenants/add",
"/internal/ms-teams-tenants/:ms_teams_tenant_sid/edit"
]}>
<MsTeamsTenantsAddEdit />
</Route>
<Route exact path="/internal/settings"><Settings /></Route>
</div>
</Route>
<Route><InvalidRoute /></Route>
+110
View File
@@ -0,0 +1,110 @@
import React, { useEffect, useContext } from 'react';
import { NavLink } from 'react-router-dom';
import styled from 'styled-components/macro';
import { ModalStateContext } from '../../contexts/ModalContext';
import { ShowMsTeamsStateContext, ShowMsTeamsDispatchContext } from '../../contexts/ShowMsTeamsContext';
import { ReactComponent as AccountsIcon } from '../../images/AccountsIcon.svg';
import { ReactComponent as ApplicationsIcon } from '../../images/ApplicationsIcon.svg';
import { ReactComponent as SipTrunksIcon } from '../../images/SipTrunksIcon.svg';
import { ReactComponent as PhoneNumbersIcon } from '../../images/PhoneNumbersIcon.svg';
import { ReactComponent as MsTeamsIcon } from '../../images/MsTeamsIcon.svg';
import { ReactComponent as SettingsIcon } from '../../images/SettingsIcon.svg';
const StyledSideMenu = styled.div`
width: 15rem;
flex-shrink: 0;
height: calc(100vh - 4rem);
overflow: auto;
background: #FFF;
padding-top: 3.25rem;
`;
const activeClassName = 'nav-item-active';
const StyledNavLink = styled(NavLink).attrs({ activeClassName })`
height: 2.75rem;
margin-bottom: 1rem;
display: flex;
align-items: stretch;
font-weight: 500;
text-decoration: none;
color: #565656;
fill: #565656;
&.${activeClassName} {
box-shadow: inset 3px 0 0 0 #D91C5C;
color: #D91C5C;
fill: #D91C5C;
}
&:focus {
outline: 0;
box-shadow: inset 0 0 0 3px #D91C5C;
}
&:hover {
background: RGBA(217, 28, 92, 0.1);
color: #C0134D;
fill: #C0134D;
}
&.${activeClassName}:hover {
color: #D91C5C;
fill: #D91C5C;
}
`;
const IconContainer = styled.span`
width: 3rem;
display: flex;
justify-content: center;
align-items: center;
outline: 0;
`;
const MenuText = styled.span`
display: flex;
flex-grow: 1;
align-items: center;
outline: 0;
`;
const MenuLink = props => {
const modalOpen = useContext(ModalStateContext);
return (
<StyledNavLink
to={props.to}
activeClassName={activeClassName}
tabIndex={modalOpen ? '-1' : ''}
>
<IconContainer tabIndex="-1">
{props.icon}
</IconContainer>
<MenuText tabIndex="-1">
{props.name}
</MenuText>
</StyledNavLink>
);
};
const SideMenu = () => {
const showMsTeams = useContext(ShowMsTeamsStateContext);
const getMsTeamsData = useContext(ShowMsTeamsDispatchContext);
useEffect(() => {
getMsTeamsData();
// eslint-disable-next-line
}, []);
return (
<StyledSideMenu>
<MenuLink to="/internal/accounts" name="Accounts" icon={<AccountsIcon />} />
<MenuLink to="/internal/applications" name="Applications" icon={<ApplicationsIcon />} />
<MenuLink to="/internal/sip-trunks" name="SIP Trunks" icon={<SipTrunksIcon />} />
<MenuLink to="/internal/phone-numbers" name="Phone Numbers" icon={<PhoneNumbersIcon />} />
{showMsTeams && (
<MenuLink to="/internal/ms-teams-tenants" name="MS Teams Tenants" icon={<MsTeamsIcon />} />
)}
<MenuLink to="/internal/settings" name="Settings" icon={<SettingsIcon />} />
</StyledSideMenu>
);
};
export default SideMenu;
+1 -1
View File
@@ -158,7 +158,7 @@ const TableContent = props => {
//=============================================================================
return (
<React.Fragment>
{contentToDelete && (contentToDelete.name || contentToDelete.number) && (
{contentToDelete && (contentToDelete.name || contentToDelete.number || contentToDelete.tenant_fqdn) && (
<Modal
title={`Are you sure you want to delete the following ${props.name}?`}
loader={showModalLoader}
+387
View File
@@ -0,0 +1,387 @@
import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import Form from '../elements/Form';
import Input from '../elements/Input';
import Label from '../elements/Label';
import Select from '../elements/Select';
import InputGroup from '../elements/InputGroup';
import FormError from '../blocks/FormError';
import Loader from '../blocks/Loader';
import Button from '../elements/Button';
const MsTeamsTenantForm = props => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
// Refs
const refDomainName = useRef(null);
const refAccount = useRef(null);
// Form inputs
const [ domainName, setDomainName ] = useState('');
const [ account, setAccount ] = useState('');
const [ application, setApplication ] = useState('');
// Select list values
const [ accountValues, setAccountValues ] = useState('');
const [ applicationValues, setApplicationValues ] = useState('');
// Invalid form inputs
const [ invalidDomainName, setInvalidDomainName ] = useState(false);
const [ invalidAccount, setInvalidAccount ] = useState(false);
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
const [ tenants, setTenants ] = useState('');
const [ showLoader, setShowLoader ] = useState(true);
const [ errorMessage, setErrorMessage ] = useState('');
// Check if user is logged in
useEffect(() => {
const getAPIData = async () => {
try {
if (!localStorage.getItem('token')) {
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'You must log in to view that page.',
});
return;
}
const tenantsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/MicrosoftTeamsTenants',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const accountsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Accounts',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const applicationsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Applications',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const promises = [
tenantsPromise,
accountsPromise,
applicationsPromise,
];
if (props.type === 'add') {
promises.push(axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/ServiceProviders',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
}));
}
const promiseAllValues = await Promise.all(promises);
const tenants = promiseAllValues[0].data;
const accounts = promiseAllValues[1].data;
const applications = promiseAllValues[2].data;
setTenants(tenants);
setAccountValues(accounts);
setApplicationValues(applications);
if (props.type === 'add') {
const serviceProviders = promiseAllValues[3].data;
setServiceProviderSid(serviceProviders[0].service_provider_sid);
}
if (!accounts.length) {
dispatch({
type: 'ADD',
level: 'error',
message: 'You must create an account before you can create a Microsoft Teams Tenant.',
});
history.push('/internal/accounts');
return;
}
if (props.type === 'edit') {
const tenantData = tenants.filter(tenant => {
return tenant.ms_teams_tenant_sid === props.ms_teams_tenant_sid;
});
if (!tenantData.length) {
history.push('/internal/ms-teams-tenants');
dispatch({
type: 'ADD',
level: 'error',
message: 'That tenant does not exist.',
});
return;
}
setDomainName (( tenantData[0] && tenantData[0].tenant_fqdn ) || '');
setAccount (( tenantData[0] && tenantData[0].account_sid ) || '');
setApplication(( tenantData[0] && tenantData[0].application_sid) || '');
}
if (props.type === 'add' && accounts.length === 1) {
setAccount(accounts[0].account_sid);
}
setShowLoader(false);
} catch (err) {
if (err.response && err.response.status === 401) {
localStorage.removeItem('token');
sessionStorage.clear();
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'Your session has expired. Please log in and try again.',
});
} else {
dispatch({
type: 'ADD',
level: 'error',
message: (err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.',
});
console.log(err.response || err);
}
setShowLoader(false);
}
};
getAPIData();
// eslint-disable-next-line
}, []);
const handleSubmit = async e => {
let isMounted = true;
try {
setShowLoader(true);
e.preventDefault();
setErrorMessage('');
setInvalidDomainName(false);
setInvalidAccount(false);
let errorMessages = [];
let focusHasBeenSet = false;
if (!domainName) {
errorMessages.push('Please provide a domain name');
setInvalidDomainName(true);
if (!focusHasBeenSet) {
refDomainName.current.focus();
focusHasBeenSet = true;
}
}
// check if domain name is already in use
for (const tenant of tenants) {
if (tenant.ms_teams_tenant_sid === props.ms_teams_tenant_sid) {
continue;
}
if (tenant.tenant_fqdn === domainName) {
errorMessages.push(
'The domain name you have entered is already in use.'
);
setInvalidDomainName(true);
if (!focusHasBeenSet) {
refDomainName.current.focus();
focusHasBeenSet = true;
}
}
};
if (!account) {
errorMessages.push('Please select an account');
setInvalidAccount(true);
if (!focusHasBeenSet) {
refAccount.current.focus();
focusHasBeenSet = true;
}
}
if (errorMessages.length > 1) {
setErrorMessage(errorMessages);
return;
} else if (errorMessages.length === 1) {
setErrorMessage(errorMessages[0]);
return;
}
//=============================================================================
// Submit
//=============================================================================
const method = props.type === 'add'
? 'post'
: 'put';
const url = props.type === 'add'
? `/MicrosoftTeamsTenants`
: `/MicrosoftTeamsTenants/${props.ms_teams_tenant_sid}`;
const data = {
tenant_fqdn: domainName.trim(),
account_sid: account,
application_sid: application || null,
};
if (props.type === 'add') {
data.service_provider_sid = serviceProviderSid;
}
await axios({
method,
baseURL: process.env.REACT_APP_API_BASE_URL,
url,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
data
});
const dispatchMessage = props.type === 'add'
? 'Tenant created successfully'
: 'Tenant updated successfully';
dispatch({
type: 'ADD',
level: 'success',
message: dispatchMessage
});
isMounted = false;
history.push('/internal/ms-teams-tenants');
} catch (err) {
setErrorMessage(
(err.response && err.response.data && err.response.data.msg) ||
'Something went wrong, please try again.'
);
console.log(err.response || err);
} finally {
if (isMounted) {
setShowLoader(false);
}
}
};
return (
showLoader
? <Loader height={'258px'}/>
: <Form
large
onSubmit={handleSubmit}
>
<Label htmlFor="domainName">Domain Name</Label>
<Input
name="domainName"
id="domainName"
value={domainName}
onChange={e => setDomainName(e.target.value)}
placeholder="Tenant's fully qualified domain name"
invalid={invalidDomainName}
autoFocus
ref={refDomainName}
/>
<Label htmlFor="account">Account</Label>
<Select
name="account"
id="account"
value={account}
onChange={e => setAccount(e.target.value)}
invalid={invalidAccount}
ref={refAccount}
>
{(
(accountValues.length > 1) ||
(props.type === 'edit' && account !== accountValues[0].account_sid)
) && (
<option value="">-- Choose the account that this tenant should be associated with --</option>
)}
{accountValues.map(a => (
<option
key={a.account_sid}
value={a.account_sid}
>
{a.name}
</option>
))}
</Select>
<Label htmlFor="application">Application</Label>
<Select
name="application"
id="application"
value={application}
onChange={e => setApplication(e.target.value)}
>
<option value="">
{props.type === 'add'
? '-- OPTIONAL: Choose the application that this tenant should be associated with --'
: '-- NONE --'
}
</option>
{applicationValues.map(a => (
<option
key={a.application_sid}
value={a.application_sid}
>
{a.name}
</option>
))}
</Select>
{errorMessage && (
<FormError grid message={errorMessage} />
)}
<InputGroup flexEnd spaced>
{props.type === 'edit' && (
<Button
grid
gray
type="button"
onClick={() => {
history.push('/internal/ms-teams-tenants');
dispatch({
type: 'ADD',
level: 'info',
message: 'Changes canceled',
});
}}
>
Cancel
</Button>
)}
<Button
grid
fullWidth={props.type === 'add'}
>
{props.type === 'add'
? 'Add Microsoft Teams Tenant'
: 'Save'
}
</Button>
</InputGroup>
</Form>
);
};
export default MsTeamsTenantForm;
+457
View File
@@ -0,0 +1,457 @@
import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import { ShowMsTeamsDispatchContext } from '../../contexts/ShowMsTeamsContext';
import Form from '../elements/Form';
import Input from '../elements/Input';
import Label from '../elements/Label';
import Select from '../elements/Select';
import Checkbox from '../elements/Checkbox';
import InputGroup from '../elements/InputGroup';
import PasswordInput from '../elements/PasswordInput';
import FormError from '../blocks/FormError';
import Button from '../elements/Button';
import Loader from '../blocks/Loader';
const SettingsForm = () => {
const history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const refreshMsTeamsData = useContext(ShowMsTeamsDispatchContext);
// Refs
const refEnableMsTeams = useRef(null);
const refSbcDomainName = useRef(null);
const refSipDomain = useRef(null);
const refRegWebhook = useRef(null);
const refUser = useRef(null);
const refPassword = useRef(null);
// Form inputs
const [ enableMsTeams, setEnableMsTeams ] = useState(false);
const [ sbcDomainName, setSbcDomainName ] = useState('');
const [ sipDomain, setSipDomain ] = useState('');
const [ regWebhook, setRegWebhook ] = useState('');
const [ method, setMethod ] = useState('POST');
const [ user, setUser ] = useState('');
const [ password, setPassword ] = useState('');
// For when user has data in sbcDomainName and then taps the checkbox to disable MsTeams
const [ savedSbcDomainName, setSavedSbcDomainName ] = useState('');
// Invalid form inputs
const [ invalidEnableMsTeams, setInvalidEnableMsTeams ] = useState(false);
const [ invalidSbcDomainName, setInvalidSbcDomainName ] = useState(false);
const [ invalidSipDomain, setInvalidSipDomain ] = useState(false);
const [ invalidRegWebhook, setInvalidRegWebhook ] = useState(false);
const [ invalidUser, setInvalidUser ] = useState(false);
const [ invalidPassword, setInvalidPassword ] = useState(false);
const [ showLoader, setShowLoader ] = useState(true);
const [ errorMessage, setErrorMessage ] = useState('');
const [ showAuth, setShowAuth ] = useState(false);
const toggleAuth = () => setShowAuth(!showAuth);
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
useEffect(() => {
const getSettingsData = async () => {
try {
if (!localStorage.getItem('token')) {
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'You must log in to view that page.',
});
return;
}
const serviceProvidersResponse = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/ServiceProviders',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const sp = serviceProvidersResponse.data[0];
setServiceProviderSid(sp.service_provider_sid || '');
setEnableMsTeams(sp.ms_teams_fqdn ? true : false);
setSbcDomainName(sp.ms_teams_fqdn || '');
setSipDomain(sp.root_domain || '');
setRegWebhook((sp.registration_hook && sp.registration_hook.url) || '');
setMethod((sp.registration_hook && sp.registration_hook.method) || 'post');
setUser((sp.registration_hook && sp.registration_hook.username) || '');
setPassword((sp.registration_hook && sp.registration_hook.password) || '');
if (
(sp.registration_hook && sp.registration_hook.username) ||
(sp.registration_hook && sp.registration_hook.password)
) {
setShowAuth(true);
}
setShowLoader(false);
} catch (err) {
if (err.response && err.response.status === 401) {
localStorage.removeItem('token');
sessionStorage.clear();
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'Your session has expired. Please log in and try again.',
});
} else {
dispatch({
type: 'ADD',
level: 'error',
message: (err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.',
});
console.log(err.response || err);
}
setShowLoader(false);
}
};
getSettingsData();
// eslint-disable-next-line
}, []);
const toggleMsTeams = (e) => {
if (!e.target.checked && sbcDomainName) {
setSavedSbcDomainName(sbcDomainName);
setSbcDomainName('');
}
if (e.target.checked && savedSbcDomainName) {
setSbcDomainName(savedSbcDomainName);
setSavedSbcDomainName('');
}
setEnableMsTeams(e.target.checked);
};
const handleSubmit = async (e) => {
let isMounted = true;
try {
//=============================================================================
// reset
//=============================================================================
setShowLoader(true);
e.preventDefault();
setErrorMessage('');
setInvalidEnableMsTeams(false);
setInvalidSbcDomainName(false);
setInvalidSipDomain(false);
setInvalidRegWebhook(false);
setInvalidUser(false);
setInvalidPassword(false);
let errorMessages = [];
let focusHasBeenSet = false;
//=============================================================================
// data checks
//=============================================================================
if (enableMsTeams && !sbcDomainName) {
errorMessages.push(
'You must provide an SBC Domain Name in order to enable Microsoft Teams Direct Routing'
);
setInvalidSbcDomainName(true);
if (!focusHasBeenSet) {
refSbcDomainName.current.focus();
focusHasBeenSet = true;
}
}
if (!enableMsTeams && sbcDomainName) {
errorMessages.push(
'You must check "Enable Microsoft Teams Direct Routing" to enable this feature, or remove the SBC Domain Name provided'
);
setInvalidEnableMsTeams(true);
if (!focusHasBeenSet) {
refEnableMsTeams.current.focus();
focusHasBeenSet = true;
}
}
if (!sipDomain && (regWebhook || user || password)) {
errorMessages.push(
'You must provide a SIP Domain in order to provide a Registration Webhook'
);
setInvalidSipDomain(true);
if (!focusHasBeenSet) {
refSipDomain.current.focus();
focusHasBeenSet = true;
}
}
if (sipDomain && !regWebhook) {
errorMessages.push(
'You must provide a Registration Webhook when providing a SIP Domain'
);
setInvalidRegWebhook(true);
if (!focusHasBeenSet) {
refRegWebhook.current.focus();
focusHasBeenSet = true;
}
}
if ((user && !password) || (!user && password)) {
errorMessages.push('Username and password must be either both filled out or both empty.');
setInvalidUser(true);
setInvalidPassword(true);
if (!focusHasBeenSet) {
if (!user) {
refUser.current.focus();
} else {
refPassword.current.focus();
}
focusHasBeenSet = true;
}
}
if (errorMessages.length > 1) {
setErrorMessage(errorMessages);
return;
} else if (errorMessages.length === 1) {
setErrorMessage(errorMessages[0]);
return;
}
//=============================================================================
// submit data
//=============================================================================
const data = {
ms_teams_fqdn: sbcDomainName.trim() || null,
root_domain: sipDomain.trim() || null,
};
if (regWebhook) {
data.registration_hook = {
url: regWebhook.trim() || null,
method,
username: user.trim() || null,
password: password || null,
};
}
await axios({
method: 'put',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${serviceProviderSid}`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
data,
});
refreshMsTeamsData();
//=============================================================================
// redirect
//=============================================================================
isMounted = false;
history.push('/internal/accounts');
dispatch({
type: 'ADD',
level: 'success',
message: 'Settings updated'
});
} catch (err) {
if (err.response && err.response.status === 401) {
localStorage.removeItem('token');
sessionStorage.clear();
isMounted = false;
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'Your session has expired. Please log in and try again.',
});
} else {
setErrorMessage((err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.');
console.log(err.response || err);
}
} finally {
if (isMounted) {
setShowLoader(false);
}
}
};
return (
showLoader
? <Loader height="365px" />
: <Form
large
wideLabel
onSubmit={handleSubmit}
>
<div>{/* needed for CSS grid layout */}</div>
<Checkbox
noLeftMargin
id="enableMsTeams"
label="Enable Microsoft Teams Direct Routing"
checked={enableMsTeams}
onChange={toggleMsTeams}
invalid={invalidEnableMsTeams}
ref={refEnableMsTeams}
/>
<Label htmlFor="sbcDomainName">SBC Domain Name</Label>
<Input
name="sbcDomainName"
id="sbcDomainName"
value={sbcDomainName}
onChange={e => setSbcDomainName(e.target.value)}
placeholder="Fully qualified domain name used for Microsoft Teams"
invalid={invalidSbcDomainName}
autoFocus={enableMsTeams}
ref={refSbcDomainName}
disabled={!enableMsTeams}
title={(!enableMsTeams && "You must enable Microsoft Teams Direct Routing in order to provide an SBC Domain Name") || ""}
/>
<hr />
<Label htmlFor="sipDomain">Fallback SIP Domain</Label>
<Input
name="sipDomain"
id="sipDomain"
value={sipDomain}
onChange={e => setSipDomain(e.target.value)}
placeholder="Domain name that accounts will use as a fallback"
invalid={invalidSipDomain}
autoFocus={!enableMsTeams}
ref={refSipDomain}
/>
<Label htmlFor="regWebhook">Registration Webhook</Label>
<InputGroup>
<Input
name="regWebhook"
id="regWebhook"
value={regWebhook}
onChange={e => setRegWebhook(e.target.value)}
placeholder="URL for your web application that handles registrations"
invalid={invalidRegWebhook}
ref={refRegWebhook}
disabled={!sipDomain && !regWebhook && !user && !password}
title={(
!sipDomain &&
!regWebhook &&
!user &&
!password &&
"You must provide a SIP Domain in order to enter a registration webhook"
) || ""}
/>
<Label
middle
htmlFor="method"
>
Method
</Label>
<Select
name="method"
id="method"
value={method}
onChange={e => setMethod(e.target.value)}
disabled={!sipDomain && !regWebhook && !user && !password}
title={(
!sipDomain &&
!regWebhook &&
!user &&
!password &&
"You must provide a SIP Domain in order to enter a registration webhook"
) || ""}
>
<option value="POST">POST</option>
<option value="GET">GET</option>
</Select>
</InputGroup>
{showAuth ? (
<InputGroup>
<Label indented htmlFor="user">User</Label>
<Input
name="user"
id="user"
value={user || ''}
onChange={e => setUser(e.target.value)}
placeholder="Optional"
invalid={invalidUser}
ref={refUser}
disabled={!sipDomain && !regWebhook && !user && !password}
title={(
!sipDomain &&
!regWebhook &&
!user &&
!password &&
"You must provide a SIP Domain in order to enter a registration webhook"
) || ""}
/>
<Label htmlFor="password" middle>Password</Label>
<PasswordInput
allowShowPassword
name="password"
id="password"
password={password}
setPassword={setPassword}
setErrorMessage={setErrorMessage}
placeholder="Optional"
invalid={invalidPassword}
ref={refPassword}
disabled={!sipDomain && !regWebhook && !user && !password}
title={(
!sipDomain &&
!regWebhook &&
!user &&
!password &&
"You must provide a SIP Domain in order to enter a registration webhook"
) || ""}
/>
</InputGroup>
) : (
<Button
text
formLink
type="button"
onClick={toggleAuth}
>
Use HTTP Basic Authentication
</Button>
)}
{errorMessage && (
<FormError grid message={errorMessage} />
)}
<InputGroup flexEnd spaced>
<Button
grid
gray
type="button"
onClick={() => {
history.push('/internal/accounts');
dispatch({
type: 'ADD',
level: 'info',
message: 'Changes canceled',
});
}}
>
Cancel
</Button>
<Button grid>Save</Button>
</InputGroup>
</Form>
);
};
export default SettingsForm;
+19 -3
View File
@@ -88,7 +88,7 @@ const AccountsList = () => {
return;
}
// Check if any application or phone number uses this account
// Check if any application, phone number, or MS Teams tenant uses this account
const applicationsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
@@ -105,12 +105,22 @@ const AccountsList = () => {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const msTeamsTenantsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/MicrosoftTeamsTenants',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const promiseAllValues = await Promise.all([
applicationsPromise,
phoneNumbersPromise,
msTeamsTenantsPromise,
]);
const applications = promiseAllValues[0].data;
const phoneNumbers = promiseAllValues[1].data;
const applications = promiseAllValues[0].data;
const phoneNumbers = promiseAllValues[1].data;
const msTeamsTenants = promiseAllValues[2].data;
const accountApps = applications.filter(app => (
app.account_sid === accountToDelete.sid
@@ -118,6 +128,9 @@ const AccountsList = () => {
const accountPhoneNumbers = phoneNumbers.filter(p => (
p.account_sid === accountToDelete.sid
));
const accountMsTeamsTenants = msTeamsTenants.filter(tenant => (
tenant.account_sid === accountToDelete.sid
));
let errorMessages = [];
for (const app of accountApps) {
errorMessages.push(`Application: ${app.name}`);
@@ -125,6 +138,9 @@ const AccountsList = () => {
for (const num of accountPhoneNumbers) {
errorMessages.push(`Phone Number: ${num.number}`);
}
for (const tenant of accountMsTeamsTenants) {
errorMessages.push(`Microsoft Teams Tenant: ${tenant.tenant_fqdn}`);
}
if (errorMessages.length) {
return (
<React.Fragment>
@@ -107,8 +107,8 @@ const ApplicationsList = () => {
return;
}
// check if any account requires this application for SIP device calls
const accounts = await axios({
// check if any account or Microsoft Teams Tenant uses this application
const accountsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Accounts',
@@ -116,26 +116,51 @@ const ApplicationsList = () => {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const accountsRequiringThisApp = accounts.data.filter(acc => {
return acc.device_calling_application_sid === applicationToDelete.sid;
const msTeamsTenantsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/MicrosoftTeamsTenants',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const promiseAllValues = await Promise.all([
accountsPromise,
msTeamsTenantsPromise,
]);
if (accountsRequiringThisApp.length) {
const accountName = accountsRequiringThisApp[0].name;
const accounts = promiseAllValues[0].data;
const msTeamsTenants = promiseAllValues[1].data;
const appAccounts = accounts.filter(acc => (
acc.device_calling_application_sid === applicationToDelete.sid
));
const appMsTeamsTenants = msTeamsTenants.filter(tenant => (
tenant.application_sid === applicationToDelete.sid
));
let errorMessages = [];
for (const account of appAccounts) {
errorMessages.push(`Account: ${account.name}`);
}
for (const tenant of appMsTeamsTenants) {
errorMessages.push(`Microsoft Teams Tenant: ${tenant.tenant_fqdn}`);
}
if (errorMessages.length) {
return (
<React.Fragment>
<p style={{ margin: '0.5rem 0' }}>
This application cannot be deleted because the following
account uses it to receive SIP Device Calls:
This application cannot be deleted because it is in use by:
</p>
<ul style={{ margin: '0.5rem 0' }}>
<li>{accountName}</li>
{errorMessages.map((err, i) => (
<li key={i}>{err}</li>
))}
</ul>
</React.Fragment>
);
}
// Delete application
await axios({
method: 'delete',
baseURL: process.env.REACT_APP_API_BASE_URL,
@@ -0,0 +1,29 @@
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import InternalTemplate from '../../templates/InternalTemplate';
import MsTeamsTenantForm from '../../forms/MsTeamsTenantForm';
const MsTeamsTenantsAddEdit = () => {
let { ms_teams_tenant_sid } = useParams();
const pageTitle = ms_teams_tenant_sid ? 'Edit Microsoft Teams Tenant' : 'Add Microsoft Teams Tenant';
useEffect(() => {
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
});
return (
<InternalTemplate
type="form"
title={pageTitle}
breadcrumbs={[
{ name: 'Microsoft Teams Tenants', url: '/internal/ms-teams-tenants' },
{ name: pageTitle },
]}
>
<MsTeamsTenantForm
type={ms_teams_tenant_sid ? 'edit' : 'add'}
ms_teams_tenant_sid={ms_teams_tenant_sid}
/>
</InternalTemplate>
);
};
export default MsTeamsTenantsAddEdit;
@@ -0,0 +1,166 @@
import React, { useEffect, useContext } from 'react';
import axios from 'axios';
import { useHistory } from 'react-router-dom';
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
import InternalTemplate from '../../templates/InternalTemplate';
import TableContent from '../../blocks/TableContent.js';
const MsTeamsTenantsList = () => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
useEffect(() => {
document.title = `Microsoft Teams Tenants | Jambonz | Open Source CPAAS`;
});
//=============================================================================
// Get data
//=============================================================================
const getMsTeamsTenants = async () => {
try {
if (!localStorage.getItem('token')) {
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'You must log in to view that page.',
});
return;
}
const msTeamsTenantsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/MicrosoftTeamsTenants',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const accountsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Accounts',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const applicationsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Applications',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const promiseAllValues = await Promise.all([
msTeamsTenantsPromise,
accountsPromise,
applicationsPromise,
]);
const msTeamsTenants = promiseAllValues[0].data;
const accounts = promiseAllValues[1].data;
const applications = promiseAllValues[2].data;
const combinedData = msTeamsTenants.map(team => {
const account = accounts.filter(a => a.account_sid === team.account_sid );
const application = applications.filter(a => a.application_sid === team.application_sid);
return {
sid: team.ms_teams_tenant_sid,
tenant_fqdn: team.tenant_fqdn,
account: account[0] && account[0].name,
application: application[0] && application[0].name,
};
});
return(combinedData);
} catch (err) {
if (err.response && err.response.status === 401) {
localStorage.removeItem('token');
sessionStorage.clear();
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'Your session has expired. Please log in and try again.',
});
} else {
dispatch({
type: 'ADD',
level: 'error',
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get Microsoft Teams Tenant data',
});
console.log(err.response || err);
}
}
};
//=============================================================================
// Delete Microsoft Teams Tenant
//=============================================================================
const formatTenantsToDelete = team => {
return [
{ name: 'Domain Name:', content: team.tenant_fqdn || '[none]' },
{ name: 'Account:', content: team.account || '[none]' },
{ name: 'Application:', content: team.application || '[none]' },
];
};
const deleteTenant = async tenant => {
try {
if (!localStorage.getItem('token')) {
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'You must log in to view that page.',
});
return;
}
await axios({
method: 'delete',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/MicrosoftTeamsTenants/${tenant.sid}`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
return 'success';
} catch (err) {
if (err.response && err.response.status === 401) {
localStorage.removeItem('token');
sessionStorage.clear();
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'Your session has expired. Please log in and try again.',
});
} else {
console.log(err.response || err);
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete Microsoft Teams Tenant');
}
}
};
//=============================================================================
// Render
//=============================================================================
return (
<InternalTemplate
title="Microsoft Teams Tenants"
addButtonText="Add a Microsoft Teams Tenant"
addButtonLink="/internal/ms-teams-tenants/add"
>
<TableContent
name="tenant"
urlParam="ms-teams-tenants"
getContent={getMsTeamsTenants}
columns={[
{ header: 'Domain Name', key: 'tenant_fqdn' },
{ header: 'Account', key: 'account' },
{ header: 'Application', key: 'application' },
]}
formatContentToDelete={formatTenantsToDelete}
deleteContent={deleteTenant}
/>
</InternalTemplate>
);
};
export default MsTeamsTenantsList;
+20
View File
@@ -0,0 +1,20 @@
import React, { useEffect } from 'react';
import InternalTemplate from '../../templates/InternalTemplate';
import SettingsForm from '../../forms/SettingsForm';
const Settings = () => {
const pageTitle = 'Settings';
useEffect(() => {
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
});
return (
<InternalTemplate
type="form"
title={pageTitle}
>
<SettingsForm />
</InternalTemplate>
);
};
export default Settings;
+22 -115
View File
@@ -1,78 +1,11 @@
import React, { useEffect, useContext } from 'react';
import { NavLink, useHistory } from 'react-router-dom';
import { ModalStateContext } from '../../contexts/ModalContext';
import { useHistory } from 'react-router-dom';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import styled from 'styled-components/macro';
import H1 from '../elements/H1';
import { ReactComponent as AccountsIcon } from '../../images/AccountsIcon.svg';
import { ReactComponent as ApplicationsIcon } from '../../images/ApplicationsIcon.svg';
import { ReactComponent as SipTrunksIcon } from '../../images/SipTrunksIcon.svg';
import { ReactComponent as PhoneNumbersIcon } from '../../images/PhoneNumbersIcon.svg';
import AddButton from '../elements/AddButton';
import Breadcrumbs from '../blocks/Breadcrumbs';
const PageContainer = styled.div`
display: flex;
`;
const SideMenu = styled.div`
width: 15rem;
flex-shrink: 0;
height: calc(100vh - 4rem);
overflow: auto;
background: #FFF;
padding-top: 3.25rem;
`;
const activeClassName = 'nav-item-active';
const StyledNavLink = styled(NavLink).attrs({ activeClassName })`
height: 2.75rem;
margin-bottom: 1rem;
display: flex;
align-items: stretch;
font-weight: 500;
text-decoration: none;
color: #565656;
fill: #565656;
&.${activeClassName} {
box-shadow: inset 3px 0 0 0 #D91C5C;
color: #D91C5C;
fill: #D91C5C;
}
&:focus {
outline: 0;
box-shadow: inset 0 0 0 3px #D91C5C;
}
&:hover {
background: RGBA(217, 28, 92, 0.1);
color: #C0134D;
fill: #C0134D;
}
&.${activeClassName}:hover {
color: #D91C5C;
fill: #D91C5C;
}
`;
const IconContainer = styled.span`
width: 3rem;
display: flex;
justify-content: center;
align-items: center;
outline: 0;
`;
const MenuText = styled.span`
display: flex;
flex-grow: 1;
align-items: center;
outline: 0;
`;
const PageMain = styled.main`
height: calc(100vh - 4rem);
width: calc(100% - 15rem);
@@ -101,24 +34,6 @@ const ContentContainer = styled.div`
}
`;
const MenuLink = props => {
const modalOpen = useContext(ModalStateContext);
return (
<StyledNavLink
to={props.to}
activeClassName={activeClassName}
tabIndex={modalOpen ? '-1' : ''}
>
<IconContainer tabIndex="-1">
{props.icon}
</IconContainer>
<MenuText tabIndex="-1">
{props.name}
</MenuText>
</StyledNavLink>
);
};
const InternalTemplate = props => {
const history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
@@ -135,35 +50,27 @@ const InternalTemplate = props => {
}, [history, dispatch]);
return (
<PageContainer>
<SideMenu>
<MenuLink to="/internal/accounts" name="Accounts" icon={<AccountsIcon />} />
<MenuLink to="/internal/applications" name="Applications" icon={<ApplicationsIcon />} />
<MenuLink to="/internal/sip-trunks" name="SIP Trunks" icon={<SipTrunksIcon />} />
<MenuLink to="/internal/phone-numbers" name="Phone Numbers" icon={<PhoneNumbersIcon />} />
</SideMenu>
<PageMain>
{props.breadcrumbs && (
<Breadcrumbs breadcrumbs={props.breadcrumbs} />
)}
<H1>{props.title}</H1>
{props.addButtonText && (
<AddButton
addButtonText={props.addButtonText}
to={props.addButtonLink}
/>
)}
{typeof props.subtitle === 'object'
? props.subtitle
: <P>{props.subtitle}</P>
}
<ContentContainer
type={props.type}
>
{props.children}
</ContentContainer>
</PageMain>
</PageContainer>
<PageMain>
{props.breadcrumbs && (
<Breadcrumbs breadcrumbs={props.breadcrumbs} />
)}
<H1>{props.title}</H1>
{props.addButtonText && (
<AddButton
addButtonText={props.addButtonText}
to={props.addButtonLink}
/>
)}
{typeof props.subtitle === 'object'
? props.subtitle
: <P>{props.subtitle}</P>
}
<ContentContainer
type={props.type}
>
{props.children}
</ContentContainer>
</PageMain>
);
};
+46
View File
@@ -0,0 +1,46 @@
import React, { useState, createContext, useContext } from 'react';
import axios from 'axios';
import { NotificationDispatchContext } from './NotificationContext';
export const ShowMsTeamsStateContext = createContext();
export const ShowMsTeamsDispatchContext = createContext();
export function ShowMsTeamsProvider(props) {
const dispatch = useContext(NotificationDispatchContext);
const [ showMsTeams, setShowMsTeams ] = useState(false);
const getMsTeamsData = async () => {
try {
const serviceProvidersResponse = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/ServiceProviders',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
if (serviceProvidersResponse.data[0].ms_teams_fqdn) {
setShowMsTeams(true);
} else {
setShowMsTeams(false);
}
} catch (err) {
dispatch({
type: 'ADD',
level: 'error',
message: (err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.',
});
console.log(err.response || err);
}
};
return (
<ShowMsTeamsStateContext.Provider value={showMsTeams}>
<ShowMsTeamsDispatchContext.Provider value={getMsTeamsData}>
{props.children}
</ShowMsTeamsDispatchContext.Provider>
</ShowMsTeamsStateContext.Provider>
);
};
+6
View File
@@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<rect x="11" y="11" width="9" height="9"/>
<rect y="11" width="9" height="9"/>
<rect x="11" width="9" height="9"/>
<rect width="9" height="9"/>
</svg>

After

Width:  |  Height:  |  Size: 235 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="21" height="20" viewBox="0 0 21 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.204789 7.87962L2.45962 7.51441C2.83787 6.26593 3.49894 5.14037 4.36828 4.21229L3.557 2.07657C4.59273 1.16294 5.81005 0.450407 7.14789 2.97165e-05L8.59238 1.77115C9.20524 1.62883 9.84384 1.55359 10.5 1.55359C11.1561 1.55359 11.7947 1.62882 12.4075 1.77112L13.852 0C15.1899 0.450363 16.4072 1.16289 17.4429 2.07649L16.6316 4.21221C17.501 5.14031 18.1621 6.26591 18.5404 7.51443L20.7952 7.87964C20.9295 8.55009 21 9.24361 21 9.95359C21 10.6635 20.9295 11.3571 20.7952 12.0275L18.5404 12.3927C18.1622 13.6412 17.501 14.7668 16.6317 15.695L17.4429 17.8307C16.4072 18.7443 15.1899 19.4568 13.8521 19.9072L12.4076 18.1361C11.7947 18.2784 11.1561 18.3536 10.5 18.3536C9.84382 18.3536 9.20521 18.2784 8.59233 18.136L7.14786 19.9071C5.81003 19.4568 4.59271 18.7442 3.55699 17.8306L4.36827 15.6949C3.49892 14.7668 2.83784 13.6412 2.4596 12.3927L0.204778 12.0275C0.0704634 11.3571 0 10.6636 0 9.95359C0 9.2436 0.070467 8.55007 0.204789 7.87962ZM10.5 14.1536C12.8196 14.1536 14.7 12.2732 14.7 9.95359C14.7 7.634 12.8196 5.75359 10.5 5.75359C8.1804 5.75359 6.3 7.634 6.3 9.95359C6.3 12.2732 8.1804 14.1536 10.5 14.1536Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+4 -1
View File
@@ -3,12 +3,15 @@ import ReactDOM from 'react-dom';
import './index.css';
import { NotificationProvider } from './contexts/NotificationContext';
import { ModalProvider } from './contexts/ModalContext';
import { ShowMsTeamsProvider } from './contexts/ShowMsTeamsContext';
import App from './App';
ReactDOM.render(
<NotificationProvider>
<ModalProvider>
<App />
<ShowMsTeamsProvider>
<App />
</ShowMsTeamsProvider>
</ModalProvider>
</NotificationProvider>,
document.getElementById('root')