From 54dfd1b677672dacdb1459e0eae91dd3e9860347 Mon Sep 17 00:00:00 2001 From: user Date: Fri, 29 May 2020 10:33:42 -0700 Subject: [PATCH 1/7] Implement settings page --- src/App.js | 3 + src/components/forms/SettingsForm.js | 453 +++++++++++++++++++ src/components/pages/internal/Settings.js | 20 + src/components/templates/InternalTemplate.js | 2 + src/images/SettingsIcon.svg | 3 + 5 files changed, 481 insertions(+) create mode 100644 src/components/forms/SettingsForm.js create mode 100644 src/components/pages/internal/Settings.js create mode 100644 src/images/SettingsIcon.svg diff --git a/src/App.js b/src/App.js index b2fcdaf..282ac3a 100644 --- a/src/App.js +++ b/src/App.js @@ -16,6 +16,7 @@ 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 Settings from './components/pages/internal/Settings'; import InvalidRoute from './components/pages/InvalidRoute'; import Notification from './components/blocks/Notification'; @@ -68,6 +69,8 @@ function App() { + + diff --git a/src/components/forms/SettingsForm.js b/src/components/forms/SettingsForm.js new file mode 100644 index 0000000..4df49f0 --- /dev/null +++ b/src/components/forms/SettingsForm.js @@ -0,0 +1,453 @@ +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 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); + + // 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, + }); + + //============================================================================= + // 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 + ? + :
+
{/* needed for CSS grid layout */}
+ + + + 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") || ""} + /> + +
+ + + setSipDomain(e.target.value)} + placeholder="Domain name that accounts will use as a fallback" + invalid={invalidSipDomain} + autoFocus={!enableMsTeams} + ref={refSipDomain} + /> + + + + 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" + ) || ""} + /> + + + + + + {showAuth ? ( + + + 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" + ) || ""} + /> + + + + ) : ( + + )} + + {errorMessage && ( + + )} + + + + + + + + ); +}; + +export default SettingsForm; diff --git a/src/components/pages/internal/Settings.js b/src/components/pages/internal/Settings.js new file mode 100644 index 0000000..756767f --- /dev/null +++ b/src/components/pages/internal/Settings.js @@ -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 ( + + + + ); +}; + +export default Settings; diff --git a/src/components/templates/InternalTemplate.js b/src/components/templates/InternalTemplate.js index b52562b..da6d5bc 100644 --- a/src/components/templates/InternalTemplate.js +++ b/src/components/templates/InternalTemplate.js @@ -8,6 +8,7 @@ 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 SettingsIcon } from '../../images/SettingsIcon.svg'; import AddButton from '../elements/AddButton'; import Breadcrumbs from '../blocks/Breadcrumbs'; @@ -141,6 +142,7 @@ const InternalTemplate = props => { } /> } /> } /> + } /> {props.breadcrumbs && ( diff --git a/src/images/SettingsIcon.svg b/src/images/SettingsIcon.svg new file mode 100644 index 0000000..c564236 --- /dev/null +++ b/src/images/SettingsIcon.svg @@ -0,0 +1,3 @@ + + + From 651f308cdbbabe04be98ce36effc142a646b9ec1 Mon Sep 17 00:00:00 2001 From: user Date: Fri, 29 May 2020 16:27:37 -0700 Subject: [PATCH 2/7] Refactor side menu out of internal template into a separate file --- src/App.js | 70 +++++----- src/components/blocks/SideMenu.js | 99 +++++++++++++ src/components/templates/InternalTemplate.js | 139 +++---------------- 3 files changed, 159 insertions(+), 149 deletions(-) create mode 100644 src/components/blocks/SideMenu.js diff --git a/src/App.js b/src/App.js index 282ac3a..a6844fc 100644 --- a/src/App.js +++ b/src/App.js @@ -21,6 +21,7 @@ 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); @@ -36,41 +37,46 @@ function App() { - - - - + +
+ + + + + - - + + + + + + + + + + + + + + + + + +
- - - - - - - - - - - - - - diff --git a/src/components/blocks/SideMenu.js b/src/components/blocks/SideMenu.js new file mode 100644 index 0000000..b20ed54 --- /dev/null +++ b/src/components/blocks/SideMenu.js @@ -0,0 +1,99 @@ +import React, { useContext } from 'react'; +import { NavLink } from 'react-router-dom'; +import styled from 'styled-components/macro'; +import { ModalStateContext } from '../../contexts/ModalContext'; +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 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 ( + + + {props.icon} + + + {props.name} + + + ); +}; + +const SideMenu = () => { + return ( + + } /> + } /> + } /> + } /> + } /> + + ); +}; + +export default SideMenu; diff --git a/src/components/templates/InternalTemplate.js b/src/components/templates/InternalTemplate.js index da6d5bc..f7a3581 100644 --- a/src/components/templates/InternalTemplate.js +++ b/src/components/templates/InternalTemplate.js @@ -1,79 +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 { ReactComponent as SettingsIcon } from '../../images/SettingsIcon.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); @@ -102,24 +34,6 @@ const ContentContainer = styled.div` } `; -const MenuLink = props => { - const modalOpen = useContext(ModalStateContext); - return ( - - - {props.icon} - - - {props.name} - - - ); -}; - const InternalTemplate = props => { const history = useHistory(); const dispatch = useContext(NotificationDispatchContext); @@ -136,36 +50,27 @@ const InternalTemplate = props => { }, [history, dispatch]); return ( - - - } /> - } /> - } /> - } /> - } /> - - - {props.breadcrumbs && ( - - )} -

{props.title}

- {props.addButtonText && ( - - )} - {typeof props.subtitle === 'object' - ? props.subtitle - :

{props.subtitle}

- } - - {props.children} - -
-
+ + {props.breadcrumbs && ( + + )} +

{props.title}

+ {props.addButtonText && ( + + )} + {typeof props.subtitle === 'object' + ? props.subtitle + :

{props.subtitle}

+ } + + {props.children} + +
); }; From 9d6d8bfb5d1a9c217fe961372909ad3743a0afd0 Mon Sep 17 00:00:00 2001 From: user Date: Fri, 29 May 2020 16:43:58 -0700 Subject: [PATCH 3/7] Implement Microsoft Teams menu item with context for whether or not to show it --- src/components/blocks/SideMenu.js | 13 +++++++- src/components/forms/SettingsForm.js | 4 +++ src/contexts/ShowMsTeamsContext.js | 46 ++++++++++++++++++++++++++++ src/images/MsTeamsIcon.svg | 6 ++++ src/index.js | 5 ++- 5 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/contexts/ShowMsTeamsContext.js create mode 100644 src/images/MsTeamsIcon.svg diff --git a/src/components/blocks/SideMenu.js b/src/components/blocks/SideMenu.js index b20ed54..14de95d 100644 --- a/src/components/blocks/SideMenu.js +++ b/src/components/blocks/SideMenu.js @@ -1,11 +1,13 @@ -import React, { useContext } from 'react'; +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` @@ -85,12 +87,21 @@ const MenuLink = props => { }; const SideMenu = () => { + const showMsTeams = useContext(ShowMsTeamsStateContext); + const getMsTeamsData = useContext(ShowMsTeamsDispatchContext); + useEffect(() => { + getMsTeamsData(); + // eslint-disable-next-line + }, []); return ( } /> } /> } /> } /> + {showMsTeams && ( + } /> + )} } /> ); diff --git a/src/components/forms/SettingsForm.js b/src/components/forms/SettingsForm.js index 4df49f0..1c3f89f 100644 --- a/src/components/forms/SettingsForm.js +++ b/src/components/forms/SettingsForm.js @@ -2,6 +2,7 @@ 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'; @@ -16,6 +17,7 @@ import Loader from '../blocks/Loader'; const SettingsForm = () => { const history = useHistory(); const dispatch = useContext(NotificationDispatchContext); + const refreshMsTeamsData = useContext(ShowMsTeamsDispatchContext); // Refs const refEnableMsTeams = useRef(null); @@ -246,6 +248,8 @@ const SettingsForm = () => { data, }); + refreshMsTeamsData(); + //============================================================================= // redirect //============================================================================= diff --git a/src/contexts/ShowMsTeamsContext.js b/src/contexts/ShowMsTeamsContext.js new file mode 100644 index 0000000..8ad7985 --- /dev/null +++ b/src/contexts/ShowMsTeamsContext.js @@ -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 ( + + + {props.children} + + + ); +}; diff --git a/src/images/MsTeamsIcon.svg b/src/images/MsTeamsIcon.svg new file mode 100644 index 0000000..e986291 --- /dev/null +++ b/src/images/MsTeamsIcon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/index.js b/src/index.js index 86610d9..5e56809 100644 --- a/src/index.js +++ b/src/index.js @@ -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( - + + + , document.getElementById('root') From cc6e7bc6cab573936111d5e316b8caa6f367cc96 Mon Sep 17 00:00:00 2001 From: user Date: Fri, 29 May 2020 17:22:21 -0700 Subject: [PATCH 4/7] Implement Microsoft Teams Tenants list view --- src/App.js | 2 + src/components/blocks/TableContent.js | 2 +- .../pages/internal/MsTeamsTenantsList.js | 166 ++++++++++++++++++ 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/components/pages/internal/MsTeamsTenantsList.js diff --git a/src/App.js b/src/App.js index a6844fc..42f05f1 100644 --- a/src/App.js +++ b/src/App.js @@ -12,6 +12,7 @@ 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'; @@ -44,6 +45,7 @@ function App() { + { //============================================================================= return ( - {contentToDelete && (contentToDelete.name || contentToDelete.number) && ( + {contentToDelete && (contentToDelete.name || contentToDelete.number || contentToDelete.fqdn) && ( { + 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, + 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: 'FQDN:', content: team.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 ( + + + + ); +}; + +export default MsTeamsTenantsList; From f360b5bef551ceaac7da1c15f2baff56e8c26d9e Mon Sep 17 00:00:00 2001 From: user Date: Fri, 29 May 2020 18:43:48 -0700 Subject: [PATCH 5/7] Rename FQDN in MS Teams Tenants table view --- src/components/blocks/TableContent.js | 2 +- src/components/pages/internal/MsTeamsTenantsList.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/blocks/TableContent.js b/src/components/blocks/TableContent.js index 4948364..a516c06 100644 --- a/src/components/blocks/TableContent.js +++ b/src/components/blocks/TableContent.js @@ -158,7 +158,7 @@ const TableContent = props => { //============================================================================= return ( - {contentToDelete && (contentToDelete.name || contentToDelete.number || contentToDelete.fqdn) && ( + {contentToDelete && (contentToDelete.name || contentToDelete.number || contentToDelete.tenant_fqdn) && ( { const application = applications.filter(a => a.application_sid === team.application_sid); return { sid: team.ms_teams_tenant_sid, - fqdn: team.tenant_fqdn, + tenant_fqdn: team.tenant_fqdn, account: account[0] && account[0].name, application: application[0] && application[0].name, }; @@ -96,7 +96,7 @@ const MsTeamsTenantsList = () => { //============================================================================= const formatTenantsToDelete = team => { return [ - { name: 'FQDN:', content: team.fqdn || '[none]' }, + { name: 'Domain Name:', content: team.tenant_fqdn || '[none]' }, { name: 'Account:', content: team.account || '[none]' }, { name: 'Application:', content: team.application || '[none]' }, ]; @@ -152,7 +152,7 @@ const MsTeamsTenantsList = () => { urlParam="ms-teams-tenants" getContent={getMsTeamsTenants} columns={[ - { header: 'FQDN', key: 'fqdn' }, + { header: 'Domain Name', key: 'tenant_fqdn' }, { header: 'Account', key: 'account' }, { header: 'Application', key: 'application' }, ]} From e4124b7fe2eb649d158911ee3d1a3a6bbcedf23f Mon Sep 17 00:00:00 2001 From: user Date: Fri, 29 May 2020 18:46:06 -0700 Subject: [PATCH 6/7] Implement Microsoft Teams Tenants add and edit forms --- src/App.js | 8 + src/components/forms/MsTeamsTenantForm.js | 387 ++++++++++++++++++ .../pages/internal/MsTeamsTenantsAddEdit.js | 29 ++ 3 files changed, 424 insertions(+) create mode 100644 src/components/forms/MsTeamsTenantForm.js create mode 100644 src/components/pages/internal/MsTeamsTenantsAddEdit.js diff --git a/src/App.js b/src/App.js index 42f05f1..530379b 100644 --- a/src/App.js +++ b/src/App.js @@ -17,6 +17,7 @@ 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'; @@ -75,6 +76,13 @@ function App() { + + + + diff --git a/src/components/forms/MsTeamsTenantForm.js b/src/components/forms/MsTeamsTenantForm.js new file mode 100644 index 0000000..22bceb3 --- /dev/null +++ b/src/components/forms/MsTeamsTenantForm.js @@ -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 + ? + :
+ + setDomainName(e.target.value)} + placeholder="Tenant's fully qualified domain name" + invalid={invalidDomainName} + autoFocus + ref={refDomainName} + /> + + + + + + + + {errorMessage && ( + + )} + + + {props.type === 'edit' && ( + + )} + + + + + ); +}; + +export default MsTeamsTenantForm; diff --git a/src/components/pages/internal/MsTeamsTenantsAddEdit.js b/src/components/pages/internal/MsTeamsTenantsAddEdit.js new file mode 100644 index 0000000..6a2ac12 --- /dev/null +++ b/src/components/pages/internal/MsTeamsTenantsAddEdit.js @@ -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 ( + + + + ); +}; + +export default MsTeamsTenantsAddEdit; From 4fc3add3280d086bcbbcdbe5a510591a74d13c7b Mon Sep 17 00:00:00 2001 From: user Date: Fri, 29 May 2020 18:48:32 -0700 Subject: [PATCH 7/7] Check if account or app is in use by a Microsoft Teams tenant before deleting --- src/components/pages/internal/AccountsList.js | 22 +++++++-- .../pages/internal/ApplicationsList.js | 45 ++++++++++++++----- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/src/components/pages/internal/AccountsList.js b/src/components/pages/internal/AccountsList.js index d36a6c2..cbf914d 100644 --- a/src/components/pages/internal/AccountsList.js +++ b/src/components/pages/internal/AccountsList.js @@ -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 ( diff --git a/src/components/pages/internal/ApplicationsList.js b/src/components/pages/internal/ApplicationsList.js index 5f51247..81ec92a 100644 --- a/src/components/pages/internal/ApplicationsList.js +++ b/src/components/pages/internal/ApplicationsList.js @@ -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 (

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

    -
  • {accountName}
  • + {errorMessages.map((err, i) => ( +
  • {err}
  • + ))}
); } + // Delete application await axios({ method: 'delete', baseURL: process.env.REACT_APP_API_BASE_URL,