mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-02-09 02:29:45 +00:00
Compare commits
8 Commits
v0.6.7-rc7
...
v0.7.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7db92f0c7 | ||
|
|
f5201d2d69 | ||
|
|
128ca045b0 | ||
|
|
3403996946 | ||
|
|
87dbb461e0 | ||
|
|
9bce9c5510 | ||
|
|
2db5f26dbf | ||
|
|
bfc7cc971c |
2
.env
2
.env
@@ -1 +1 @@
|
|||||||
REACT_APP_API_BASE_URL=http://[ip]:[port]/v1
|
REACT_APP_API_BASE_URL=http://127.0.0.1:3002/v1
|
||||||
|
|||||||
13
.eslintrc
13
.eslintrc
@@ -2,15 +2,12 @@
|
|||||||
"extends": "react-app",
|
"extends": "react-app",
|
||||||
"rules": {
|
"rules": {
|
||||||
"linebreak-style": [
|
"linebreak-style": [
|
||||||
"error",
|
"error",
|
||||||
"unix"
|
"unix"
|
||||||
],
|
],
|
||||||
"semi": [
|
"semi": [
|
||||||
"error",
|
"error",
|
||||||
"always"
|
"always"
|
||||||
],
|
|
||||||
"no-trailing-spaces": [
|
|
||||||
"error"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
51
.github/workflows/docker-publish.yml
vendored
Normal file
51
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# Publish `main` as Docker `latest` image.
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
# Publish `v1.2.3` tags as releases.
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: webapp
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Build image
|
||||||
|
run: docker build . --file Dockerfile --tag $IMAGE_NAME
|
||||||
|
|
||||||
|
- name: Log into registry
|
||||||
|
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||||
|
|
||||||
|
- name: Push image
|
||||||
|
run: |
|
||||||
|
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||||
|
|
||||||
|
# Change all uppercase to lowercase
|
||||||
|
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||||
|
|
||||||
|
# Strip git ref prefix from version
|
||||||
|
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||||
|
|
||||||
|
# Strip "v" prefix from tag name
|
||||||
|
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||||
|
|
||||||
|
# Use Docker `latest` tag convention
|
||||||
|
[ "$VERSION" == "main" ] && VERSION=latest
|
||||||
|
|
||||||
|
echo IMAGE_ID=$IMAGE_ID
|
||||||
|
echo VERSION=$VERSION
|
||||||
|
|
||||||
|
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||||
|
docker push $IMAGE_ID:$VERSION
|
||||||
36385
package-lock.json
generated
36385
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "jambonz-webapp",
|
"name": "jambonz-webapp",
|
||||||
"version": "0.6.7-rc5",
|
"version": "v0.7.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^5.16.1",
|
||||||
"@testing-library/react": "^9.5.0",
|
"@testing-library/react": "^12.1.2",
|
||||||
"@testing-library/user-event": "^7.2.1",
|
"@testing-library/user-event": "^7.2.1",
|
||||||
"antd": "^4.15.4",
|
"antd": "^4.15.4",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "3.4.1",
|
"react-scripts": "^5.0.0",
|
||||||
"serve": "^11.3.0",
|
"serve": "^13.0.2",
|
||||||
"styled-components": "^5.0.1"
|
"styled-components": "^5.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ const Checkbox = (props, ref) => {
|
|||||||
name={props.id}
|
name={props.id}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={props.checked}
|
checked={props.checked}
|
||||||
|
disabled={props.disabled}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|||||||
@@ -2,19 +2,8 @@ import React, { useContext } from 'react';
|
|||||||
import styled from 'styled-components/macro';
|
import styled from 'styled-components/macro';
|
||||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
|
import Span from './Span';
|
||||||
|
|
||||||
const Span = styled.span`
|
|
||||||
text-align: left;
|
|
||||||
${props => props.hasBorder ? `
|
|
||||||
height: 2.25rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 1rem;
|
|
||||||
border: 1px solid #B6B6B6;
|
|
||||||
border-radius: 0.125rem;
|
|
||||||
` : ''}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledButton = styled(Button)`
|
const StyledButton = styled(Button)`
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
|
|||||||
16
src/components/elements/Span.js
Normal file
16
src/components/elements/Span.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import styled from 'styled-components/macro';
|
||||||
|
|
||||||
|
const Span = styled.span`
|
||||||
|
text-align: left;
|
||||||
|
${props => props.hasBorder ? `
|
||||||
|
height: 2.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 1rem;
|
||||||
|
border: 1px solid #B6B6B6;
|
||||||
|
border-radius: 0.125rem;
|
||||||
|
` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Span;
|
||||||
@@ -6,6 +6,7 @@ import { ServiceProviderValueContext } from '../../contexts/ServiceProviderConte
|
|||||||
import Form from '../elements/Form';
|
import Form from '../elements/Form';
|
||||||
import Input from '../elements/Input';
|
import Input from '../elements/Input';
|
||||||
import Label from '../elements/Label';
|
import Label from '../elements/Label';
|
||||||
|
import Radio from '../elements/Radio';
|
||||||
import Select from '../elements/Select';
|
import Select from '../elements/Select';
|
||||||
import InputGroup from '../elements/InputGroup';
|
import InputGroup from '../elements/InputGroup';
|
||||||
import PasswordInput from '../elements/PasswordInput';
|
import PasswordInput from '../elements/PasswordInput';
|
||||||
@@ -17,6 +18,7 @@ import Button from '../elements/Button';
|
|||||||
import Link from '../elements/Link';
|
import Link from '../elements/Link';
|
||||||
import Tooltip from '../elements/Tooltip';
|
import Tooltip from '../elements/Tooltip';
|
||||||
import CopyableText from '../elements/CopyableText';
|
import CopyableText from '../elements/CopyableText';
|
||||||
|
import Span from '../elements/Span';
|
||||||
import handleErrors from "../../helpers/handleErrors";
|
import handleErrors from "../../helpers/handleErrors";
|
||||||
import styled from 'styled-components/macro';
|
import styled from 'styled-components/macro';
|
||||||
|
|
||||||
@@ -61,6 +63,9 @@ const AccountForm = props => {
|
|||||||
const refQueueWebhook = useRef(null);
|
const refQueueWebhook = useRef(null);
|
||||||
const refQueueUser = useRef(null);
|
const refQueueUser = useRef(null);
|
||||||
const refQueuePassword = useRef(null);
|
const refQueuePassword = useRef(null);
|
||||||
|
const refSubspaceId = useRef(null);
|
||||||
|
const refSubspaceSecret = useRef(null);
|
||||||
|
const refSubspaceOtherSip = useRef(null);
|
||||||
|
|
||||||
// Form inputs
|
// Form inputs
|
||||||
const [ name, setName ] = useState('');
|
const [ name, setName ] = useState('');
|
||||||
@@ -68,13 +73,24 @@ const AccountForm = props => {
|
|||||||
const [ deviceCallingApplication, setDeviceCallingApplication ] = useState('');
|
const [ deviceCallingApplication, setDeviceCallingApplication ] = useState('');
|
||||||
const [ regWebhook, setRegWebhook ] = useState('');
|
const [ regWebhook, setRegWebhook ] = useState('');
|
||||||
const [ regMethod, setRegMethod ] = useState('POST');
|
const [ regMethod, setRegMethod ] = useState('POST');
|
||||||
const [ regUser, setRegUser ] = useState('' || '');
|
const [ regUser, setRegUser ] = useState('');
|
||||||
const [ regPassword, setRegPassword ] = useState('' || '');
|
const [ regPassword, setRegPassword ] = useState('');
|
||||||
const [ webhookSecret, setWebhookSecret ] = useState('');
|
const [ webhookSecret, setWebhookSecret ] = useState('');
|
||||||
const [ queueWebhook, setQueueWebhook ] = useState('');
|
const [ queueWebhook, setQueueWebhook ] = useState('');
|
||||||
const [ queueMethod, setQueueMethod ] = useState('POST');
|
const [ queueMethod, setQueueMethod ] = useState('POST');
|
||||||
const [ queueUser, setQueueUser ] = useState('' || '');
|
const [ queueUser, setQueueUser ] = useState('');
|
||||||
const [ queuePassword, setQueuePassword ] = useState('' || '');
|
const [ queuePassword, setQueuePassword ] = useState('');
|
||||||
|
const [ hasSubspace, setHasSubspace ] = useState(false);
|
||||||
|
const [ subspaceId, setSubspaceId ] = useState('');
|
||||||
|
const [ subspaceSecret, setSubspaceSecret ] = useState('');
|
||||||
|
const [ subspaceSipTeleportId, setSubspaceSipTeleportId ] = useState('');
|
||||||
|
const [ subspaceSipTeleportEntryPoints, setSubspaceSipTeleportEntryPoints ] = useState([]);
|
||||||
|
const [ showSubspaceModal, setShowSubspaceModal ] = useState(false);
|
||||||
|
const [ generatingSubspace, setGeneratingSubspace ] = useState(false);
|
||||||
|
const [ subspaceSipRealm, setSubspaceSipRealm ] = useState('');
|
||||||
|
const [ sbcs, setSbcs ] = useState([]);
|
||||||
|
const [ subspaceSipRealmOtherValue, setSubspaceSipRealmOtherValue ] = useState('');
|
||||||
|
const [ subspaceEnable, setSubspaceEnable ] = useState(false);
|
||||||
|
|
||||||
// Invalid form inputs
|
// Invalid form inputs
|
||||||
const [ invalidName, setInvalidName ] = useState(false);
|
const [ invalidName, setInvalidName ] = useState(false);
|
||||||
@@ -85,6 +101,10 @@ const AccountForm = props => {
|
|||||||
const [ invalidQueueWebhook, setInvalidQueueWebhook ] = useState(false);
|
const [ invalidQueueWebhook, setInvalidQueueWebhook ] = useState(false);
|
||||||
const [ invalidQueueUser, setInvalidQueueUser ] = useState(false);
|
const [ invalidQueueUser, setInvalidQueueUser ] = useState(false);
|
||||||
const [ invalidQueuePassword, setInvalidQueuePassword ] = useState(false);
|
const [ invalidQueuePassword, setInvalidQueuePassword ] = useState(false);
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const [ invalidSubspaceId, setInvalidSubspaceId ] = useState(false);
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const [ invalidSubspaceClient, setInvalidSubspaceClient ] = useState(false);
|
||||||
|
|
||||||
const [ showLoader, setShowLoader ] = useState(true);
|
const [ showLoader, setShowLoader ] = useState(true);
|
||||||
const [ errorMessage, setErrorMessage ] = useState('');
|
const [ errorMessage, setErrorMessage ] = useState('');
|
||||||
@@ -110,6 +130,14 @@ const AccountForm = props => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubspaceMenuOpen = sid => {
|
||||||
|
if (menuOpen === sid) {
|
||||||
|
setMenuOpen(null);
|
||||||
|
} else {
|
||||||
|
setMenuOpen(sid);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const copyWebhookSecret = async e => {
|
const copyWebhookSecret = async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setMenuOpen(null);
|
setMenuOpen(null);
|
||||||
@@ -164,6 +192,96 @@ const AccountForm = props => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleSubspaceTeleport = (enable, e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setMenuOpen(null);
|
||||||
|
setSubspaceEnable(enable);
|
||||||
|
setShowSubspaceModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetSubspaceState = () => {
|
||||||
|
setMenuOpen(null);
|
||||||
|
setShowSubspaceModal(false);
|
||||||
|
setSubspaceSipRealmOtherValue('');
|
||||||
|
setGeneratingSubspace(false);
|
||||||
|
setSubspaceEnable(false);
|
||||||
|
setSubspaceSipRealm('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubspaceEnable = async () => {
|
||||||
|
try {
|
||||||
|
setGeneratingSubspace(true);
|
||||||
|
|
||||||
|
const destination = subspaceSipRealm === 'other'
|
||||||
|
? subspaceSipRealmOtherValue
|
||||||
|
: subspaceSipRealm;
|
||||||
|
const response = await axios({
|
||||||
|
method: 'post',
|
||||||
|
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||||
|
url: `/Accounts/${accountSid}/SubspaceTeleport`,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
},
|
||||||
|
data: { destination },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
setSubspaceSipTeleportId(response.data.subspace_sip_teleport_id || '');
|
||||||
|
setSubspaceSipTeleportEntryPoints(response.data.subspace_sip_teleport_destinations || []);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'ADD',
|
||||||
|
level: 'success',
|
||||||
|
message: 'Successfully enabled subspace teleport.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSubspaceState();
|
||||||
|
} catch (err) {
|
||||||
|
resetSubspaceState();
|
||||||
|
if (err.response.status === 500 && err.response.data.msg === 'Too Many Requests') {
|
||||||
|
dispatch({
|
||||||
|
type: 'ADD',
|
||||||
|
level: 'error',
|
||||||
|
message: 'You have already created the maximum number of SIP Teleports allowed for your Subspace account.',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handleErrors({ err, history, dispatch });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubspaceDisable = async () => {
|
||||||
|
try {
|
||||||
|
setGeneratingSubspace(true);
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
method: 'delete',
|
||||||
|
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||||
|
url: `/Accounts/${accountSid}/SubspaceTeleport`,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
setSubspaceSipTeleportId('');
|
||||||
|
setSubspaceSipTeleportEntryPoints([]);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'ADD',
|
||||||
|
level: 'success',
|
||||||
|
message: `Successfully disabled subspace teleport.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSubspaceState();
|
||||||
|
} catch (err) {
|
||||||
|
resetSubspaceState();
|
||||||
|
handleErrors({ err, history, dispatch });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getAccounts = async () => {
|
const getAccounts = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -200,6 +318,16 @@ const AccountForm = props => {
|
|||||||
promiseList.push(applicationsPromise);
|
promiseList.push(applicationsPromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sbcsPromise = await axios({
|
||||||
|
method: 'get',
|
||||||
|
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||||
|
url: '/Sbcs',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
promiseList.push(sbcsPromise);
|
||||||
|
|
||||||
const promiseAllValues = await Promise.all(promiseList);
|
const promiseAllValues = await Promise.all(promiseList);
|
||||||
|
|
||||||
const accountsData = (promiseAllValues[0] && promiseAllValues[0].data) || [];
|
const accountsData = (promiseAllValues[0] && promiseAllValues[0].data) || [];
|
||||||
@@ -212,6 +340,7 @@ const AccountForm = props => {
|
|||||||
});
|
});
|
||||||
setAccountApplications(accountApplicationsData);
|
setAccountApplications(accountApplicationsData);
|
||||||
}
|
}
|
||||||
|
setSbcs(promiseAllValues[2].data);
|
||||||
|
|
||||||
if (props.type === 'setup' && accountsData.length > 1) {
|
if (props.type === 'setup' && accountsData.length > 1) {
|
||||||
history.push('/internal/accounts');
|
history.push('/internal/accounts');
|
||||||
@@ -254,7 +383,11 @@ const AccountForm = props => {
|
|||||||
setQueueUser((acc.queue_event_hook && acc.queue_event_hook.username) || '');
|
setQueueUser((acc.queue_event_hook && acc.queue_event_hook.username) || '');
|
||||||
setQueuePassword((acc.queue_event_hook && acc.queue_event_hook.password) || '');
|
setQueuePassword((acc.queue_event_hook && acc.queue_event_hook.password) || '');
|
||||||
setWebhookSecret(acc.webhook_secret || '');
|
setWebhookSecret(acc.webhook_secret || '');
|
||||||
|
setSubspaceId(acc.subspace_client_id || '');
|
||||||
|
setSubspaceSecret(acc.subspace_client_secret || '');
|
||||||
|
setSubspaceSipTeleportId(acc.subspace_sip_teleport_id || '');
|
||||||
|
setSubspaceSipTeleportEntryPoints(acc.subspace_sip_teleport_destinations ? JSON.parse(acc.subspace_sip_teleport_destinations) : []);
|
||||||
|
setHasSubspace(acc.subspace_client_id ? true : false);
|
||||||
if (
|
if (
|
||||||
(acc.registration_hook && acc.registration_hook.username) ||
|
(acc.registration_hook && acc.registration_hook.username) ||
|
||||||
(acc.registration_hook && acc.registration_hook.password)
|
(acc.registration_hook && acc.registration_hook.password)
|
||||||
@@ -404,6 +537,8 @@ const AccountForm = props => {
|
|||||||
password: queuePassword || null,
|
password: queuePassword || null,
|
||||||
},
|
},
|
||||||
webhook_secret: webhookSecret || null,
|
webhook_secret: webhookSecret || null,
|
||||||
|
subspace_client_id: subspaceId || null,
|
||||||
|
subspace_client_secret: subspaceSecret || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.type === 'add') {
|
if (props.type === 'add') {
|
||||||
@@ -480,6 +615,19 @@ const AccountForm = props => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const subspaceMenuItems = [
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
name: 'Enable',
|
||||||
|
action: toggleSubspaceTeleport.bind(toggleSubspaceTeleport, true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
name: 'Disable',
|
||||||
|
action: toggleSubspaceTeleport.bind(toggleSubspaceTeleport, false),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
showLoader
|
showLoader
|
||||||
? <Loader
|
? <Loader
|
||||||
@@ -596,7 +744,7 @@ const AccountForm = props => {
|
|||||||
<Select
|
<Select
|
||||||
large={props.type === 'setup'}
|
large={props.type === 'setup'}
|
||||||
name="method"
|
name="method"
|
||||||
id="method"
|
id="regMethod"
|
||||||
value={regMethod}
|
value={regMethod}
|
||||||
onChange={e => setRegMethod(e.target.value)}
|
onChange={e => setRegMethod(e.target.value)}
|
||||||
>
|
>
|
||||||
@@ -665,7 +813,7 @@ const AccountForm = props => {
|
|||||||
<Select
|
<Select
|
||||||
large={props.type === 'setup'}
|
large={props.type === 'setup'}
|
||||||
name="method"
|
name="method"
|
||||||
id="method"
|
id="queueMethod"
|
||||||
value={queueMethod}
|
value={queueMethod}
|
||||||
onChange={e => setQueueMethod(e.target.value)}
|
onChange={e => setQueueMethod(e.target.value)}
|
||||||
>
|
>
|
||||||
@@ -711,6 +859,137 @@ const AccountForm = props => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{ process.env.REACT_APP_ENABLE_SUBSPACE ? (
|
||||||
|
<>
|
||||||
|
<Label htmlFor="subspaceId">Subspace</Label>
|
||||||
|
<InputGroup>
|
||||||
|
<Input
|
||||||
|
large={props.type === 'setup'}
|
||||||
|
name="subspaceId"
|
||||||
|
id="subspaceId"
|
||||||
|
value={subspaceId}
|
||||||
|
onChange={e => setSubspaceId(e.target.value)}
|
||||||
|
placeholder="Client Id for Subspace"
|
||||||
|
ref={refSubspaceId}
|
||||||
|
style={{ margin: '0 4px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
large={props.type === 'setup'}
|
||||||
|
allowShowPassword
|
||||||
|
name="subspaceSecret"
|
||||||
|
id="subspaceSecret"
|
||||||
|
password={subspaceSecret}
|
||||||
|
setPassword={setSubspaceSecret}
|
||||||
|
setErrorMessage={setErrorMessage}
|
||||||
|
placeholder="Client Secret for Subspace"
|
||||||
|
ref={refSubspaceSecret}
|
||||||
|
style={{ margin: '0 4px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StyledInputGroup>
|
||||||
|
<TableMenu
|
||||||
|
disabled={!hasSubspace}
|
||||||
|
sid="subspace"
|
||||||
|
open={menuOpen === "subspace"}
|
||||||
|
handleMenuOpen={handleSubspaceMenuOpen}
|
||||||
|
menuItems={subspaceSipTeleportId ? [subspaceMenuItems[1]] : [subspaceMenuItems[0]]}
|
||||||
|
/>
|
||||||
|
</StyledInputGroup>
|
||||||
|
</InputGroup>
|
||||||
|
{subspaceSipTeleportId ? (
|
||||||
|
<div style={{ gridColumn: 2, textAlign: 'left' }}>
|
||||||
|
<div>Subspace is now enabled. To send your traffic through Subspace:</div>
|
||||||
|
{subspaceSipTeleportEntryPoints.map(entrypoint => (
|
||||||
|
<div key={entrypoint.transport_type}>
|
||||||
|
<Span>send {entrypoint.transport_type.split('_').join(' and ')} traffic to </Span>
|
||||||
|
<CopyableText text={entrypoint.address} textType="Address" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{showSubspaceModal && (
|
||||||
|
<Modal
|
||||||
|
title={subspaceEnable ? 'Have Subspace send SIP to:' : 'Are you sure you want to delete your Subspace SIP Teleport?'}
|
||||||
|
loader={generatingSubspace}
|
||||||
|
hideButtons={generatingSubspace}
|
||||||
|
maskClosable={!generatingSubspace}
|
||||||
|
actionText={subspaceEnable ? 'Save' : 'Disable'}
|
||||||
|
content={
|
||||||
|
<ModalContainer>
|
||||||
|
{subspaceEnable ? (
|
||||||
|
<>
|
||||||
|
{sipRealm && (
|
||||||
|
<Radio
|
||||||
|
noLeftMargin
|
||||||
|
name="subspaceSipRealm"
|
||||||
|
id="sipRealmAccount"
|
||||||
|
label={sipRealm}
|
||||||
|
checked={subspaceSipRealm === sipRealm}
|
||||||
|
onChange={() => {
|
||||||
|
setSubspaceSipRealm(sipRealm);
|
||||||
|
setSubspaceSipRealmOtherValue('');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{sbcs.map((sbc) => {
|
||||||
|
return (
|
||||||
|
<Radio
|
||||||
|
key={sbc.ipv4}
|
||||||
|
noLeftMargin
|
||||||
|
name="subspaceSipRealm"
|
||||||
|
id={sbc.sbc_address_sid}
|
||||||
|
label={`${sbc.ipv4}:${sbc.port}`}
|
||||||
|
checked={subspaceSipRealm === `${sbc.ipv4}:${sbc.port}`}
|
||||||
|
onChange={() => {
|
||||||
|
setSubspaceSipRealm(`${sbc.ipv4}:${sbc.port}`);
|
||||||
|
setSubspaceSipRealmOtherValue('');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Radio
|
||||||
|
noLeftMargin
|
||||||
|
name="subspaceSipRealm"
|
||||||
|
id="sipRealmOther"
|
||||||
|
label="Other"
|
||||||
|
checked={subspaceSipRealm === 'other'}
|
||||||
|
onChange={() => {
|
||||||
|
setSubspaceSipRealm('other');
|
||||||
|
setTimeout(() => refSubspaceOtherSip.current.focus(), 0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{subspaceSipRealm === 'other' && (
|
||||||
|
<Input
|
||||||
|
ref={refSubspaceOtherSip}
|
||||||
|
name="subspaceSipRealm"
|
||||||
|
id="sipRealmOtherValue"
|
||||||
|
value={subspaceSipRealmOtherValue}
|
||||||
|
onChange={e => setSubspaceSipRealmOtherValue(e.target.value)}
|
||||||
|
placeholder="IP address or DNS name"
|
||||||
|
style={{ marginTop: '8px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</ModalContainer>
|
||||||
|
}
|
||||||
|
handleCancel={() => {
|
||||||
|
setShowSubspaceModal(false);
|
||||||
|
resetSubspaceState();
|
||||||
|
}}
|
||||||
|
handleSubmit={() => {
|
||||||
|
if (subspaceEnable) {
|
||||||
|
handleSubspaceEnable();
|
||||||
|
} else {
|
||||||
|
handleSubspaceDisable();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null }
|
||||||
|
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<FormError grid message={errorMessage} />
|
<FormError grid message={errorMessage} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import SpeechRecognizerLanguageGoogle from '../../data/SpeechRecognizerLanguageG
|
|||||||
import SpeechRecognizerLanguageAws from '../../data/SpeechRecognizerLanguageAws';
|
import SpeechRecognizerLanguageAws from '../../data/SpeechRecognizerLanguageAws';
|
||||||
import SpeechRecognizerLanguageMicrosoft from '../../data/SpeechRecognizerLanguageMicrosoft';
|
import SpeechRecognizerLanguageMicrosoft from '../../data/SpeechRecognizerLanguageMicrosoft';
|
||||||
import SpeechSynthesisLanguageMicrosoft from '../../data/SpeechSynthesisLanguageMicrosoft';
|
import SpeechSynthesisLanguageMicrosoft from '../../data/SpeechSynthesisLanguageMicrosoft';
|
||||||
|
import SpeechSynthesisLanguageWellSaid from '../../data/SpeechSynthesisLanguageWellSaid';
|
||||||
import Loader from '../blocks/Loader';
|
import Loader from '../blocks/Loader';
|
||||||
import CopyableText from '../elements/CopyableText';
|
import CopyableText from '../elements/CopyableText';
|
||||||
|
|
||||||
@@ -751,6 +752,10 @@ const ApplicationForm = props => {
|
|||||||
? SpeechSynthesisLanguageMicrosoft.find(l => (
|
? SpeechSynthesisLanguageMicrosoft.find(l => (
|
||||||
l.code === speechSynthesisLanguage
|
l.code === speechSynthesisLanguage
|
||||||
))
|
))
|
||||||
|
: e.target.value === 'wellsaid'
|
||||||
|
? SpeechSynthesisLanguageWellSaid.find(l => (
|
||||||
|
l.code === speechSynthesisLanguage
|
||||||
|
))
|
||||||
: SpeechSynthesisLanguageAws.find(l => (
|
: SpeechSynthesisLanguageAws.find(l => (
|
||||||
l.code === speechSynthesisLanguage
|
l.code === speechSynthesisLanguage
|
||||||
));
|
));
|
||||||
@@ -780,6 +785,7 @@ const ApplicationForm = props => {
|
|||||||
<option value="google">Google</option>
|
<option value="google">Google</option>
|
||||||
<option value="aws">AWS</option>
|
<option value="aws">AWS</option>
|
||||||
<option value="microsoft">Microsoft</option>
|
<option value="microsoft">Microsoft</option>
|
||||||
|
<option value="wellsaid">WellSaid</option>
|
||||||
</Select>
|
</Select>
|
||||||
<Label middle htmlFor="speechSynthesisLanguage">Language</Label>
|
<Label middle htmlFor="speechSynthesisLanguage">Language</Label>
|
||||||
<Select
|
<Select
|
||||||
@@ -807,6 +813,10 @@ const ApplicationForm = props => {
|
|||||||
? SpeechSynthesisLanguageMicrosoft.find(l => (
|
? SpeechSynthesisLanguageMicrosoft.find(l => (
|
||||||
l.code === e.target.value
|
l.code === e.target.value
|
||||||
))
|
))
|
||||||
|
: speechSynthesisVendor === 'wellsaid'
|
||||||
|
? SpeechSynthesisLanguageWellSaid.find(l => (
|
||||||
|
l.code === e.target.value
|
||||||
|
))
|
||||||
: SpeechSynthesisLanguageAws.find(l => (
|
: SpeechSynthesisLanguageAws.find(l => (
|
||||||
l.code === e.target.value
|
l.code === e.target.value
|
||||||
));
|
));
|
||||||
@@ -823,6 +833,10 @@ const ApplicationForm = props => {
|
|||||||
SpeechSynthesisLanguageMicrosoft.map(l => (
|
SpeechSynthesisLanguageMicrosoft.map(l => (
|
||||||
<option key={l.code} value={l.code}>{l.name}</option>
|
<option key={l.code} value={l.code}>{l.name}</option>
|
||||||
))
|
))
|
||||||
|
) : speechSynthesisVendor === 'wellsaid' ? (
|
||||||
|
SpeechSynthesisLanguageWellSaid.map(l => (
|
||||||
|
<option key={l.code} value={l.code}>{l.name}</option>
|
||||||
|
))
|
||||||
) : (
|
) : (
|
||||||
SpeechSynthesisLanguageAws.map(l => (
|
SpeechSynthesisLanguageAws.map(l => (
|
||||||
<option key={l.code} value={l.code}>{l.name}</option>
|
<option key={l.code} value={l.code}>{l.name}</option>
|
||||||
@@ -849,6 +863,12 @@ const ApplicationForm = props => {
|
|||||||
.map(m => m.voices.map(v => (
|
.map(m => m.voices.map(v => (
|
||||||
<option key={v.value} value={v.value}>{v.name}</option>
|
<option key={v.value} value={v.value}>{v.name}</option>
|
||||||
)))
|
)))
|
||||||
|
) : speechSynthesisVendor === 'wellsaid' ? (
|
||||||
|
SpeechSynthesisLanguageWellSaid
|
||||||
|
.filter(l => l.code === speechSynthesisLanguage)
|
||||||
|
.map(m => m.voices.map(v => (
|
||||||
|
<option key={v.value} value={v.value}>{v.name}</option>
|
||||||
|
)))
|
||||||
) : (
|
) : (
|
||||||
SpeechSynthesisLanguageAws
|
SpeechSynthesisLanguageAws
|
||||||
.filter(l => l.code === speechSynthesisLanguage)
|
.filter(l => l.code === speechSynthesisLanguage)
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ const CarrierForm = (props) => {
|
|||||||
promises.push(applicationPromise); // 0
|
promises.push(applicationPromise); // 0
|
||||||
promises.push(accountsPromise); // 1
|
promises.push(accountsPromise); // 1
|
||||||
promises.push(smppsPromise); // 2
|
promises.push(smppsPromise); // 2
|
||||||
promises.push(sbcsPromise) // 3
|
promises.push(sbcsPromise); // 3
|
||||||
|
|
||||||
if (type === 'edit') {
|
if (type === 'edit') {
|
||||||
const carrierPromise = axios({
|
const carrierPromise = axios({
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import Code from '../elements/Code';
|
|||||||
import FormError from '../blocks/FormError';
|
import FormError from '../blocks/FormError';
|
||||||
import Button from '../elements/Button';
|
import Button from '../elements/Button';
|
||||||
import Loader from '../blocks/Loader';
|
import Loader from '../blocks/Loader';
|
||||||
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext'
|
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||||
|
|
||||||
import MicrosoftAzureRegions from '../../data/MicrosoftAzureRegions';
|
import MicrosoftAzureRegions from '../../data/MicrosoftAzureRegions';
|
||||||
|
|
||||||
@@ -70,6 +70,7 @@ const SpeechServicesAddEdit = (props) => {
|
|||||||
const refVendorGoogle = useRef(null);
|
const refVendorGoogle = useRef(null);
|
||||||
const refVendorAws = useRef(null);
|
const refVendorAws = useRef(null);
|
||||||
const refVendorMs = useRef(null);
|
const refVendorMs = useRef(null);
|
||||||
|
const refVendorWellSaid = useRef(null);
|
||||||
const refAccessKeyId = useRef(null);
|
const refAccessKeyId = useRef(null);
|
||||||
const refSecretAccessKey = useRef(null);
|
const refSecretAccessKey = useRef(null);
|
||||||
const refUseForTts = useRef(null);
|
const refUseForTts = useRef(null);
|
||||||
@@ -93,7 +94,8 @@ const SpeechServicesAddEdit = (props) => {
|
|||||||
// Invalid form inputs
|
// Invalid form inputs
|
||||||
const [ invalidVendorGoogle, setInvalidVendorGoogle ] = useState(false);
|
const [ invalidVendorGoogle, setInvalidVendorGoogle ] = useState(false);
|
||||||
const [ invalidVendorAws, setInvalidVendorAws ] = useState(false);
|
const [ invalidVendorAws, setInvalidVendorAws ] = useState(false);
|
||||||
const [ invalidVendorMs, setInvalidVendorMs ] = useState(false);
|
const [ invalidVendorMs, setInvalidVendorMs ] = useState(false);
|
||||||
|
const [ invalidVendorWellSaid, setInvalidVendorWellSaid ] = useState(false);
|
||||||
const [ invalidAccessKeyId, setInvalidAccessKeyId ] = useState(false);
|
const [ invalidAccessKeyId, setInvalidAccessKeyId ] = useState(false);
|
||||||
const [ invalidSecretAccessKey, setInvalidSecretAccessKey ] = useState(false);
|
const [ invalidSecretAccessKey, setInvalidSecretAccessKey ] = useState(false);
|
||||||
const [ invalidUseForTts, setInvalidUseForTts ] = useState(false);
|
const [ invalidUseForTts, setInvalidUseForTts ] = useState(false);
|
||||||
@@ -219,6 +221,7 @@ const SpeechServicesAddEdit = (props) => {
|
|||||||
setInvalidVendorGoogle(false);
|
setInvalidVendorGoogle(false);
|
||||||
setInvalidVendorAws(false);
|
setInvalidVendorAws(false);
|
||||||
setInvalidVendorMs(false);
|
setInvalidVendorMs(false);
|
||||||
|
setInvalidVendorWellSaid(false);
|
||||||
setInvalidAccessKeyId(false);
|
setInvalidAccessKeyId(false);
|
||||||
setInvalidSecretAccessKey(false);
|
setInvalidSecretAccessKey(false);
|
||||||
setInvalidUseForTts(false);
|
setInvalidUseForTts(false);
|
||||||
@@ -232,6 +235,7 @@ const SpeechServicesAddEdit = (props) => {
|
|||||||
setInvalidVendorGoogle(true);
|
setInvalidVendorGoogle(true);
|
||||||
setInvalidVendorAws(true);
|
setInvalidVendorAws(true);
|
||||||
setInvalidVendorMs(true);
|
setInvalidVendorMs(true);
|
||||||
|
setInvalidVendorWellSaid(true);
|
||||||
if (!focusHasBeenSet) {
|
if (!focusHasBeenSet) {
|
||||||
refVendorGoogle.current.focus();
|
refVendorGoogle.current.focus();
|
||||||
focusHasBeenSet = true;
|
focusHasBeenSet = true;
|
||||||
@@ -278,6 +282,15 @@ const SpeechServicesAddEdit = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (vendor === 'wellsaid' && !apiKey) {
|
||||||
|
errorMessages.push('Please provide an API key.');
|
||||||
|
setInvalidApiKey(true);
|
||||||
|
if (!focusHasBeenSet) {
|
||||||
|
refApiKey.current.focus();
|
||||||
|
focusHasBeenSet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (errorMessages.length > 1) {
|
if (errorMessages.length > 1) {
|
||||||
setErrorMessage(errorMessages);
|
setErrorMessage(errorMessages);
|
||||||
return;
|
return;
|
||||||
@@ -309,7 +322,7 @@ const SpeechServicesAddEdit = (props) => {
|
|||||||
service_key: vendor === 'google' ? JSON.stringify(serviceKey) : null,
|
service_key: vendor === 'google' ? JSON.stringify(serviceKey) : null,
|
||||||
access_key_id: vendor === 'aws' ? accessKeyId : null,
|
access_key_id: vendor === 'aws' ? accessKeyId : null,
|
||||||
secret_access_key: vendor === 'aws' ? secretAccessKey : null,
|
secret_access_key: vendor === 'aws' ? secretAccessKey : null,
|
||||||
api_key: vendor === 'microsoft' ? apiKey : null,
|
api_key: ['microsoft', 'wellsaid'].includes(vendor) ? apiKey : null,
|
||||||
region: vendor === 'microsoft' ? region : null,
|
region: vendor === 'microsoft' ? region : null,
|
||||||
use_for_tts: useForTts,
|
use_for_tts: useForTts,
|
||||||
use_for_stt: useForStt,
|
use_for_stt: useForStt,
|
||||||
@@ -490,6 +503,17 @@ const SpeechServicesAddEdit = (props) => {
|
|||||||
ref={refVendorMs}
|
ref={refVendorMs}
|
||||||
disabled={type === 'edit'}
|
disabled={type === 'edit'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Radio
|
||||||
|
name="vendor"
|
||||||
|
id="wellsaid"
|
||||||
|
label="WellSaid"
|
||||||
|
checked={vendor === 'wellsaid'}
|
||||||
|
onChange={() => setVendor('wellsaid')}
|
||||||
|
invalid={invalidVendorWellSaid}
|
||||||
|
ref={refVendorWellSaid}
|
||||||
|
disabled={type === 'edit'}
|
||||||
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
<Label htmlFor="account">Used by</Label>
|
<Label htmlFor="account">Used by</Label>
|
||||||
@@ -594,11 +618,25 @@ const SpeechServicesAddEdit = (props) => {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</>
|
</>
|
||||||
|
) : vendor === 'wellsaid' ? (
|
||||||
|
<>
|
||||||
|
<Label htmlFor="apiKey">API Key</Label>
|
||||||
|
<Input
|
||||||
|
name="apiKey"
|
||||||
|
id="apiKey"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={e => setApiKey(e.target.value)}
|
||||||
|
placeholder=""
|
||||||
|
invalid={invalidApiKey}
|
||||||
|
ref={refApiKey}
|
||||||
|
disabled={type === 'edit'}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
null
|
null
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{vendor === 'google' || vendor === 'aws' || vendor === 'microsoft' ? (
|
{['google', 'aws', 'microsoft', 'wellsaid'].includes(vendor) ? (
|
||||||
<>
|
<>
|
||||||
<div/>
|
<div/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -617,13 +655,15 @@ const SpeechServicesAddEdit = (props) => {
|
|||||||
name="useForStt"
|
name="useForStt"
|
||||||
id="useForStt"
|
id="useForStt"
|
||||||
label="Use for speech-to-text"
|
label="Use for speech-to-text"
|
||||||
|
disabled={'wellsaid' === vendor}
|
||||||
checked={useForStt}
|
checked={useForStt}
|
||||||
onChange={e => setUseForStt(e.target.checked)}
|
onChange={e => setUseForStt(e.target.checked)}
|
||||||
invalid={invalidUseForStt}
|
invalid={invalidUseForStt}
|
||||||
ref={refUseForStt}
|
ref={refUseForStt}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) :
|
||||||
|
(
|
||||||
null
|
null
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const AccountsList = () => {
|
|||||||
sip_realm: a.sip_realm,
|
sip_realm: a.sip_realm,
|
||||||
url_reg: a.registration_hook && a.registration_hook.url,
|
url_reg: a.registration_hook && a.registration_hook.url,
|
||||||
url_queue: a.queue_event_hook && a.queue_event_hook.url,
|
url_queue: a.queue_event_hook && a.queue_event_hook.url,
|
||||||
|
subspace_enabled: a.subspace_sip_teleport_id ? 'Enabled' : ''
|
||||||
}));
|
}));
|
||||||
return(simplifiedAccounts);
|
return(simplifiedAccounts);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -220,7 +221,7 @@ const AccountsList = () => {
|
|||||||
{ header: 'AccountSid', key: 'sid' },
|
{ header: 'AccountSid', key: 'sid' },
|
||||||
{ header: 'SIP Realm', key: 'sip_realm' },
|
{ header: 'SIP Realm', key: 'sip_realm' },
|
||||||
{ header: 'Registration Webhook', key: 'url_reg' },
|
{ header: 'Registration Webhook', key: 'url_reg' },
|
||||||
{ header: 'Queue Event Webhook', key: 'url_queue' },
|
{ header: 'Queue Event Webhook', key: 'url_queue' }
|
||||||
]}
|
]}
|
||||||
formatContentToDelete={formatAccountToDelete}
|
formatContentToDelete={formatAccountToDelete}
|
||||||
deleteContent={deleteAccount}
|
deleteContent={deleteAccount}
|
||||||
|
|||||||
39
src/data/SpeechSynthesisLanguageWellSaid.js
Normal file
39
src/data/SpeechSynthesisLanguageWellSaid.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
code: 'en-US',
|
||||||
|
name: 'English (US)',
|
||||||
|
voices: [
|
||||||
|
{ value: '3', name: 'Alana B.' },
|
||||||
|
{ value: '4', name: 'Ramona J.' },
|
||||||
|
{ value: '5', name: 'Ramona J. (promo)' },
|
||||||
|
{ value: '7', name: 'Wade C.' },
|
||||||
|
{ value: '8', name: 'Sofia H.' },
|
||||||
|
{ value: '9', name: 'David D.' },
|
||||||
|
{ value: '11', name: 'Isabel V.' },
|
||||||
|
{ value: '12', name: 'Ava H.' },
|
||||||
|
{ value: '13', name: 'Jeremy G.' },
|
||||||
|
{ value: '14', name: 'Nicole L.' },
|
||||||
|
{ value: '15', name: 'Paige L.' },
|
||||||
|
{ value: '16', name: 'Tobin A.' },
|
||||||
|
{ value: '17', name: 'Kai M.' },
|
||||||
|
{ value: '18', name: 'Tristan F.' },
|
||||||
|
{ value: '19', name: 'Patrick K.' },
|
||||||
|
{ value: '20', name: 'Soifia H. (promo)' },
|
||||||
|
{ value: '21', name: 'Damian P. (promo)' },
|
||||||
|
{ value: '22', name: 'Jodi P. (promo)' },
|
||||||
|
{ value: '23', name: 'Lee M. (promo)' },
|
||||||
|
{ value: '24', name: 'Selene R. (promo)' },
|
||||||
|
{ value: '26', name: 'Wade C. (promo)' },
|
||||||
|
{ value: '27', name: 'Joe F.' },
|
||||||
|
{ value: '28', name: 'Joe F. (promo)' },
|
||||||
|
{ value: '29', name: 'Garry J. (character)' },
|
||||||
|
{ value: '33', name: 'Jude D.' },
|
||||||
|
{ value: '34', name: 'Eric S. (promo)' },
|
||||||
|
{ value: '35', name: 'Chase J.' },
|
||||||
|
{ value: '37', name: 'Steve B. (promo)' },
|
||||||
|
{ value: '38', name: 'Bella B. (promo)' },
|
||||||
|
{ value: '39', name: 'Tilda C. (promo)' },
|
||||||
|
{ value: '41', name: 'Paul B. (promo)' }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user