Implement settings page

This commit is contained in:
user
2020-05-29 10:33:42 -07:00
parent 2c031f093d
commit 54dfd1b677
5 changed files with 481 additions and 0 deletions
+3
View File
@@ -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() {
<PhoneNumbersAddEdit />
</Route>
<Route exact path="/internal/settings"><Settings /></Route>
<Route><InvalidRoute /></Route>
</Switch>
</Router>
+453
View File
@@ -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
? <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;
+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;
@@ -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 => {
<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 />} />
<MenuLink to="/internal/settings" name="Settings" icon={<SettingsIcon />} />
</SideMenu>
<PageMain>
{props.breadcrumbs && (
+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