mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-01-25 02:08:19 +00:00
Compare commits
2 Commits
main
...
v0.7.7-rc5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b9d206f4f | ||
|
|
68d88decb5 |
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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,12 @@ 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('');
|
||||
const [tmpCustomTtsEndpoint, setTmpCustomTtsEndpoint] = useState('');
|
||||
const [tmpCustomSttEndpoint, setTmpCustomSttEndpoint] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [invalidVendorGoogle, setInvalidVendorGoogle] = useState(false);
|
||||
@@ -106,6 +114,8 @@ const SpeechServicesAddEdit = (props) => {
|
||||
const [invalidApiKey, setInvalidApiKey] = useState(false);
|
||||
const [invalidRegion, setInvalidRegion] = useState(false);
|
||||
const [invalidAwsRegion, setInvalidAwsRegion] = useState(false);
|
||||
const [invalidUseCustomTts, setInvalidUseCustomTts] = useState(false);
|
||||
const [invalidUseCustomStt, setInvalidUseCustomStt] = useState(false);
|
||||
|
||||
const [originalTtsValue, setOriginalTtsValue] = useState(null);
|
||||
const [originalSttValue, setOriginalSttValue] = useState(null);
|
||||
@@ -162,6 +172,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 +246,8 @@ const SpeechServicesAddEdit = (props) => {
|
||||
setInvalidUseForTts(false);
|
||||
setInvalidUseForStt(false);
|
||||
setInvalidApiKey(false);
|
||||
setInvalidUseCustomTts(false);
|
||||
setInvalidUseCustomStt(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
@@ -305,6 +321,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.');
|
||||
setInvalidUseCustomStt(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refUseCustomStt.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
@@ -333,16 +367,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 +702,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</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</Label>
|
||||
<Input
|
||||
name="customSpeechEndpoint"
|
||||
id="customSpeechEndpoint"
|
||||
value={customSttEndpoint}
|
||||
onChange={e => setCustomSttEndpoint(e.target.value)}
|
||||
placeholder="Custom speech endpoint"
|
||||
invalid={invalidUseCustomStt}
|
||||
ref={refUseCustomStt}
|
||||
disabled={!useCustomStt}
|
||||
/>
|
||||
</>
|
||||
) : vendor === 'wellsaid' ? (
|
||||
<>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user