Compare commits

...

9 Commits

Author SHA1 Message Date
Dave Horton
b8c67143bd add support for Azure custom vocies 2022-11-04 08:27:40 -04:00
Dave Horton
a67f6ad214 update package-lock.json 2022-10-23 12:26:13 -04:00
Markus Frindt
57dd168272 [snyk] Fix vulnerabilities (#137)
Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2022-10-20 10:24:58 -04:00
Dave Horton
6b7e4a34bb bump version 2022-10-13 16:03:25 -04:00
Dave Horton
50216d5345 comment out azure custom voice for now.. 2022-10-09 17:12:28 +01:00
Dave Horton
4bdbd6fc0f add support for adding new carrier settings for outbound register 2022-10-07 09:42:50 +01:00
xquanluu
ebcdefb945 Fix: update custome speech label content (#132) 2022-10-04 20:13:40 -07:00
xquanluu
6b9d206f4f Legacy: Microsoft custom TTS and STT endpoints (#120)
* feat: add custom tts and stt endpoints for microsoft

* feat: add custom tts and stt endpoints for microsoft

* fix: send null microsoft custom endpoints incase nothing is provided

* fix: don't sent speech credentials in PUT method

* fix:does not clear the custom stt/tts endpoints value when it's disabled

* use tmp state to store custom tts/stt endpoint when enable/disable it
2022-09-30 10:35:23 +01:00
xquanluu
68d88decb5 feat: add Service provider limits and account limits to settings (#112)
* feat: add Service provider limits and account limits to settings

* fix: review comments

* fix: review comments

* fix: review comments

* fix: review comments

* fix: review comment

* fix: review comment

* fix: review comment

* fix: review comment

* fix: review comments

* fix: review comments

* fix: review comments

* fix: review comments
2022-09-27 08:17:27 +01:00
8 changed files with 399 additions and 69 deletions

View File

@@ -1,4 +1,4 @@
FROM node:18.8.0-alpine as builder
FROM node:18.9.0-alpine3.16 as builder
RUN apk update && apk add --no-cache python3 make g++
COPY . /opt/app
WORKDIR /opt/app/

150
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "jambonz-webapp",
"version": "v0.7.6",
"version": "v0.7.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "jambonz-webapp",
"version": "v0.7.6",
"version": "v0.7.7",
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.5",
@@ -2418,6 +2418,58 @@
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.17",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
"integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
"dependencies": {
"@jridgewell/resolve-uri": "3.1.0",
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -13819,9 +13871,9 @@
}
},
"node_modules/serve": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/serve/-/serve-13.0.2.tgz",
"integrity": "sha512-71R6fKvNgKrqARAag6lYJNnxDzpH7DCNrMuvPY5PLVaC2PDhJsGTj/34o4o4tPWhTuLgEXqvgnAWbATQ9zGZTQ==",
"version": "13.0.4",
"resolved": "https://registry.npmjs.org/serve/-/serve-13.0.4.tgz",
"integrity": "sha512-Lj8rhXmphJCRQVv5qwu0NQZ2h+0MrRyRJxDZu5y3qLH2i/XY6a0FPj/VmjMUdkJb672MBfE8hJ274PU6JzBd0Q==",
"dependencies": {
"@zeit/schemas": "2.6.0",
"ajv": "6.12.6",
@@ -14730,12 +14782,13 @@
}
},
"node_modules/terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz",
"integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==",
"dependencies": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"bin": {
@@ -14743,14 +14796,6 @@
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"acorn": "^8.5.0"
},
"peerDependenciesMeta": {
"acorn": {
"optional": true
}
}
},
"node_modules/terser-webpack-plugin": {
@@ -14791,14 +14836,6 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/terser/node_modules/source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"engines": {
"node": ">= 8"
}
},
"node_modules/test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -17794,6 +17831,49 @@
"chalk": "^4.0.0"
}
},
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"requires": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
},
"@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="
},
"@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"requires": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
},
"@jridgewell/trace-mapping": {
"version": "0.3.17",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
"integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
"requires": {
"@jridgewell/resolve-uri": "3.1.0",
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -26006,9 +26086,9 @@
}
},
"serve": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/serve/-/serve-13.0.2.tgz",
"integrity": "sha512-71R6fKvNgKrqARAag6lYJNnxDzpH7DCNrMuvPY5PLVaC2PDhJsGTj/34o4o4tPWhTuLgEXqvgnAWbATQ9zGZTQ==",
"version": "13.0.4",
"resolved": "https://registry.npmjs.org/serve/-/serve-13.0.4.tgz",
"integrity": "sha512-Lj8rhXmphJCRQVv5qwu0NQZ2h+0MrRyRJxDZu5y3qLH2i/XY6a0FPj/VmjMUdkJb672MBfE8hJ274PU6JzBd0Q==",
"requires": {
"@zeit/schemas": "2.6.0",
"ajv": "6.12.6",
@@ -26710,12 +26790,13 @@
}
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz",
"integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==",
"requires": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
@@ -26723,11 +26804,6 @@
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
}
}
},

View File

@@ -1,6 +1,6 @@
{
"name": "jambonz-webapp",
"version": "v0.7.6",
"version": "v0.7.7",
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.5",

View File

@@ -22,7 +22,7 @@ import CopyableText from '../elements/CopyableText';
import Span from '../elements/Span';
import handleErrors from "../../helpers/handleErrors";
import styled from 'styled-components/macro';
import { APP_API_BASE_URL } from "../../constants";
import { APP_API_BASE_URL, LIMITS } from "../../constants";
const StyledInputGroup = styled(InputGroup)`
position: relative;
@@ -94,6 +94,7 @@ const AccountForm = props => {
const [ sbcs, setSbcs ] = useState([]);
const [ subspaceSipRealmOtherValue, setSubspaceSipRealmOtherValue ] = useState('');
const [ subspaceEnable, setSubspaceEnable ] = useState(false);
const [localLimits, setLocalLimits] = useState([]);
// Invalid form inputs
const [ invalidName, setInvalidName ] = useState(false);
@@ -319,6 +320,16 @@ const AccountForm = props => {
},
});
promiseList.push(applicationsPromise);
const limitsPromise = axios({
method: 'get',
baseURL: APP_API_BASE_URL,
url: `/Accounts/${props.account_sid}/Limits`,
headers: {
Authorization: `Bearer ${jwt}`,
},
});
promiseList.push(limitsPromise);
}
const sbcsPromise = await axios({
@@ -337,13 +348,16 @@ const AccountForm = props => {
setAccounts(accountsData);
if (props.type === 'edit') {
// Application Data
const allApplications = (promiseAllValues[1] && promiseAllValues[1].data) || [];
const accountApplicationsData = allApplications.filter(app => {
return app.account_sid === props.account_sid;
});
setAccountApplications(accountApplicationsData);
// Limits Data
setLocalLimits(promiseAllValues[2]?.data);
}
setSbcs(promiseAllValues[2]?.data);
setSbcs(promiseAllValues[3]?.data);
if (props.type === 'setup' && accountsData.length > 1) {
history.push('/internal/accounts');
@@ -558,7 +572,7 @@ const AccountForm = props => {
? `/Accounts`
: `/Accounts/${accountSid}`;
await axios({
const accountResp = await axios({
method: props.type === 'add' ? 'post' : 'put',
baseURL: APP_API_BASE_URL,
url,
@@ -567,6 +581,23 @@ const AccountForm = props => {
},
data: axiosData,
});
// Update Limits
const acc_sid = accountSid ? accountSid : accountResp.data.sid;
await Promise.all(
localLimits.map(l => {
const method = l.quantity === "" ? 'delete' : 'post';
const limitUrl = l.quantity === "" ? `/Accounts/${props.account_sid}/Limits?category=${l.category}` : `/Accounts/${acc_sid}/Limits`;
return axios({
method: method,
baseURL: APP_API_BASE_URL,
url: limitUrl,
headers: {
Authorization: `Bearer ${jwt}`,
},
...(method === 'post' && {data: l})
});
})
);
if (props.type === 'setup') {
isMounted = false;
@@ -892,6 +923,31 @@ const AccountForm = props => {
</Button>
)}
{LIMITS.map(({ label, category }) => {
const quantity = localLimits?.find(l => l.category === category)?.quantity;
return <React.Fragment key={category}>
<Label htmlFor={category}>{label}</Label>
<Input
name={category}
id={category}
type="number"
placeholder="Enter Quantity (0=unlimited)"
min="0"
value={quantity >= 0 ? quantity : ""}
onChange={e => {
const limit = localLimits.find(l => l.category === category);
const value = e.target.value ? Number(e.target.value) : "";
if (limit) {
setLocalLimits(localLimits.map(l => l.category === category ? {...l, quantity: value} : l));
} else {
setLocalLimits([...localLimits, {category, quantity: value}]);
}
}}
>
</Input>
</React.Fragment>;
})}
{ process.env.REACT_APP_ENABLE_SUBSPACE ? (
<>
<Label htmlFor="subspaceId">Subspace</Label>
@@ -1022,7 +1078,6 @@ const AccountForm = props => {
)}
</>
) : null }
{errorMessage && (
<FormError grid message={errorMessage} />
)}

View File

@@ -221,6 +221,8 @@ const CarrierForm = (props) => {
const refUsername = useRef(null);
const refPassword = useRef(null);
const refRealm = useRef(null);
const refFromUser = useRef(null);
const refFromDomain = useRef(null);
const refIp = useRef([]);
const refPort = useRef([]);
const refInbound = useRef([]);
@@ -250,6 +252,8 @@ const CarrierForm = (props) => {
const [ passwordInvalid, setPasswordInvalid ] = useState(false);
const [ realm, setRealm ] = useState('');
const [ realmInvalid, setRealmInvalid ] = useState(false);
const [ fromUser, setFromUser ] = useState('');
const [ fromDomain, setFromDomain ] = useState('');
const [ sipGateways, setSipGateways ] = useState([
{
sip_gateway_sid: '',
@@ -276,6 +280,7 @@ const CarrierForm = (props) => {
const [suportSIP, setSupportSIP] = useState(false);
const [diversion, setDiversion] = useState("");
const [carrierActive, setCarrierActive] = useState(true);
const [registerHasPublicIp, setRegisterHasPublicIp] = useState(false);
const [predefinedCarriers, setPredefinedCarriers] = useState([]);
const [activeTab, setActiveTab] = useState('1');
const [sbcs, setSbcs] = useState([]);
@@ -438,6 +443,8 @@ const CarrierForm = (props) => {
setUsername(carrier.register_username || '');
setPassword(carrier.register_password || '');
setRealm(carrier.register_sip_realm || '');
setFromUser(carrier.register_from_user || '');
setFromDomain(carrier.register_from_domain || '');
setSipGateways(currentSipGateways.map(s => ({
sip_gateway_sid: s.sip_gateway_sid,
ip: s.ipv4,
@@ -473,6 +480,7 @@ const CarrierForm = (props) => {
setSupportSIP(carrier.diversion ? true : false);
setDiversion(carrier.diversion || '');
setCarrierActive(carrier.is_active === 1);
setRegisterHasPublicIp(carrier.register_public_ip_in_contact === 1);
} else {
const result = await axios({
method: 'get',
@@ -1028,6 +1036,9 @@ const CarrierForm = (props) => {
register_username: username ? username.trim() : null,
register_password: password ? password : null,
register_sip_realm: register ? realm.trim() : null,
register_from_user: register && fromUser ? fromUser.trim() : null,
register_from_domain: register && fromDomain ? fromDomain.trim() : null,
register_public_ip_in_contact: register && registerHasPublicIp ? 1 : 0,
tech_prefix: techPrefix ? techPrefix.trim() : null,
diversion: diversion ? diversion.trim() : null,
is_active: carrierActive ? 1 : 0,
@@ -1484,6 +1495,33 @@ const CarrierForm = (props) => {
invalid={realmInvalid}
ref={refRealm}
/>
<Label htmlFor="fromUser">SIP From User</Label>
<Input
name="fromUser"
id="fromUser"
value={fromUser}
onChange={e => setFromUser(e.target.value)}
placeholder="Optional: specify user part of SIP From header"
ref={refFromUser}
/>
<Label htmlFor="fromDomain">SIP From Domain</Label>
<Input
name="fromDomain"
id="fromDomain"
value={fromDomain}
onChange={e => setFromDomain(e.target.value)}
placeholder="Optional: specify host part of SIP From header"
ref={refFromDomain}
/>
<Label htmlFor="regPublicIp">Use Public IP in Contact</Label>
<Checkbox
noLeftMargin
name="regPublicIp"
id="regPublicIp"
label=""
checked={registerHasPublicIp}
onChange={e => setRegisterHasPublicIp(e.target.checked)}
/>
</>
) : (
null

View File

@@ -16,7 +16,7 @@ import Loader from '../blocks/Loader';
import Modal from '../blocks/Modal';
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
import handleErrors from "../../helpers/handleErrors";
import { APP_API_BASE_URL } from "../../constants";
import { APP_API_BASE_URL, LIMITS } from "../../constants";
const Td = styled.td`
padding: 0.5rem 0;
@@ -60,6 +60,19 @@ const SettingsForm = () => {
const [serviceProviderSid, setServiceProviderSid] = useState('');
const [serviceProviders, setServiceProviders] = useState([]);
const [confirmDelete, setConfirmDelete] = useState(false);
const [localLimits, setLocalLimits] = useState([]);
const callApi = async (path, method, data) => {
return await axios({
method: method,
baseURL: APP_API_BASE_URL,
url: path,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
...(data && {data})
});
};
useEffect(() => {
const getSettingsData = async () => {
@@ -74,14 +87,7 @@ const SettingsForm = () => {
return;
}
const serviceProvidersResponse = await axios({
method: 'get',
baseURL: APP_API_BASE_URL,
url: `/ServiceProviders`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const serviceProvidersResponse = await callApi(`/ServiceProviders`, 'get');
const sps = serviceProvidersResponse.data;
const sp = sps.find(s => s.service_provider_sid === currentServiceProvider);
@@ -91,6 +97,12 @@ const SettingsForm = () => {
setServiceProviderSid(sp.service_provider_sid || '');
setEnableMsTeams(sp.ms_teams_fqdn ? true : false);
setSbcDomainName(sp.ms_teams_fqdn || '');
// Fetch Service provider Limits
if (sp.service_provider_sid) {
const serviceProvidersLimitsResponse = await callApi(`/ServiceProviders/${sp.service_provider_sid}/Limits`, 'get');
setLocalLimits(serviceProvidersLimitsResponse.data);
}
} catch (err) {
handleErrors({ err, history, dispatch });
} finally {
@@ -208,15 +220,12 @@ const SettingsForm = () => {
name: serviceProviderName.trim(),
};
await axios({
method: 'put',
baseURL: APP_API_BASE_URL,
url: `/ServiceProviders/${serviceProviderSid}`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
data,
});
await callApi(`/ServiceProviders/${serviceProviderSid}`, 'put', data);
await Promise.all(
localLimits.map(l => l.quantity === "" ?
callApi(`/ServiceProviders/${serviceProviderSid}/Limits?category=${l.category}`, 'delete') :
callApi(`/ServiceProviders/${serviceProviderSid}/Limits`, 'post', l))
);
refreshMsTeamsData();
@@ -272,6 +281,7 @@ const SettingsForm = () => {
invalid={invalidServiceProviderName}
ref={refServiceProviderName}
/>
<div>{/* needed for CSS grid layout */}</div>
<Checkbox
noLeftMargin
@@ -297,6 +307,30 @@ const SettingsForm = () => {
title={(!enableMsTeams && "You must enable Microsoft Teams Direct Routing in order to provide an SBC Domain Name") || ""}
/>
{LIMITS.map(({ label, category }) => {
const quantity = localLimits?.find(l => l.category === category)?.quantity;
return <React.Fragment key={category}>
<Label htmlFor={category}>{label}</Label>
<Input
name={category}
id={category}
type="number"
placeholder="Enter Quantity (0=unlimited)"
min="0"
value={quantity >= 0 ? quantity : ""}
onChange={e => {
const limit = localLimits.find(l => l.category === category);
const value = e.target.value ? Number(e.target.value) : "";
if (limit) {
setLocalLimits(localLimits.map(l => l.category === category ? {...l, quantity: value} : l));
} else {
setLocalLimits([...localLimits, {category, quantity: value}]);
}
}}
/>
</React.Fragment>;
})}
{errorMessage && !confirmDelete && (
<FormError grid message={errorMessage} />
)}

View File

@@ -79,6 +79,8 @@ const SpeechServicesAddEdit = (props) => {
const refApiKey = useRef(null);
const refRegion = useRef(null);
const refAwsRegion = useRef(null);
const refUseCustomTts = useRef(null);
const refUseCustomStt = useRef(null);
// Form inputs
const [vendor, setVendor] = useState('');
@@ -93,6 +95,13 @@ const SpeechServicesAddEdit = (props) => {
const [apiKey, setApiKey] = useState('');
const [region, setRegion] = useState('');
const [awsregion, setAwsRegion] = useState('');
const [useCustomTts, setUseCustomTts] = useState(false);
const [useCustomStt, setUseCustomStt] = useState(false);
const [customTtsEndpoint, setCustomTtsEndpoint] = useState('');
const [customSttEndpoint, setCustomSttEndpoint] = useState('');
// eslint-disable-next-line no-unused-vars
const [tmpCustomTtsEndpoint, setTmpCustomTtsEndpoint] = useState('');
const [tmpCustomSttEndpoint, setTmpCustomSttEndpoint] = useState('');
// Invalid form inputs
const [invalidVendorGoogle, setInvalidVendorGoogle] = useState(false);
@@ -106,6 +115,9 @@ const SpeechServicesAddEdit = (props) => {
const [invalidApiKey, setInvalidApiKey] = useState(false);
const [invalidRegion, setInvalidRegion] = useState(false);
const [invalidAwsRegion, setInvalidAwsRegion] = useState(false);
// eslint-disable-next-line no-unused-vars
const [invalidUseCustomTts, setInvalidUseCustomTts] = useState(false);
const [invalidUseCustomStt, setInvalidUseCustomStt] = useState(false);
const [originalTtsValue, setOriginalTtsValue] = useState(null);
const [originalSttValue, setOriginalSttValue] = useState(null);
@@ -162,6 +174,10 @@ const SpeechServicesAddEdit = (props) => {
setUseForStt(speechCredential.data.use_for_stt || false);
setOriginalTtsValue(speechCredential.data.use_for_tts || false);
setOriginalSttValue(speechCredential.data.use_for_stt || false);
setUseCustomTts(speechCredential.data.use_custom_tts || false);
setCustomTtsEndpoint(speechCredential.data.custom_tts_endpoint || '');
setUseCustomStt(speechCredential.data.use_custom_stt || false);
setCustomSttEndpoint(speechCredential.data.custom_stt_endpoint || '');
}
setShowLoader(false);
} catch (err) {
@@ -232,6 +248,8 @@ const SpeechServicesAddEdit = (props) => {
setInvalidUseForTts(false);
setInvalidUseForStt(false);
setInvalidApiKey(false);
setInvalidUseCustomTts(false);
setInvalidUseCustomStt(false);
let errorMessages = [];
let focusHasBeenSet = false;
@@ -305,6 +323,24 @@ const SpeechServicesAddEdit = (props) => {
}
}
if (useCustomTts && !customTtsEndpoint) {
errorMessages.push('Please provide a custom voice endpoint.');
setInvalidUseCustomTts(true);
if (!focusHasBeenSet) {
refUseCustomTts.current.focus();
focusHasBeenSet = true;
}
}
if (useCustomStt && !customSttEndpoint) {
errorMessages.push('Please provide a custom speech endpoint Id.');
setInvalidUseCustomStt(true);
if (!focusHasBeenSet) {
refUseCustomStt.current.focus();
focusHasBeenSet = true;
}
}
if (errorMessages.length > 1) {
setErrorMessage(errorMessages);
return;
@@ -333,16 +369,29 @@ const SpeechServicesAddEdit = (props) => {
},
data: {
vendor,
service_key: vendor === 'google' ? JSON.stringify(serviceKey) : null,
access_key_id: vendor === 'aws' ? accessKeyId : null,
secret_access_key: vendor === 'aws' ? secretAccessKey : null,
aws_region: vendor === 'aws' ? awsregion : 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,
use_for_tts: useForTts,
use_for_stt: useForStt,
...(vendor === 'google' && method === 'post' && {
service_key: serviceKey ? JSON.stringify(serviceKey) : null,
}),
...(vendor === 'aws' && {
...(method === 'post' && {
access_key_id: accessKeyId || null,
secret_access_key: secretAccessKey || null}),
aws_region: awsregion || null
}),
...(vendor === 'microsoft' && {
region: region || null,
use_custom_tts: useCustomTts ? 1 : 0,
use_custom_stt: useCustomStt ? 1 : 0,
custom_tts_endpoint: customTtsEndpoint || null,
custom_stt_endpoint: customSttEndpoint || null,
}),
...(['wellsaid', 'microsoft'].includes(vendor) && method === 'post' && {
api_key: apiKey || null,
}),
}
});
@@ -655,6 +704,70 @@ const SpeechServicesAddEdit = (props) => {
</option>
))}
</Select>
<div />
<Checkbox
noLeftMargin
name="useACustomVoice"
id="useACustomVoice"
label="Use a custom voice"
checked={useCustomTts}
onChange={e => {
setUseCustomTts(e.target.checked);
if (e.target.checked && tmpCustomTtsEndpoint) {
setCustomTtsEndpoint(tmpCustomTtsEndpoint);
}
if (!e.target.checked) {
setTmpCustomTtsEndpoint(customTtsEndpoint);
setCustomTtsEndpoint("");
}
}}
invalid={invalidUseCustomTts}
ref={refUseCustomTts}
/>
<Label htmlFor="customVoiceEndpoint">Custom voice endpoint Id</Label>
<Input
name="customVoiceEndpoint"
id="customVoiceEndpoint"
value={customTtsEndpoint}
onChange={e => setCustomTtsEndpoint(e.target.value)}
placeholder="Custom voice endpoint"
invalid={invalidUseCustomTts}
ref={refUseCustomTts}
disabled={!useCustomTts}
/>
<div />
<Checkbox
noLeftMargin
name="useACustomSpeechModel"
id="useACustomSpeechModel"
label="Use a custom speech model"
checked={useCustomStt}
onChange={e => {
setUseCustomStt(e.target.checked);
if(e.target.checked && tmpCustomSttEndpoint) {
setCustomSttEndpoint(tmpCustomSttEndpoint);
}
if(!e.target.checked) {
setTmpCustomSttEndpoint(customSttEndpoint);
setCustomSttEndpoint("");
}
}}
invalid={invalidUseCustomStt}
ref={refUseCustomStt}
/>
<Label htmlFor="customSpeechEndpoint">Custom speech endpoint Id</Label>
<Input
name="customSpeechEndpoint"
id="customSpeechEndpoint"
value={customSttEndpoint}
onChange={e => setCustomSttEndpoint(e.target.value)}
placeholder="Custom speech endpoint Id"
invalid={invalidUseCustomStt}
ref={refUseCustomStt}
disabled={!useCustomStt}
/>
</>
) : vendor === 'wellsaid' ? (
<>

View File

@@ -1,2 +1,16 @@
const { REACT_APP_API_BASE_URL } = process.env;
export const APP_API_BASE_URL = (window.JAMBONZ) ? window.JAMBONZ.APP_API_BASE_URL : REACT_APP_API_BASE_URL;
export const LIMITS = [
// {
// label: "Max registered devices (0=unlimited)",
// category: "device",
// },
// {
// label: "Max api calls per minute (0=unlimited)",
// category: "api_rate",
// },
{
label: "Max concurrent calls (0=unlimited)",
category: "voice_call_session",
},
];