mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-01-25 02:08:19 +00:00
Compare commits
33 Commits
v0.7.5-rc9
...
legacy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a75ea8fe5c | ||
|
|
06f826c6da | ||
|
|
20b77575c0 | ||
|
|
3c3ebe0948 | ||
|
|
4509c52f67 | ||
|
|
4535820828 | ||
|
|
f743ca47a9 | ||
|
|
b8c67143bd | ||
|
|
a67f6ad214 | ||
|
|
57dd168272 | ||
|
|
6b7e4a34bb | ||
|
|
50216d5345 | ||
|
|
4bdbd6fc0f | ||
|
|
ebcdefb945 | ||
|
|
6b9d206f4f | ||
|
|
68d88decb5 | ||
|
|
1276687cc0 | ||
|
|
b97d5e538f | ||
|
|
2704e97a96 | ||
|
|
4cfdfc3b49 | ||
|
|
47d73a7edd | ||
|
|
c14fa5db34 | ||
|
|
668d7f05f6 | ||
|
|
5ae88ff13e | ||
|
|
06c21f2545 | ||
|
|
663aabc80c | ||
|
|
a7de0a494e | ||
|
|
b742e67715 | ||
|
|
013681e7eb | ||
|
|
4912758120 | ||
|
|
bb335d0838 | ||
|
|
38d26dddc8 | ||
|
|
c0d531c63f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
39
Dockerfile
39
Dockerfile
@@ -1,18 +1,23 @@
|
||||
FROM node:alpine as builder
|
||||
RUN apk update && apk add --no-cache python3 make g++
|
||||
COPY . /opt/app
|
||||
WORKDIR /opt/app/
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
RUN npm prune
|
||||
FROM --platform=linux/amd64 node:18.14.0-alpine3.16 as base
|
||||
|
||||
FROM node:alpine as webapp
|
||||
RUN apk add curl
|
||||
WORKDIR /opt/app
|
||||
COPY . /opt/app
|
||||
COPY --from=builder /opt/app/node_modules ./node_modules
|
||||
COPY --from=builder /opt/app/build ./build
|
||||
COPY ./entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
RUN apk --update --no-cache add --virtual .builds-deps build-base curl python3
|
||||
|
||||
WORKDIR /opt/app/
|
||||
|
||||
FROM base as build
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM base
|
||||
|
||||
COPY --from=build /opt/app /opt/app/
|
||||
|
||||
RUN chmod +x /opt/app/entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/opt/app/entrypoint.sh"]
|
||||
|
||||
@@ -4,7 +4,9 @@ API_PORT="${API_PORT:-3000}"
|
||||
API_VERSION="${API_VERSION:-v1}"
|
||||
REACT_APP_API_BASE_URL=${REACT_APP_API_BASE_URL:-http://$PUBLIC_IPV4:$API_PORT/$API_VERSION}
|
||||
echo "REACT_APP_API_BASE_URL=${REACT_APP_API_BASE_URL}" > /opt/app/.env
|
||||
REACT_APP_ENABLE_ACCOUNT_LIMITS_ALL=${REACT_APP_ENABLE_ACCOUNT_LIMITS_ALL:-0}
|
||||
echo "REACT_APP_ENABLE_ACCOUNT_LIMITS_ALL=${REACT_APP_ENABLE_ACCOUNT_LIMITS_ALL}" >> /opt/app/.env
|
||||
cd /opt/app/
|
||||
TAG="<script>window.JAMBONZ = { APP_API_BASE_URL: '${REACT_APP_API_BASE_URL}'};</script>"
|
||||
TAG="<script>window.JAMBONZ = { APP_API_BASE_URL: '${REACT_APP_API_BASE_URL}', APP_ENABLE_ACCOUNT_LIMITS_ALL: ${REACT_APP_ENABLE_ACCOUNT_LIMITS_ALL}};</script>"
|
||||
sed -i -e "\@</head>@i\ $TAG" ./build/index.html
|
||||
npm run serve
|
||||
|
||||
1771
package-lock.json
generated
1771
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "jambonz-webapp",
|
||||
"version": "v0.7.5",
|
||||
"version": "v0.7.9",
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"antd": "^4.15.4",
|
||||
"antd": "^4.21.0",
|
||||
"axios": "^0.21.1",
|
||||
"moment": "^2.29.1",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
|
||||
BIN
public/fonts/objectivity-bold-webfont.woff
Normal file
BIN
public/fonts/objectivity-bold-webfont.woff
Normal file
Binary file not shown.
BIN
public/fonts/objectivity-bold-webfont.woff2
Normal file
BIN
public/fonts/objectivity-bold-webfont.woff2
Normal file
Binary file not shown.
BIN
public/fonts/objectivity-boldslanted-webfont.woff
Normal file
BIN
public/fonts/objectivity-boldslanted-webfont.woff
Normal file
Binary file not shown.
BIN
public/fonts/objectivity-boldslanted-webfont.woff2
Normal file
BIN
public/fonts/objectivity-boldslanted-webfont.woff2
Normal file
Binary file not shown.
BIN
public/fonts/objectivity-medium-webfont.woff
Normal file
BIN
public/fonts/objectivity-medium-webfont.woff
Normal file
Binary file not shown.
BIN
public/fonts/objectivity-medium-webfont.woff2
Normal file
BIN
public/fonts/objectivity-medium-webfont.woff2
Normal file
Binary file not shown.
BIN
public/fonts/objectivity-mediumslanted-webfont.woff
Normal file
BIN
public/fonts/objectivity-mediumslanted-webfont.woff
Normal file
Binary file not shown.
BIN
public/fonts/objectivity-mediumslanted-webfont.woff2
Normal file
BIN
public/fonts/objectivity-mediumslanted-webfont.woff2
Normal file
Binary file not shown.
BIN
public/fonts/objectivity-regular-webfont.woff
Normal file
BIN
public/fonts/objectivity-regular-webfont.woff
Normal file
Binary file not shown.
BIN
public/fonts/objectivity-regular-webfont.woff2
Normal file
BIN
public/fonts/objectivity-regular-webfont.woff2
Normal file
Binary file not shown.
BIN
public/fonts/objectivity-regularslanted-webfont.woff
Normal file
BIN
public/fonts/objectivity-regularslanted-webfont.woff
Normal file
Binary file not shown.
BIN
public/fonts/objectivity-regularslanted-webfont.woff2
Normal file
BIN
public/fonts/objectivity-regularslanted-webfont.woff2
Normal file
Binary file not shown.
@@ -21,6 +21,7 @@ const StyledLink = styled(FilteredLink)`
|
||||
border-radius: 50%;
|
||||
text-decoration: none;
|
||||
color: #565656;
|
||||
z-index: 1;
|
||||
|
||||
& > span:first-child {
|
||||
display: flex;
|
||||
|
||||
@@ -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;
|
||||
@@ -73,6 +73,7 @@ const AccountForm = props => {
|
||||
const [ name, setName ] = useState('');
|
||||
const [ sipRealm, setSipRealm ] = useState('');
|
||||
const [ deviceCallingApplication, setDeviceCallingApplication ] = useState('');
|
||||
const [ siprecCallingApplication, setSiprecCallingApplication ] = useState('');
|
||||
const [ regWebhook, setRegWebhook ] = useState('');
|
||||
const [ regMethod, setRegMethod ] = useState('POST');
|
||||
const [ regUser, setRegUser ] = useState('');
|
||||
@@ -93,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);
|
||||
@@ -318,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({
|
||||
@@ -336,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');
|
||||
@@ -376,6 +391,7 @@ const AccountForm = props => {
|
||||
setName(acc.name || '');
|
||||
setSipRealm(acc.sip_realm || '');
|
||||
setDeviceCallingApplication(acc.device_calling_application_sid || '');
|
||||
setSiprecCallingApplication(acc.siprec_hook_sid || '');
|
||||
setRegWebhook((acc.registration_hook && acc.registration_hook.url ) || '');
|
||||
setRegMethod((acc.registration_hook && acc.registration_hook.method ) || 'post');
|
||||
setRegUser((acc.registration_hook && acc.registration_hook.username) || '');
|
||||
@@ -549,13 +565,14 @@ const AccountForm = props => {
|
||||
|
||||
if (props.type === 'edit') {
|
||||
axiosData.device_calling_application_sid = deviceCallingApplication || null;
|
||||
axiosData.siprec_hook_sid = siprecCallingApplication || null;
|
||||
}
|
||||
|
||||
const url = props.type === 'add'
|
||||
? `/Accounts`
|
||||
: `/Accounts/${accountSid}`;
|
||||
|
||||
await axios({
|
||||
const accountResp = await axios({
|
||||
method: props.type === 'add' ? 'post' : 'put',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
@@ -564,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;
|
||||
@@ -695,33 +729,61 @@ const AccountForm = props => {
|
||||
</StyledInputGroup>
|
||||
|
||||
{props.type === 'edit' && (
|
||||
<React.Fragment>
|
||||
<Label tooltip htmlFor="deviceCallingApplication">
|
||||
<span style={{ position: 'relative' }}>
|
||||
Application for SIP Device Calls
|
||||
<Tooltip large>
|
||||
This application is used to handle incoming calls from SIP users who have registered to the Account’s SIP Realm.
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="deviceCallingApplication"
|
||||
id="deviceCallingApplication"
|
||||
value={deviceCallingApplication}
|
||||
onChange={e => setDeviceCallingApplication(e.target.value)}
|
||||
>
|
||||
<option value="">-- NONE --</option>
|
||||
{accountApplications && accountApplications.map(app => (
|
||||
<option
|
||||
key={app.application_sid}
|
||||
value={app.application_sid}
|
||||
>
|
||||
{app.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</React.Fragment>
|
||||
<>
|
||||
<React.Fragment>
|
||||
<Label tooltip htmlFor="deviceCallingApplication">
|
||||
<span style={{ position: 'relative' }}>
|
||||
Application for SIP Device Calls
|
||||
<Tooltip large>
|
||||
This application is used to handle incoming calls from SIP users who have registered to the Account’s SIP Realm.
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="deviceCallingApplication"
|
||||
id="deviceCallingApplication"
|
||||
value={deviceCallingApplication}
|
||||
onChange={e => setDeviceCallingApplication(e.target.value)}
|
||||
>
|
||||
<option value="">-- NONE --</option>
|
||||
{accountApplications && accountApplications.map(app => (
|
||||
<option
|
||||
key={app.application_sid}
|
||||
value={app.application_sid}
|
||||
>
|
||||
{app.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</React.Fragment>
|
||||
|
||||
<React.Fragment>
|
||||
<Label tooltip htmlFor="siprecCallingApplication">
|
||||
<span style={{ position: 'relative' }}>
|
||||
Application for SIPREC Calls
|
||||
</span>
|
||||
</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="siprecCallingApplication"
|
||||
id="siprecCallingApplication"
|
||||
value={siprecCallingApplication}
|
||||
onChange={e => setSiprecCallingApplication(e.target.value)}
|
||||
right
|
||||
>
|
||||
<option value="">-- NONE --</option>
|
||||
{accountApplications && accountApplications.map(app => (
|
||||
<option
|
||||
key={app.application_sid}
|
||||
value={app.application_sid}
|
||||
>
|
||||
{app.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</React.Fragment>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Label htmlFor="regWebhook">Registration Webhook</Label>
|
||||
@@ -861,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>
|
||||
@@ -991,7 +1078,6 @@ const AccountForm = props => {
|
||||
)}
|
||||
</>
|
||||
) : null }
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -63,7 +63,7 @@ const PhoneNumberForm = props => {
|
||||
const sipTrunksPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/VoipCarriers',
|
||||
url: `ServiceProviders/${currentServiceProvider}/VoipCarriers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
@@ -71,7 +71,7 @@ const PhoneNumberForm = props => {
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
url: `ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
@@ -79,7 +79,7 @@ const PhoneNumberForm = props => {
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
url: `ServiceProviders/${currentServiceProvider}/Applications`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
@@ -87,7 +87,7 @@ const PhoneNumberForm = props => {
|
||||
const phoneNumbersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/PhoneNumbers',
|
||||
url: `ServiceProviders/${currentServiceProvider}/PhoneNumbers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
@@ -43,23 +43,36 @@ const SettingsForm = () => {
|
||||
const refServiceProviderName = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ enableMsTeams, setEnableMsTeams ] = useState(false);
|
||||
const [ sbcDomainName, setSbcDomainName ] = useState('');
|
||||
const [enableMsTeams, setEnableMsTeams] = useState(false);
|
||||
const [sbcDomainName, setSbcDomainName] = useState('');
|
||||
const [serviceProviderName, setServiceProviderName] = useState('');
|
||||
|
||||
// For when user has data in sbcDomainName and then taps the checkbox to disable MsTeams
|
||||
const [ savedSbcDomainName, setSavedSbcDomainName ] = useState('');
|
||||
const [savedSbcDomainName, setSavedSbcDomainName] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidEnableMsTeams, setInvalidEnableMsTeams ] = useState(false);
|
||||
const [ invalidSbcDomainName, setInvalidSbcDomainName ] = useState(false);
|
||||
const [invalidEnableMsTeams, setInvalidEnableMsTeams] = useState(false);
|
||||
const [invalidSbcDomainName, setInvalidSbcDomainName] = useState(false);
|
||||
const [invalidServiceProviderName, setInvalidServiceProviderName] = useState(false);
|
||||
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
|
||||
const [ serviceProviders, setServiceProviders ] = useState([]);
|
||||
const [ confirmDelete, setConfirmDelete ] = useState(false);
|
||||
const [showLoader, setShowLoader] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
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 {
|
||||
@@ -127,19 +139,19 @@ const SettingsForm = () => {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
setConfirmDelete(false);
|
||||
setErrorMessage('');
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: 'Service Provider Deleted'
|
||||
.then(() => {
|
||||
setConfirmDelete(false);
|
||||
setErrorMessage('');
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: 'Service Provider Deleted'
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.response.data.msg);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.response.data.msg);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
@@ -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();
|
||||
|
||||
@@ -255,115 +264,140 @@ 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}
|
||||
/>
|
||||
? <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}
|
||||
/>
|
||||
|
||||
<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") || ""}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
|
||||
{errorMessage && !confirmDelete && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
<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") || ""}
|
||||
/>
|
||||
|
||||
<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 && (
|
||||
{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} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => setConfirmDelete(true)}
|
||||
onClick={() => {
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button grid>Save</Button>
|
||||
</InputGroup>
|
||||
</Form>
|
||||
{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('');
|
||||
}}
|
||||
handleSubmit={handleDelete}
|
||||
actionText="Delete"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
{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('');
|
||||
}}
|
||||
handleSubmit={handleDelete}
|
||||
actionText="Delete"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ 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';
|
||||
import Checkbox from '../elements/Checkbox';
|
||||
import FileUpload from '../elements/FileUpload';
|
||||
import Code from '../elements/Code';
|
||||
@@ -80,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('');
|
||||
@@ -94,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);
|
||||
@@ -107,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);
|
||||
@@ -163,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) {
|
||||
@@ -233,6 +248,8 @@ const SpeechServicesAddEdit = (props) => {
|
||||
setInvalidUseForTts(false);
|
||||
setInvalidUseForStt(false);
|
||||
setInvalidApiKey(false);
|
||||
setInvalidUseCustomTts(false);
|
||||
setInvalidUseCustomStt(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
@@ -306,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;
|
||||
@@ -334,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,
|
||||
}),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -484,53 +532,23 @@ const SpeechServicesAddEdit = (props) => {
|
||||
large
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="name">Vendor</Label>
|
||||
<InputGroup>
|
||||
<Radio
|
||||
noLeftMargin
|
||||
name="vendor"
|
||||
id="google"
|
||||
label="Google"
|
||||
checked={vendor === 'google'}
|
||||
onChange={() => setVendor('google')}
|
||||
invalid={invalidVendorGoogle}
|
||||
ref={refVendorGoogle}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
|
||||
<Radio
|
||||
name="vendor"
|
||||
id="aws"
|
||||
label="Amazon Web Services"
|
||||
checked={vendor === 'aws'}
|
||||
onChange={() => setVendor('aws')}
|
||||
invalid={invalidVendorAws}
|
||||
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="vendor">Vendor</Label>
|
||||
<Select
|
||||
name="vendor"
|
||||
id="vendor"
|
||||
value={vendor}
|
||||
onChange={e => setVendor(e.target.value)}
|
||||
{...[refVendorGoogle, refVendorAws, refVendorMs, refVendorWellSaid]}
|
||||
invalid={[invalidVendorGoogle, invalidVendorAws, invalidVendorMs, invalidVendorWellSaid].includes(true)}
|
||||
>
|
||||
<option value="">
|
||||
Select a Vendor
|
||||
</option>
|
||||
<option value="google">Google</option>
|
||||
<option value="aws">AWS</option>
|
||||
<option value="microsoft">Microsoft</option>
|
||||
<option value="wellsaid">WellSaid</option>
|
||||
</Select>
|
||||
|
||||
<Label htmlFor="account">Used by</Label>
|
||||
<Select
|
||||
@@ -552,6 +570,37 @@ const SpeechServicesAddEdit = (props) => {
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{['google', 'aws', 'microsoft', 'wellsaid'].includes(vendor) ? (
|
||||
<>
|
||||
<div />
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
name="useForTts"
|
||||
id="useForTts"
|
||||
label="Use for text-to-speech"
|
||||
checked={useForTts}
|
||||
onChange={e => setUseForTts(e.target.checked)}
|
||||
invalid={invalidUseForTts}
|
||||
ref={refUseForTts}
|
||||
/>
|
||||
<div />
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
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
|
||||
)}
|
||||
|
||||
{vendor === 'google' ? (
|
||||
<>
|
||||
<Label htmlFor="serviceKey">Service Key</Label>
|
||||
@@ -607,7 +656,7 @@ const SpeechServicesAddEdit = (props) => {
|
||||
ref={refAwsRegion}
|
||||
invalid={invalidAwsRegion}
|
||||
>
|
||||
<option value="">
|
||||
<option value="">
|
||||
Select a region
|
||||
</option>
|
||||
{AwsRegions.map(r => (
|
||||
@@ -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' ? (
|
||||
<>
|
||||
@@ -674,37 +787,6 @@ const SpeechServicesAddEdit = (props) => {
|
||||
null
|
||||
)}
|
||||
|
||||
{['google', 'aws', 'microsoft', 'wellsaid'].includes(vendor) ? (
|
||||
<>
|
||||
<div />
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
name="useForTts"
|
||||
id="useForTts"
|
||||
label="Use for text-to-speech"
|
||||
checked={useForTts}
|
||||
onChange={e => setUseForTts(e.target.checked)}
|
||||
invalid={invalidUseForTts}
|
||||
ref={refUseForTts}
|
||||
/>
|
||||
<div />
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
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
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
@@ -726,7 +808,7 @@ const SpeechServicesAddEdit = (props) => {
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button rounded="true">
|
||||
<Button rounded="true" disabled={!vendor}>
|
||||
{type === 'add'
|
||||
? 'Add Speech Service'
|
||||
: 'Save'
|
||||
|
||||
@@ -209,7 +209,7 @@ const AccountsAddEdit = () => {
|
||||
name="API key"
|
||||
getContent={getApiKeys}
|
||||
columns={[
|
||||
{ header: 'API Key', key: 'token', width: '27rem', fontWeight: 'normal' },
|
||||
{ header: 'Account API Keys', key: 'token', width: '27rem', fontWeight: 'normal' },
|
||||
{ header: 'Last Used', key: 'last_used', width: '10rem' },
|
||||
]}
|
||||
addContent={createApiKey}
|
||||
|
||||
@@ -104,7 +104,6 @@ const CarriersList = () => {
|
||||
return;
|
||||
}
|
||||
if(!currentServiceProvider) return [];
|
||||
if (!accountList.length) return [];
|
||||
// Get all SIP trunks
|
||||
const trunkResults = await axios({
|
||||
method: 'get',
|
||||
|
||||
@@ -1,16 +1,221 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import SettingsForm from '../../forms/SettingsForm';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import axios from 'axios';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
|
||||
|
||||
const Settings = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
let service_provider_sid = useContext(ServiceProviderValueContext);
|
||||
const pageTitle = 'Settings';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
//=============================================================================
|
||||
// Get API keys
|
||||
//=============================================================================
|
||||
const getApiKeys = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const results = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${service_provider_sid}/ApiKeys`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const simplifiedApiKeys = results.data.map(a => {
|
||||
const { token } = a;
|
||||
const maskLength = token.length - 4;
|
||||
const maskedPortion = token.substring(0, maskLength).replace(/[a-zA-Z0-9]/g, '*');
|
||||
const revealedPortion = token.substring(maskLength);
|
||||
const maskedToken = `${maskedPortion}${revealedPortion}`;
|
||||
|
||||
const { last_used } = a;
|
||||
let lastUsedString = 'Never used';
|
||||
if (last_used) {
|
||||
const currentDate = new Date();
|
||||
const lastUsedDate = new Date(last_used);
|
||||
currentDate.setHours(0, 0, 0, 0);
|
||||
lastUsedDate.setHours(0, 0, 0, 0);
|
||||
const daysDifference = Math.round((currentDate - lastUsedDate) / 1000 / 60 / 60 / 24);
|
||||
lastUsedString = daysDifference > 1
|
||||
? `${daysDifference} days ago`
|
||||
: daysDifference === 1
|
||||
? 'Yesterday'
|
||||
: daysDifference === 0
|
||||
? 'Today'
|
||||
: 'Never used';
|
||||
}
|
||||
|
||||
return {
|
||||
sid: a.api_key_sid,
|
||||
token: {
|
||||
type: 'masked',
|
||||
masked: maskedToken,
|
||||
revealed: token,
|
||||
},
|
||||
last_used: {
|
||||
type: 'normal',
|
||||
content: lastUsedString,
|
||||
},
|
||||
};
|
||||
});
|
||||
return (simplifiedApiKeys);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get API key data',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Create API key
|
||||
//=============================================================================
|
||||
const createApiKey = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const result = await axios({
|
||||
method: 'post',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/ApiKeys',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data: {
|
||||
"service_provider_sid": service_provider_sid,
|
||||
}
|
||||
});
|
||||
return result.data.token;
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to create API key',
|
||||
});
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Delete API key
|
||||
//=============================================================================
|
||||
const formatApiKeyToDelete = apiKey => {
|
||||
const items = [
|
||||
{ name: 'API Key:', content: apiKey.token.masked || '[none]' },
|
||||
{ name: 'Last Used:', content: apiKey.last_used.content || 'Never used' },
|
||||
];
|
||||
return items;
|
||||
};
|
||||
const deleteApiKey = async apiKeyToDelete => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Apikeys/${apiKeyToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete API key');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
breadcrumbs={[
|
||||
{ name: 'Service Provider', url: '/internal/settings' },
|
||||
{ name: pageTitle },
|
||||
]}
|
||||
additionalTable={service_provider_sid && (
|
||||
<TableContent
|
||||
name="API key"
|
||||
getContent={getApiKeys}
|
||||
columns={[
|
||||
{ header: 'Service Provider API Keys', key: 'token', width: '27rem', fontWeight: 'normal' },
|
||||
{ header: 'Last Used', key: 'last_used', width: '10rem' },
|
||||
]}
|
||||
addContent={createApiKey}
|
||||
formatContentToDelete={formatApiKeyToDelete}
|
||||
deleteContent={deleteApiKey}
|
||||
rowsHaveDeleteButtons
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<SettingsForm />
|
||||
</InternalTemplate>
|
||||
|
||||
@@ -1,2 +1,31 @@
|
||||
const { REACT_APP_API_BASE_URL } = process.env;
|
||||
const { REACT_APP_API_BASE_URL, REACT_APP_ENABLE_ACCOUNT_LIMITS_ALL } = process.env;
|
||||
export const APP_API_BASE_URL = (window.JAMBONZ) ? window.JAMBONZ.APP_API_BASE_URL : REACT_APP_API_BASE_URL;
|
||||
export const APP_ENABLE_ACCOUNT_LIMITS_ALL = (window.JAMBONZ) ? window.JAMBONZ.APP_ENABLE_ACCOUNT_LIMITS_ALL : JSON.parse(REACT_APP_ENABLE_ACCOUNT_LIMITS_ALL);
|
||||
export const LIMITS = [
|
||||
// {
|
||||
// label: "Max registered devices (0=unlimited)",
|
||||
// category: "device",
|
||||
// },
|
||||
// {
|
||||
// label: "Max api calls per minute (0=unlimited)",
|
||||
// category: "api_rate",
|
||||
// },
|
||||
{
|
||||
label: "Max calls",
|
||||
category: "voice_call_session",
|
||||
}
|
||||
];
|
||||
if (APP_ENABLE_ACCOUNT_LIMITS_ALL || APP_ENABLE_ACCOUNT_LIMITS_ALL === "true") {
|
||||
LIMITS.push({
|
||||
label: "Licensed calls",
|
||||
category: "voice_call_session_license",
|
||||
});
|
||||
LIMITS.push({
|
||||
label: "Max minutes",
|
||||
category: "voice_call_minutes",
|
||||
});
|
||||
LIMITS.push({
|
||||
label: "Licensed minutes",
|
||||
category: "voice_call_minutes_license",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,6 +54,38 @@ const regions = [
|
||||
{
|
||||
name: 'Canada (Central)',
|
||||
value: 'ca-central-1'
|
||||
},
|
||||
{
|
||||
name: 'Europe (Frankfurt)',
|
||||
value: 'eu-central-1'
|
||||
},
|
||||
{
|
||||
name: 'Europe (Ireland)',
|
||||
value: 'eu-west-1'
|
||||
},
|
||||
{
|
||||
name: 'Europe (London)',
|
||||
value: 'eu-west-2'
|
||||
},
|
||||
{
|
||||
name: 'Europe (Milan)',
|
||||
value: 'eu-south-1'
|
||||
},
|
||||
{
|
||||
name: 'Europe (Paris)',
|
||||
value: 'eu-west-3'
|
||||
},
|
||||
{
|
||||
name: 'Europe (Stockholm)',
|
||||
value: 'eu-north-1'
|
||||
},
|
||||
{
|
||||
name: 'Middle East (Bahrain)',
|
||||
value: 'me-south-1'
|
||||
},
|
||||
{
|
||||
name: 'South America (São Paulo)',
|
||||
value: 'sa-east-1'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,96 +1,124 @@
|
||||
const regions = [
|
||||
{
|
||||
name: 'Asia (East)',
|
||||
value: 'eastasia'
|
||||
{
|
||||
name: 'Australia - East (australiaeast)',
|
||||
value: 'australiaeast'
|
||||
},
|
||||
{
|
||||
name: 'Asia (Southeast)',
|
||||
value: 'southeastasia'
|
||||
{
|
||||
name: 'Brazil - South (brazilsouth)',
|
||||
value: 'brazilsouth'
|
||||
},
|
||||
{
|
||||
name: 'Australia (East)',
|
||||
value: 'australiaeast'
|
||||
{
|
||||
name: 'Canada - Central (canadacentral)',
|
||||
value: 'canadacentral'
|
||||
},
|
||||
{
|
||||
name: 'Brazil (South)',
|
||||
value: 'brazilsouth'
|
||||
{
|
||||
name: 'East Asia (eastasia)',
|
||||
value: 'eastasia'
|
||||
},
|
||||
{
|
||||
name: 'Canada (Central)',
|
||||
value: 'canadacentral'
|
||||
{
|
||||
name: 'Europe - North (northeurope)',
|
||||
value: 'northeurope'
|
||||
},
|
||||
{
|
||||
name: 'Europe (North)',
|
||||
value: 'northeurope'
|
||||
{
|
||||
name: 'Europe - West (westeurope)',
|
||||
value: 'westeurope'
|
||||
},
|
||||
{
|
||||
name: 'Europe (West)',
|
||||
value: 'westeurope'
|
||||
{
|
||||
name: 'France - Central (francecentral)',
|
||||
value: 'francecentral'
|
||||
},
|
||||
{
|
||||
name: 'France (Central)',
|
||||
value: 'francecentral'
|
||||
{
|
||||
name: 'Germany - West Central (germanywestcentral)',
|
||||
value: 'germanywestcentral'
|
||||
},
|
||||
{
|
||||
name: 'Switzerland (North)',
|
||||
value: 'switzerlandnorth'
|
||||
{
|
||||
name: 'India - Central (centralindia)',
|
||||
value: 'centralindia'
|
||||
},
|
||||
{
|
||||
name: 'India (Central)',
|
||||
value: 'centralindia'
|
||||
{
|
||||
name: 'Japan - East (japaneast)',
|
||||
value: 'japaneast'
|
||||
},
|
||||
{
|
||||
name: 'Japan (West)',
|
||||
value: 'japanwest'
|
||||
{
|
||||
name: 'Japan - West (japanwest)',
|
||||
value: 'japanwest'
|
||||
},
|
||||
{
|
||||
name: 'Japan (East)',
|
||||
value: 'japaneast'
|
||||
{
|
||||
name: 'Korea - Central (koreacentral)',
|
||||
value: 'koreacentral'
|
||||
},
|
||||
{
|
||||
name: 'Korea (Central)',
|
||||
value: 'koreacentral'
|
||||
{
|
||||
name: 'Norway - East (norwayeast)',
|
||||
value: 'norwayeast'
|
||||
},
|
||||
{
|
||||
name: 'South Africa (North)',
|
||||
value: 'southafricanorth'
|
||||
{
|
||||
name: 'South Africa - North (southafricanorth)',
|
||||
value: 'southafricanorth'
|
||||
},
|
||||
{
|
||||
name: 'UK (South)',
|
||||
value: 'uksouth'
|
||||
{
|
||||
name: 'Southeast Asia (southeastasia)',
|
||||
value: 'southeastasia'
|
||||
},
|
||||
{
|
||||
name: 'US (Cental)',
|
||||
value: 'centralus'
|
||||
{
|
||||
name: 'Switzerland - North (switzerlandnorth)',
|
||||
value: 'switzerlandnorth'
|
||||
},
|
||||
{
|
||||
name: 'US (West Central)',
|
||||
value: 'westcentralus'
|
||||
{
|
||||
name: 'Switzerland - West (switzerlandwest)',
|
||||
value: 'switzerlandwest'
|
||||
},
|
||||
{
|
||||
name: 'US (East)',
|
||||
value: 'eastus'
|
||||
{
|
||||
name: 'UAE - North (uaenorth)',
|
||||
value: 'uaenorth'
|
||||
},
|
||||
{
|
||||
name: 'US (East 2)',
|
||||
value: 'eastus2'
|
||||
{
|
||||
name: 'UK - South (uksouth)',
|
||||
value: 'uksouth'
|
||||
},
|
||||
{
|
||||
name: 'US (North Central)',
|
||||
value: 'northcentralus'
|
||||
{
|
||||
name: 'US - Central (centralus)',
|
||||
value: 'centralus'
|
||||
},
|
||||
{
|
||||
name: 'US (South Central)',
|
||||
value: 'southcentralus'
|
||||
{
|
||||
name: 'US - East (eastus)',
|
||||
value: 'eastus'
|
||||
},
|
||||
{
|
||||
name: 'US (West)',
|
||||
value: 'westus'
|
||||
{
|
||||
name: 'US - East 2 (eastus2)',
|
||||
value: 'eastus2'
|
||||
},
|
||||
{
|
||||
name: 'US (West 2)',
|
||||
value: 'westus2'
|
||||
{
|
||||
name: 'US - Gov Arizona (usgovarizona)',
|
||||
value: 'usgovarizona'
|
||||
},
|
||||
{
|
||||
name: 'US - Gov Virginia (usgovvirginia)',
|
||||
value: 'usgovvirginia'
|
||||
},
|
||||
{
|
||||
name: 'US - North Central (northcentralus)',
|
||||
value: 'northcentralus'
|
||||
},
|
||||
{
|
||||
name: 'US - South Central (southcentralus)',
|
||||
value: 'southcentralus'
|
||||
},
|
||||
{
|
||||
name: 'US - West Central (westcentralus)',
|
||||
value: 'westcentralus'
|
||||
},
|
||||
{
|
||||
name: 'US - West (westus)',
|
||||
value: 'westus'
|
||||
},
|
||||
{
|
||||
name: 'US - West 2 (westus2)',
|
||||
value: 'westus2'
|
||||
},
|
||||
{
|
||||
name: 'US - West 3 (westus3)',
|
||||
value: 'westus3'
|
||||
}
|
||||
];
|
||||
|
||||
export default regions;
|
||||
@@ -1,4 +1,12 @@
|
||||
const languages = [
|
||||
{
|
||||
name: 'Afrikaans (South Africa)',
|
||||
code: 'af-ZA'
|
||||
},
|
||||
{
|
||||
name: 'Amharic (Ethiopia)',
|
||||
code: 'am-ET'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Algeria)',
|
||||
code: 'ar-DZ'
|
||||
@@ -75,6 +83,10 @@ const languages = [
|
||||
name: 'Bulgarian (Bulgaria)',
|
||||
code: 'bg-BG'
|
||||
},
|
||||
{
|
||||
name: 'Bengali (India)',
|
||||
code: 'bn-IN'
|
||||
},
|
||||
{
|
||||
name: 'Catalan (Spain)',
|
||||
code: 'ca-ES'
|
||||
@@ -107,6 +119,10 @@ const languages = [
|
||||
name: 'Dutch (Netherlands)',
|
||||
code: 'nl-NL'
|
||||
},
|
||||
{
|
||||
name: 'Dutch (Belgium)',
|
||||
code: 'nl-BE'
|
||||
},
|
||||
{
|
||||
name: 'English (Australia)',
|
||||
code: 'en-AU'
|
||||
@@ -179,6 +195,10 @@ const languages = [
|
||||
name: 'Finnish (Finland)',
|
||||
code: 'fi-FI'
|
||||
},
|
||||
{
|
||||
name: 'French (Belgium)',
|
||||
code: 'fr-BE'
|
||||
},
|
||||
{
|
||||
name: 'French (Canada)',
|
||||
code: 'fr-CA'
|
||||
@@ -227,6 +247,10 @@ const languages = [
|
||||
name: 'Indonesian (Indonesia)',
|
||||
code: 'id-ID'
|
||||
},
|
||||
{
|
||||
name: 'Icelandic (Iceland)',
|
||||
code: 'is-IS'
|
||||
},
|
||||
{
|
||||
name: 'Irish (Ireland)',
|
||||
code: 'ga-IE'
|
||||
@@ -239,10 +263,18 @@ const languages = [
|
||||
name: 'Japanese (Japan)',
|
||||
code: 'ja-JP'
|
||||
},
|
||||
{
|
||||
name: 'Javanese (Indonesia)',
|
||||
code: 'jv-ID'
|
||||
},
|
||||
{
|
||||
name: 'Kannada (India)',
|
||||
code: 'kn-IN'
|
||||
},
|
||||
{
|
||||
name: 'Khmer (Cambodia)',
|
||||
code: 'km-KH'
|
||||
},
|
||||
{
|
||||
name: 'Korean (Korea)',
|
||||
code: 'ko-KR'
|
||||
@@ -251,6 +283,10 @@ const languages = [
|
||||
name: 'Latvian (Latvia)',
|
||||
code: 'lv-LV'
|
||||
},
|
||||
{
|
||||
name: 'Lao (Laos)',
|
||||
code: 'lo-LA'
|
||||
},
|
||||
{
|
||||
name: 'Lithuanian (Lithuania)',
|
||||
code: 'lt-LT'
|
||||
@@ -259,6 +295,10 @@ const languages = [
|
||||
name: 'Malay (Malaysia)',
|
||||
code: 'ms-MY'
|
||||
},
|
||||
{
|
||||
name: 'Macedonian (North Macedonia)',
|
||||
code: 'mk-MK'
|
||||
},
|
||||
{
|
||||
name: 'Maltese (Malta)',
|
||||
code: 'mt-MT'
|
||||
@@ -267,6 +307,10 @@ const languages = [
|
||||
name: 'Marathi (India)',
|
||||
code: 'mr-IN'
|
||||
},
|
||||
{
|
||||
name: 'Burmese (Myanmar)',
|
||||
code: 'my-MM'
|
||||
},
|
||||
{
|
||||
name: 'Norwegian (Bokmål, Norway)',
|
||||
code: 'nb-NO'
|
||||
@@ -395,10 +439,22 @@ const languages = [
|
||||
name: 'Swahili (Kenya)',
|
||||
code: 'sw-KE'
|
||||
},
|
||||
{
|
||||
name: 'Swahili (Tanzania)',
|
||||
code: 'sw-TZ'
|
||||
},
|
||||
{
|
||||
name: 'Sinhala (Sri Lanka)',
|
||||
code: 'si-LK'
|
||||
},
|
||||
{
|
||||
name: 'Swedish (Sweden)',
|
||||
code: 'sv-SE'
|
||||
},
|
||||
{
|
||||
name: 'Serbian (Serbia)',
|
||||
code: 'sr-RS'
|
||||
},
|
||||
{
|
||||
name: 'Tamil (India)',
|
||||
code: 'ta-IN'
|
||||
@@ -415,6 +471,18 @@ const languages = [
|
||||
name: 'Turkish (Turkey)',
|
||||
code: 'tr-TR'
|
||||
},
|
||||
{
|
||||
name: 'Ukrainian (Ukraine)',
|
||||
code: 'uk-UA'
|
||||
},
|
||||
{
|
||||
name: 'Uzbek (Uzbekistan)',
|
||||
code: 'uz-UZ'
|
||||
},
|
||||
{
|
||||
name: 'Zulu (South Africa)',
|
||||
code: 'zu-ZA'
|
||||
},
|
||||
{
|
||||
name: 'Vietnamese (Vietnam)',
|
||||
code: 'vi-VN'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user