Compare commits

...

24 Commits

Author SHA1 Message Date
Dave Horton
b5f2e5fc25 bump version 2022-02-09 15:43:14 -05:00
Dave Horton
6390cc6b81 add missing Azure regions 2022-02-03 08:59:47 -05:00
Dave Horton
d7db92f0c7 update version to 0.7.2 2022-01-31 07:32:10 -05:00
Dave Horton
f5201d2d69 Feature/wellsaid tts (#38)
* initial changes to support WellSaid TTS

* disable stt choice for WellSaid since they dont provide
2022-01-27 08:07:22 -05:00
akirilyuk
128ca045b0 update depenecies and fix security vulnerabilities (#37)
Co-authored-by: akirilyuk <a.kirilyuk@cognigy.com>
2022-01-14 08:00:35 -05:00
Dave Horton
3403996946 bump version 2021-12-21 09:43:00 -05:00
Dave Horton
87dbb461e0 added docker publish 2021-12-13 14:18:43 -05:00
Dave Horton
9bce9c5510 version bump 2021-12-13 09:55:03 -05:00
Brandon Lee Kitajchuk
2db5f26dbf Subspace (#35)
* pushing up what ive got from laptop

* beginnings of a UI for setting up subspace on a jambonz account

* enable the env flag and move content to right place

* changes to support subspace (thanks to nimbleape)

* fix column names

* Implement SIP realm selection for Subspace API calls

* Hook up Subspace disable method

* Finish up Subspace API handling

Co-authored-by: Dan Jenkins <dan@nimblea.pe>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2021-12-06 17:58:42 -05:00
Dave Horton
bfc7cc971c version bump 2021-12-02 19:34:28 -05:00
Dave Horton
35f353c905 bugfix: azure tts needs to be referenced by ShortName (#33) 2021-11-19 14:31:30 -05:00
Brandon Lee Kitajchuk
922d664bf8 Add Microsoft vendor to Jambonz Webapp (#32)
* Add Microsoft vendor to Speech Form

Add Microsoft vendor to Application Form

Clean up UI for Speech and Application forms

* Remove Sbcs from SpeechServiceAddEdit Form
2021-11-17 20:49:31 -05:00
Dave Horton
ff4d6b6e11 version bump 2021-11-03 13:53:11 -04:00
Dave Horton
70387ff4f1 bump version 2021-10-21 13:09:07 -04:00
Dave Horton
7a4c583345 bump version 2021-10-21 13:01:30 -04:00
Brandon Lee Kitajchuk
d54fbc4782 Update IP whitelist tooltips for carrier form (#29) 2021-10-21 11:41:19 -04:00
Brandon Lee Kitajchuk
14dd1319d9 SMS for Carrier Form (#28) 2021-10-17 12:29:26 -04:00
Brandon Lee Kitajchuk
8538d40696 Add account sid selector to speech form (#24)
* Add account sid selector to speech form

* Add account selector to speech service list view

* Add account selector to carrier list view

* Remove client-side check for existing speech services
2021-09-01 12:47:32 -04:00
Brandon Lee Kitajchuk
eda1fa0dc4 Chore/carrier apps accounts (#23)
* Scope accounts and applications to current service provider for Phone Numbers

* Scope accounts to current service provider when adding or editing an Application

* Implement account and application logic for add or edit Carrier form

* Implement delete action for service providers
2021-08-28 09:20:49 -04:00
Brandon Lee Kitajchuk
0174315a68 Only show accounts for current service provider when adding a new application (#21) 2021-08-26 12:25:26 -04:00
Brandon Lee Kitajchuk
b86bf0c403 User service provider context when adding an account (#20) 2021-08-26 12:21:32 -04:00
Brandon Lee Kitajchuk
3fc1c800ac Add queue event webhook to accounts list (#19) 2021-08-25 19:30:52 -04:00
Brandon Lee Kitajchuk
ee4483288d Adding pcap file download button to RecentCalls view (#17) 2021-08-01 21:02:01 -04:00
Dave Horton
d3f1dbf332 LICENSE 2021-07-21 12:37:41 -04:00
30 changed files with 27945 additions and 10848 deletions

2
.env
View File

@@ -1 +1 @@
REACT_APP_API_BASE_URL=http://[ip]:[port]/v1
REACT_APP_API_BASE_URL=http://127.0.0.1:3002/v1

View File

@@ -2,15 +2,12 @@
"extends": "react-app",
"rules": {
"linebreak-style": [
"error",
"unix"
"error",
"unix"
],
"semi": [
"error",
"always"
],
"no-trailing-spaces": [
"error"
"error",
"always"
]
}
}
}

51
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: Docker
on:
push:
# Publish `main` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags:
- v*
env:
IMAGE_NAME: webapp
jobs:
push:
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v2
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Drachtio Communications Services, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

31805
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
{
"name": "jambonz-cpaas-ui",
"version": "1.0.0",
"private": true,
"name": "jambonz-webapp",
"version": "v0.7.3",
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^7.2.1",
"antd": "^4.15.4",
"axios": "^0.21.1",
@@ -13,8 +12,8 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.1",
"serve": "^11.3.0",
"react-scripts": "^5.0.0",
"serve": "^13.0.2",
"styled-components": "^5.0.1"
},
"scripts": {

View File

@@ -3,7 +3,7 @@ import axios from 'axios';
import styled from 'styled-components';
import { useHistory } from 'react-router-dom';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
// import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
const Container = styled.div`
margin-top: 0.25rem;
@@ -21,7 +21,7 @@ const Container = styled.div`
const Sbcs = props => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const currentServiceProvider = useContext(ServiceProviderValueContext);
// const currentServiceProvider = useContext(ServiceProviderValueContext);
const [ sbcs, setSbcs ] = useState('');
useEffect(() => {
const getAPIData = async () => {
@@ -38,7 +38,8 @@ const Sbcs = props => {
const sbcResults = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/Sbcs?service_provider_sid=${currentServiceProvider}`,
// url: `/Sbcs?service_provider_sid=${currentServiceProvider}`,
url: '/Sbcs',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},

View File

@@ -220,7 +220,8 @@ const TableContent = props => {
contentToDelete.name ||
contentToDelete.number ||
contentToDelete.tenant_fqdn ||
contentToDelete.token
contentToDelete.token ||
contentToDelete.vendor
) && (
<Modal
title={`Are you sure you want to delete the following ${props.name}?`}

View File

@@ -111,6 +111,7 @@ const Checkbox = (props, ref) => {
name={props.id}
type="checkbox"
checked={props.checked}
disabled={props.disabled}
onChange={props.onChange}
value={props.value}
ref={inputRef}

View File

@@ -2,19 +2,8 @@ import React, { useContext } from 'react';
import styled from 'styled-components/macro';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import Button from './Button';
import Span from './Span';
const Span = styled.span`
text-align: left;
${props => props.hasBorder ? `
height: 2.25rem;
display: flex;
align-items: center;
width: 100%;
padding: 0 1rem;
border: 1px solid #B6B6B6;
border-radius: 0.125rem;
` : ''}
`;
const StyledButton = styled(Button)`
margin-left: 1rem;

View File

@@ -46,6 +46,38 @@ const StyledInput = styled.input`
&:active:not([disabled]):after {
background: #A40D40;
}
&::file-selector-button {
content: '${props => props.validFile ? 'Choose a Different File' : 'Choose File'}';
position: absolute;
display: flex;
justify-content: center;
align-items: center;
height: 2.25rem;
top: 0;
left: 0;
padding: 0 1rem;
border-radius: 0.25rem;
background: #D91C5C;
color: #FFF;
font-weight: 500;
cursor: pointer;
outline: 0;
border: 0;
}
&:focus::file-selector-button {
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12),
inset 0 0 0 0.25rem #890934;
}
&:hover:not([disabled])::file-selector-button {
background: #BD164E;
}
&:active:not([disabled])::file-selector-button {
background: #A40D40;
}
`;
const FileUpload = (props, ref) => {

View File

@@ -10,6 +10,7 @@ const Select = styled.select`
border-radius: 0.125rem;
background: #fff;
color: inherit;
max-width: 230px;
&:focus {
border-color: #565656;
outline: none;

View File

@@ -0,0 +1,16 @@
import styled from 'styled-components/macro';
const Span = styled.span`
text-align: left;
${props => props.hasBorder ? `
height: 2.25rem;
display: flex;
align-items: center;
width: 100%;
padding: 0 1rem;
border: 1px solid #B6B6B6;
border-radius: 0.125rem;
` : ''}
`;
export default Span;

View File

@@ -1,7 +1,11 @@
import styled from 'styled-components/macro';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import Link from './Link';
const Tooltip = styled.span`
display: none;
label > span:hover > & {
display: inline;
position: absolute;
@@ -14,15 +18,105 @@ const Tooltip = styled.span`
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
0 0 0.25rem rgba(0, 0, 0, 0.18);
z-index: 80;
${props => !props.large ? `
white-space: nowrap;
` : `
text-align: left;
width: 22rem;
bottom: calc(100% + 0.5rem);
`}
white-space: nowrap;
}
`;
const StyledLinkWithTooltip = styled.span`
position: relative;
> span {
font-size: 14px;
position: absolute;
left: 50%;
transform: translate3d(-50%, calc(-100% - 5px), 0);
padding: 0.75rem 1rem;
border-radius: 0.25rem;
border: 1px solid #C6C6C6;
background: #FFF;
z-index: 80;
white-space: pre;
&:after {
content: "";
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #FFF;
position: absolute;
left: 50%;
top: 100%;
transform: translateX(-50%);
z-index: 2;
}
&:before {
content: "";
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #C6C6C6;
position: absolute;
left: 50%;
top: 100%;
transform: translateX(-50%);
z-index: 1;
}
}
`;
const LinkWithTooltip = props => {
const [isActive, setIsActive] = useState(false);
const tooltipRef = useRef();
const triggerRef = useRef();
const handleLinkClick = useCallback(() => {
setIsActive((oldActive) => {
const newActive = !oldActive;
return newActive;
});
}, [setIsActive]);
const handleOuterClick = useCallback((e) => {
if (!tooltipRef.current) {
return;
}
if (tooltipRef.current.contains(e.target)) {
return;
}
if (triggerRef.current.contains(e.target)) {
return;
}
handleLinkClick();
}, [tooltipRef, triggerRef, handleLinkClick]);
useEffect(() => {
document.addEventListener('click', handleOuterClick, false);
return () => document.removeEventListener('click', handleOuterClick, false);
}, [handleOuterClick]);
return (
<StyledLinkWithTooltip>
<Link to="#" onClick={handleLinkClick}>
<span ref={triggerRef}>{props.children}</span>
</Link>
{isActive ? (
<span ref={tooltipRef}>
{props.tipText}
</span>
) : null}
</StyledLinkWithTooltip>
);
};
export {
LinkWithTooltip,
};
export default Tooltip;

View File

@@ -2,9 +2,11 @@ import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
import Form from '../elements/Form';
import Input from '../elements/Input';
import Label from '../elements/Label';
import Radio from '../elements/Radio';
import Select from '../elements/Select';
import InputGroup from '../elements/InputGroup';
import PasswordInput from '../elements/PasswordInput';
@@ -16,6 +18,7 @@ import Button from '../elements/Button';
import Link from '../elements/Link';
import Tooltip from '../elements/Tooltip';
import CopyableText from '../elements/CopyableText';
import Span from '../elements/Span';
import handleErrors from "../../helpers/handleErrors";
import styled from 'styled-components/macro';
@@ -49,40 +52,70 @@ const AccountForm = props => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const jwt = localStorage.getItem("token");
const currentServiceProvider = useContext(ServiceProviderValueContext);
// Refs
const refName = useRef(null);
const refSipRealm = useRef(null);
const refRegWebhook = useRef(null);
const refUser = useRef(null);
const refPassword = useRef(null);
const refRegUser = useRef(null);
const refRegPassword = useRef(null);
const refQueueWebhook = useRef(null);
const refQueueUser = useRef(null);
const refQueuePassword = useRef(null);
const refSubspaceId = useRef(null);
const refSubspaceSecret = useRef(null);
const refSubspaceOtherSip = useRef(null);
// Form inputs
const [ name, setName ] = useState('');
const [ sipRealm, setSipRealm ] = useState('');
const [ name, setName ] = useState('');
const [ sipRealm, setSipRealm ] = useState('');
const [ deviceCallingApplication, setDeviceCallingApplication ] = useState('');
const [ regWebhook, setRegWebhook ] = useState('');
const [ method, setMethod ] = useState('POST');
const [ user, setUser ] = useState('' || '');
const [ password, setPassword ] = useState('' || '');
const [ regWebhook, setRegWebhook ] = useState('');
const [ regMethod, setRegMethod ] = useState('POST');
const [ regUser, setRegUser ] = useState('');
const [ regPassword, setRegPassword ] = useState('');
const [ webhookSecret, setWebhookSecret ] = useState('');
const [ queueWebhook, setQueueWebhook ] = useState('');
const [ queueMethod, setQueueMethod ] = useState('POST');
const [ queueUser, setQueueUser ] = useState('');
const [ queuePassword, setQueuePassword ] = useState('');
const [ hasSubspace, setHasSubspace ] = useState(false);
const [ subspaceId, setSubspaceId ] = useState('');
const [ subspaceSecret, setSubspaceSecret ] = useState('');
const [ subspaceSipTeleportId, setSubspaceSipTeleportId ] = useState('');
const [ subspaceSipTeleportEntryPoints, setSubspaceSipTeleportEntryPoints ] = useState([]);
const [ showSubspaceModal, setShowSubspaceModal ] = useState(false);
const [ generatingSubspace, setGeneratingSubspace ] = useState(false);
const [ subspaceSipRealm, setSubspaceSipRealm ] = useState('');
const [ sbcs, setSbcs ] = useState([]);
const [ subspaceSipRealmOtherValue, setSubspaceSipRealmOtherValue ] = useState('');
const [ subspaceEnable, setSubspaceEnable ] = useState(false);
// Invalid form inputs
const [ invalidName, setInvalidName ] = useState(false);
const [ invalidSipRealm, setInvalidSipRealm ] = useState(false);
const [ invalidRegWebhook, setInvalidRegWebhook ] = useState(false);
const [ invalidUser, setInvalidUser ] = useState(false);
const [ invalidPassword, setInvalidPassword ] = useState(false);
const [ invalidName, setInvalidName ] = useState(false);
const [ invalidSipRealm, setInvalidSipRealm ] = useState(false);
const [ invalidRegWebhook, setInvalidRegWebhook ] = useState(false);
const [ invalidRegUser, setInvalidRegUser ] = useState(false);
const [ invalidRegPassword, setInvalidRegPassword ] = useState(false);
const [ invalidQueueWebhook, setInvalidQueueWebhook ] = useState(false);
const [ invalidQueueUser, setInvalidQueueUser ] = useState(false);
const [ invalidQueuePassword, setInvalidQueuePassword ] = useState(false);
// eslint-disable-next-line no-unused-vars
const [ invalidSubspaceId, setInvalidSubspaceId ] = useState(false);
// eslint-disable-next-line no-unused-vars
const [ invalidSubspaceClient, setInvalidSubspaceClient ] = useState(false);
const [ showLoader, setShowLoader ] = useState(true);
const [ errorMessage, setErrorMessage ] = useState('');
const [ showAuth, setShowAuth ] = useState(false);
const toggleAuth = () => setShowAuth(!showAuth);
const [ showRegAuth, setShowRegAuth ] = useState(false);
const [ showQueueAuth, setShowQueueAuth ] = useState(false);
const toggleRegAuth = () => setShowRegAuth(!showRegAuth);
const toggleQueueAuth = () => setShowQueueAuth(!showQueueAuth);
const [ accounts, setAccounts ] = useState([]);
const [ accountSid, setAccountSid ] = useState('');
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
const [ accountApplications, setAccountApplications ] = useState([]);
const [ menuOpen, setMenuOpen ] = useState(null);
@@ -97,6 +130,14 @@ const AccountForm = props => {
}
};
const handleSubspaceMenuOpen = sid => {
if (menuOpen === sid) {
setMenuOpen(null);
} else {
setMenuOpen(sid);
}
};
const copyWebhookSecret = async e => {
e.preventDefault();
setMenuOpen(null);
@@ -151,6 +192,96 @@ const AccountForm = props => {
}
};
const toggleSubspaceTeleport = (enable, e) => {
e.preventDefault();
setMenuOpen(null);
setSubspaceEnable(enable);
setShowSubspaceModal(true);
};
const resetSubspaceState = () => {
setMenuOpen(null);
setShowSubspaceModal(false);
setSubspaceSipRealmOtherValue('');
setGeneratingSubspace(false);
setSubspaceEnable(false);
setSubspaceSipRealm('');
};
const handleSubspaceEnable = async () => {
try {
setGeneratingSubspace(true);
const destination = subspaceSipRealm === 'other'
? subspaceSipRealmOtherValue
: subspaceSipRealm;
const response = await axios({
method: 'post',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/Accounts/${accountSid}/SubspaceTeleport`,
headers: {
Authorization: `Bearer ${jwt}`,
},
data: { destination },
});
if (response.status === 200) {
setSubspaceSipTeleportId(response.data.subspace_sip_teleport_id || '');
setSubspaceSipTeleportEntryPoints(response.data.subspace_sip_teleport_destinations || []);
dispatch({
type: 'ADD',
level: 'success',
message: 'Successfully enabled subspace teleport.',
});
}
resetSubspaceState();
} catch (err) {
resetSubspaceState();
if (err.response.status === 500 && err.response.data.msg === 'Too Many Requests') {
dispatch({
type: 'ADD',
level: 'error',
message: 'You have already created the maximum number of SIP Teleports allowed for your Subspace account.',
});
} else {
handleErrors({ err, history, dispatch });
}
}
};
const handleSubspaceDisable = async () => {
try {
setGeneratingSubspace(true);
const response = await axios({
method: 'delete',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/Accounts/${accountSid}/SubspaceTeleport`,
headers: {
Authorization: `Bearer ${jwt}`,
},
});
if (response.status === 204) {
setSubspaceSipTeleportId('');
setSubspaceSipTeleportEntryPoints([]);
dispatch({
type: 'ADD',
level: 'success',
message: `Successfully disabled subspace teleport.`,
});
}
resetSubspaceState();
} catch (err) {
resetSubspaceState();
handleErrors({ err, history, dispatch });
}
};
useEffect(() => {
const getAccounts = async () => {
try {
@@ -187,17 +318,15 @@ const AccountForm = props => {
promiseList.push(applicationsPromise);
}
if (props.type === 'add') {
const serviceProvidersPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/ServiceProviders',
headers: {
Authorization: `Bearer ${jwt}`,
},
});
promiseList.push(serviceProvidersPromise);
}
const sbcsPromise = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Sbcs',
headers: {
Authorization: `Bearer ${jwt}`,
},
});
promiseList.push(sbcsPromise);
const promiseAllValues = await Promise.all(promiseList);
@@ -211,11 +340,7 @@ const AccountForm = props => {
});
setAccountApplications(accountApplicationsData);
}
if (props.type === 'add') {
const serviceProviders = (promiseAllValues[1] && promiseAllValues[1].data) || '';
setServiceProviderSid(serviceProviders[0].service_provider_sid);
}
setSbcs(promiseAllValues[2].data);
if (props.type === 'setup' && accountsData.length > 1) {
history.push('/internal/accounts');
@@ -250,16 +375,31 @@ const AccountForm = props => {
setSipRealm(acc.sip_realm || '');
setDeviceCallingApplication(acc.device_calling_application_sid || '');
setRegWebhook((acc.registration_hook && acc.registration_hook.url ) || '');
setMethod((acc.registration_hook && acc.registration_hook.method ) || 'post');
setUser((acc.registration_hook && acc.registration_hook.username) || '');
setPassword((acc.registration_hook && acc.registration_hook.password) || '');
setRegMethod((acc.registration_hook && acc.registration_hook.method ) || 'post');
setRegUser((acc.registration_hook && acc.registration_hook.username) || '');
setRegPassword((acc.registration_hook && acc.registration_hook.password) || '');
setQueueWebhook((acc.queue_event_hook && acc.queue_event_hook.url ) || '');
setQueueMethod((acc.queue_event_hook && acc.queue_event_hook.method ) || 'post');
setQueueUser((acc.queue_event_hook && acc.queue_event_hook.username) || '');
setQueuePassword((acc.queue_event_hook && acc.queue_event_hook.password) || '');
setWebhookSecret(acc.webhook_secret || '');
setSubspaceId(acc.subspace_client_id || '');
setSubspaceSecret(acc.subspace_client_secret || '');
setSubspaceSipTeleportId(acc.subspace_sip_teleport_id || '');
setSubspaceSipTeleportEntryPoints(acc.subspace_sip_teleport_destinations ? JSON.parse(acc.subspace_sip_teleport_destinations) : []);
setHasSubspace(acc.subspace_client_id ? true : false);
if (
(acc.registration_hook && acc.registration_hook.username) ||
(acc.registration_hook && acc.registration_hook.password)
) {
setShowAuth(true);
setShowRegAuth(true);
}
if (
(acc.queue_event_hook && acc.queue_event_hook.username) ||
(acc.queue_event_hook && acc.queue_event_hook.password)
) {
setShowQueueAuth(true);
}
}
setShowLoader(false);
@@ -298,8 +438,11 @@ const AccountForm = props => {
setInvalidName(false);
setInvalidSipRealm(false);
setInvalidRegWebhook(false);
setInvalidUser(false);
setInvalidPassword(false);
setInvalidRegUser(false);
setInvalidRegPassword(false);
setInvalidQueueWebhook(false);
setInvalidQueueUser(false);
setInvalidQueuePassword(false);
let errorMessages = [];
let focusHasBeenSet = false;
@@ -342,15 +485,29 @@ const AccountForm = props => {
});
if ((user && !password) || (!user && password)) {
errorMessages.push('Username and password must be either both filled out or both empty.');
setInvalidUser(true);
setInvalidPassword(true);
if ((regUser && !regPassword) || (!regUser && regPassword)) {
errorMessages.push('Registration webhook username and password must be either both filled out or both empty.');
setInvalidRegUser(true);
setInvalidRegPassword(true);
if (!focusHasBeenSet) {
if (!user) {
refUser.current.focus();
if (!regUser) {
refRegUser.current.focus();
} else {
refPassword.current.focus();
refRegPassword.current.focus();
}
focusHasBeenSet = true;
}
}
if ((queueUser && !queuePassword) || (!queueUser && queuePassword)) {
errorMessages.push('Queue event webhook username and password must be either both filled out or both empty.');
setInvalidQueueUser(true);
setInvalidQueuePassword(true);
if (!focusHasBeenSet) {
if (!queueUser) {
refQueueUser.current.focus();
} else {
refQueuePassword.current.focus();
}
focusHasBeenSet = true;
}
@@ -369,15 +526,23 @@ const AccountForm = props => {
sip_realm: sipRealm.trim() || null,
registration_hook: {
url: regWebhook.trim(),
method: method,
username: user.trim() || null,
password: password || null,
method: regMethod,
username: regUser.trim() || null,
password: regPassword || null,
},
queue_event_hook: {
url: queueWebhook.trim(),
method: queueMethod,
username: queueUser.trim() || null,
password: queuePassword || null,
},
webhook_secret: webhookSecret || null,
subspace_client_id: subspaceId || null,
subspace_client_secret: subspaceSecret || null,
};
if (props.type === 'add') {
axiosData.service_provider_sid = serviceProviderSid;
axiosData.service_provider_sid = currentServiceProvider;
}
if (props.type === 'edit') {
@@ -450,6 +615,19 @@ const AccountForm = props => {
},
];
const subspaceMenuItems = [
{
type: 'button',
name: 'Enable',
action: toggleSubspaceTeleport.bind(toggleSubspaceTeleport, true),
},
{
type: 'button',
name: 'Disable',
action: toggleSubspaceTeleport.bind(toggleSubspaceTeleport, false),
},
];
return (
showLoader
? <Loader
@@ -566,27 +744,27 @@ const AccountForm = props => {
<Select
large={props.type === 'setup'}
name="method"
id="method"
value={method}
onChange={e => setMethod(e.target.value)}
id="regMethod"
value={regMethod}
onChange={e => setRegMethod(e.target.value)}
>
<option value="POST">POST</option>
<option value="GET">GET</option>
</Select>
</InputGroup>
{showAuth ? (
{showRegAuth ? (
<InputGroup>
<Label indented htmlFor="user">User</Label>
<Input
large={props.type === 'setup'}
name="user"
id="user"
value={user || ''}
onChange={e => setUser(e.target.value)}
value={regUser || ''}
onChange={e => setRegUser(e.target.value)}
placeholder="Optional"
invalid={invalidUser}
ref={refUser}
invalid={invalidRegUser}
ref={refRegUser}
/>
<Label htmlFor="password" middle>Password</Label>
<PasswordInput
@@ -594,12 +772,12 @@ const AccountForm = props => {
allowShowPassword
name="password"
id="password"
password={password}
setPassword={setPassword}
password={regPassword}
setPassword={setRegPassword}
setErrorMessage={setErrorMessage}
placeholder="Optional"
invalid={invalidPassword}
ref={refPassword}
invalid={invalidRegPassword}
ref={refRegPassword}
/>
</InputGroup>
) : (
@@ -607,12 +785,211 @@ const AccountForm = props => {
text
formLink
type="button"
onClick={toggleAuth}
onClick={toggleRegAuth}
>
Use HTTP Basic Authentication
</Button>
)}
<Label htmlFor="queueWebhook">Queue Event Webhook</Label>
<InputGroup>
<Input
large={props.type === 'setup'}
name="queueWebhook"
id="queueWebhook"
value={queueWebhook}
onChange={e => setQueueWebhook(e.target.value)}
placeholder="URL to notify when a member joins or leaves a queue"
invalid={invalidQueueWebhook}
ref={refQueueWebhook}
/>
<Label
middle
htmlFor="method"
>
Method
</Label>
<Select
large={props.type === 'setup'}
name="method"
id="queueMethod"
value={queueMethod}
onChange={e => setQueueMethod(e.target.value)}
>
<option value="POST">POST</option>
</Select>
</InputGroup>
{showQueueAuth ? (
<InputGroup>
<Label indented htmlFor="user">User</Label>
<Input
large={props.type === 'setup'}
name="user"
id="user"
value={queueUser || ''}
onChange={e => setQueueUser(e.target.value)}
placeholder="Optional"
invalid={invalidQueueUser}
ref={refQueueUser}
/>
<Label htmlFor="password" middle>Password</Label>
<PasswordInput
large={props.type === 'setup'}
allowShowPassword
name="password"
id="password"
password={queuePassword}
setPassword={setQueuePassword}
setErrorMessage={setErrorMessage}
placeholder="Optional"
invalid={invalidQueuePassword}
ref={refQueuePassword}
/>
</InputGroup>
) : (
<Button
text
formLink
type="button"
onClick={toggleQueueAuth}
>
Use HTTP Basic Authentication
</Button>
)}
{ process.env.REACT_APP_ENABLE_SUBSPACE ? (
<>
<Label htmlFor="subspaceId">Subspace</Label>
<InputGroup>
<Input
large={props.type === 'setup'}
name="subspaceId"
id="subspaceId"
value={subspaceId}
onChange={e => setSubspaceId(e.target.value)}
placeholder="Client Id for Subspace"
ref={refSubspaceId}
style={{ margin: '0 4px' }}
/>
<PasswordInput
large={props.type === 'setup'}
allowShowPassword
name="subspaceSecret"
id="subspaceSecret"
password={subspaceSecret}
setPassword={setSubspaceSecret}
setErrorMessage={setErrorMessage}
placeholder="Client Secret for Subspace"
ref={refSubspaceSecret}
style={{ margin: '0 4px' }}
/>
<StyledInputGroup>
<TableMenu
disabled={!hasSubspace}
sid="subspace"
open={menuOpen === "subspace"}
handleMenuOpen={handleSubspaceMenuOpen}
menuItems={subspaceSipTeleportId ? [subspaceMenuItems[1]] : [subspaceMenuItems[0]]}
/>
</StyledInputGroup>
</InputGroup>
{subspaceSipTeleportId ? (
<div style={{ gridColumn: 2, textAlign: 'left' }}>
<div>Subspace is now enabled. To send your traffic through Subspace:</div>
{subspaceSipTeleportEntryPoints.map(entrypoint => (
<div key={entrypoint.transport_type}>
<Span>send {entrypoint.transport_type.split('_').join(' and ')} traffic to&nbsp;</Span>
<CopyableText text={entrypoint.address} textType="Address" />
</div>
))}
</div>
) : null}
{showSubspaceModal && (
<Modal
title={subspaceEnable ? 'Have Subspace send SIP to:' : 'Are you sure you want to delete your Subspace SIP Teleport?'}
loader={generatingSubspace}
hideButtons={generatingSubspace}
maskClosable={!generatingSubspace}
actionText={subspaceEnable ? 'Save' : 'Disable'}
content={
<ModalContainer>
{subspaceEnable ? (
<>
{sipRealm && (
<Radio
noLeftMargin
name="subspaceSipRealm"
id="sipRealmAccount"
label={sipRealm}
checked={subspaceSipRealm === sipRealm}
onChange={() => {
setSubspaceSipRealm(sipRealm);
setSubspaceSipRealmOtherValue('');
}}
/>
)}
{sbcs.map((sbc) => {
return (
<Radio
key={sbc.ipv4}
noLeftMargin
name="subspaceSipRealm"
id={sbc.sbc_address_sid}
label={`${sbc.ipv4}:${sbc.port}`}
checked={subspaceSipRealm === `${sbc.ipv4}:${sbc.port}`}
onChange={() => {
setSubspaceSipRealm(`${sbc.ipv4}:${sbc.port}`);
setSubspaceSipRealmOtherValue('');
}}
/>
);
})}
<Radio
noLeftMargin
name="subspaceSipRealm"
id="sipRealmOther"
label="Other"
checked={subspaceSipRealm === 'other'}
onChange={() => {
setSubspaceSipRealm('other');
setTimeout(() => refSubspaceOtherSip.current.focus(), 0);
}}
/>
{subspaceSipRealm === 'other' && (
<Input
ref={refSubspaceOtherSip}
name="subspaceSipRealm"
id="sipRealmOtherValue"
value={subspaceSipRealmOtherValue}
onChange={e => setSubspaceSipRealmOtherValue(e.target.value)}
placeholder="IP address or DNS name"
style={{ marginTop: '8px' }}
/>
)}
</>
) : null}
</ModalContainer>
}
handleCancel={() => {
setShowSubspaceModal(false);
resetSubspaceState();
}}
handleSubmit={() => {
if (subspaceEnable) {
handleSubspaceEnable();
} else {
handleSubspaceDisable();
}
}}
/>
)}
</>
) : null }
{errorMessage && (
<FormError grid message={errorMessage} />
)}

View File

@@ -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 { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
import Form from '../elements/Form';
import Input from '../elements/Input';
import Label from '../elements/Label';
@@ -14,12 +15,16 @@ import SpeechSynthesisLanguageGoogle from '../../data/SpeechSynthesisLanguageGoo
import SpeechSynthesisLanguageAws from '../../data/SpeechSynthesisLanguageAws';
import SpeechRecognizerLanguageGoogle from '../../data/SpeechRecognizerLanguageGoogle';
import SpeechRecognizerLanguageAws from '../../data/SpeechRecognizerLanguageAws';
import SpeechRecognizerLanguageMicrosoft from '../../data/SpeechRecognizerLanguageMicrosoft';
import SpeechSynthesisLanguageMicrosoft from '../../data/SpeechSynthesisLanguageMicrosoft';
import SpeechSynthesisLanguageWellSaid from '../../data/SpeechSynthesisLanguageWellSaid';
import Loader from '../blocks/Loader';
import CopyableText from '../elements/CopyableText';
const ApplicationForm = props => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const currentServiceProvider = useContext(ServiceProviderValueContext);
// Refs
const refName = useRef(null);
@@ -118,7 +123,7 @@ const ApplicationForm = props => {
applicationsPromise,
]);
const accounts = promiseAllValues[0].data;
const accounts = promiseAllValues[0].data.filter(a => a.service_provider_sid === currentServiceProvider);
const applications = promiseAllValues[1].data;
setAccounts(accounts);
@@ -493,7 +498,7 @@ const ApplicationForm = props => {
-- Choose the account this application will be associated with --
</option>
)}
{accounts.map(a => (
{accounts.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
<option
key={a.account_sid}
value={a.account_sid}
@@ -743,6 +748,14 @@ const ApplicationForm = props => {
? SpeechSynthesisLanguageGoogle.find(l => (
l.code === speechSynthesisLanguage
))
: e.target.value === 'microsoft'
? SpeechSynthesisLanguageMicrosoft.find(l => (
l.code === speechSynthesisLanguage
))
: e.target.value === 'wellsaid'
? SpeechSynthesisLanguageWellSaid.find(l => (
l.code === speechSynthesisLanguage
))
: SpeechSynthesisLanguageAws.find(l => (
l.code === speechSynthesisLanguage
));
@@ -756,9 +769,13 @@ const ApplicationForm = props => {
return;
}
newLang = SpeechSynthesisLanguageAws.find(l => (
l.code === 'en-US'
));
newLang = e.target.value === 'aws'
? SpeechSynthesisLanguageAws.find(l => (
l.code === 'en-US'
))
: SpeechSynthesisLanguageMicrosoft.find(l => (
l.code === 'en-US'
));
}
// Update state to reflect first voice option for language
@@ -767,6 +784,8 @@ const ApplicationForm = props => {
>
<option value="google">Google</option>
<option value="aws">AWS</option>
<option value="microsoft">Microsoft</option>
<option value="wellsaid">WellSaid</option>
</Select>
<Label middle htmlFor="speechSynthesisLanguage">Language</Label>
<Select
@@ -790,6 +809,14 @@ const ApplicationForm = props => {
? SpeechSynthesisLanguageGoogle.find(l => (
l.code === e.target.value
))
: speechSynthesisVendor === 'microsoft'
? SpeechSynthesisLanguageMicrosoft.find(l => (
l.code === e.target.value
))
: speechSynthesisVendor === 'wellsaid'
? SpeechSynthesisLanguageWellSaid.find(l => (
l.code === e.target.value
))
: SpeechSynthesisLanguageAws.find(l => (
l.code === e.target.value
));
@@ -802,6 +829,14 @@ const ApplicationForm = props => {
SpeechSynthesisLanguageGoogle.map(l => (
<option key={l.code} value={l.code}>{l.name}</option>
))
) : speechSynthesisVendor === 'microsoft' ? (
SpeechSynthesisLanguageMicrosoft.map(l => (
<option key={l.code} value={l.code}>{l.name}</option>
))
) : speechSynthesisVendor === 'wellsaid' ? (
SpeechSynthesisLanguageWellSaid.map(l => (
<option key={l.code} value={l.code}>{l.name}</option>
))
) : (
SpeechSynthesisLanguageAws.map(l => (
<option key={l.code} value={l.code}>{l.name}</option>
@@ -822,6 +857,18 @@ const ApplicationForm = props => {
.map(m => m.voices.map(v => (
<option key={v.value} value={v.value}>{v.name}</option>
)))
) : speechSynthesisVendor === 'microsoft' ? (
SpeechSynthesisLanguageMicrosoft
.filter(l => l.code === speechSynthesisLanguage)
.map(m => m.voices.map(v => (
<option key={v.value} value={v.value}>{v.name}</option>
)))
) : speechSynthesisVendor === 'wellsaid' ? (
SpeechSynthesisLanguageWellSaid
.filter(l => l.code === speechSynthesisLanguage)
.map(m => m.voices.map(v => (
<option key={v.value} value={v.value}>{v.name}</option>
)))
) : (
SpeechSynthesisLanguageAws
.filter(l => l.code === speechSynthesisLanguage)
@@ -860,6 +907,7 @@ const ApplicationForm = props => {
>
<option value="google">Google</option>
<option value="aws">AWS</option>
<option value="microsoft">Microsoft</option>
</Select>
<Label middle htmlFor="speechRecognizerLanguage">Language</Label>
<Select
@@ -873,6 +921,10 @@ const ApplicationForm = props => {
SpeechRecognizerLanguageGoogle.map(l => (
<option key={l.code} value={l.code}>{l.name}</option>
))
) : speechRecognizerVendor === 'microsoft' ? (
SpeechRecognizerLanguageMicrosoft.map(l => (
<option key={l.code} value={l.code}>{l.name}</option>
))
) : (
SpeechRecognizerLanguageAws.map(l => (
<option key={l.code} value={l.code}>{l.name}</option>

File diff suppressed because it is too large Load Diff

View File

@@ -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 { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
import Form from '../elements/Form';
import Input from '../elements/Input';
import Label from '../elements/Label';
@@ -16,6 +17,7 @@ const PhoneNumberForm = props => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const currentServiceProvider = useContext(ServiceProviderValueContext);
// Refs
const refPhoneNumber = useRef(null);
@@ -358,7 +360,10 @@ const PhoneNumberForm = props => {
name="account"
id="account"
value={account}
onChange={e => setAccount(e.target.value)}
onChange={(e) => {
setAccount(e.target.value);
setApplication('');
}}
invalid={invalidAccount}
ref={refAccount}
>
@@ -368,7 +373,7 @@ const PhoneNumberForm = props => {
) && (
<option value="">-- Choose the account that this phone number should be associated with --</option>
)}
{accountValues.map(a => (
{accountValues.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
<option
key={a.account_sid}
value={a.account_sid}
@@ -391,7 +396,16 @@ const PhoneNumberForm = props => {
: '-- NONE --'
}
</option>
{applicationValues.map(a => (
{applicationValues.filter((a) => {
// Map an application to a service provider through it's account_sid
const acct = accountValues.find(ac => a.account_sid === ac.account_sid);
if (account) {
return a.account_sid === account;
}
return acct.service_provider_sid === currentServiceProvider;
}).map(a => (
<option
key={a.application_sid}
value={a.application_sid}

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import styled from 'styled-components';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import { ShowMsTeamsDispatchContext } from '../../contexts/ShowMsTeamsContext';
import Form from '../elements/Form';
@@ -11,9 +12,23 @@ import InputGroup from '../elements/InputGroup';
import FormError from '../blocks/FormError';
import Button from '../elements/Button';
import Loader from '../blocks/Loader';
import Modal from '../blocks/Modal';
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
import handleErrors from "../../helpers/handleErrors";
const Td = styled.td`
padding: 0.5rem 0;
&:first-child {
font-weight: 500;
padding-right: 1.5rem;
vertical-align: top;
}
& ul {
margin: 0;
padding-left: 1.25rem;
}
`;
const SettingsForm = () => {
const history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
@@ -40,8 +55,9 @@ const SettingsForm = () => {
const [ showLoader, setShowLoader ] = useState(true);
const [ errorMessage, setErrorMessage ] = useState('');
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
const [ serviceProviders, setServiceProviders ] = useState([]);
const [ confirmDelete, setConfirmDelete ] = useState(false);
useEffect(() => {
const getSettingsData = async () => {
@@ -59,14 +75,16 @@ const SettingsForm = () => {
const serviceProvidersResponse = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${currentServiceProvider}`,
url: `/ServiceProviders`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const sp = serviceProvidersResponse.data;
const sps = serviceProvidersResponse.data;
const sp = sps.find(s => s.service_provider_sid === currentServiceProvider);
setServiceProviders(sps);
setServiceProviderName(sp.name || '');
setServiceProviderSid(sp.service_provider_sid || '');
setEnableMsTeams(sp.ms_teams_fqdn ? true : false);
@@ -96,6 +114,32 @@ const SettingsForm = () => {
setEnableMsTeams(e.target.checked);
};
const handleDelete = () => {
setErrorMessage('');
axios({
method: 'delete',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${serviceProviderSid}`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
})
.then(() => {
setConfirmDelete(false);
setErrorMessage('');
history.push('/internal/accounts');
dispatch({
type: 'ADD',
level: 'success',
message: 'Service Provider Deleted'
});
})
.catch((error) => {
setErrorMessage(error.response.data.msg);
});
};
const handleSubmit = async (e) => {
let isMounted = true;
try {
@@ -210,69 +254,114 @@ const SettingsForm = () => {
return (
showLoader
? <Loader height="365px" />
: <Form
large
wideLabel
onSubmit={handleSubmit}
>
<Label htmlFor="serviceProviderName">Service Provider Name</Label>
<Input
name="serviceProviderName"
id="serviceProviderName"
value={serviceProviderName}
onChange={e => setServiceProviderName(e.target.value)}
invalid={invalidServiceProviderName}
ref={refServiceProviderName}
/>
<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}
/>
: (
<>
<Form
large
wideLabel
onSubmit={handleSubmit}
>
<Label htmlFor="serviceProviderName">Service Provider Name</Label>
<Input
name="serviceProviderName"
id="serviceProviderName"
value={serviceProviderName}
onChange={e => setServiceProviderName(e.target.value)}
invalid={invalidServiceProviderName}
ref={refServiceProviderName}
/>
<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") || ""}
/>
<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") || ""}
/>
{errorMessage && (
<FormError grid message={errorMessage} />
)}
{errorMessage && !confirmDelete && (
<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',
});
<InputGroup flexEnd spaced>
<Button
grid
gray
type="button"
onClick={() => {
history.push('/internal/accounts');
dispatch({
type: 'ADD',
level: 'info',
message: 'Changes canceled',
});
}}
>
Cancel
</Button>
{serviceProviders.length > 1 && (
<Button
grid
gray
type="button"
onClick={() => setConfirmDelete(true)}
>
Delete
</Button>
)}
<Button grid>Save</Button>
</InputGroup>
</Form>
{confirmDelete && serviceProviders.length > 1 && (
<Modal
title="Are you sure you want to delete the Service Provider?"
loader={false}
content={
<div>
<table>
<tbody>
<tr>
<Td>Service Provider Name:</Td>
<Td>{serviceProviderName}</Td>
</tr>
<tr>
<Td>SBC Domain Name:</Td>
<Td>{sbcDomainName || '[none]'}</Td>
</tr>
</tbody>
</table>
{errorMessage && (
<FormError message={errorMessage} />
)}
</div>
}
handleCancel={() => {
setConfirmDelete(false);
setErrorMessage('');
}}
>
Cancel
</Button>
<Button grid>Save</Button>
</InputGroup>
</Form>
handleSubmit={handleDelete}
actionText="Delete"
/>
)}
</>
)
);
};

View File

@@ -8,6 +8,7 @@ import handleErrors from '../../helpers/handleErrors';
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 PasswordInput from '../elements/PasswordInput';
import Radio from '../elements/Radio';
@@ -17,7 +18,9 @@ import Code from '../elements/Code';
import FormError from '../blocks/FormError';
import Button from '../elements/Button';
import Loader from '../blocks/Loader';
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext'
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
import MicrosoftAzureRegions from '../../data/MicrosoftAzureRegions';
const StyledButtonGroup = styled(InputGroup)`
@@ -66,10 +69,14 @@ const SpeechServicesAddEdit = (props) => {
// Refs
const refVendorGoogle = useRef(null);
const refVendorAws = useRef(null);
const refVendorMs = useRef(null);
const refVendorWellSaid = useRef(null);
const refAccessKeyId = useRef(null);
const refSecretAccessKey = useRef(null);
const refUseForTts = useRef(null);
const refUseForStt = useRef(null);
const refApiKey = useRef(null);
const refRegion = useRef(null);
// Form inputs
const [ vendor, setVendor ] = useState('');
@@ -79,14 +86,22 @@ const SpeechServicesAddEdit = (props) => {
const [ secretAccessKey, setSecretAccessKey ] = useState('');
const [ useForTts, setUseForTts ] = useState(false);
const [ useForStt, setUseForStt ] = useState(false);
const [ accounts, setAccounts ] = useState([]);
const [ accountSid, setAccountSid ] = useState('');
const [ apiKey, setApiKey ] = useState('');
const [ region, setRegion ] = useState('');
// Invalid form inputs
const [ invalidVendorGoogle, setInvalidVendorGoogle ] = useState(false);
const [ invalidVendorAws, setInvalidVendorAws ] = useState(false);
const [ invalidVendorMs, setInvalidVendorMs ] = useState(false);
const [ invalidVendorWellSaid, setInvalidVendorWellSaid ] = useState(false);
const [ invalidAccessKeyId, setInvalidAccessKeyId ] = useState(false);
const [ invalidSecretAccessKey, setInvalidSecretAccessKey ] = useState(false);
const [ invalidUseForTts, setInvalidUseForTts ] = useState(false);
const [ invalidUseForStt, setInvalidUseForStt ] = useState(false);
const [ invalidApiKey, setInvalidApiKey ] = useState(false);
const [ invalidRegion, setInvalidRegion ] = useState(false);
const [ originalTtsValue, setOriginalTtsValue ] = useState(null);
const [ originalSttValue, setOriginalSttValue ] = useState(null);
@@ -100,6 +115,17 @@ const SpeechServicesAddEdit = (props) => {
const getAPIData = async () => {
let isMounted = true;
try {
const accountsResponse = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Accounts',
headers: {
Authorization: `Bearer ${jwt}`,
},
});
setAccounts(accountsResponse.data);
if (type === 'edit') {
const speechCredential = await axios({
method: 'get',
@@ -119,11 +145,14 @@ const SpeechServicesAddEdit = (props) => {
} catch (err) {
}
setAccountSid( speechCredential.data.account_sid || '');
setVendor( speechCredential.data.vendor || undefined);
setServiceKey( serviceKeyJson || '');
setDisplayedServiceKey( displayedServiceKeyJson || '');
setAccessKeyId( speechCredential.data.access_key_id || '');
setSecretAccessKey( speechCredential.data.secret_access_key || '');
setApiKey( speechCredential.data.api_key || '');
setRegion( speechCredential.data.region || '');
setUseForTts( speechCredential.data.use_for_tts || false);
setUseForStt( speechCredential.data.use_for_stt || false);
setOriginalTtsValue( speechCredential.data.use_for_tts || false);
@@ -191,10 +220,13 @@ const SpeechServicesAddEdit = (props) => {
setErrorMessage('');
setInvalidVendorGoogle(false);
setInvalidVendorAws(false);
setInvalidVendorMs(false);
setInvalidVendorWellSaid(false);
setInvalidAccessKeyId(false);
setInvalidSecretAccessKey(false);
setInvalidUseForTts(false);
setInvalidUseForStt(false);
setInvalidApiKey(false);
let errorMessages = [];
let focusHasBeenSet = false;
@@ -202,6 +234,8 @@ const SpeechServicesAddEdit = (props) => {
errorMessages.push('Please select a vendor.');
setInvalidVendorGoogle(true);
setInvalidVendorAws(true);
setInvalidVendorMs(true);
setInvalidVendorWellSaid(true);
if (!focusHasBeenSet) {
refVendorGoogle.current.focus();
focusHasBeenSet = true;
@@ -230,6 +264,33 @@ const SpeechServicesAddEdit = (props) => {
}
}
if (vendor === 'microsoft' && !apiKey) {
errorMessages.push('Please provide an API key.');
setInvalidApiKey(true);
if (!focusHasBeenSet) {
refApiKey.current.focus();
focusHasBeenSet = true;
}
}
if (vendor === 'microsoft' && !region) {
errorMessages.push('Please select a region.');
setInvalidRegion(true);
if (!focusHasBeenSet) {
refRegion.current.focus();
focusHasBeenSet = true;
}
}
if (vendor === 'wellsaid' && !apiKey) {
errorMessages.push('Please provide an API key.');
setInvalidApiKey(true);
if (!focusHasBeenSet) {
refApiKey.current.focus();
focusHasBeenSet = true;
}
}
if (errorMessages.length > 1) {
setErrorMessage(errorMessages);
return;
@@ -238,33 +299,6 @@ const SpeechServicesAddEdit = (props) => {
return;
}
// Check if user already has a speech service with the selected vendor
const speechServices = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials`,
headers: {
Authorization: `Bearer ${jwt}`,
},
});
if (type === 'add' && speechServices.data.some(speech => speech.vendor === vendor)) {
setErrorMessage('You can only have one speech credential per vendor.');
setShowLoader(false);
if (vendor === 'google') {
setInvalidVendorGoogle(true);
if (!focusHasBeenSet) {
refVendorGoogle.current.focus();
}
} else if (vendor === 'aws') {
setInvalidVendorAws(true);
if (!focusHasBeenSet) {
refVendorAws.current.focus();
}
}
return;
}
//===============================================
// Submit
//===============================================
@@ -288,8 +322,12 @@ const SpeechServicesAddEdit = (props) => {
service_key: vendor === 'google' ? JSON.stringify(serviceKey) : null,
access_key_id: vendor === 'aws' ? accessKeyId : null,
secret_access_key: vendor === 'aws' ? secretAccessKey : null,
api_key: ['microsoft', 'wellsaid'].includes(vendor) ? apiKey : null,
region: vendor === 'microsoft' ? region : null,
use_for_tts: useForTts,
use_for_stt: useForStt,
service_provider_sid: accountSid ? null : currentServiceProvider,
account_sid: accountSid || null,
}
});
@@ -383,7 +421,11 @@ const SpeechServicesAddEdit = (props) => {
// If successful, go to speech services
//===============================================
isMounted = false;
history.push('/internal/speech-services');
if (accountSid) {
history.push(`/internal/speech-services?account_sid=${accountSid}`);
} else {
history.push('/internal/speech-services');
}
const dispatchMessage = type === 'add'
? 'Speech service created successfully'
: 'Speech service updated successfully';
@@ -450,8 +492,50 @@ const SpeechServicesAddEdit = (props) => {
ref={refVendorAws}
disabled={type === 'edit'}
/>
<Radio
name="vendor"
id="microsoft"
label="Microsoft"
checked={vendor === 'microsoft'}
onChange={() => setVendor('microsoft')}
invalid={invalidVendorMs}
ref={refVendorMs}
disabled={type === 'edit'}
/>
<Radio
name="vendor"
id="wellsaid"
label="WellSaid"
checked={vendor === 'wellsaid'}
onChange={() => setVendor('wellsaid')}
invalid={invalidVendorWellSaid}
ref={refVendorWellSaid}
disabled={type === 'edit'}
/>
</InputGroup>
<Label htmlFor="account">Used by</Label>
<Select
name="account"
id="account"
value={accountSid}
onChange={e => setAccountSid(e.target.value)}
>
<option value="">
All accounts
</option>
{accounts.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
<option
key={a.account_sid}
value={a.account_sid}
>
{a.name}
</option>
))}
</Select>
{vendor === 'google' ? (
<>
<Label htmlFor="serviceKey">Service Key</Label>
@@ -498,11 +582,61 @@ const SpeechServicesAddEdit = (props) => {
disabled={type === 'edit'}
/>
</>
) : vendor === 'microsoft' ? (
<>
<Label htmlFor="apiKey">API Key</Label>
<Input
name="apiKey"
id="apiKey"
value={apiKey}
onChange={e => setApiKey(e.target.value)}
placeholder=""
invalid={invalidApiKey}
ref={refApiKey}
disabled={type === 'edit'}
/>
<Label htmlFor="region">Region</Label>
<Select
name="region"
id="region"
value={region}
onChange={e => setRegion(e.target.value)}
ref={refRegion}
invalid={invalidRegion}
>
<option value="">
All regions
</option>
{MicrosoftAzureRegions.map(r => (
<option
key={r.value}
value={r.value}
>
{r.name}
</option>
))}
</Select>
</>
) : vendor === 'wellsaid' ? (
<>
<Label htmlFor="apiKey">API Key</Label>
<Input
name="apiKey"
id="apiKey"
value={apiKey}
onChange={e => setApiKey(e.target.value)}
placeholder=""
invalid={invalidApiKey}
ref={refApiKey}
disabled={type === 'edit'}
/>
</>
) : (
null
)}
{vendor === 'google' || vendor === 'aws' ? (
{['google', 'aws', 'microsoft', 'wellsaid'].includes(vendor) ? (
<>
<div/>
<Checkbox
@@ -521,13 +655,15 @@ const SpeechServicesAddEdit = (props) => {
name="useForStt"
id="useForStt"
label="Use for speech-to-text"
disabled={'wellsaid' === vendor}
checked={useForStt}
onChange={e => setUseForStt(e.target.checked)}
invalid={invalidUseForStt}
ref={refUseForStt}
/>
</>
) : (
) :
(
null
)}

View File

@@ -41,10 +41,9 @@ const AccountsList = () => {
sid: a.account_sid,
name: a.name,
sip_realm: a.sip_realm,
url: a.registration_hook && a.registration_hook.url,
method: a.registration_hook && a.registration_hook.method,
username: a.registration_hook && a.registration_hook.username,
password: a.registration_hook && a.registration_hook.password,
url_reg: a.registration_hook && a.registration_hook.url,
url_queue: a.queue_event_hook && a.queue_event_hook.url,
subspace_enabled: a.subspace_sip_teleport_id ? 'Enabled' : ''
}));
return(simplifiedAccounts);
} catch (err) {
@@ -75,7 +74,7 @@ const AccountsList = () => {
const items = [
{ name: 'Name:' , content: account.name || '[none]' },
{ name: 'SIP Realm:' , content: account.sip_realm || '[none]' },
{ name: 'Registration Webhook:' , content: account.url || '[none]' },
{ name: 'Registration Webhook:' , content: account.url_reg || '[none]' },
];
return items;
};
@@ -221,7 +220,8 @@ const AccountsList = () => {
{ header: 'Name', key: 'name' },
{ header: 'AccountSid', key: 'sid' },
{ header: 'SIP Realm', key: 'sip_realm' },
{ header: 'Registration Webhook', key: 'url' },
{ header: 'Registration Webhook', key: 'url_reg' },
{ header: 'Queue Event Webhook', key: 'url_queue' }
]}
formatContentToDelete={formatAccountToDelete}
deleteContent={deleteAccount}

View File

@@ -2,7 +2,6 @@ import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import InternalTemplate from '../../templates/InternalTemplate';
import CarrierForm from '../../forms/CarrierForm';
import Sbcs from '../../blocks/Sbcs';
const CarriersAddEdit = () => {
let { voip_carrier_sid } = useParams();
@@ -15,7 +14,6 @@ const CarriersAddEdit = () => {
<InternalTemplate
type="form"
title={pageTitle}
subtitle={<Sbcs />}
breadcrumbs={[
{ name: 'Carriers', url: '/internal/carriers' },
{ name: pageTitle },

View File

@@ -1,27 +1,96 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useContext, useCallback } from 'react';
import React, { useEffect, useContext, useState } from 'react';
import axios from 'axios';
import { useHistory } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import styled from 'styled-components/macro';
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
import InternalTemplate from '../../templates/InternalTemplate';
import TableContent from '../../blocks/TableContent.js';
import Sbcs from '../../blocks/Sbcs';
import sortSipGateways from '../../../helpers/sortSipGateways';
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
import InputGroup from '../../../components/elements/InputGroup';
import Select from '../../../components/elements/Select';
import handleErrors from '../../../helpers/handleErrors';
const FilterLabel = styled.span`
color: #231f20;
text-align: right;
font-size: 14px;
margin-left: 1rem;
margin-right: 0.5rem;
`;
const StyledInputGroup = styled(InputGroup)`
padding: 1rem 1rem 0;
@media (max-width: 767.98px) {
display: grid;
grid-template-columns: auto 1fr auto 1fr;
grid-row-gap: 1rem;
}
@media (max-width: 575.98px) {
display: grid;
grid-template-columns: auto 1fr;
grid-row-gap: 1rem;
}
`;
const AccountSelect = styled(Select)`
min-width: 150px;
`;
const CarriersList = () => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const currentServiceProvider = useContext(ServiceProviderValueContext);
const location = useLocation();
const locationAccountSid = new URLSearchParams(location.search).get('account_sid');
const [accountSid, setAccountSid] = useState('');
const [accountList, setAccountList] = useState([]);
useEffect(() => {
document.title = `Carriers | Jambonz | Open Source CPAAS`;
}, []);
//=============================================================================
// Get accounts
//=============================================================================
useEffect(() => {
if (currentServiceProvider) {
const getAccounts = async () => {
try {
const accountResponse = await axios({
method: "get",
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
if (locationAccountSid) {
setAccountSid(locationAccountSid);
}
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
} catch (err) {
handleErrors({ err, history, dispatch });
}
};
getAccounts();
} else {
setAccountList([]);
}
}, [currentServiceProvider]);
//=============================================================================
// Get sip trunks
//=============================================================================
const getCarriers = useCallback(async () => {
const getCarriers = async () => {
try {
if (!localStorage.getItem('token')) {
history.push('/');
@@ -33,6 +102,7 @@ const CarriersList = () => {
return;
}
if(!currentServiceProvider) return [];
if (!accountList.length) return [];
// Get all SIP trunks
const trunkResults = await axios({
method: 'get',
@@ -43,9 +113,13 @@ const CarriersList = () => {
},
});
const trunkResultsFiltered = accountSid ?
trunkResults.data.filter(t => t.account_sid === accountSid) :
trunkResults.data.filter(t => t.account_sid === null);
// Add appropriate gateways to each trunk
const trunkMap = {};
for (const t of trunkResults.data) {
for (const t of trunkResultsFiltered) {
const gws = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
@@ -57,7 +131,7 @@ const CarriersList = () => {
trunkMap[t.voip_carrier_sid] = gws.data;
}
const trunksWithGateways = trunkResults.data.map(t => {
const trunksWithGateways = trunkResultsFiltered.map(t => {
const gateways = trunkMap[t.voip_carrier_sid] || [];
sortSipGateways(gateways);
return {
@@ -98,7 +172,7 @@ const CarriersList = () => {
console.log(err.response || err);
}
}
}, [currentServiceProvider, history, dispatch]);
};
//=============================================================================
// Delete sip trunk
@@ -171,8 +245,23 @@ const CarriersList = () => {
title="Carriers"
addButtonText="Add a Carrier"
addButtonLink="/internal/carriers/add"
subtitle={<Sbcs />}
>
<StyledInputGroup flexEnd space>
<FilterLabel htmlFor="account">Used By:</FilterLabel>
<AccountSelect
name="account"
id="account"
value={accountSid}
onChange={e => setAccountSid(e.target.value)}
>
<option value="">
All accounts
</option>
{accountList.map((acc) => (
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
))}
</AccountSelect>
</StyledInputGroup>
<TableContent
name="Carrier"
urlParam="carriers"

View File

@@ -58,6 +58,72 @@ const AccountSelect = styled(Select)`
min-width: 150px;
`;
const StyledPcapLink = styled.a`
display: flex;
justify-content: center;
align-items: center;
position: relative;
outline: 0;
height: 36px;
padding: 10px 26px 8px;
border-radius: 0.25rem;
background: #D91C5C;
color: #FFF;
text-decoration: none;
&:hover {
color: #FFF;
background: #BD164E;
}
`;
const PcapButton = ({call_data, account_sid, jwt_token}) => {
const [pcap, setPcap] = useState(null);
useEffect(() => {
axios({
method: "get",
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/Accounts/${account_sid}/RecentCalls/${call_data.sip_callid}`,
headers: {
Authorization: `Bearer ${jwt_token}`,
},
}).then((result_1) => {
if (result_1.status === 200 && result_1.data.total > 0) {
axios({
method: "get",
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/Accounts/${account_sid}/RecentCalls/${call_data.sip_callid}/pcap`,
headers: {
Authorization: `Bearer ${jwt_token}`,
},
responseType: "blob",
}).then((result_2) => {
setPcap({
dataUrl: URL.createObjectURL(result_2.data),
fileName: `callid-${call_data.sip_callid}.pcap`,
});
});
}
});
}, [call_data, account_sid, jwt_token, setPcap]);
if (pcap) {
return (
<Label>
<StyledPcapLink
href={pcap.dataUrl}
download={pcap.fileName}
>
Download pcap
</StyledPcapLink>
</Label>
);
}
return null;
};
const RecentCallsIndex = () => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
@@ -253,6 +319,7 @@ const RecentCallsIndex = () => {
</React.Fragment>
);
})}
<PcapButton call_data={data} account_sid={account} jwt_token={jwt} />
</ExpandedSection>
);
};

View File

@@ -2,7 +2,6 @@ import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import InternalTemplate from '../../templates/InternalTemplate';
import SpeechForm from '../../forms/SpeechForm';
import Sbcs from '../../blocks/Sbcs';
const SpeechServicesAddEdit = () => {
let { speech_service_sid } = useParams();
@@ -14,7 +13,6 @@ const SpeechServicesAddEdit = () => {
<InternalTemplate
type="form"
title={pageTitle}
subtitle={<Sbcs />}
breadcrumbs={[
{ name: 'Speech Services', url: '/internal/speech-services' },
{ name: pageTitle },

View File

@@ -1,24 +1,92 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useContext, useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import React, { useContext, useState, useEffect } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import axios from 'axios';
import styled from 'styled-components/macro';
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
import handleErrors from '../../../helpers/handleErrors';
import InternalTemplate from '../../templates/InternalTemplate';
import TableContent from '../../../components/blocks/TableContent';
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
import Sbcs from '../../blocks/Sbcs';
import InputGroup from '../../../components/elements/InputGroup';
import Select from '../../../components/elements/Select';
const FilterLabel = styled.span`
color: #231f20;
text-align: right;
font-size: 14px;
margin-left: 1rem;
margin-right: 0.5rem;
`;
const StyledInputGroup = styled(InputGroup)`
padding: 1rem 1rem 0;
@media (max-width: 767.98px) {
display: grid;
grid-template-columns: auto 1fr auto 1fr;
grid-row-gap: 1rem;
}
@media (max-width: 575.98px) {
display: grid;
grid-template-columns: auto 1fr;
grid-row-gap: 1rem;
}
`;
const AccountSelect = styled(Select)`
min-width: 150px;
`;
const SpeechServicesList = () => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const currentServiceProvider = useContext(ServiceProviderValueContext);
const jwt = localStorage.getItem('token');
const location = useLocation();
const locationAccountSid = new URLSearchParams(location.search).get('account_sid');
const [accountSid, setAccountSid] = useState('');
const [accountList, setAccountList] = useState([]);
//=============================================================================
// Get accounts
//=============================================================================
useEffect(() => {
if (currentServiceProvider) {
const getAccounts = async () => {
try {
const accountResponse = await axios({
method: "get",
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
headers: {
Authorization: `Bearer ${jwt}`,
},
});
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
if (locationAccountSid) {
setAccountSid(locationAccountSid);
}
} catch (err) {
handleErrors({ err, history, dispatch });
}
};
getAccounts();
} else {
setAccountList([]);
}
}, [currentServiceProvider]);
//=============================================================================
// Get speech services
//=============================================================================
const getSpeechServices = useCallback(async () => {
const jwt = localStorage.getItem('token');
const getSpeechServices = async () => {
try {
if (!jwt) {
history.push('/');
@@ -31,10 +99,14 @@ const SpeechServicesList = () => {
}
if(!currentServiceProvider) return [];
const speechApiUrl = accountSid ?
`/Accounts/${accountSid}/SpeechCredentials` :
`/ServiceProviders/${currentServiceProvider}/SpeechCredentials`;
const speechServices = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials`,
url: speechApiUrl,
headers: {
Authorization: `Bearer ${jwt}`,
},
@@ -137,7 +209,7 @@ const SpeechServicesList = () => {
} catch (err) {
handleErrors({ err, history, dispatch, fallbackMessage: 'Unable to get speech services' });
}
}, [currentServiceProvider]);
};
//=============================================================================
// Delete speech service
@@ -149,8 +221,7 @@ const SpeechServicesList = () => {
{ name: 'Last Used', content: s.last_used || 'Never' },
];
};
const deleteSpeechService = useCallback(async speechServiceToDelete => {
const jwt = localStorage.getItem('token');
const deleteSpeechService = async speechServiceToDelete => {
try {
if (!jwt) {
history.push('/');
@@ -187,7 +258,7 @@ const SpeechServicesList = () => {
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete speech service');
}
}
}, [currentServiceProvider]);
};
//=============================================================================
// Render
@@ -196,10 +267,25 @@ const SpeechServicesList = () => {
<InternalTemplate
type="normalTable"
title="Speech Services"
subtitle={<Sbcs />}
addButtonText="Add Speech Service"
addButtonLink="/internal/speech-services/add"
>
<StyledInputGroup flexEnd space>
<FilterLabel htmlFor="account">Used By:</FilterLabel>
<AccountSelect
name="account"
id="account"
value={accountSid}
onChange={e => setAccountSid(e.target.value)}
>
<option value="">
All accounts
</option>
{accountList.map((acc) => (
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
))}
</AccountSelect>
</StyledInputGroup>
<TableContent
normalTable
name="speech service"

View File

@@ -0,0 +1,94 @@
export default [
{
name: 'Asia (East)',
value: 'eastasia'
},
{
name: 'Asia (Southeast)',
value: 'southeastasia'
},
{
name: 'Australia (East)',
value: 'australiaeast'
},
{
name: 'Brazil (South)',
value: 'brazilsouth'
},
{
name: 'Canada (Central)',
value: 'canadacentral'
},
{
name: 'Europe (North)',
value: 'northeurope'
},
{
name: 'Europe (West)',
value: 'westeurope'
},
{
name: 'France (Central)',
value: 'francecentral'
},
{
name: 'Switzerland (North)',
value: 'switzerlandnorth'
},
{
name: 'India (Central)',
value: 'centralindia'
},
{
name: 'Japan (West)',
value: 'japanwest'
},
{
name: 'Japan (East)',
value: 'japaneast'
},
{
name: 'Korea (Central)',
value: 'koreacentral'
},
{
name: 'South Africa (North)',
value: 'southafricanorth'
},
{
name: 'UK (South)',
value: 'uksouth'
},
{
name: 'US (Cental)',
value: 'centralus'
},
{
name: 'US (West Central)',
value: 'westcentralus'
},
{
name: 'US (East)',
value: 'eastus'
},
{
name: 'US (East 2)',
value: 'eastus2'
},
{
name: 'US (North Central)',
value: 'northcentralus'
},
{
name: 'US (South Central)',
value: 'southcentralus'
},
{
name: 'US (West)',
value: 'westus'
},
{
name: 'US (West 2)',
value: 'westus2'
},
];

View File

@@ -0,0 +1,422 @@
export default [
{
name: 'Arabic (Algeria)',
code: 'ar-DZ'
},
{
name: 'Arabic (Bahrain)',
code: 'ar-BH'
},
{
name: 'Arabic (Egypt)',
code: 'ar-EG'
},
{
name: 'Arabic (Iraq)',
code: 'ar-IQ'
},
{
name: 'Arabic (Israel)',
code: 'ar-IL'
},
{
name: 'Arabic (Jordan)',
code: 'ar-JO'
},
{
name: 'Arabic (Kuwait)',
code: 'ar-KW'
},
{
name: 'Arabic (Lebanon)',
code: 'ar-LB'
},
{
name: 'Arabic (Libya)',
code: 'ar-LY'
},
{
name: 'Arabic (Morocco)',
code: 'ar-MA'
},
{
name: 'Arabic (Oman)',
code: 'ar-OM'
},
{
name: 'Arabic (Qatar)',
code: 'ar-QA'
},
{
name: 'Arabic (Saudi Arabia)',
code: 'ar-SA'
},
{
name: 'Arabic (Palestinian Authority)',
code: 'ar-PS'
},
{
name: 'Arabic (Syria)',
code: 'ar-SY'
},
{
name: 'Arabic (Tunisia)',
code: 'ar-TN'
},
{
name: 'Arabic (United Arab Emirates)',
code: 'ar-AE'
},
{
name: 'Arabic (Yemen)',
code: 'ar-YE'
},
{
name: 'Bulgarian (Bulgaria)',
code: 'bg-BG'
},
{
name: 'Catalan (Spain)',
code: 'ca-ES'
},
{
name: 'Chinese (Cantonese, Traditional)',
code: 'zh-HK'
},
{
name: 'Chinese (Mandarin, Simplified)',
code: 'zh-CN'
},
{
name: 'Chinese (Taiwanese Mandarin)',
code: 'zh-TW'
},
{
name: 'Croatian (Croatia)',
code: 'hr-HR'
},
{
name: 'Czech (Czech)',
code: 'cs-CZ'
},
{
name: 'Danish (Denmark)',
code: 'da-DK'
},
{
name: 'Dutch (Netherlands)',
code: 'nl-NL'
},
{
name: 'English (Australia)',
code: 'en-AU'
},
{
name: 'English (Canada)',
code: 'en-CA'
},
{
name: 'English (Ghana)',
code: 'en-GH'
},
{
name: 'English (Hong Kong)',
code: 'en-HK'
},
{
name: 'English (India)',
code: 'en-IN'
},
{
name: 'English (Ireland)',
code: 'en-IE'
},
{
name: 'English (Kenya)',
code: 'en-KE'
},
{
name: 'English (New Zealand)',
code: 'en-NZ'
},
{
name: 'English (Nigeria)',
code: 'en-NG'
},
{
name: 'English (Philippines)',
code: 'en-PH'
},
{
name: 'English (Singapore)',
code: 'en-SG'
},
{
name: 'English (South Africa)',
code: 'en-ZA'
},
{
name: 'English (Tanzania)',
code: 'en-TZ'
},
{
name: 'English (United Kingdom)',
code: 'en-GB'
},
{
name: 'English (United States)',
code: 'en-US'
},
{
name: 'Estonian(Estonia)',
code: 'et-EE'
},
{
name: 'Filipino (Philippines)',
code: 'fil-PH'
},
{
name: 'Finnish (Finland)',
code: 'fi-FI'
},
{
name: 'French (Canada)',
code: 'fr-CA'
},
{
name: 'French (France)',
code: 'fr-FR'
},
{
name: 'French (Switzerland)',
code: 'fr-CH'
},
{
name: 'German (Austria)',
code: 'de-AT'
},
{
name: 'German (Switzerland)',
code: 'de-CH'
},
{
name: 'German (Germany)',
code: 'de-DE'
},
{
name: 'Greek (Greece)',
code: 'el-GR'
},
{
name: 'Gujarati (Indian)',
code: 'gu-IN'
},
{
name: 'Hebrew (Israel)',
code: 'he-IL'
},
{
name: 'Hindi (India)',
code: 'hi-IN'
},
{
name: 'Hungarian (Hungary)',
code: 'hu-HU'
},
{
name: 'Indonesian (Indonesia)',
code: 'id-ID'
},
{
name: 'Irish (Ireland)',
code: 'ga-IE'
},
{
name: 'Italian (Italy)',
code: 'it-IT'
},
{
name: 'Japanese (Japan)',
code: 'ja-JP'
},
{
name: 'Kannada (India)',
code: 'kn-IN'
},
{
name: 'Korean (Korea)',
code: 'ko-KR'
},
{
name: 'Latvian (Latvia)',
code: 'lv-LV'
},
{
name: 'Lithuanian (Lithuania)',
code: 'lt-LT'
},
{
name: 'Malay (Malaysia)',
code: 'ms-MY'
},
{
name: 'Maltese (Malta)',
code: 'mt-MT'
},
{
name: 'Marathi (India)',
code: 'mr-IN'
},
{
name: 'Norwegian (Bokmål, Norway)',
code: 'nb-NO'
},
{
name: 'Persian (Iran)',
code: 'fa-IR'
},
{
name: 'Polish (Poland)',
code: 'pl-PL'
},
{
name: 'Portuguese (Brazil)',
code: 'pt-BR'
},
{
name: 'Portuguese (Portugal)',
code: 'pt-PT'
},
{
name: 'Romanian (Romania)',
code: 'ro-RO'
},
{
name: 'Russian (Russia)',
code: 'ru-RU'
},
{
name: 'Slovak (Slovakia)',
code: 'sk-SK'
},
{
name: 'Slovenian (Slovenia)',
code: 'sl-SI'
},
{
name: 'Spanish (Argentina)',
code: 'es-AR'
},
{
name: 'Spanish (Bolivia)',
code: 'es-BO'
},
{
name: 'Spanish (Chile)',
code: 'es-CL'
},
{
name: 'Spanish (Colombia)',
code: 'es-CO'
},
{
name: 'Spanish (Costa Rica)',
code: 'es-CR'
},
{
name: 'Spanish (Cuba)',
code: 'es-CU'
},
{
name: 'Spanish (Dominican Republic)',
code: 'es-DO'
},
{
name: 'Spanish (Ecuador)',
code: 'es-EC'
},
{
name: 'Spanish (El Salvador)',
code: 'es-SV'
},
{
name: 'Spanish (Equatorial Guinea)',
code: 'es-GQ'
},
{
name: 'Spanish (Guatemala)',
code: 'es-GT'
},
{
name: 'Spanish (Honduras)',
code: 'es-HN'
},
{
name: 'Spanish (Mexico)',
code: 'es-MX'
},
{
name: 'Spanish (Nicaragua)',
code: 'es-NI'
},
{
name: 'Spanish (Panama)',
code: 'es-PA'
},
{
name: 'Spanish (Paraguay)',
code: 'es-PY'
},
{
name: 'Spanish (Peru)',
code: 'es-PE'
},
{
name: 'Spanish (Puerto Rico)',
code: 'es-PR'
},
{
name: 'Spanish (Spain)',
code: 'es-ES'
},
{
name: 'Spanish (Uruguay)',
code: 'es-UY'
},
{
name: 'Spanish (USA)',
code: 'es-US'
},
{
name: 'Spanish (Venezuela)',
code: 'es-VE'
},
{
name: 'Swahili (Kenya)',
code: 'sw-KE'
},
{
name: 'Swedish (Sweden)',
code: 'sv-SE'
},
{
name: 'Tamil (India)',
code: 'ta-IN'
},
{
name: 'Telugu (India)',
code: 'te-IN'
},
{
name: 'Thai (Thailand)',
code: 'th-TH'
},
{
name: 'Turkish (Turkey)',
code: 'tr-TR'
},
{
name: 'Vietnamese (Vietnam)',
code: 'vi-VN'
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
export default [
{
code: 'en-US',
name: 'English (US)',
voices: [
{ value: '3', name: 'Alana B.' },
{ value: '4', name: 'Ramona J.' },
{ value: '5', name: 'Ramona J. (promo)' },
{ value: '7', name: 'Wade C.' },
{ value: '8', name: 'Sofia H.' },
{ value: '9', name: 'David D.' },
{ value: '11', name: 'Isabel V.' },
{ value: '12', name: 'Ava H.' },
{ value: '13', name: 'Jeremy G.' },
{ value: '14', name: 'Nicole L.' },
{ value: '15', name: 'Paige L.' },
{ value: '16', name: 'Tobin A.' },
{ value: '17', name: 'Kai M.' },
{ value: '18', name: 'Tristan F.' },
{ value: '19', name: 'Patrick K.' },
{ value: '20', name: 'Soifia H. (promo)' },
{ value: '21', name: 'Damian P. (promo)' },
{ value: '22', name: 'Jodi P. (promo)' },
{ value: '23', name: 'Lee M. (promo)' },
{ value: '24', name: 'Selene R. (promo)' },
{ value: '26', name: 'Wade C. (promo)' },
{ value: '27', name: 'Joe F.' },
{ value: '28', name: 'Joe F. (promo)' },
{ value: '29', name: 'Garry J. (character)' },
{ value: '33', name: 'Jude D.' },
{ value: '34', name: 'Eric S. (promo)' },
{ value: '35', name: 'Chase J.' },
{ value: '37', name: 'Steve B. (promo)' },
{ value: '38', name: 'Bella B. (promo)' },
{ value: '39', name: 'Tilda C. (promo)' },
{ value: '41', name: 'Paul B. (promo)' }
],
}
];