mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-01-25 02:08:19 +00:00
Compare commits
64 Commits
v0.4.2
...
v0.7.5-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38d26dddc8 | ||
|
|
c0d531c63f | ||
|
|
420080ba84 | ||
|
|
c40fb9cc01 | ||
|
|
40143ae79d | ||
|
|
536b183535 | ||
|
|
bf88a27330 | ||
|
|
831450306d | ||
|
|
c8d1034dc9 | ||
|
|
37af9522aa | ||
|
|
6bb81a499b | ||
|
|
58f97dcfb2 | ||
|
|
23a067b6dd | ||
|
|
92db20965e | ||
|
|
8f8d635bd3 | ||
|
|
b075028b7b | ||
|
|
b5f2e5fc25 | ||
|
|
6390cc6b81 | ||
|
|
d7db92f0c7 | ||
|
|
f5201d2d69 | ||
|
|
128ca045b0 | ||
|
|
3403996946 | ||
|
|
87dbb461e0 | ||
|
|
9bce9c5510 | ||
|
|
2db5f26dbf | ||
|
|
bfc7cc971c | ||
|
|
35f353c905 | ||
|
|
922d664bf8 | ||
|
|
ff4d6b6e11 | ||
|
|
70387ff4f1 | ||
|
|
7a4c583345 | ||
|
|
d54fbc4782 | ||
|
|
14dd1319d9 | ||
|
|
8538d40696 | ||
|
|
eda1fa0dc4 | ||
|
|
0174315a68 | ||
|
|
b86bf0c403 | ||
|
|
3fc1c800ac | ||
|
|
ee4483288d | ||
|
|
d3f1dbf332 | ||
|
|
5141989bb5 | ||
|
|
35889ba122 | ||
|
|
09b0bc8dde | ||
|
|
4f1b928f8c | ||
|
|
94c0fc88c1 | ||
|
|
b5a559bd08 | ||
|
|
24cb269379 | ||
|
|
c73ca9f46a | ||
|
|
25a93edeac | ||
|
|
7e2488a9c3 | ||
|
|
42872e9878 | ||
|
|
809e1ae30f | ||
|
|
04c8d05266 | ||
|
|
e3d384158f | ||
|
|
01c0aa321e | ||
|
|
30bcf9414f | ||
|
|
fb2880b465 | ||
|
|
f5f92e58e1 | ||
|
|
33734c91f6 | ||
|
|
478949ef73 | ||
|
|
3ede7d5077 | ||
|
|
d36a291543 | ||
|
|
630b555fe7 | ||
|
|
f16063ba44 |
3
.env
3
.env
@@ -1 +1,2 @@
|
||||
REACT_APP_API_BASE_URL=http://[ip]:[port]/v1
|
||||
REACT_APP_API_BASE_URL=http://127.0.0.1:3002/v1
|
||||
GENERATE_SOURCEMAP=false
|
||||
13
.eslintrc
13
.eslintrc
@@ -2,15 +2,12 @@
|
||||
"extends": "react-app",
|
||||
"rules": {
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-trailing-spaces": [
|
||||
"error"
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
51
.github/workflows/docker-publish.yml
vendored
Normal file
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
|
||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
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 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"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Drachtio Communications Services, LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -25,7 +25,11 @@ If there is an update to this code base, you can update the code without re-depl
|
||||
|
||||
## Development
|
||||
|
||||
Like production, you must specify the IP:port of the Jambonz API you will be hitting.
|
||||
### Local server
|
||||
See [howto-setup-test-environment](./howto-setup-test-environment.md) for details on how to set up a complete local test environment on your laptop.
|
||||
|
||||
### Remote server
|
||||
If you want to test against a remote server, you must specify the IP:port of the Jambonz API you will be hitting.
|
||||
|
||||
1. Copy `.env` to `.env.local`
|
||||
2. In `.env.local`, replace `[ip]:[port]` with the API's IP and port
|
||||
|
||||
10
entrypoint.sh
Normal file
10
entrypoint.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
PUBLIC_IPV4="$(curl --fail -qs whatismyip.akamai.com)"
|
||||
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
|
||||
cd /opt/app/
|
||||
TAG="<script>window.JAMBONZ = { APP_API_BASE_URL: '${REACT_APP_API_BASE_URL}'};</script>"
|
||||
sed -i -e "\@</head>@i\ $TAG" ./build/index.html
|
||||
npm run serve
|
||||
59
howto-setup-test-environment.md
Normal file
59
howto-setup-test-environment.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Setting up a local test environment
|
||||
This document describes how to set up a local development and test environment on your laptop. Testing the jambonz-webapp requires a back-end system to run against, and we use docker-compose to run these back-end components, allowing you to develop and test the react UI locally.
|
||||
|
||||
## Prerequisites
|
||||
- You will need to have docker and docker-compose installed on your laptop.
|
||||
- You need to have cloned the [jambonz-api-server](https://github.com/jambonz/jambonz-api-server) repo to a folder on your laptop.
|
||||
|
||||
## Running the back-end services
|
||||
Make sure the docker daemon is running on your laptop. Open a terminal window and cd into the project folder for jambonz-api-server, then run the following command to start the back-end processes.
|
||||
|
||||
```bash
|
||||
cd jambonz-api-server
|
||||
npm run integration-test
|
||||
```
|
||||
|
||||
This will take a few minutes to start, but eventually a successfull startup will eventually look something like this:
|
||||
```bash
|
||||
$ npm run integration-test
|
||||
|
||||
> jambonz-api-server@v0.7.5 integration-test
|
||||
> NODE_ENV=test JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js
|
||||
|
||||
starting dockerized mysql and redis..
|
||||
mysql is running
|
||||
creating database..
|
||||
creating schema..
|
||||
seeding database..
|
||||
creating admin user..
|
||||
reset_admin_password, initial admin password is admin
|
||||
|
||||
|
||||
sipp exited with non-zero code 1 signal null
|
||||
1
|
||||
ready for testing!
|
||||
{"level":30, "time": "2022-04-14T18:07:49.318Z","pid":5292,"hostname":"MacBook-Pro-2.local","msg":"listening for HTTP traffic on port 3000","v":1}
|
||||
{"level":20, "time": "2022-04-14T18:07:49.325Z","pid":5292,"hostname":"MacBook-Pro-2.local","args":[],"msg":"redis event connect","v":1}
|
||||
{"level":20, "time": "2022-04-14T18:07:49.345Z","pid":5292,"hostname":"MacBook-Pro-2.local","args":[],"msg":"redis event ready","v":1}
|
||||
```
|
||||
|
||||
This starts the a docker-compose network running the following containers:
|
||||
- mysql
|
||||
- redis
|
||||
- influxdb
|
||||
- heplify-server
|
||||
- drachtio
|
||||
- homer-webapp
|
||||
|
||||
Leaving the jambonz-api-server process running, open another terminal window, cd into the folder where you have checked out this project, and start it as shown below:
|
||||
|
||||
```
|
||||
cd jambonz-webapp
|
||||
npm start
|
||||
```
|
||||
This will start the react UI and open a browser page to http://localhost:3001.
|
||||
|
||||
You should now see the login page to the jambonz webapp and can log in with username admin and password admin. You will be forced to change the password, and then you should see the main page of the application.
|
||||
|
||||
From here you can make and test changes locally.
|
||||
|
||||
32892
package-lock.json
generated
32892
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -1,25 +1,27 @@
|
||||
{
|
||||
"name": "jambonz-cpaas-ui",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"name": "jambonz-webapp",
|
||||
"version": "v0.7.5",
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"axios": "^0.19.2",
|
||||
"antd": "^4.15.4",
|
||||
"axios": "^0.21.1",
|
||||
"moment": "^2.29.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.4.1",
|
||||
"serve": "^11.3.0",
|
||||
"react-scripts": "^5.0.0",
|
||||
"serve": "^13.0.2",
|
||||
"styled-components": "^5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "PORT=3001 react-scripts start",
|
||||
"start": "PORT=${HTTP_PORT:-3001} react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"serve": "serve -s build -l 3001",
|
||||
"serve": "serve -s build -l ${HTTP_PORT:-3001}",
|
||||
"pm2": "pm2 start npm --name \"jambonz-webapp\" -- run serve",
|
||||
"deploy": "npm i && npm run build && npm run pm2"
|
||||
},
|
||||
|
||||
27
src/App.js
27
src/App.js
@@ -10,16 +10,20 @@ import ConfigureSipTrunk from './components/pages/setup/ConfigureSipTrunk';
|
||||
import SetupComplete from './components/pages/setup/SetupComplete';
|
||||
import AccountsList from './components/pages/internal/AccountsList';
|
||||
import ApplicationsList from './components/pages/internal/ApplicationsList';
|
||||
import SipTrunksList from './components/pages/internal/SipTrunksList';
|
||||
import CarriersList from './components/pages/internal/CarriersList';
|
||||
import PhoneNumbersList from './components/pages/internal/PhoneNumbersList';
|
||||
import MsTeamsTenantsList from './components/pages/internal/MsTeamsTenantsList';
|
||||
import AccountsAddEdit from './components/pages/internal/AccountsAddEdit';
|
||||
import ApplicationsAddEdit from './components/pages/internal/ApplicationsAddEdit';
|
||||
import SipTrunksAddEdit from './components/pages/internal/SipTrunksAddEdit';
|
||||
import CarriersAddEdit from './components/pages/internal/CarriersAddEdit';
|
||||
import PhoneNumbersAddEdit from './components/pages/internal/PhoneNumbersAddEdit';
|
||||
import MsTeamsTenantsAddEdit from './components/pages/internal/MsTeamsTenantsAddEdit';
|
||||
import Settings from './components/pages/internal/Settings';
|
||||
import RecentCallsList from './components/pages/internal/RecentCallsList';
|
||||
import AlertsList from './components/pages/internal/AlertsList';
|
||||
import InvalidRoute from './components/pages/InvalidRoute';
|
||||
import SpeechServicesList from './components/pages/internal/SpeechServicesList';
|
||||
import SpeechServicesAddEdit from './components/pages/internal/SpeechServicesAddEdit';
|
||||
|
||||
import Notification from './components/blocks/Notification';
|
||||
import Nav from './components/blocks/Nav';
|
||||
@@ -44,7 +48,8 @@ function App() {
|
||||
<SideMenu />
|
||||
<Route exact path="/internal/accounts"><AccountsList /></Route>
|
||||
<Route exact path="/internal/applications"><ApplicationsList /></Route>
|
||||
<Route exact path="/internal/sip-trunks"><SipTrunksList /></Route>
|
||||
<Route exact path="/internal/carriers"><CarriersList /></Route>
|
||||
<Route exact path="/internal/speech-services"><SpeechServicesList /></Route>
|
||||
<Route exact path="/internal/phone-numbers"><PhoneNumbersList /></Route>
|
||||
<Route exact path="/internal/ms-teams-tenants"><MsTeamsTenantsList /></Route>
|
||||
|
||||
@@ -63,10 +68,17 @@ function App() {
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/sip-trunks/add",
|
||||
"/internal/sip-trunks/:voip_carrier_sid/edit"
|
||||
"/internal/carriers/add",
|
||||
"/internal/carriers/:voip_carrier_sid/edit"
|
||||
]}>
|
||||
<SipTrunksAddEdit />
|
||||
<CarriersAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/speech-services/add",
|
||||
"/internal/speech-services/:speech_service_sid/edit"
|
||||
]}>
|
||||
<SpeechServicesAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
@@ -84,6 +96,9 @@ function App() {
|
||||
</Route>
|
||||
|
||||
<Route exact path="/internal/settings"><Settings /></Route>
|
||||
|
||||
<Route exact path="/internal/recent-calls"><RecentCallsList /></Route>
|
||||
<Route exact path="/internal/alerts"><AlertsList /></Route>
|
||||
</div>
|
||||
</Route>
|
||||
|
||||
|
||||
89
src/components/blocks/AntdTable.js
Normal file
89
src/components/blocks/AntdTable.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components/macro";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import Loader from "../../components/blocks/Loader";
|
||||
|
||||
import Table from "antd/lib/table";
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
width: 100%;
|
||||
margin-top: 1rem !important;
|
||||
|
||||
table {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
|
||||
tr,
|
||||
th,
|
||||
td {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
height: 32px;
|
||||
|
||||
.ant-pagination-simple-pager {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-item {
|
||||
border: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLoader = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const AntdTable = ({ dataSource, columns, loading, ...rest }) => {
|
||||
let props = {
|
||||
...rest,
|
||||
dataSource,
|
||||
columns,
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
props = {
|
||||
...props,
|
||||
loading: {
|
||||
spinning: true,
|
||||
indicator: (
|
||||
<StyledLoader>
|
||||
<Loader />
|
||||
</StyledLoader>
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return <StyledTable {...props} />;
|
||||
};
|
||||
|
||||
AntdTable.propTypes = {
|
||||
dataSource: PropTypes.array,
|
||||
loading: PropTypes.bool,
|
||||
columns: PropTypes.array,
|
||||
};
|
||||
|
||||
AntdTable.defaultProps = {
|
||||
dataSource: [],
|
||||
loading: false,
|
||||
columns: [],
|
||||
};
|
||||
|
||||
export default AntdTable;
|
||||
@@ -25,6 +25,8 @@ const ModalContainer = styled.div`
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
background: #FFF;
|
||||
text-align: left;
|
||||
|
||||
& h1 {
|
||||
margin-top: 0;
|
||||
font-size: 1.25rem;
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import React, { useContext } from 'react';
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useContext, useEffect, useState, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import Button from '../elements/Button';
|
||||
import Label from '../elements/Label';
|
||||
import Select from '../elements/Select';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Modal from '../blocks/Modal';
|
||||
import FormError from '../blocks/FormError';
|
||||
import handleErrors from "../../helpers/handleErrors";
|
||||
import { Link as ReactRouterLink } from 'react-router-dom';
|
||||
import { ServiceProviderValueContext, ServiceProviderMethodContext } from '../../contexts/ServiceProviderContext';
|
||||
import LogoJambong from "../../images/LogoJambong.svg";
|
||||
import AddModalButton from '../elements/AddModalButton';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const StyledNav = styled.nav`
|
||||
position: relative;
|
||||
@@ -14,13 +29,6 @@ const StyledNav = styled.nav`
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.12);
|
||||
`;
|
||||
|
||||
const NavH1 = styled.h1`
|
||||
margin: 1.25rem 0 1.25rem 2rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: normal;
|
||||
line-height: 1em;
|
||||
`;
|
||||
|
||||
const LogOutContainer = styled.div`
|
||||
margin-right: 3rem;
|
||||
@media (max-width: 34rem) {
|
||||
@@ -28,10 +36,53 @@ const LogOutContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled(ReactRouterLink)`
|
||||
text-decoration: none;
|
||||
margin: 0 0 0 2rem;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledForm = styled(Form)`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
`;
|
||||
|
||||
const StyledLabel = styled(Label)`
|
||||
margin-right: 1rem;
|
||||
`;
|
||||
|
||||
const ModalContainer = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
width: 500;
|
||||
`;
|
||||
|
||||
const StyledFormError = styled(FormError)`
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
const Nav = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const setCurrentServiceProvider = useContext(ServiceProviderMethodContext);
|
||||
const [serviceProviders, setServiceProviders] = useState([]);
|
||||
const [showServiceProviderModal, setShowServiceProviderModal] = useState(false);
|
||||
const [showModalLoader, setShowModalLoader] = useState(false);
|
||||
const [serviceProviderName, setServiceProviderName] = useState("");
|
||||
const [serviceProviderInvalid, setServiceProviderInvalid] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const refServiceProvider = useRef(null);
|
||||
|
||||
const logOut = () => {
|
||||
localStorage.removeItem('token');
|
||||
@@ -44,9 +95,112 @@ const Nav = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeServiceProvider = (sp) => {
|
||||
if (sp === "add") {
|
||||
setShowServiceProviderModal(true);
|
||||
} else {
|
||||
setCurrentServiceProvider(sp);
|
||||
}
|
||||
};
|
||||
|
||||
const getServiceProviders = async () => {
|
||||
const jwt = localStorage.getItem('token');
|
||||
if (history.location.pathname !== '' && jwt) {
|
||||
const serviceProvidersResponse = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/ServiceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setServiceProviders(
|
||||
(serviceProvidersResponse.data || []).sort(
|
||||
(a, b) => a.name.localeCompare(b.name)
|
||||
)
|
||||
);
|
||||
|
||||
const isExisted = serviceProvidersResponse.data.find(item => item.service_provider_sid === currentServiceProvider);
|
||||
if (!isExisted) {
|
||||
setCurrentServiceProvider(serviceProvidersResponse.data[0].service_provider_sid);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddServiceProvider = async () => {
|
||||
if (serviceProviderName) {
|
||||
setServiceProviderInvalid(false);
|
||||
setErrorMessage("");
|
||||
|
||||
try {
|
||||
setShowModalLoader(true);
|
||||
const jwt = localStorage.getItem('token');
|
||||
|
||||
const serviceProviderResponse = await axios({
|
||||
method: 'post',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
data: {
|
||||
name: serviceProviderName,
|
||||
},
|
||||
});
|
||||
|
||||
setCurrentServiceProvider(serviceProviderResponse.data.sid);
|
||||
|
||||
getServiceProviders();
|
||||
setShowServiceProviderModal(false);
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch, setErrorMessage });
|
||||
} finally {
|
||||
setShowModalLoader(false);
|
||||
}
|
||||
} else {
|
||||
setServiceProviderInvalid(true);
|
||||
setErrorMessage("Please enter a name for Service Provider");
|
||||
if (refServiceProvider && refServiceProvider.current) {
|
||||
refServiceProvider.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getServiceProviders();
|
||||
}, [history.location.pathname]);
|
||||
|
||||
return (
|
||||
<StyledNav>
|
||||
<NavH1>Jambonz</NavH1>
|
||||
<StyledLink to="/internal/accounts">
|
||||
<img src={LogoJambong} alt="link-img" />
|
||||
</StyledLink>
|
||||
{location.pathname !== '/' && (
|
||||
<StyledForm>
|
||||
<StyledLabel htmlFor="serviceProvider">Service Provider:</StyledLabel>
|
||||
<Select
|
||||
name="serviceProvider"
|
||||
id="serviceProvider"
|
||||
value={currentServiceProvider}
|
||||
onChange={e => onChangeServiceProvider(e.target.value)}
|
||||
>
|
||||
{serviceProviders.map(a => (
|
||||
<option
|
||||
key={a.service_provider_sid}
|
||||
value={a.service_provider_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<AddModalButton
|
||||
addButtonText="Add Service Provider"
|
||||
onClick={()=>setShowServiceProviderModal(true)}
|
||||
/>
|
||||
</StyledForm>
|
||||
|
||||
)}
|
||||
{location.pathname !== '/' && (
|
||||
<LogOutContainer>
|
||||
<Button
|
||||
@@ -59,6 +213,37 @@ const Nav = () => {
|
||||
</Button>
|
||||
</LogOutContainer>
|
||||
)}
|
||||
{showServiceProviderModal && (
|
||||
<Modal
|
||||
title="Add New Service Provider"
|
||||
loader={showModalLoader}
|
||||
closeText="Close"
|
||||
actionText="Add"
|
||||
handleCancel={() => {
|
||||
setServiceProviderName("");
|
||||
setShowServiceProviderModal(false);
|
||||
}}
|
||||
handleSubmit={handleAddServiceProvider}
|
||||
content={
|
||||
<ModalContainer>
|
||||
<StyledLabel htmlFor="name">Name:</StyledLabel>
|
||||
<Input
|
||||
name="name"
|
||||
id="name"
|
||||
value={serviceProviderName}
|
||||
onChange={e => setServiceProviderName(e.target.value)}
|
||||
placeholder="Service provider name"
|
||||
invalid={serviceProviderInvalid}
|
||||
autoFocus
|
||||
ref={refServiceProvider}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<StyledFormError grid message={errorMessage} />
|
||||
)}
|
||||
</ModalContainer>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StyledNav>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@ import axios from 'axios';
|
||||
import styled from 'styled-components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
// import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const Container = styled.div`
|
||||
margin-top: 0.25rem;
|
||||
@@ -20,6 +22,7 @@ const Container = styled.div`
|
||||
const Sbcs = props => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
// const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const [ sbcs, setSbcs ] = useState('');
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
@@ -35,7 +38,8 @@ const Sbcs = props => {
|
||||
}
|
||||
const sbcResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
// url: `/Sbcs?service_provider_sid=${currentServiceProvider}`,
|
||||
url: '/Sbcs',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
|
||||
@@ -5,10 +5,13 @@ import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
import { ShowMsTeamsStateContext, ShowMsTeamsDispatchContext } from '../../contexts/ShowMsTeamsContext';
|
||||
import { ReactComponent as AccountsIcon } from '../../images/AccountsIcon.svg';
|
||||
import { ReactComponent as ApplicationsIcon } from '../../images/ApplicationsIcon.svg';
|
||||
import { ReactComponent as SipTrunksIcon } from '../../images/SipTrunksIcon.svg';
|
||||
import { ReactComponent as CarriersIcon } from '../../images/CarriersIcon.svg';
|
||||
import { ReactComponent as PhoneNumbersIcon } from '../../images/PhoneNumbersIcon.svg';
|
||||
import { ReactComponent as MsTeamsIcon } from '../../images/MsTeamsIcon.svg';
|
||||
import { ReactComponent as SettingsIcon } from '../../images/SettingsIcon.svg';
|
||||
import { ReactComponent as RecentCallsIcon } from '../../images/RecentCallsIcon.svg';
|
||||
import { ReactComponent as AlertsIcon } from '../../images/AlertsIcon.svg';
|
||||
import { ReactComponent as SpeechIcon } from '../../images/SpeechIcon.svg';
|
||||
|
||||
const StyledSideMenu = styled.div`
|
||||
width: 15rem;
|
||||
@@ -32,14 +35,12 @@ const StyledNavLink = styled(NavLink).attrs({ activeClassName })`
|
||||
fill: #565656;
|
||||
|
||||
&.${activeClassName} {
|
||||
box-shadow: inset 3px 0 0 0 #D91C5C;
|
||||
color: #D91C5C;
|
||||
fill: #D91C5C;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
box-shadow: inset 0 0 0 3px #D91C5C;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -68,6 +69,13 @@ const MenuText = styled.span`
|
||||
outline: 0;
|
||||
`;
|
||||
|
||||
const StyledH2 = styled.h2`
|
||||
margin: 3rem 0 1rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #757575;
|
||||
`;
|
||||
|
||||
const MenuLink = props => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
return (
|
||||
@@ -95,14 +103,18 @@ const SideMenu = () => {
|
||||
}, []);
|
||||
return (
|
||||
<StyledSideMenu>
|
||||
<MenuLink to="/internal/settings" name="Settings" icon={<SettingsIcon />} />
|
||||
<MenuLink to="/internal/accounts" name="Accounts" icon={<AccountsIcon />} />
|
||||
<MenuLink to="/internal/applications" name="Applications" icon={<ApplicationsIcon />} />
|
||||
<MenuLink to="/internal/sip-trunks" name="SIP Trunks" icon={<SipTrunksIcon />} />
|
||||
<MenuLink to="/internal/recent-calls" name="Recent Calls" icon={<RecentCallsIcon />} />
|
||||
<MenuLink to="/internal/alerts" name="Alerts" icon={<AlertsIcon />} />
|
||||
<StyledH2>Bring Your Own Services</StyledH2>
|
||||
<MenuLink to="/internal/carriers" name="Carriers" icon={<CarriersIcon />} />
|
||||
<MenuLink to="/internal/speech-services" name="Speech" icon={<SpeechIcon />} />
|
||||
<MenuLink to="/internal/phone-numbers" name="Phone Numbers" icon={<PhoneNumbersIcon />} />
|
||||
{showMsTeams && (
|
||||
<MenuLink to="/internal/ms-teams-tenants" name="MS Teams Tenants" icon={<MsTeamsIcon />} />
|
||||
)}
|
||||
<MenuLink to="/internal/settings" name="Settings" icon={<SettingsIcon />} />
|
||||
</StyledSideMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,8 @@ import Modal from '../blocks/Modal.js';
|
||||
import FormError from '../blocks/FormError.js';
|
||||
import CopyableText from '../elements/CopyableText';
|
||||
import ToggleText from '../blocks/ToggleText.js';
|
||||
import { ReactComponent as CheckGreen } from '../../images/CheckGreen.svg';
|
||||
import { ReactComponent as ErrorIcon } from '../../images/ErrorIcon.svg';
|
||||
|
||||
const Td = styled.td`
|
||||
padding: 0.5rem 0;
|
||||
@@ -91,7 +93,7 @@ const TableContent = props => {
|
||||
};
|
||||
getNewContent();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
}, [props.getContent]);
|
||||
|
||||
//=============================================================================
|
||||
// Handle checkboxes
|
||||
@@ -218,7 +220,8 @@ const TableContent = props => {
|
||||
contentToDelete.name ||
|
||||
contentToDelete.number ||
|
||||
contentToDelete.tenant_fqdn ||
|
||||
contentToDelete.token
|
||||
contentToDelete.token ||
|
||||
contentToDelete.vendor
|
||||
) && (
|
||||
<Modal
|
||||
title={`Are you sure you want to delete the following ${props.name}?`}
|
||||
@@ -389,6 +392,11 @@ const TableContent = props => {
|
||||
columnTitle = columnContent;
|
||||
} else if (a[c.key].type === 'masked') {
|
||||
columnContent = <ToggleText masked={a[c.key].masked} revealed={a[c.key].revealed} />;
|
||||
} else if (a[c.key].type === 'status') {
|
||||
columnContent = a[c.key].content === 'ok' ? <CheckGreen />
|
||||
: a[c.key].content === 'fail' ? <ErrorIcon />
|
||||
: a[c.key].content;
|
||||
columnTitle = a[c.key].title;
|
||||
}
|
||||
} else {
|
||||
columnContent = a[c.key];
|
||||
|
||||
@@ -70,6 +70,7 @@ const TableMenu = props => (
|
||||
selected={props.open}
|
||||
disabled={props.disabled}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
props.handleMenuOpen(props.sid);
|
||||
}}
|
||||
|
||||
@@ -51,6 +51,7 @@ const StyledLink = styled(FilteredLink)`
|
||||
|
||||
const Tooltip = styled.span`
|
||||
display: none;
|
||||
color: #767676;
|
||||
a:focus > &,
|
||||
a:hover > & {
|
||||
display: inline;
|
||||
|
||||
84
src/components/elements/AddModalButton.js
Normal file
84
src/components/elements/AddModalButton.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
|
||||
const FilteredLink = ({ addButtonText, ...props }) => (
|
||||
<Link {...props}>{props.children}</Link>
|
||||
);
|
||||
|
||||
const StyledLink = styled(FilteredLink)`
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
grid-column: 2;
|
||||
border-radius: 50%;
|
||||
text-decoration: none;
|
||||
color: #565656;
|
||||
margin-left: 1rem;
|
||||
position: relative;
|
||||
& > span:first-child {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
border-radius: 50%;
|
||||
outline: 0;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
font-size: 2.5rem;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
&:focus > span:first-child {
|
||||
border: 0.25rem solid #890934;
|
||||
}
|
||||
|
||||
&:hover > span:first-child {
|
||||
}
|
||||
|
||||
&:active > span:first-child {
|
||||
}
|
||||
`;
|
||||
|
||||
const Tooltip = styled.span`
|
||||
display: none;
|
||||
color: #767676;
|
||||
a:focus > &,
|
||||
a:hover > & {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
left: calc(100% + 0.75rem);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #FFF;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
z-index: 60;
|
||||
}
|
||||
`;
|
||||
|
||||
const AddModalButton = props => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
const history = useHistory();
|
||||
return (
|
||||
<StyledLink
|
||||
{...props}
|
||||
to={history.location.pathname}
|
||||
tabIndex={modalOpen ? '-1' : ''}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
+
|
||||
</span>
|
||||
<Tooltip>{props.addButtonText}</Tooltip>
|
||||
</StyledLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddModalButton;
|
||||
@@ -111,6 +111,7 @@ const Checkbox = (props, ref) => {
|
||||
name={props.id}
|
||||
type="checkbox"
|
||||
checked={props.checked}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
value={props.value}
|
||||
ref={inputRef}
|
||||
|
||||
14
src/components/elements/Code.js
Normal file
14
src/components/elements/Code.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Code = styled.code`
|
||||
white-space: pre-wrap;
|
||||
text-align: left;
|
||||
background: #fbfbfb;
|
||||
padding: 0.5rem;
|
||||
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #B6B6B6;
|
||||
max-height: 120px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export default Code;
|
||||
@@ -2,19 +2,8 @@ import React, { useContext } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import Button from './Button';
|
||||
import Span from './Span';
|
||||
|
||||
const Span = styled.span`
|
||||
text-align: left;
|
||||
${props => props.hasBorder ? `
|
||||
height: 2.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
border: 1px solid #B6B6B6;
|
||||
border-radius: 0.125rem;
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
margin-left: 1rem;
|
||||
|
||||
97
src/components/elements/FileUpload.js
Normal file
97
src/components/elements/FileUpload.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
height: 2.25rem;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
border: 0;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
&:after {
|
||||
content: '${props => props.validFile ? 'Choose a Different File' : 'Choose File'}';
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2.25rem;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus:after {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12),
|
||||
inset 0 0 0 0.25rem #890934;
|
||||
}
|
||||
|
||||
&:hover:not([disabled]):after {
|
||||
background: #BD164E;
|
||||
}
|
||||
|
||||
&:active:not([disabled]):after {
|
||||
background: #A40D40;
|
||||
}
|
||||
|
||||
&::file-selector-button {
|
||||
content: '${props => props.validFile ? 'Choose a Different File' : 'Choose File'}';
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2.25rem;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&:focus::file-selector-button {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12),
|
||||
inset 0 0 0 0.25rem #890934;
|
||||
}
|
||||
|
||||
&:hover:not([disabled])::file-selector-button {
|
||||
background: #BD164E;
|
||||
}
|
||||
|
||||
&:active:not([disabled])::file-selector-button {
|
||||
background: #A40D40;
|
||||
}
|
||||
`;
|
||||
|
||||
const FileUpload = (props, ref) => {
|
||||
return (
|
||||
<Container>
|
||||
<StyledInput
|
||||
id={props.id}
|
||||
name={props.id}
|
||||
type="file"
|
||||
onChange={props.onChange}
|
||||
validFile={props.validFile}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(FileUpload);
|
||||
@@ -24,6 +24,7 @@ const StyledReactRouterLink = styled(FilteredLink)`
|
||||
align-items: center;
|
||||
position: relative;
|
||||
outline: 0;
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
@@ -31,14 +32,18 @@ const StyledReactRouterLink = styled(FilteredLink)`
|
||||
margin: -0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0 0 0.125rem #D91C5C;
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
box-shadow: 0 0.125rem 0 #D91C5C;
|
||||
border-radius: 0;
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
&:active > span {}
|
||||
&:active > span {
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
${props => props.formLink && `
|
||||
grid-column: 2;
|
||||
|
||||
119
src/components/elements/Radio.js
Normal file
119
src/components/elements/Radio.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import Label from './Label';
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
const RadioContainer = styled.div`
|
||||
margin-left: ${props => props.noLeftMargin
|
||||
? '-0.5rem'
|
||||
: '0.5rem'
|
||||
};
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.25rem;
|
||||
padding: 0 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.125rem;
|
||||
${props => props.invalid && `
|
||||
border-color: #D91C5C;
|
||||
background: RGBA(217,28,92,0.2);
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledRadio = styled.input`
|
||||
outline: none;
|
||||
margin: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled(Label)`
|
||||
padding-left: 0.5rem;
|
||||
cursor: ${props => props.disabled
|
||||
? 'not-allowed'
|
||||
: 'pointer'
|
||||
};
|
||||
${props => props.tooltip && `
|
||||
& > span {
|
||||
border-bottom: 1px dotted;
|
||||
border-left: 1px solid transparent;
|
||||
cursor: help;
|
||||
}
|
||||
`}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.375rem;
|
||||
left: 0.5rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 1px solid #A5A5A5;
|
||||
border-radius: 50%;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
input:focus + &::before {
|
||||
border-color: #565656;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
input:checked + &::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0.75rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 50%;
|
||||
background: ${props => props.disabled
|
||||
? '#959595'
|
||||
: '#707070'
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
const Radio = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<RadioContainer
|
||||
invalid={props.invalid}
|
||||
noLeftMargin={props.noLeftMargin}
|
||||
>
|
||||
<StyledRadio
|
||||
name={props.name}
|
||||
id={props.id}
|
||||
value={props.id}
|
||||
type="radio"
|
||||
checked={props.checked}
|
||||
onChange={props.onChange}
|
||||
ref={inputRef}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
<StyledLabel
|
||||
htmlFor={props.id}
|
||||
tooltip={props.tooltip}
|
||||
invalid={props.invalid}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
<span>
|
||||
{props.label}
|
||||
{
|
||||
props.tooltip &&
|
||||
<Tooltip>
|
||||
{props.tooltip}
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
</StyledLabel>
|
||||
</RadioContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Radio);
|
||||
@@ -10,6 +10,7 @@ const Select = styled.select`
|
||||
border-radius: 0.125rem;
|
||||
background: #fff;
|
||||
color: inherit;
|
||||
max-width: 230px;
|
||||
&:focus {
|
||||
border-color: #565656;
|
||||
outline: none;
|
||||
|
||||
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;
|
||||
@@ -1,7 +1,11 @@
|
||||
import styled from 'styled-components/macro';
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
|
||||
import Link from './Link';
|
||||
|
||||
const Tooltip = styled.span`
|
||||
display: none;
|
||||
|
||||
label > span:hover > & {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
@@ -14,15 +18,105 @@ const Tooltip = styled.span`
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
z-index: 80;
|
||||
${props => !props.large ? `
|
||||
white-space: nowrap;
|
||||
` : `
|
||||
text-align: left;
|
||||
width: 22rem;
|
||||
bottom: calc(100% + 0.5rem);
|
||||
`}
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLinkWithTooltip = styled.span`
|
||||
position: relative;
|
||||
|
||||
> span {
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, calc(-100% - 5px), 0);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid #C6C6C6;
|
||||
background: #FFF;
|
||||
z-index: 80;
|
||||
white-space: pre;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-top: 8px solid #FFF;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid #C6C6C6;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const LinkWithTooltip = props => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const tooltipRef = useRef();
|
||||
const triggerRef = useRef();
|
||||
|
||||
const handleLinkClick = useCallback(() => {
|
||||
setIsActive((oldActive) => {
|
||||
const newActive = !oldActive;
|
||||
return newActive;
|
||||
});
|
||||
}, [setIsActive]);
|
||||
|
||||
const handleOuterClick = useCallback((e) => {
|
||||
if (!tooltipRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tooltipRef.current.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (triggerRef.current.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleLinkClick();
|
||||
}, [tooltipRef, triggerRef, handleLinkClick]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleOuterClick, false);
|
||||
|
||||
return () => document.removeEventListener('click', handleOuterClick, false);
|
||||
}, [handleOuterClick]);
|
||||
|
||||
return (
|
||||
<StyledLinkWithTooltip>
|
||||
<Link to="#" onClick={handleLinkClick}>
|
||||
<span ref={triggerRef}>{props.children}</span>
|
||||
</Link>
|
||||
{isActive ? (
|
||||
<span ref={tooltipRef}>
|
||||
{props.tipText}
|
||||
</span>
|
||||
) : null}
|
||||
</StyledLinkWithTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
LinkWithTooltip,
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
|
||||
@@ -1,62 +1,293 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
import Radio from '../elements/Radio';
|
||||
import Select from '../elements/Select';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import FormError from '../blocks/FormError';
|
||||
import TableMenu from '../blocks/TableMenu';
|
||||
import Loader from '../blocks/Loader';
|
||||
import Modal from '../blocks/Modal';
|
||||
import Button from '../elements/Button';
|
||||
import Link from '../elements/Link';
|
||||
import Tooltip from '../elements/Tooltip';
|
||||
import CopyableText from '../elements/CopyableText';
|
||||
import Span from '../elements/Span';
|
||||
import handleErrors from "../../helpers/handleErrors";
|
||||
import styled from 'styled-components/macro';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
|
||||
& > label {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
& > div:last-child {
|
||||
margin-top: -24px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ModalContainer = styled.div`
|
||||
margin-top: 2rem;
|
||||
`;
|
||||
|
||||
const P = styled.p`
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-weight: 500;
|
||||
color: #231f20;
|
||||
`;
|
||||
|
||||
const AccountForm = props => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const jwt = localStorage.getItem("token");
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Refs
|
||||
const refName = useRef(null);
|
||||
const refSipRealm = useRef(null);
|
||||
const refRegWebhook = useRef(null);
|
||||
const refUser = useRef(null);
|
||||
const refPassword = useRef(null);
|
||||
const refRegUser = useRef(null);
|
||||
const refRegPassword = useRef(null);
|
||||
const refQueueWebhook = useRef(null);
|
||||
const refQueueUser = useRef(null);
|
||||
const refQueuePassword = useRef(null);
|
||||
const refSubspaceId = useRef(null);
|
||||
const refSubspaceSecret = useRef(null);
|
||||
const refSubspaceOtherSip = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ name, setName ] = useState('');
|
||||
const [ sipRealm, setSipRealm ] = useState('');
|
||||
const [ name, setName ] = useState('');
|
||||
const [ sipRealm, setSipRealm ] = useState('');
|
||||
const [ deviceCallingApplication, setDeviceCallingApplication ] = useState('');
|
||||
const [ regWebhook, setRegWebhook ] = useState('');
|
||||
const [ method, setMethod ] = useState('POST');
|
||||
const [ user, setUser ] = useState('' || '');
|
||||
const [ password, setPassword ] = useState('' || '');
|
||||
const [ regWebhook, setRegWebhook ] = useState('');
|
||||
const [ regMethod, setRegMethod ] = useState('POST');
|
||||
const [ regUser, setRegUser ] = useState('');
|
||||
const [ regPassword, setRegPassword ] = useState('');
|
||||
const [ webhookSecret, setWebhookSecret ] = useState('');
|
||||
const [ queueWebhook, setQueueWebhook ] = useState('');
|
||||
const [ queueMethod, setQueueMethod ] = useState('POST');
|
||||
const [ queueUser, setQueueUser ] = useState('');
|
||||
const [ queuePassword, setQueuePassword ] = useState('');
|
||||
const [ hasSubspace, setHasSubspace ] = useState(false);
|
||||
const [ subspaceId, setSubspaceId ] = useState('');
|
||||
const [ subspaceSecret, setSubspaceSecret ] = useState('');
|
||||
const [ subspaceSipTeleportId, setSubspaceSipTeleportId ] = useState('');
|
||||
const [ subspaceSipTeleportEntryPoints, setSubspaceSipTeleportEntryPoints ] = useState([]);
|
||||
const [ showSubspaceModal, setShowSubspaceModal ] = useState(false);
|
||||
const [ generatingSubspace, setGeneratingSubspace ] = useState(false);
|
||||
const [ subspaceSipRealm, setSubspaceSipRealm ] = useState('');
|
||||
const [ sbcs, setSbcs ] = useState([]);
|
||||
const [ subspaceSipRealmOtherValue, setSubspaceSipRealmOtherValue ] = useState('');
|
||||
const [ subspaceEnable, setSubspaceEnable ] = useState(false);
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidName, setInvalidName ] = useState(false);
|
||||
const [ invalidSipRealm, setInvalidSipRealm ] = useState(false);
|
||||
const [ invalidRegWebhook, setInvalidRegWebhook ] = useState(false);
|
||||
const [ invalidUser, setInvalidUser ] = useState(false);
|
||||
const [ invalidPassword, setInvalidPassword ] = useState(false);
|
||||
const [ invalidName, setInvalidName ] = useState(false);
|
||||
const [ invalidSipRealm, setInvalidSipRealm ] = useState(false);
|
||||
const [ invalidRegWebhook, setInvalidRegWebhook ] = useState(false);
|
||||
const [ invalidRegUser, setInvalidRegUser ] = useState(false);
|
||||
const [ invalidRegPassword, setInvalidRegPassword ] = useState(false);
|
||||
const [ invalidQueueWebhook, setInvalidQueueWebhook ] = useState(false);
|
||||
const [ invalidQueueUser, setInvalidQueueUser ] = useState(false);
|
||||
const [ invalidQueuePassword, setInvalidQueuePassword ] = useState(false);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [ invalidSubspaceId, setInvalidSubspaceId ] = useState(false);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [ invalidSubspaceClient, setInvalidSubspaceClient ] = useState(false);
|
||||
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
const [ showAuth, setShowAuth ] = useState(false);
|
||||
const toggleAuth = () => setShowAuth(!showAuth);
|
||||
const [ showRegAuth, setShowRegAuth ] = useState(false);
|
||||
const [ showQueueAuth, setShowQueueAuth ] = useState(false);
|
||||
const toggleRegAuth = () => setShowRegAuth(!showRegAuth);
|
||||
const toggleQueueAuth = () => setShowQueueAuth(!showQueueAuth);
|
||||
|
||||
const [ accounts, setAccounts ] = useState([]);
|
||||
const [ accountSid, setAccountSid ] = useState('');
|
||||
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
|
||||
const [ accountApplications, setAccountApplications ] = useState([]);
|
||||
|
||||
const [ menuOpen, setMenuOpen ] = useState(null);
|
||||
const [showConfirmSecret, setShowConfirmSecret] = useState(false);
|
||||
const [generatingSecret, setGeneratingSecret] = useState(false);
|
||||
|
||||
const handleMenuOpen = sid => {
|
||||
if (menuOpen === sid) {
|
||||
setMenuOpen(null);
|
||||
} else {
|
||||
setMenuOpen(sid);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubspaceMenuOpen = sid => {
|
||||
if (menuOpen === sid) {
|
||||
setMenuOpen(null);
|
||||
} else {
|
||||
setMenuOpen(sid);
|
||||
}
|
||||
};
|
||||
|
||||
const copyWebhookSecret = async e => {
|
||||
e.preventDefault();
|
||||
setMenuOpen(null);
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(webhookSecret);
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: `Webhook Secret copied to clipboard`,
|
||||
});
|
||||
} catch (err) {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: `Unable to copy Webhook Secret.`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const generateWebhookSecret = async e => {
|
||||
e.preventDefault();
|
||||
setShowConfirmSecret(true);
|
||||
setMenuOpen(null);
|
||||
};
|
||||
|
||||
const updateWebhookSecret = async () => {
|
||||
try {
|
||||
setGeneratingSecret(true);
|
||||
const apiKeyResponse = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${accountSid}/WebhookSecret?regenerate=true`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (apiKeyResponse.status === 200) {
|
||||
setWebhookSecret(apiKeyResponse.data.webhook_secret);
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: 'Webhook signing secret was successfully generated.',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
setGeneratingSecret(false);
|
||||
setShowConfirmSecret(false);
|
||||
}
|
||||
};
|
||||
|
||||
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: 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: APP_API_BASE_URL,
|
||||
url: `/Accounts/${accountSid}/SubspaceTeleport`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
setSubspaceSipTeleportId('');
|
||||
setSubspaceSipTeleportEntryPoints([]);
|
||||
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: `Successfully disabled subspace teleport.`,
|
||||
});
|
||||
}
|
||||
|
||||
resetSubspaceState();
|
||||
} catch (err) {
|
||||
resetSubspaceState();
|
||||
handleErrors({ err, history, dispatch });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getAccounts = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
if (!jwt) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
@@ -69,10 +300,10 @@ const AccountForm = props => {
|
||||
const promiseList = [];
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
promiseList.push(accountsPromise);
|
||||
@@ -80,26 +311,24 @@ const AccountForm = props => {
|
||||
if (props.type === 'edit') {
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
promiseList.push(applicationsPromise);
|
||||
}
|
||||
|
||||
if (props.type === 'add') {
|
||||
const serviceProvidersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/ServiceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
promiseList.push(serviceProvidersPromise);
|
||||
}
|
||||
const sbcsPromise = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Sbcs',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
promiseList.push(sbcsPromise);
|
||||
|
||||
const promiseAllValues = await Promise.all(promiseList);
|
||||
|
||||
@@ -113,11 +342,7 @@ const AccountForm = props => {
|
||||
});
|
||||
setAccountApplications(accountApplicationsData);
|
||||
}
|
||||
|
||||
if (props.type === 'add') {
|
||||
const serviceProviders = (promiseAllValues[1] && promiseAllValues[1].data) || '';
|
||||
setServiceProviderSid(serviceProviders[0].service_provider_sid);
|
||||
}
|
||||
setSbcs(promiseAllValues[2]?.data);
|
||||
|
||||
if (props.type === 'setup' && accountsData.length > 1) {
|
||||
history.push('/internal/accounts');
|
||||
@@ -152,15 +377,31 @@ const AccountForm = props => {
|
||||
setSipRealm(acc.sip_realm || '');
|
||||
setDeviceCallingApplication(acc.device_calling_application_sid || '');
|
||||
setRegWebhook((acc.registration_hook && acc.registration_hook.url ) || '');
|
||||
setMethod((acc.registration_hook && acc.registration_hook.method ) || 'post');
|
||||
setUser((acc.registration_hook && acc.registration_hook.username) || '');
|
||||
setPassword((acc.registration_hook && acc.registration_hook.password) || '');
|
||||
|
||||
setRegMethod((acc.registration_hook && acc.registration_hook.method ) || 'post');
|
||||
setRegUser((acc.registration_hook && acc.registration_hook.username) || '');
|
||||
setRegPassword((acc.registration_hook && acc.registration_hook.password) || '');
|
||||
setQueueWebhook((acc.queue_event_hook && acc.queue_event_hook.url ) || '');
|
||||
setQueueMethod((acc.queue_event_hook && acc.queue_event_hook.method ) || 'post');
|
||||
setQueueUser((acc.queue_event_hook && acc.queue_event_hook.username) || '');
|
||||
setQueuePassword((acc.queue_event_hook && acc.queue_event_hook.password) || '');
|
||||
setWebhookSecret(acc.webhook_secret || '');
|
||||
setSubspaceId(acc.subspace_client_id || '');
|
||||
setSubspaceSecret(acc.subspace_client_secret || '');
|
||||
setSubspaceSipTeleportId(acc.subspace_sip_teleport_id || '');
|
||||
setSubspaceSipTeleportEntryPoints(acc.subspace_sip_teleport_destinations ? JSON.parse(acc.subspace_sip_teleport_destinations) : []);
|
||||
setHasSubspace(acc.subspace_client_id ? true : false);
|
||||
if (
|
||||
(acc.registration_hook && acc.registration_hook.username) ||
|
||||
(acc.registration_hook && acc.registration_hook.password)
|
||||
) {
|
||||
setShowAuth(true);
|
||||
setShowRegAuth(true);
|
||||
}
|
||||
|
||||
if (
|
||||
(acc.queue_event_hook && acc.queue_event_hook.username) ||
|
||||
(acc.queue_event_hook && acc.queue_event_hook.password)
|
||||
) {
|
||||
setShowQueueAuth(true);
|
||||
}
|
||||
}
|
||||
setShowLoader(false);
|
||||
@@ -199,8 +440,11 @@ const AccountForm = props => {
|
||||
setInvalidName(false);
|
||||
setInvalidSipRealm(false);
|
||||
setInvalidRegWebhook(false);
|
||||
setInvalidUser(false);
|
||||
setInvalidPassword(false);
|
||||
setInvalidRegUser(false);
|
||||
setInvalidRegPassword(false);
|
||||
setInvalidQueueWebhook(false);
|
||||
setInvalidQueueUser(false);
|
||||
setInvalidQueuePassword(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
@@ -243,15 +487,29 @@ const AccountForm = props => {
|
||||
});
|
||||
|
||||
|
||||
if ((user && !password) || (!user && password)) {
|
||||
errorMessages.push('Username and password must be either both filled out or both empty.');
|
||||
setInvalidUser(true);
|
||||
setInvalidPassword(true);
|
||||
if ((regUser && !regPassword) || (!regUser && regPassword)) {
|
||||
errorMessages.push('Registration webhook username and password must be either both filled out or both empty.');
|
||||
setInvalidRegUser(true);
|
||||
setInvalidRegPassword(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!user) {
|
||||
refUser.current.focus();
|
||||
if (!regUser) {
|
||||
refRegUser.current.focus();
|
||||
} else {
|
||||
refPassword.current.focus();
|
||||
refRegPassword.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((queueUser && !queuePassword) || (!queueUser && queuePassword)) {
|
||||
errorMessages.push('Queue event webhook username and password must be either both filled out or both empty.');
|
||||
setInvalidQueueUser(true);
|
||||
setInvalidQueuePassword(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!queueUser) {
|
||||
refQueueUser.current.focus();
|
||||
} else {
|
||||
refQueuePassword.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
@@ -270,14 +528,23 @@ const AccountForm = props => {
|
||||
sip_realm: sipRealm.trim() || null,
|
||||
registration_hook: {
|
||||
url: regWebhook.trim(),
|
||||
method: method,
|
||||
username: user.trim() || null,
|
||||
password: password || null,
|
||||
method: regMethod,
|
||||
username: regUser.trim() || null,
|
||||
password: regPassword || null,
|
||||
},
|
||||
queue_event_hook: {
|
||||
url: queueWebhook.trim(),
|
||||
method: queueMethod,
|
||||
username: queueUser.trim() || null,
|
||||
password: queuePassword || null,
|
||||
},
|
||||
webhook_secret: webhookSecret || null,
|
||||
subspace_client_id: subspaceId || null,
|
||||
subspace_client_secret: subspaceSecret || null,
|
||||
};
|
||||
|
||||
if (props.type === 'add') {
|
||||
axiosData.service_provider_sid = serviceProviderSid;
|
||||
axiosData.service_provider_sid = currentServiceProvider;
|
||||
}
|
||||
|
||||
if (props.type === 'edit') {
|
||||
@@ -290,10 +557,10 @@ const AccountForm = props => {
|
||||
|
||||
await axios({
|
||||
method: props.type === 'add' ? 'post' : 'put',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
data: axiosData,
|
||||
});
|
||||
@@ -337,6 +604,32 @@ const AccountForm = props => {
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
type: 'button',
|
||||
name: 'Copy',
|
||||
action: copyWebhookSecret,
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'Generate new secret',
|
||||
action: generateWebhookSecret,
|
||||
},
|
||||
];
|
||||
|
||||
const subspaceMenuItems = [
|
||||
{
|
||||
type: 'button',
|
||||
name: 'Enable',
|
||||
action: toggleSubspaceTeleport.bind(toggleSubspaceTeleport, true),
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'Disable',
|
||||
action: toggleSubspaceTeleport.bind(toggleSubspaceTeleport, false),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
showLoader
|
||||
? <Loader
|
||||
@@ -390,6 +683,16 @@ const AccountForm = props => {
|
||||
autoFocus={props.type === 'setup'}
|
||||
ref={refSipRealm}
|
||||
/>
|
||||
<Label htmlFor="webhookSecret">Webhook Secret</Label>
|
||||
<StyledInputGroup>
|
||||
<Label>{webhookSecret || "None"}</Label>
|
||||
<TableMenu
|
||||
sid="webhook"
|
||||
open={menuOpen === "webhook"}
|
||||
handleMenuOpen={handleMenuOpen}
|
||||
menuItems={webhookSecret ? menuItems: menuItems.slice(1)}
|
||||
/>
|
||||
</StyledInputGroup>
|
||||
|
||||
{props.type === 'edit' && (
|
||||
<React.Fragment>
|
||||
@@ -443,27 +746,27 @@ const AccountForm = props => {
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="method"
|
||||
id="method"
|
||||
value={method}
|
||||
onChange={e => setMethod(e.target.value)}
|
||||
id="regMethod"
|
||||
value={regMethod}
|
||||
onChange={e => setRegMethod(e.target.value)}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showAuth ? (
|
||||
{showRegAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="user">User</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="user"
|
||||
id="user"
|
||||
value={user || ''}
|
||||
onChange={e => setUser(e.target.value)}
|
||||
value={regUser || ''}
|
||||
onChange={e => setRegUser(e.target.value)}
|
||||
placeholder="Optional"
|
||||
invalid={invalidUser}
|
||||
ref={refUser}
|
||||
invalid={invalidRegUser}
|
||||
ref={refRegUser}
|
||||
/>
|
||||
<Label htmlFor="password" middle>Password</Label>
|
||||
<PasswordInput
|
||||
@@ -471,12 +774,12 @@ const AccountForm = props => {
|
||||
allowShowPassword
|
||||
name="password"
|
||||
id="password"
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
password={regPassword}
|
||||
setPassword={setRegPassword}
|
||||
setErrorMessage={setErrorMessage}
|
||||
placeholder="Optional"
|
||||
invalid={invalidPassword}
|
||||
ref={refPassword}
|
||||
invalid={invalidRegPassword}
|
||||
ref={refRegPassword}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
@@ -484,12 +787,211 @@ const AccountForm = props => {
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleAuth}
|
||||
onClick={toggleRegAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Label htmlFor="queueWebhook">Queue Event Webhook</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="queueWebhook"
|
||||
id="queueWebhook"
|
||||
value={queueWebhook}
|
||||
onChange={e => setQueueWebhook(e.target.value)}
|
||||
placeholder="URL to notify when a member joins or leaves a queue"
|
||||
invalid={invalidQueueWebhook}
|
||||
ref={refQueueWebhook}
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="method"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="method"
|
||||
id="queueMethod"
|
||||
value={queueMethod}
|
||||
onChange={e => setQueueMethod(e.target.value)}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showQueueAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="user">User</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="user"
|
||||
id="user"
|
||||
value={queueUser || ''}
|
||||
onChange={e => setQueueUser(e.target.value)}
|
||||
placeholder="Optional"
|
||||
invalid={invalidQueueUser}
|
||||
ref={refQueueUser}
|
||||
/>
|
||||
<Label htmlFor="password" middle>Password</Label>
|
||||
<PasswordInput
|
||||
large={props.type === 'setup'}
|
||||
allowShowPassword
|
||||
name="password"
|
||||
id="password"
|
||||
password={queuePassword}
|
||||
setPassword={setQueuePassword}
|
||||
setErrorMessage={setErrorMessage}
|
||||
placeholder="Optional"
|
||||
invalid={invalidQueuePassword}
|
||||
ref={refQueuePassword}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleQueueAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{ process.env.REACT_APP_ENABLE_SUBSPACE ? (
|
||||
<>
|
||||
<Label htmlFor="subspaceId">Subspace</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="subspaceId"
|
||||
id="subspaceId"
|
||||
value={subspaceId}
|
||||
onChange={e => setSubspaceId(e.target.value)}
|
||||
placeholder="Client Id for Subspace"
|
||||
ref={refSubspaceId}
|
||||
style={{ margin: '0 4px' }}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
large={props.type === 'setup'}
|
||||
allowShowPassword
|
||||
name="subspaceSecret"
|
||||
id="subspaceSecret"
|
||||
password={subspaceSecret}
|
||||
setPassword={setSubspaceSecret}
|
||||
setErrorMessage={setErrorMessage}
|
||||
placeholder="Client Secret for Subspace"
|
||||
ref={refSubspaceSecret}
|
||||
style={{ margin: '0 4px' }}
|
||||
/>
|
||||
|
||||
<StyledInputGroup>
|
||||
<TableMenu
|
||||
disabled={!hasSubspace}
|
||||
sid="subspace"
|
||||
open={menuOpen === "subspace"}
|
||||
handleMenuOpen={handleSubspaceMenuOpen}
|
||||
menuItems={subspaceSipTeleportId ? [subspaceMenuItems[1]] : [subspaceMenuItems[0]]}
|
||||
/>
|
||||
</StyledInputGroup>
|
||||
</InputGroup>
|
||||
{subspaceSipTeleportId ? (
|
||||
<div style={{ gridColumn: 2, textAlign: 'left' }}>
|
||||
<div>Subspace is now enabled. To send your traffic through Subspace:</div>
|
||||
{subspaceSipTeleportEntryPoints.map(entrypoint => (
|
||||
<div key={entrypoint.transport_type}>
|
||||
<Span>send {entrypoint.transport_type.split('_').join(' and ')} traffic to </Span>
|
||||
<CopyableText text={entrypoint.address} textType="Address" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{showSubspaceModal && (
|
||||
<Modal
|
||||
title={subspaceEnable ? 'Have Subspace send SIP to:' : 'Are you sure you want to delete your Subspace SIP Teleport?'}
|
||||
loader={generatingSubspace}
|
||||
hideButtons={generatingSubspace}
|
||||
maskClosable={!generatingSubspace}
|
||||
actionText={subspaceEnable ? 'Save' : 'Disable'}
|
||||
content={
|
||||
<ModalContainer>
|
||||
{subspaceEnable ? (
|
||||
<>
|
||||
{sipRealm && (
|
||||
<Radio
|
||||
noLeftMargin
|
||||
name="subspaceSipRealm"
|
||||
id="sipRealmAccount"
|
||||
label={sipRealm}
|
||||
checked={subspaceSipRealm === sipRealm}
|
||||
onChange={() => {
|
||||
setSubspaceSipRealm(sipRealm);
|
||||
setSubspaceSipRealmOtherValue('');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{sbcs.map((sbc) => {
|
||||
return (
|
||||
<Radio
|
||||
key={sbc.ipv4}
|
||||
noLeftMargin
|
||||
name="subspaceSipRealm"
|
||||
id={sbc.sbc_address_sid}
|
||||
label={`${sbc.ipv4}:${sbc.port}`}
|
||||
checked={subspaceSipRealm === `${sbc.ipv4}:${sbc.port}`}
|
||||
onChange={() => {
|
||||
setSubspaceSipRealm(`${sbc.ipv4}:${sbc.port}`);
|
||||
setSubspaceSipRealmOtherValue('');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Radio
|
||||
noLeftMargin
|
||||
name="subspaceSipRealm"
|
||||
id="sipRealmOther"
|
||||
label="Other"
|
||||
checked={subspaceSipRealm === 'other'}
|
||||
onChange={() => {
|
||||
setSubspaceSipRealm('other');
|
||||
setTimeout(() => refSubspaceOtherSip.current.focus(), 0);
|
||||
}}
|
||||
/>
|
||||
{subspaceSipRealm === 'other' && (
|
||||
<Input
|
||||
ref={refSubspaceOtherSip}
|
||||
name="subspaceSipRealm"
|
||||
id="sipRealmOtherValue"
|
||||
value={subspaceSipRealmOtherValue}
|
||||
onChange={e => setSubspaceSipRealmOtherValue(e.target.value)}
|
||||
placeholder="IP address or DNS name"
|
||||
style={{ marginTop: '8px' }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</ModalContainer>
|
||||
}
|
||||
handleCancel={() => {
|
||||
setShowSubspaceModal(false);
|
||||
resetSubspaceState();
|
||||
}}
|
||||
handleSubmit={() => {
|
||||
if (subspaceEnable) {
|
||||
handleSubspaceEnable();
|
||||
} else {
|
||||
handleSubspaceDisable();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null }
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
@@ -536,6 +1038,23 @@ const AccountForm = props => {
|
||||
Skip for now — I'll complete later
|
||||
</Link>
|
||||
)}
|
||||
{showConfirmSecret && (
|
||||
<Modal
|
||||
title={generatingSecret ? "" : "Generate new secret"}
|
||||
loader={generatingSecret}
|
||||
hideButtons={generatingSecret}
|
||||
maskClosable={!generatingSecret}
|
||||
actionText="OK"
|
||||
content={
|
||||
<ModalContainer>
|
||||
<P>Press OK to generate a new webhook signing secret.</P>
|
||||
<P>Note: this will immediately invalidate the old webhook signing secret.</P>
|
||||
</ModalContainer>
|
||||
}
|
||||
handleCancel={() => setShowConfirmSecret(false)}
|
||||
handleSubmit={updateWebhookSecret}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
@@ -13,12 +15,18 @@ import Button from '../elements/Button';
|
||||
import SpeechSynthesisLanguageGoogle from '../../data/SpeechSynthesisLanguageGoogle';
|
||||
import SpeechSynthesisLanguageAws from '../../data/SpeechSynthesisLanguageAws';
|
||||
import SpeechRecognizerLanguageGoogle from '../../data/SpeechRecognizerLanguageGoogle';
|
||||
import SpeechRecognizerLanguageAws from '../../data/SpeechRecognizerLanguageAws';
|
||||
import SpeechRecognizerLanguageMicrosoft from '../../data/SpeechRecognizerLanguageMicrosoft';
|
||||
import SpeechSynthesisLanguageMicrosoft from '../../data/SpeechSynthesisLanguageMicrosoft';
|
||||
import SpeechSynthesisLanguageWellSaid from '../../data/SpeechSynthesisLanguageWellSaid';
|
||||
import Loader from '../blocks/Loader';
|
||||
import CopyableText from '../elements/CopyableText';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const ApplicationForm = props => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Refs
|
||||
const refName = useRef(null);
|
||||
@@ -97,7 +105,7 @@ const ApplicationForm = props => {
|
||||
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -105,7 +113,7 @@ const ApplicationForm = props => {
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -117,7 +125,7 @@ const ApplicationForm = props => {
|
||||
applicationsPromise,
|
||||
]);
|
||||
|
||||
const accounts = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[0].data.filter(a => a.service_provider_sid === currentServiceProvider);
|
||||
const applications = promiseAllValues[1].data;
|
||||
|
||||
setAccounts(accounts);
|
||||
@@ -388,7 +396,7 @@ const ApplicationForm = props => {
|
||||
|
||||
await axios({
|
||||
method,
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -492,7 +500,7 @@ const ApplicationForm = props => {
|
||||
-- Choose the account this application will be associated with --
|
||||
</option>
|
||||
)}
|
||||
{accounts.map(a => (
|
||||
{accounts.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
|
||||
<option
|
||||
key={a.account_sid}
|
||||
value={a.account_sid}
|
||||
@@ -742,6 +750,14 @@ const ApplicationForm = props => {
|
||||
? SpeechSynthesisLanguageGoogle.find(l => (
|
||||
l.code === speechSynthesisLanguage
|
||||
))
|
||||
: e.target.value === 'microsoft'
|
||||
? SpeechSynthesisLanguageMicrosoft.find(l => (
|
||||
l.code === speechSynthesisLanguage
|
||||
))
|
||||
: e.target.value === 'wellsaid'
|
||||
? SpeechSynthesisLanguageWellSaid.find(l => (
|
||||
l.code === speechSynthesisLanguage
|
||||
))
|
||||
: SpeechSynthesisLanguageAws.find(l => (
|
||||
l.code === speechSynthesisLanguage
|
||||
));
|
||||
@@ -755,9 +771,13 @@ const ApplicationForm = props => {
|
||||
return;
|
||||
}
|
||||
|
||||
newLang = SpeechSynthesisLanguageAws.find(l => (
|
||||
l.code === 'en-US'
|
||||
));
|
||||
newLang = e.target.value === 'aws'
|
||||
? SpeechSynthesisLanguageAws.find(l => (
|
||||
l.code === 'en-US'
|
||||
))
|
||||
: SpeechSynthesisLanguageMicrosoft.find(l => (
|
||||
l.code === 'en-US'
|
||||
));
|
||||
}
|
||||
|
||||
// Update state to reflect first voice option for language
|
||||
@@ -766,6 +786,8 @@ const ApplicationForm = props => {
|
||||
>
|
||||
<option value="google">Google</option>
|
||||
<option value="aws">AWS</option>
|
||||
<option value="microsoft">Microsoft</option>
|
||||
<option value="wellsaid">WellSaid</option>
|
||||
</Select>
|
||||
<Label middle htmlFor="speechSynthesisLanguage">Language</Label>
|
||||
<Select
|
||||
@@ -789,6 +811,14 @@ const ApplicationForm = props => {
|
||||
? SpeechSynthesisLanguageGoogle.find(l => (
|
||||
l.code === e.target.value
|
||||
))
|
||||
: speechSynthesisVendor === 'microsoft'
|
||||
? SpeechSynthesisLanguageMicrosoft.find(l => (
|
||||
l.code === e.target.value
|
||||
))
|
||||
: speechSynthesisVendor === 'wellsaid'
|
||||
? SpeechSynthesisLanguageWellSaid.find(l => (
|
||||
l.code === e.target.value
|
||||
))
|
||||
: SpeechSynthesisLanguageAws.find(l => (
|
||||
l.code === e.target.value
|
||||
));
|
||||
@@ -801,6 +831,14 @@ const ApplicationForm = props => {
|
||||
SpeechSynthesisLanguageGoogle.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
) : speechSynthesisVendor === 'microsoft' ? (
|
||||
SpeechSynthesisLanguageMicrosoft.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
) : speechSynthesisVendor === 'wellsaid' ? (
|
||||
SpeechSynthesisLanguageWellSaid.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
) : (
|
||||
SpeechSynthesisLanguageAws.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
@@ -821,6 +859,18 @@ const ApplicationForm = props => {
|
||||
.map(m => m.voices.map(v => (
|
||||
<option key={v.value} value={v.value}>{v.name}</option>
|
||||
)))
|
||||
) : speechSynthesisVendor === 'microsoft' ? (
|
||||
SpeechSynthesisLanguageMicrosoft
|
||||
.filter(l => l.code === speechSynthesisLanguage)
|
||||
.map(m => m.voices.map(v => (
|
||||
<option key={v.value} value={v.value}>{v.name}</option>
|
||||
)))
|
||||
) : speechSynthesisVendor === 'wellsaid' ? (
|
||||
SpeechSynthesisLanguageWellSaid
|
||||
.filter(l => l.code === speechSynthesisLanguage)
|
||||
.map(m => m.voices.map(v => (
|
||||
<option key={v.value} value={v.value}>{v.name}</option>
|
||||
)))
|
||||
) : (
|
||||
SpeechSynthesisLanguageAws
|
||||
.filter(l => l.code === speechSynthesisLanguage)
|
||||
@@ -840,9 +890,26 @@ const ApplicationForm = props => {
|
||||
name="speechRecognizerVendor"
|
||||
id="speechRecognizerVendor"
|
||||
value={speechRecognizerVendor}
|
||||
onChange={e => setSpeechRecognizerVendor(e.target.value)}
|
||||
onChange={e => {
|
||||
setSpeechRecognizerVendor(e.target.value);
|
||||
|
||||
// Google and AWS have different language lists. If the newly chosen
|
||||
// vendor doesn't have the same language that was already in use,
|
||||
// select US English
|
||||
if ((
|
||||
e.target.value === 'google' &&
|
||||
!SpeechRecognizerLanguageGoogle.some(l => l.code === speechRecognizerLanguage)
|
||||
) || (
|
||||
e.target.value === 'aws' &&
|
||||
!SpeechRecognizerLanguageAws.some(l => l.code === speechRecognizerLanguage)
|
||||
)) {
|
||||
setSpeechRecognizerLanguage('en-US');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="google">Google</option>
|
||||
<option value="aws">AWS</option>
|
||||
<option value="microsoft">Microsoft</option>
|
||||
</Select>
|
||||
<Label middle htmlFor="speechRecognizerLanguage">Language</Label>
|
||||
<Select
|
||||
@@ -852,9 +919,19 @@ const ApplicationForm = props => {
|
||||
value={speechRecognizerLanguage}
|
||||
onChange={e => setSpeechRecognizerLanguage(e.target.value)}
|
||||
>
|
||||
{SpeechRecognizerLanguageGoogle.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))}
|
||||
{speechRecognizerVendor === 'google' ? (
|
||||
SpeechRecognizerLanguageGoogle.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
) : speechRecognizerVendor === 'microsoft' ? (
|
||||
SpeechRecognizerLanguageMicrosoft.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
) : (
|
||||
SpeechRecognizerLanguageAws.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
|
||||
1868
src/components/forms/CarrierForm.js
Normal file
1868
src/components/forms/CarrierForm.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
@@ -10,6 +11,7 @@ import InputGroup from '../elements/InputGroup';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Loader from '../blocks/Loader';
|
||||
import Button from '../elements/Button';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const MsTeamsTenantForm = props => {
|
||||
|
||||
@@ -54,7 +56,7 @@ const MsTeamsTenantForm = props => {
|
||||
|
||||
const tenantsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/MicrosoftTeamsTenants',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -62,7 +64,7 @@ const MsTeamsTenantForm = props => {
|
||||
});
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -70,7 +72,7 @@ const MsTeamsTenantForm = props => {
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -86,7 +88,7 @@ const MsTeamsTenantForm = props => {
|
||||
if (props.type === 'add') {
|
||||
promises.push(axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/ServiceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -247,7 +249,7 @@ const MsTeamsTenantForm = props => {
|
||||
|
||||
await axios({
|
||||
method,
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
@@ -11,11 +13,13 @@ import FormError from '../blocks/FormError';
|
||||
import Loader from '../blocks/Loader';
|
||||
import Button from '../elements/Button';
|
||||
import phoneNumberFormat from '../../helpers/phoneNumberFormat';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const PhoneNumberForm = props => {
|
||||
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Refs
|
||||
const refPhoneNumber = useRef(null);
|
||||
@@ -58,7 +62,7 @@ const PhoneNumberForm = props => {
|
||||
|
||||
const sipTrunksPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/VoipCarriers',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -66,7 +70,7 @@ const PhoneNumberForm = props => {
|
||||
});
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -74,7 +78,7 @@ const PhoneNumberForm = props => {
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -82,7 +86,7 @@ const PhoneNumberForm = props => {
|
||||
});
|
||||
const phoneNumbersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/PhoneNumbers',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -128,7 +132,7 @@ const PhoneNumberForm = props => {
|
||||
history.push('/internal/accounts');
|
||||
return;
|
||||
} else if (!sipTrunks.length) {
|
||||
history.push('/internal/sip-trunks');
|
||||
history.push('/internal/carriers');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -274,7 +278,7 @@ const PhoneNumberForm = props => {
|
||||
|
||||
await axios({
|
||||
method,
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -358,7 +362,10 @@ const PhoneNumberForm = props => {
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={e => setAccount(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setAccount(e.target.value);
|
||||
setApplication('');
|
||||
}}
|
||||
invalid={invalidAccount}
|
||||
ref={refAccount}
|
||||
>
|
||||
@@ -368,7 +375,7 @@ const PhoneNumberForm = props => {
|
||||
) && (
|
||||
<option value="">-- Choose the account that this phone number should be associated with --</option>
|
||||
)}
|
||||
{accountValues.map(a => (
|
||||
{accountValues.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
|
||||
<option
|
||||
key={a.account_sid}
|
||||
value={a.account_sid}
|
||||
@@ -391,7 +398,16 @@ const PhoneNumberForm = props => {
|
||||
: '-- NONE --'
|
||||
}
|
||||
</option>
|
||||
{applicationValues.map(a => (
|
||||
{applicationValues.filter((a) => {
|
||||
// Map an application to a service provider through it's account_sid
|
||||
const acct = accountValues.find(ac => a.account_sid === ac.account_sid);
|
||||
|
||||
if (account) {
|
||||
return a.account_sid === account;
|
||||
}
|
||||
|
||||
return acct.service_provider_sid === currentServiceProvider;
|
||||
}).map(a => (
|
||||
<option
|
||||
key={a.application_sid}
|
||||
value={a.application_sid}
|
||||
|
||||
@@ -1,40 +1,51 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import styled from 'styled-components';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import { ShowMsTeamsDispatchContext } from '../../contexts/ShowMsTeamsContext';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
import Select from '../elements/Select';
|
||||
import Checkbox from '../elements/Checkbox';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Button from '../elements/Button';
|
||||
import Loader from '../blocks/Loader';
|
||||
import Modal from '../blocks/Modal';
|
||||
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
import handleErrors from "../../helpers/handleErrors";
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const Td = styled.td`
|
||||
padding: 0.5rem 0;
|
||||
&:first-child {
|
||||
font-weight: 500;
|
||||
padding-right: 1.5rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
& ul {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const SettingsForm = () => {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const refreshMsTeamsData = useContext(ShowMsTeamsDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Refs
|
||||
const refEnableMsTeams = useRef(null);
|
||||
const refSbcDomainName = useRef(null);
|
||||
const refSipRealm = useRef(null);
|
||||
const refRegWebhook = useRef(null);
|
||||
const refUser = useRef(null);
|
||||
const refPassword = useRef(null);
|
||||
const refServiceProviderName = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ enableMsTeams, setEnableMsTeams ] = useState(false);
|
||||
const [ sbcDomainName, setSbcDomainName ] = useState('');
|
||||
const [ sipRealm, setSipRealm ] = useState('');
|
||||
const [ regWebhook, setRegWebhook ] = useState('');
|
||||
const [ method, setMethod ] = useState('POST');
|
||||
const [ user, setUser ] = useState('');
|
||||
const [ password, setPassword ] = useState('');
|
||||
const [serviceProviderName, setServiceProviderName] = useState('');
|
||||
|
||||
// For when user has data in sbcDomainName and then taps the checkbox to disable MsTeams
|
||||
const [ savedSbcDomainName, setSavedSbcDomainName ] = useState('');
|
||||
@@ -42,18 +53,13 @@ const SettingsForm = () => {
|
||||
// Invalid form inputs
|
||||
const [ invalidEnableMsTeams, setInvalidEnableMsTeams ] = useState(false);
|
||||
const [ invalidSbcDomainName, setInvalidSbcDomainName ] = useState(false);
|
||||
const [ invalidSipRealm, setInvalidSipRealm ] = useState(false);
|
||||
const [ invalidRegWebhook, setInvalidRegWebhook ] = useState(false);
|
||||
const [ invalidUser, setInvalidUser ] = useState(false);
|
||||
const [ invalidPassword, setInvalidPassword ] = useState(false);
|
||||
const [invalidServiceProviderName, setInvalidServiceProviderName] = useState(false);
|
||||
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
const [ showAuth, setShowAuth ] = useState(false);
|
||||
const toggleAuth = () => setShowAuth(!showAuth);
|
||||
|
||||
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
|
||||
const [ serviceProviders, setServiceProviders ] = useState([]);
|
||||
const [ confirmDelete, setConfirmDelete ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const getSettingsData = async () => {
|
||||
@@ -70,57 +76,33 @@ const SettingsForm = () => {
|
||||
|
||||
const serviceProvidersResponse = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/ServiceProviders',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const sp = serviceProvidersResponse.data[0];
|
||||
const sps = serviceProvidersResponse.data;
|
||||
const sp = sps.find(s => s.service_provider_sid === currentServiceProvider);
|
||||
|
||||
setServiceProviders(sps);
|
||||
setServiceProviderName(sp.name || '');
|
||||
setServiceProviderSid(sp.service_provider_sid || '');
|
||||
setEnableMsTeams(sp.ms_teams_fqdn ? true : false);
|
||||
setSbcDomainName(sp.ms_teams_fqdn || '');
|
||||
setSipRealm(sp.root_domain || '');
|
||||
setRegWebhook((sp.registration_hook && sp.registration_hook.url) || '');
|
||||
setMethod((sp.registration_hook && sp.registration_hook.method) || 'post');
|
||||
setUser((sp.registration_hook && sp.registration_hook.username) || '');
|
||||
setPassword((sp.registration_hook && sp.registration_hook.password) || '');
|
||||
|
||||
if (
|
||||
(sp.registration_hook && sp.registration_hook.username) ||
|
||||
(sp.registration_hook && sp.registration_hook.password)
|
||||
) {
|
||||
setShowAuth(true);
|
||||
}
|
||||
|
||||
setShowLoader(false);
|
||||
|
||||
} 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 {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
getSettingsData();
|
||||
|
||||
if (currentServiceProvider) {
|
||||
getSettingsData();
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
const toggleMsTeams = (e) => {
|
||||
if (!e.target.checked && sbcDomainName) {
|
||||
@@ -134,6 +116,32 @@ const SettingsForm = () => {
|
||||
setEnableMsTeams(e.target.checked);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setErrorMessage('');
|
||||
|
||||
axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${serviceProviderSid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
setConfirmDelete(false);
|
||||
setErrorMessage('');
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: 'Service Provider Deleted'
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.response.data.msg);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
@@ -145,16 +153,23 @@ const SettingsForm = () => {
|
||||
setErrorMessage('');
|
||||
setInvalidEnableMsTeams(false);
|
||||
setInvalidSbcDomainName(false);
|
||||
setInvalidSipRealm(false);
|
||||
setInvalidRegWebhook(false);
|
||||
setInvalidUser(false);
|
||||
setInvalidPassword(false);
|
||||
setInvalidServiceProviderName(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
//=============================================================================
|
||||
// data checks
|
||||
//=============================================================================
|
||||
if (!serviceProviderName.trim()) {
|
||||
errorMessages.push(
|
||||
'Please enter a Service Provider Name.'
|
||||
);
|
||||
setInvalidServiceProviderName(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refServiceProviderName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
if (enableMsTeams && !sbcDomainName) {
|
||||
errorMessages.push(
|
||||
'You must provide an SBC Domain Name in order to enable Microsoft Teams Direct Routing'
|
||||
@@ -177,42 +192,6 @@ const SettingsForm = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!sipRealm && (regWebhook || user || password)) {
|
||||
errorMessages.push(
|
||||
'You must provide a SIP Realm in order to provide a Registration Webhook'
|
||||
);
|
||||
setInvalidSipRealm(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refSipRealm.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (sipRealm && !regWebhook) {
|
||||
errorMessages.push(
|
||||
'You must provide a Registration Webhook when providing a SIP Realm'
|
||||
);
|
||||
setInvalidRegWebhook(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refRegWebhook.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((user && !password) || (!user && password)) {
|
||||
errorMessages.push('Username and password must be either both filled out or both empty.');
|
||||
setInvalidUser(true);
|
||||
setInvalidPassword(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!user) {
|
||||
refUser.current.focus();
|
||||
} else {
|
||||
refPassword.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
@@ -226,21 +205,12 @@ const SettingsForm = () => {
|
||||
//=============================================================================
|
||||
const data = {
|
||||
ms_teams_fqdn: sbcDomainName.trim() || null,
|
||||
root_domain: sipRealm.trim() || null,
|
||||
name: serviceProviderName.trim(),
|
||||
};
|
||||
|
||||
if (regWebhook) {
|
||||
data.registration_hook = {
|
||||
url: regWebhook.trim() || null,
|
||||
method,
|
||||
username: user.trim() || null,
|
||||
password: password || null,
|
||||
};
|
||||
}
|
||||
|
||||
await axios({
|
||||
method: 'put',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${serviceProviderSid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -286,171 +256,114 @@ const SettingsForm = () => {
|
||||
return (
|
||||
showLoader
|
||||
? <Loader height="365px" />
|
||||
: <Form
|
||||
large
|
||||
wideLabel
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div>{/* needed for CSS grid layout */}</div>
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
id="enableMsTeams"
|
||||
label="Enable Microsoft Teams Direct Routing"
|
||||
checked={enableMsTeams}
|
||||
onChange={toggleMsTeams}
|
||||
invalid={invalidEnableMsTeams}
|
||||
ref={refEnableMsTeams}
|
||||
/>
|
||||
|
||||
<Label htmlFor="sbcDomainName">SBC Domain Name</Label>
|
||||
<Input
|
||||
name="sbcDomainName"
|
||||
id="sbcDomainName"
|
||||
value={sbcDomainName}
|
||||
onChange={e => setSbcDomainName(e.target.value)}
|
||||
placeholder="Fully qualified domain name used for Microsoft Teams"
|
||||
invalid={invalidSbcDomainName}
|
||||
autoFocus={enableMsTeams}
|
||||
ref={refSbcDomainName}
|
||||
disabled={!enableMsTeams}
|
||||
title={(!enableMsTeams && "You must enable Microsoft Teams Direct Routing in order to provide an SBC Domain Name") || ""}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="sipRealm">Fallback SIP Realm</Label>
|
||||
<Input
|
||||
name="sipRealm"
|
||||
id="sipRealm"
|
||||
value={sipRealm}
|
||||
onChange={e => setSipRealm(e.target.value)}
|
||||
placeholder="Domain name that accounts will use as a fallback"
|
||||
invalid={invalidSipRealm}
|
||||
autoFocus={!enableMsTeams}
|
||||
ref={refSipRealm}
|
||||
/>
|
||||
|
||||
<Label htmlFor="regWebhook">Registration Webhook</Label>
|
||||
<InputGroup>
|
||||
: (
|
||||
<>
|
||||
<Form
|
||||
large
|
||||
wideLabel
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="serviceProviderName">Service Provider Name</Label>
|
||||
<Input
|
||||
name="regWebhook"
|
||||
id="regWebhook"
|
||||
value={regWebhook}
|
||||
onChange={e => setRegWebhook(e.target.value)}
|
||||
placeholder="URL for your web application that handles registrations"
|
||||
invalid={invalidRegWebhook}
|
||||
ref={refRegWebhook}
|
||||
disabled={!sipRealm && !regWebhook && !user && !password}
|
||||
title={(
|
||||
!sipRealm &&
|
||||
!regWebhook &&
|
||||
!user &&
|
||||
!password &&
|
||||
"You must provide a SIP Realm in order to enter a registration webhook"
|
||||
) || ""}
|
||||
name="serviceProviderName"
|
||||
id="serviceProviderName"
|
||||
value={serviceProviderName}
|
||||
onChange={e => setServiceProviderName(e.target.value)}
|
||||
invalid={invalidServiceProviderName}
|
||||
ref={refServiceProviderName}
|
||||
/>
|
||||
<div>{/* needed for CSS grid layout */}</div>
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
id="enableMsTeams"
|
||||
label="Enable Microsoft Teams Direct Routing"
|
||||
checked={enableMsTeams}
|
||||
onChange={toggleMsTeams}
|
||||
invalid={invalidEnableMsTeams}
|
||||
ref={refEnableMsTeams}
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="method"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
name="method"
|
||||
id="method"
|
||||
value={method}
|
||||
onChange={e => setMethod(e.target.value)}
|
||||
disabled={!sipRealm && !regWebhook && !user && !password}
|
||||
title={(
|
||||
!sipRealm &&
|
||||
!regWebhook &&
|
||||
!user &&
|
||||
!password &&
|
||||
"You must provide a SIP Realm in order to enter a registration webhook"
|
||||
) || ""}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
<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") || ""}
|
||||
/>
|
||||
|
||||
{showAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="user">User</Label>
|
||||
<Input
|
||||
name="user"
|
||||
id="user"
|
||||
value={user || ''}
|
||||
onChange={e => setUser(e.target.value)}
|
||||
placeholder="Optional"
|
||||
invalid={invalidUser}
|
||||
ref={refUser}
|
||||
disabled={!sipRealm && !regWebhook && !user && !password}
|
||||
title={(
|
||||
!sipRealm &&
|
||||
!regWebhook &&
|
||||
!user &&
|
||||
!password &&
|
||||
"You must provide a SIP Realm in order to enter a registration webhook"
|
||||
) || ""}
|
||||
/>
|
||||
<Label htmlFor="password" middle>Password</Label>
|
||||
<PasswordInput
|
||||
allowShowPassword
|
||||
name="password"
|
||||
id="password"
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
setErrorMessage={setErrorMessage}
|
||||
placeholder="Optional"
|
||||
invalid={invalidPassword}
|
||||
ref={refPassword}
|
||||
disabled={!sipRealm && !regWebhook && !user && !password}
|
||||
title={(
|
||||
!sipRealm &&
|
||||
!regWebhook &&
|
||||
!user &&
|
||||
!password &&
|
||||
"You must provide a SIP Realm in order to enter a registration webhook"
|
||||
) || ""}
|
||||
/>
|
||||
{errorMessage && !confirmDelete && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{serviceProviders.length > 1 && (
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => setConfirmDelete(true)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<Button grid>Save</Button>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: 'Changes canceled',
|
||||
});
|
||||
{confirmDelete && serviceProviders.length > 1 && (
|
||||
<Modal
|
||||
title="Are you sure you want to delete the Service Provider?"
|
||||
loader={false}
|
||||
content={
|
||||
<div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<Td>Service Provider Name:</Td>
|
||||
<Td>{serviceProviderName}</Td>
|
||||
</tr>
|
||||
<tr>
|
||||
<Td>SBC Domain Name:</Td>
|
||||
<Td>{sbcDomainName || '[none]'}</Td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{errorMessage && (
|
||||
<FormError message={errorMessage} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
handleCancel={() => {
|
||||
setConfirmDelete(false);
|
||||
setErrorMessage('');
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button grid>Save</Button>
|
||||
</InputGroup>
|
||||
</Form>
|
||||
handleSubmit={handleDelete}
|
||||
actionText="Delete"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,703 +0,0 @@
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
import Checkbox from '../elements/Checkbox';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Button from '../elements/Button';
|
||||
import TrashButton from '../elements/TrashButton';
|
||||
import Loader from '../blocks/Loader';
|
||||
import sortSipGateways from '../../helpers/sortSipGateways';
|
||||
import Link from '../elements/Link';
|
||||
|
||||
const SipTrunkForm = props => {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
// Refs
|
||||
const refName = useRef(null);
|
||||
const refIp = useRef([]);
|
||||
const refPort = useRef([]);
|
||||
const refInbound = useRef([]);
|
||||
const refOutbound = useRef([]);
|
||||
const refTrash = useRef([]);
|
||||
const refAdd = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ name, setName ] = useState('');
|
||||
const [ nameInvalid, setNameInvalid ] = useState(false);
|
||||
const [ description, setDescription ] = useState('');
|
||||
const [ e164, setE164 ] = useState(false);
|
||||
const [ sipGateways, setSipGateways ] = useState([
|
||||
{
|
||||
sip_gateway_sid: '',
|
||||
ip: '',
|
||||
port: 5060,
|
||||
inbound: true,
|
||||
outbound: true,
|
||||
invalidIp: false,
|
||||
invalidPort: false,
|
||||
invalidInbound: false,
|
||||
invalidOutbound: false,
|
||||
}
|
||||
]);
|
||||
|
||||
const [ sipTrunks, setSipTrunks ] = useState([]);
|
||||
const [ sipTrunkSid, setSipTrunkSid ] = useState('');
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const sipTrunksPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/VoipCarriers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const sipGatewaysPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/SipGateways`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const promiseAllValues = await Promise.all([
|
||||
sipTrunksPromise,
|
||||
sipGatewaysPromise,
|
||||
]);
|
||||
|
||||
const allSipTrunks = promiseAllValues[0].data;
|
||||
const allSipGateways = promiseAllValues[1].data;
|
||||
|
||||
setSipTrunks(allSipTrunks);
|
||||
|
||||
if (props.type === 'setup' && allSipTrunks.length > 1) {
|
||||
history.push('/internal/sip-trunks');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'That page is only accessible during setup.',
|
||||
});
|
||||
}
|
||||
|
||||
if (props.type === 'edit' || props.type === 'setup') {
|
||||
const currentSipTrunk = props.type === 'edit'
|
||||
? allSipTrunks.filter(s => s.voip_carrier_sid === props.voip_carrier_sid)
|
||||
: allSipTrunks;
|
||||
|
||||
if (props.type === 'edit' && !currentSipTrunk.length) {
|
||||
history.push('/internal/sip-trunks');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'That SIP trunk does not exist.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSipGateways = allSipGateways.filter(s => {
|
||||
return s.voip_carrier_sid === currentSipTrunk[0].voip_carrier_sid;
|
||||
});
|
||||
sortSipGateways(currentSipGateways);
|
||||
|
||||
if (currentSipTrunk.length) {
|
||||
setName(currentSipTrunk[0].name);
|
||||
setDescription(currentSipTrunk[0].description);
|
||||
setE164(currentSipTrunk[0].e164_leading_plus === 1);
|
||||
setSipGateways(currentSipGateways.map(s => ({
|
||||
sip_gateway_sid: s.sip_gateway_sid,
|
||||
ip: s.ipv4,
|
||||
port: s.port,
|
||||
inbound: s.inbound === 1,
|
||||
outbound: s.outbound === 1,
|
||||
invalidIp: false,
|
||||
invalidPort: false,
|
||||
invalidInbound: false,
|
||||
invalidOutbound: false,
|
||||
})));
|
||||
setSipTrunkSid(currentSipTrunk[0].voip_carrier_sid);
|
||||
}
|
||||
}
|
||||
setShowLoader(false);
|
||||
} 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 {
|
||||
setErrorMessage('Something went wrong, please try again.');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get accounts',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const addSipGateway = () => {
|
||||
const newSipGateways = [
|
||||
...sipGateways,
|
||||
{
|
||||
sip_gateway_sid: '',
|
||||
ip: '',
|
||||
port: 5060,
|
||||
inbound: true,
|
||||
outbound: true,
|
||||
invalidIp: false,
|
||||
invalidPort: false,
|
||||
invalidInbound: false,
|
||||
invalidOutbound: false,
|
||||
}
|
||||
];
|
||||
setSipGateways(newSipGateways);
|
||||
};
|
||||
|
||||
const removeSipGateway = index => {
|
||||
const newSipGateways = sipGateways.filter((s,i) => i !== index);
|
||||
setSipGateways(newSipGateways);
|
||||
setErrorMessage('');
|
||||
};
|
||||
|
||||
const updateSipGateways = (e, i, key) => {
|
||||
const newSipGateways = [...sipGateways];
|
||||
const newValue =
|
||||
key === 'invalidIp' ||
|
||||
key === 'invalidPort' ||
|
||||
key === 'invalidInbound' ||
|
||||
key === 'invalidOutbound'
|
||||
? true
|
||||
: (key === 'inbound') || (key === 'outbound')
|
||||
? e.target.checked
|
||||
: e.target.value;
|
||||
newSipGateways[i][key] = newValue;
|
||||
setSipGateways(newSipGateways);
|
||||
};
|
||||
|
||||
const resetInvalidFields = () => {
|
||||
setNameInvalid(false);
|
||||
const newSipGateways = [...sipGateways];
|
||||
newSipGateways.forEach((s, i) => {
|
||||
newSipGateways[i].invalidIp = false;
|
||||
newSipGateways[i].invalidPort = false;
|
||||
newSipGateways[i].invalidInbound = false;
|
||||
newSipGateways[i].invalidOutbound = false;
|
||||
});
|
||||
setSipGateways(newSipGateways);
|
||||
};
|
||||
|
||||
const handleSubmit = async e => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
resetInvalidFields();
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if (!name) {
|
||||
errorMessages.push('Please enter a name for this SIP Trunk.');
|
||||
setNameInvalid(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sipGateways.length) {
|
||||
errorMessages.push('You must provide at least one SIP Gateway.');
|
||||
if (!focusHasBeenSet) {
|
||||
refAdd.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
const regIp = /^((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])$/;
|
||||
const regFqdn = /^([a-zA-Z][^.]*)(\.[^.]+){2,}$/;
|
||||
const regFqdnTopLevel = /^([a-zA-Z][^.]*)(\.[^.]+)$/;
|
||||
const regPort = /^[0-9]+$/;
|
||||
|
||||
sipGateways.forEach(async (gateway, i) => {
|
||||
//-----------------------------------------------------------------------------
|
||||
// IP validation
|
||||
//-----------------------------------------------------------------------------
|
||||
const type = regIp.test(gateway.ip.trim())
|
||||
? 'ip'
|
||||
: regFqdn.test(gateway.ip.trim())
|
||||
? 'fqdn'
|
||||
: regFqdnTopLevel.test(gateway.ip.trim())
|
||||
? 'fqdn-top-level'
|
||||
: 'invalid';
|
||||
|
||||
if (!gateway.ip) {
|
||||
errorMessages.push('The IP Address cannot be blank. Please provide an IP address or delete the row.');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
if (!focusHasBeenSet) {
|
||||
refIp.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
else if (type === 'fqdn-top-level') {
|
||||
errorMessages.push('When using an FQDN, you must use a subdomain (e.g. sip.example.com).');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
if (!focusHasBeenSet) {
|
||||
refIp.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
else if (type === 'invalid') {
|
||||
errorMessages.push('Please provide a valid IP address or fully qualified domain name.');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
if (!focusHasBeenSet) {
|
||||
refIp.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Port validation
|
||||
//-----------------------------------------------------------------------------
|
||||
if (
|
||||
gateway.port && (
|
||||
!(regPort.test(gateway.port.toString().trim()))
|
||||
|| (parseInt(gateway.port.toString().trim()) < 0)
|
||||
|| (parseInt(gateway.port.toString().trim()) > 65535)
|
||||
)
|
||||
) {
|
||||
errorMessages.push('Please provide a valid port number between 0 and 65535');
|
||||
updateSipGateways(null, i, 'invalidPort');
|
||||
if (!focusHasBeenSet) {
|
||||
refPort.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// inbound/outbound validation
|
||||
//-----------------------------------------------------------------------------
|
||||
if (type === 'fqdn' && (!gateway.outbound || gateway.inbound)) {
|
||||
errorMessages.push('A fully qualified domain name may only be used for outbound calls.');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
if (gateway.inbound) updateSipGateways(null, i, 'invalidInbound');
|
||||
if (!gateway.outbound) updateSipGateways(null, i, 'invalidOutbound');
|
||||
if (!focusHasBeenSet) {
|
||||
if (gateway.inbound) {
|
||||
refInbound.current[i].focus();
|
||||
} else {
|
||||
refOutbound.current[i].focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
else if (!gateway.inbound && !gateway.outbound) {
|
||||
errorMessages.push('Each SIP Gateway must accept inbound calls, outbound calls, or both.');
|
||||
updateSipGateways(null, i, 'invalidInbound');
|
||||
updateSipGateways(null, i, 'invalidOutbound');
|
||||
if (!focusHasBeenSet) {
|
||||
refInbound.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// duplicates validation
|
||||
//-----------------------------------------------------------------------------
|
||||
sipGateways.forEach((otherGateway, j) => {
|
||||
if (i >= j) return;
|
||||
if (!gateway.ip) return;
|
||||
if (type === 'invalid') return;
|
||||
if (gateway.ip === otherGateway.ip && gateway.port === otherGateway.port) {
|
||||
errorMessages.push('Each SIP gateway must have a unique IP address.');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
updateSipGateways(null, i, 'invalidPort');
|
||||
updateSipGateways(null, j, 'invalidIp');
|
||||
updateSipGateways(null, j, 'invalidPort');
|
||||
if (!focusHasBeenSet) {
|
||||
refTrash.current[j].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// remove duplicate error messages
|
||||
for (let i = 0; i < errorMessages.length; i++) {
|
||||
for (let j = 0; j < errorMessages.length; j++) {
|
||||
if (i >= j) continue;
|
||||
if (errorMessages[i] === errorMessages[j]) {
|
||||
errorMessages.splice(j, 1);
|
||||
j = j - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Submit
|
||||
//=============================================================================
|
||||
const creatingNewTrunk = props.type === 'add' || (props.type === 'setup' && !sipTrunks.length);
|
||||
|
||||
const method = creatingNewTrunk
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
|
||||
const url = creatingNewTrunk
|
||||
? '/VoipCarriers'
|
||||
: `/VoipCarriers/${sipTrunkSid}`;
|
||||
|
||||
// Create or update SIP Trunk / VoIP Carrier
|
||||
const voipCarrier = await axios({
|
||||
method,
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data: {
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
e164_leading_plus: e164 ? 1 : 0
|
||||
},
|
||||
});
|
||||
const voip_carrier_sid = voipCarrier.data.sid;
|
||||
|
||||
// get updated gateway info from API in order to delete ones that user has removed from UI
|
||||
let sipGatewaysFromAPI;
|
||||
if (!creatingNewTrunk) {
|
||||
const results = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/SipGateways',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
sipGatewaysFromAPI = results.data.filter(s => s.voip_carrier_sid === sipTrunkSid);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Create or Update SIP Gateways
|
||||
//-----------------------------------------------------------------------------
|
||||
// Keeping track of created SIP gateways in case one throws an error, then all
|
||||
// of the ones created before that (as well as the sip trunk) have to be deleted.
|
||||
let completedSipGateways = [];
|
||||
try {
|
||||
for (const s of sipGateways) {
|
||||
const creatingNewGateway = creatingNewTrunk || s.sip_gateway_sid === '';
|
||||
|
||||
const method = creatingNewGateway
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
const url = creatingNewGateway
|
||||
? '/SipGateways'
|
||||
: `/SipGateways/${s.sip_gateway_sid}`;
|
||||
|
||||
const data = {
|
||||
ipv4: s.ip.trim(),
|
||||
port: s.port.toString().trim(),
|
||||
inbound: s.inbound,
|
||||
outbound: s.outbound,
|
||||
};
|
||||
|
||||
if (creatingNewGateway) {
|
||||
data.voip_carrier_sid = voip_carrier_sid || sipTrunkSid;
|
||||
}
|
||||
|
||||
const result = await axios({
|
||||
method,
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data,
|
||||
});
|
||||
if (creatingNewGateway) {
|
||||
completedSipGateways.push(result.data.sid);
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
if (completedSipGateways.length) {
|
||||
for (const sid of completedSipGateways) {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/SipGateways/${sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (voip_carrier_sid) {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/VoipCarriers/${voip_carrier_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
// delete removed gateways (after add/update in case add/update caused errors)
|
||||
if (!creatingNewTrunk) {
|
||||
for (const remote of sipGatewaysFromAPI) {
|
||||
const match = sipGateways.filter(local => local.sip_gateway_sid === remote.sip_gateway_sid);
|
||||
if (!match.length) {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/SipGateways/${remote.sip_gateway_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (props.type === 'setup') {
|
||||
isMounted = false;
|
||||
history.push('/setup-complete');
|
||||
} else {
|
||||
isMounted = false;
|
||||
history.push('/internal/sip-trunks');
|
||||
const dispatchMessage = props.type === 'add'
|
||||
? 'SIP trunk created successfully'
|
||||
: 'SIP trunk updated successfully';
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: dispatchMessage
|
||||
});
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
isMounted = false;
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
setErrorMessage((err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.');
|
||||
console.log(err.response || err);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
showLoader
|
||||
? <Loader height={props.type === 'setup' ? '424px' : '376px'}/>
|
||||
: <Form
|
||||
large
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="name"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="SIP trunk provider name"
|
||||
invalid={nameInvalid}
|
||||
autoFocus
|
||||
ref={refName}
|
||||
/>
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="description"
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="Optional"
|
||||
/>
|
||||
<Label htmlFor="e164">E.164 Syntax</Label>
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
large={props.type === 'setup'}
|
||||
name="e164"
|
||||
id="e164"
|
||||
label="prepend a leading + on origination attempts"
|
||||
checked={e164}
|
||||
onChange={e => setE164(e.target.checked)}
|
||||
/>
|
||||
|
||||
<hr style={{ margin: '0.5rem -2rem' }} />
|
||||
<div
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>SIP Gateways</div>
|
||||
{
|
||||
sipGateways.length
|
||||
? <div>{/* for CSS grid layout */}</div>
|
||||
: null
|
||||
}
|
||||
{sipGateways.map((g, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<Label htmlFor={`sipGatewaysIp[${i}]`}>IP Address</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name={`sipGatewaysIp[${i}]`}
|
||||
id={`sipGatewaysIp[${i}]`}
|
||||
value={sipGateways[i].ip}
|
||||
onChange={e => updateSipGateways(e, i, 'ip')}
|
||||
placeholder={'1.2.3.4'}
|
||||
invalid={sipGateways[i].invalidIp}
|
||||
ref={ref => refIp.current[i] = ref}
|
||||
/>
|
||||
<Label
|
||||
middle
|
||||
htmlFor={`sipGatewaysPort[${i}]`}
|
||||
>
|
||||
Port
|
||||
</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
width="5rem"
|
||||
name={`sipGatewaysPort[${i}]`}
|
||||
id={`sipGatewaysPort[${i}]`}
|
||||
value={sipGateways[i].port}
|
||||
onChange={e => updateSipGateways(e, i, 'port')}
|
||||
placeholder="5060"
|
||||
invalid={sipGateways[i].invalidPort}
|
||||
ref={ref => refPort.current[i] = ref}
|
||||
/>
|
||||
<Checkbox
|
||||
large={props.type === 'setup'}
|
||||
id={`inbound[${i}]`}
|
||||
label="Inbound"
|
||||
tooltip="Sends us calls"
|
||||
checked={sipGateways[i].inbound}
|
||||
onChange={e => updateSipGateways(e, i, 'inbound')}
|
||||
invalid={sipGateways[i].invalidInbound}
|
||||
ref={ref => refInbound.current[i] = ref}
|
||||
/>
|
||||
<Checkbox
|
||||
large={props.type === 'setup'}
|
||||
id={`outbound[${i}]`}
|
||||
label="Outbound"
|
||||
tooltip="Accepts calls from us"
|
||||
checked={sipGateways[i].outbound}
|
||||
onChange={e => updateSipGateways(e, i, 'outbound')}
|
||||
invalid={sipGateways[i].invalidOutbound}
|
||||
ref={ref => refOutbound.current[i] = ref}
|
||||
/>
|
||||
<TrashButton
|
||||
onClick={() => removeSipGateway(i)}
|
||||
ref={ref => refTrash.current[i] = ref}
|
||||
/>
|
||||
</InputGroup>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<Button
|
||||
square
|
||||
type="button"
|
||||
onClick={addSipGateway}
|
||||
ref={refAdd}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
{props.type === 'edit' && (
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/sip-trunks');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
large={props.type === 'setup'}
|
||||
grid
|
||||
fullWidth={props.type === 'setup' || props.type === 'add'}
|
||||
>
|
||||
{props.type === 'setup'
|
||||
? 'Save and Continue'
|
||||
: props.type === 'add'
|
||||
? 'Add SIP Trunk'
|
||||
: 'Save'
|
||||
}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
|
||||
{props.type === 'setup' && (
|
||||
<Link
|
||||
formLink
|
||||
right
|
||||
to="/setup-complete"
|
||||
>
|
||||
Skip for now — I'll complete later
|
||||
</Link>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SipTrunkForm;
|
||||
710
src/components/forms/SpeechForm.js
Normal file
710
src/components/forms/SpeechForm.js
Normal file
@@ -0,0 +1,710 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import styled from "styled-components/macro";
|
||||
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import handleErrors from '../../helpers/handleErrors';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
import Select from '../elements/Select';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import Checkbox from '../elements/Checkbox';
|
||||
import FileUpload from '../elements/FileUpload';
|
||||
import Code from '../elements/Code';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Button from '../elements/Button';
|
||||
import Loader from '../blocks/Loader';
|
||||
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
|
||||
import AwsRegions from '../../data/AwsRegions';
|
||||
import MicrosoftAzureRegions from '../../data/MicrosoftAzureRegions';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const StyledButtonGroup = styled(InputGroup)`
|
||||
@media (max-width: 576.98px) {
|
||||
width: 100%;
|
||||
|
||||
& > *:first-child {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& > *:last-child {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
${props => props.type === 'add' ? `
|
||||
@media (max-width: 459.98px) {
|
||||
flex-direction: column;
|
||||
|
||||
& > *:first-child {
|
||||
width: 100%;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
}
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
const SpeechServicesAddEdit = (props) => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const jwt = localStorage.getItem('token');
|
||||
|
||||
let { speech_service_sid } = useParams();
|
||||
const type = speech_service_sid ? 'edit' : 'add';
|
||||
|
||||
// Refs
|
||||
const refVendorGoogle = useRef(null);
|
||||
const refVendorAws = useRef(null);
|
||||
const refVendorMs = useRef(null);
|
||||
const refVendorWellSaid = useRef(null);
|
||||
const refAccessKeyId = useRef(null);
|
||||
const refSecretAccessKey = useRef(null);
|
||||
const refUseForTts = useRef(null);
|
||||
const refUseForStt = useRef(null);
|
||||
const refApiKey = useRef(null);
|
||||
const refRegion = useRef(null);
|
||||
const refAwsRegion = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [vendor, setVendor] = useState('');
|
||||
const [serviceKey, setServiceKey] = useState('');
|
||||
const [displayedServiceKey, setDisplayedServiceKey] = useState('');
|
||||
const [accessKeyId, setAccessKeyId] = useState('');
|
||||
const [secretAccessKey, setSecretAccessKey] = useState('');
|
||||
const [useForTts, setUseForTts] = useState(false);
|
||||
const [useForStt, setUseForStt] = useState(false);
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
const [accountSid, setAccountSid] = useState('');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [region, setRegion] = useState('');
|
||||
const [awsregion, setAwsRegion] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [invalidVendorGoogle, setInvalidVendorGoogle] = useState(false);
|
||||
const [invalidVendorAws, setInvalidVendorAws] = useState(false);
|
||||
const [invalidVendorMs, setInvalidVendorMs] = useState(false);
|
||||
const [invalidVendorWellSaid, setInvalidVendorWellSaid] = useState(false);
|
||||
const [invalidAccessKeyId, setInvalidAccessKeyId] = useState(false);
|
||||
const [invalidSecretAccessKey, setInvalidSecretAccessKey] = useState(false);
|
||||
const [invalidUseForTts, setInvalidUseForTts] = useState(false);
|
||||
const [invalidUseForStt, setInvalidUseForStt] = useState(false);
|
||||
const [invalidApiKey, setInvalidApiKey] = useState(false);
|
||||
const [invalidRegion, setInvalidRegion] = useState(false);
|
||||
const [invalidAwsRegion, setInvalidAwsRegion] = useState(false);
|
||||
|
||||
const [originalTtsValue, setOriginalTtsValue] = useState(null);
|
||||
const [originalSttValue, setOriginalSttValue] = useState(null);
|
||||
|
||||
const [validServiceKey, setValidServiceKey] = useState(false);
|
||||
|
||||
const [showLoader, setShowLoader] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
const accountsResponse = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccounts(accountsResponse.data);
|
||||
|
||||
if (type === 'edit') {
|
||||
const speechCredential = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
let serviceKeyJson = '';
|
||||
let displayedServiceKeyJson = '';
|
||||
|
||||
try {
|
||||
serviceKeyJson = JSON.parse(speechCredential.data.service_key);
|
||||
displayedServiceKeyJson = JSON.stringify(serviceKeyJson, null, 2);
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
setAccountSid(speechCredential.data.account_sid || '');
|
||||
setVendor(speechCredential.data.vendor || undefined);
|
||||
setServiceKey(serviceKeyJson || '');
|
||||
setDisplayedServiceKey(displayedServiceKeyJson || '');
|
||||
setAccessKeyId(speechCredential.data.access_key_id || '');
|
||||
setSecretAccessKey(speechCredential.data.secret_access_key || '');
|
||||
setApiKey(speechCredential.data.api_key || '');
|
||||
setRegion(speechCredential.data.region || '');
|
||||
setAwsRegion(speechCredential.data.aws_region || '');
|
||||
setUseForTts(speechCredential.data.use_for_tts || false);
|
||||
setUseForStt(speechCredential.data.use_for_stt || false);
|
||||
setOriginalTtsValue(speechCredential.data.use_for_tts || false);
|
||||
setOriginalSttValue(speechCredential.data.use_for_stt || false);
|
||||
}
|
||||
setShowLoader(false);
|
||||
} catch (err) {
|
||||
isMounted = false;
|
||||
handleErrors({
|
||||
err,
|
||||
history,
|
||||
dispatch,
|
||||
redirect: '/internal/speech-services',
|
||||
fallbackMessage: 'That speech service does not exist',
|
||||
preferFallback: true,
|
||||
});
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const handleFileUpload = async (e) => {
|
||||
setErrorMessage('');
|
||||
setServiceKey('');
|
||||
setDisplayedServiceKey('');
|
||||
|
||||
const file = e.target.files[0];
|
||||
|
||||
if (!file) {
|
||||
setValidServiceKey(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileAsText = await file.text();
|
||||
|
||||
try {
|
||||
const fileJson = JSON.parse(fileAsText);
|
||||
|
||||
if (!fileJson.client_email || !fileJson.private_key) {
|
||||
setValidServiceKey(false);
|
||||
setErrorMessage('Invalid service key file, missing data.');
|
||||
return;
|
||||
}
|
||||
|
||||
setValidServiceKey(true);
|
||||
setServiceKey(fileJson);
|
||||
setDisplayedServiceKey(JSON.stringify(fileJson, null, 2));
|
||||
|
||||
} catch (err) {
|
||||
setValidServiceKey(false);
|
||||
setErrorMessage('Invalid service key file, could not parse as JSON.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidVendorGoogle(false);
|
||||
setInvalidVendorAws(false);
|
||||
setInvalidVendorMs(false);
|
||||
setInvalidVendorWellSaid(false);
|
||||
setInvalidAccessKeyId(false);
|
||||
setInvalidSecretAccessKey(false);
|
||||
setInvalidUseForTts(false);
|
||||
setInvalidUseForStt(false);
|
||||
setInvalidApiKey(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if (!vendor) {
|
||||
errorMessages.push('Please select a vendor.');
|
||||
setInvalidVendorGoogle(true);
|
||||
setInvalidVendorAws(true);
|
||||
setInvalidVendorMs(true);
|
||||
setInvalidVendorWellSaid(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refVendorGoogle.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'google' && !serviceKey) {
|
||||
errorMessages.push('Please upload a service key file.');
|
||||
}
|
||||
|
||||
if (vendor === 'aws' && !accessKeyId) {
|
||||
errorMessages.push('Please provide an access key ID.');
|
||||
setInvalidAccessKeyId(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refAccessKeyId.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'aws' && !secretAccessKey) {
|
||||
errorMessages.push('Please provide a secret access key.');
|
||||
setInvalidSecretAccessKey(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refSecretAccessKey.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'aws' && !awsregion) {
|
||||
errorMessages.push('Please select a region.');
|
||||
setInvalidAwsRegion(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refAwsRegion.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'microsoft' && !apiKey) {
|
||||
errorMessages.push('Please provide an API key.');
|
||||
setInvalidApiKey(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refApiKey.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'microsoft' && !region) {
|
||||
errorMessages.push('Please select a region.');
|
||||
setInvalidRegion(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refRegion.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'wellsaid' && !apiKey) {
|
||||
errorMessages.push('Please provide an API key.');
|
||||
setInvalidApiKey(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refApiKey.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//===============================================
|
||||
// Submit
|
||||
//===============================================
|
||||
const method = type === 'add'
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
const url = type === 'add'
|
||||
? `/ServiceProviders/${currentServiceProvider}/SpeechCredentials`
|
||||
: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}`;
|
||||
|
||||
const postResults = await axios({
|
||||
method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
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,
|
||||
}
|
||||
});
|
||||
|
||||
if (type === 'add') {
|
||||
if (!postResults.data || !postResults.data.sid) {
|
||||
throw new Error('Error retrieving response data');
|
||||
}
|
||||
|
||||
speech_service_sid = postResults.data.sid;
|
||||
}
|
||||
|
||||
//===============================================
|
||||
// Test speech credentials
|
||||
//===============================================
|
||||
if (useForTts || useForStt) {
|
||||
const testResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}/test`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (useForTts && testResults.data.tts.status === 'not tested') {
|
||||
errorMessages.push('text-to-speech was not tested, please try again.');
|
||||
}
|
||||
|
||||
if (useForStt && testResults.data.stt.status === 'not tested') {
|
||||
errorMessages.push('speech-to-text was not tested, please try again.');
|
||||
}
|
||||
|
||||
const ttsReason = (useForTts && testResults.data.tts.status === 'fail')
|
||||
? testResults.data.tts.reason
|
||||
: null;
|
||||
|
||||
const sttReason = (useForStt && testResults.data.stt.status === 'fail')
|
||||
? testResults.data.stt.reason
|
||||
: null;
|
||||
|
||||
if (ttsReason && (ttsReason === sttReason)) {
|
||||
errorMessages.push(ttsReason);
|
||||
} else {
|
||||
if (ttsReason) {
|
||||
errorMessages.push(`Text-to-speech error: ${ttsReason}`);
|
||||
}
|
||||
|
||||
if (sttReason) {
|
||||
errorMessages.push(`Speech-to-text error: ${sttReason}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
}
|
||||
|
||||
if (errorMessages.length) {
|
||||
if (type === 'add') {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'edit') {
|
||||
await axios({
|
||||
method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
data: {
|
||||
use_for_tts: originalTtsValue,
|
||||
use_for_stt: originalSttValue,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//===============================================
|
||||
// If successful, go to speech services
|
||||
//===============================================
|
||||
isMounted = false;
|
||||
if (accountSid) {
|
||||
history.push(`/internal/speech-services?account_sid=${accountSid}`);
|
||||
} else {
|
||||
history.push('/internal/speech-services');
|
||||
}
|
||||
const dispatchMessage = type === 'add'
|
||||
? 'Speech service created successfully'
|
||||
: 'Speech service updated successfully';
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: dispatchMessage
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
isMounted = false;
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
setErrorMessage(
|
||||
(err.response && err.response.data && err.response.data.msg) ||
|
||||
err.message || 'Something went wrong, please try again.'
|
||||
);
|
||||
console.error(err.response || err);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
showLoader ? (
|
||||
<Loader height={props.type === 'add' ? '424px' : '376px'} />
|
||||
) : (
|
||||
<Form
|
||||
large
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="vendor">Vendor</Label>
|
||||
<Select
|
||||
name="vendor"
|
||||
id="vendor"
|
||||
value={vendor}
|
||||
onChange={e => setVendor(e.target.value)}
|
||||
ref={[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
|
||||
name="account"
|
||||
id="account"
|
||||
value={accountSid}
|
||||
onChange={e => setAccountSid(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
All accounts
|
||||
</option>
|
||||
{accounts.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
|
||||
<option
|
||||
key={a.account_sid}
|
||||
value={a.account_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{['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>
|
||||
{type === 'add' && (
|
||||
<FileUpload
|
||||
id="serviceKey"
|
||||
onChange={handleFileUpload}
|
||||
validFile={validServiceKey}
|
||||
/>
|
||||
)}
|
||||
{displayedServiceKey && (
|
||||
<>
|
||||
{type === 'add' && (
|
||||
<span></span>
|
||||
)}
|
||||
<Code>{displayedServiceKey}</Code>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : vendor === 'aws' ? (
|
||||
<>
|
||||
<Label htmlFor="accessKeyId">Access Key ID</Label>
|
||||
<Input
|
||||
name="accessKeyId"
|
||||
id="accessKeyId"
|
||||
value={accessKeyId}
|
||||
onChange={e => setAccessKeyId(e.target.value)}
|
||||
placeholder=""
|
||||
invalid={invalidAccessKeyId}
|
||||
ref={refAccessKeyId}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
|
||||
<Label htmlFor="secretAccessKey">Secret Access Key</Label>
|
||||
<PasswordInput
|
||||
allowShowPassword
|
||||
name="secretAccessKey"
|
||||
id="secretAccessKey"
|
||||
password={secretAccessKey}
|
||||
setPassword={setSecretAccessKey}
|
||||
setErrorMessage={setErrorMessage}
|
||||
invalid={invalidSecretAccessKey}
|
||||
ref={refSecretAccessKey}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
|
||||
<Label htmlFor="regions">Region</Label>
|
||||
<Select
|
||||
name="regions"
|
||||
id="regions"
|
||||
value={awsregion}
|
||||
onChange={e => setAwsRegion(e.target.value)}
|
||||
ref={refAwsRegion}
|
||||
invalid={invalidAwsRegion}
|
||||
>
|
||||
<option value="">
|
||||
Select a region
|
||||
</option>
|
||||
{AwsRegions.map(r => (
|
||||
<option
|
||||
key={r.value}
|
||||
value={r.value}
|
||||
>
|
||||
{r.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
) : vendor === 'microsoft' ? (
|
||||
<>
|
||||
<Label htmlFor="apiKey">API Key</Label>
|
||||
<Input
|
||||
name="apiKey"
|
||||
id="apiKey"
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
placeholder=""
|
||||
invalid={invalidApiKey}
|
||||
ref={refApiKey}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
|
||||
<Label htmlFor="region">Region</Label>
|
||||
<Select
|
||||
name="region"
|
||||
id="region"
|
||||
value={region}
|
||||
onChange={e => setRegion(e.target.value)}
|
||||
ref={refRegion}
|
||||
invalid={invalidRegion}
|
||||
>
|
||||
<option value="">
|
||||
All regions
|
||||
</option>
|
||||
{MicrosoftAzureRegions.map(r => (
|
||||
<option
|
||||
key={r.value}
|
||||
value={r.value}
|
||||
>
|
||||
{r.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
) : vendor === 'wellsaid' ? (
|
||||
<>
|
||||
<Label htmlFor="apiKey">API Key</Label>
|
||||
<Input
|
||||
name="apiKey"
|
||||
id="apiKey"
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
placeholder=""
|
||||
invalid={invalidApiKey}
|
||||
ref={refApiKey}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<StyledButtonGroup flexEnd spaced type={type}>
|
||||
<Button
|
||||
rounded="true"
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/speech-services');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: type === 'add' ? 'New speech service canceled' : 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button rounded="true" disabled={!vendor}>
|
||||
{type === 'add'
|
||||
? 'Add Speech Service'
|
||||
: 'Save'
|
||||
}
|
||||
</Button>
|
||||
</StyledButtonGroup>
|
||||
</Form>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeechServicesAddEdit;
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
@@ -7,6 +8,7 @@ import Button from '../elements/Button';
|
||||
import Input from '../elements/Input';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import FormError from '../blocks/FormError';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const Login = props => {
|
||||
let history = useHistory();
|
||||
@@ -58,7 +60,7 @@ const Login = props => {
|
||||
// Log in
|
||||
const response = await axios({
|
||||
method: 'post',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/login',
|
||||
data: { username, password },
|
||||
});
|
||||
@@ -69,6 +71,7 @@ const Login = props => {
|
||||
// They're saved to sessionStorage so that the data does not persist.
|
||||
sessionStorage.setItem('user_sid', response.data.user_sid);
|
||||
sessionStorage.setItem('old_password', password);
|
||||
localStorage.setItem('token', response.data.token);
|
||||
history.push('/create-password');
|
||||
return;
|
||||
}
|
||||
@@ -83,7 +86,7 @@ const Login = props => {
|
||||
//-----------------------------------------------------------------------------
|
||||
const serviceProvidersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/serviceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -92,7 +95,7 @@ const Login = props => {
|
||||
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -101,7 +104,7 @@ const Login = props => {
|
||||
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -110,7 +113,7 @@ const Login = props => {
|
||||
|
||||
const voipCarriersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/voipCarriers',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -143,20 +146,20 @@ const Login = props => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sip_realm, registration_hook } = accounts[0];
|
||||
// const { sip_realm, registration_hook } = accounts[0];
|
||||
|
||||
if (
|
||||
(!sip_realm || !registration_hook) &&
|
||||
!applications.length
|
||||
) {
|
||||
history.push('/configure-account');
|
||||
return;
|
||||
}
|
||||
// if (
|
||||
// (!sip_realm || !registration_hook) &&
|
||||
// !applications.length
|
||||
// ) {
|
||||
// history.push('/configure-account');
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!applications.length) {
|
||||
history.push('/create-application');
|
||||
return;
|
||||
}
|
||||
// if (!applications.length) {
|
||||
// history.push('/create-application');
|
||||
// return;
|
||||
// }
|
||||
|
||||
history.push('/internal/accounts');
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
@@ -5,6 +6,7 @@ import { NotificationDispatchContext } from '../../../contexts/NotificationConte
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import AccountForm from '../../forms/AccountForm';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const AccountsAddEdit = () => {
|
||||
let history = useHistory();
|
||||
@@ -31,7 +33,7 @@ const AccountsAddEdit = () => {
|
||||
}
|
||||
const results = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${account_sid}/ApiKeys`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -112,7 +114,7 @@ const AccountsAddEdit = () => {
|
||||
}
|
||||
const result = await axios({
|
||||
method: 'post',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Apikeys',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -167,7 +169,7 @@ const AccountsAddEdit = () => {
|
||||
}
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Apikeys/${apiKeyToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const AccountsList = () => {
|
||||
let history = useHistory();
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
useEffect(() => {
|
||||
document.title = `Accounts | Jambonz | Open Source CPAAS`;
|
||||
@@ -26,10 +30,11 @@ const AccountsList = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(!currentServiceProvider) return [];
|
||||
const results = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
@@ -38,10 +43,9 @@ const AccountsList = () => {
|
||||
sid: a.account_sid,
|
||||
name: a.name,
|
||||
sip_realm: a.sip_realm,
|
||||
url: a.registration_hook && a.registration_hook.url,
|
||||
method: a.registration_hook && a.registration_hook.method,
|
||||
username: a.registration_hook && a.registration_hook.username,
|
||||
password: a.registration_hook && a.registration_hook.password,
|
||||
url_reg: a.registration_hook && a.registration_hook.url,
|
||||
url_queue: a.queue_event_hook && a.queue_event_hook.url,
|
||||
subspace_enabled: a.subspace_sip_teleport_id ? 'Enabled' : ''
|
||||
}));
|
||||
return(simplifiedAccounts);
|
||||
} catch (err) {
|
||||
@@ -72,7 +76,7 @@ const AccountsList = () => {
|
||||
const items = [
|
||||
{ name: 'Name:' , content: account.name || '[none]' },
|
||||
{ name: 'SIP Realm:' , content: account.sip_realm || '[none]' },
|
||||
{ name: 'Registration Webhook:' , content: account.url || '[none]' },
|
||||
{ name: 'Registration Webhook:' , content: account.url_reg || '[none]' },
|
||||
];
|
||||
return items;
|
||||
};
|
||||
@@ -92,7 +96,7 @@ const AccountsList = () => {
|
||||
// or if the account has any API keys
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -100,7 +104,7 @@ const AccountsList = () => {
|
||||
});
|
||||
const phoneNumbersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/PhoneNumbers',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -108,7 +112,7 @@ const AccountsList = () => {
|
||||
});
|
||||
const msTeamsTenantsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/MicrosoftTeamsTenants',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -116,7 +120,7 @@ const AccountsList = () => {
|
||||
});
|
||||
const apiKeysPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${accountToDelete.sid}/ApiKeys`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -177,7 +181,7 @@ const AccountsList = () => {
|
||||
// Delete account
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${accountToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -218,7 +222,8 @@ const AccountsList = () => {
|
||||
{ header: 'Name', key: 'name' },
|
||||
{ header: 'AccountSid', key: 'sid' },
|
||||
{ header: 'SIP Realm', key: 'sip_realm' },
|
||||
{ header: 'Registration Webhook', key: 'url' },
|
||||
{ header: 'Registration Webhook', key: 'url_reg' },
|
||||
{ header: 'Queue Event Webhook', key: 'url_queue' }
|
||||
]}
|
||||
formatContentToDelete={formatAccountToDelete}
|
||||
deleteContent={deleteAccount}
|
||||
|
||||
259
src/components/pages/internal/AlertsList.js
Normal file
259
src/components/pages/internal/AlertsList.js
Normal file
@@ -0,0 +1,259 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
import styled from "styled-components/macro";
|
||||
import { NotificationDispatchContext } from "../../../contexts/NotificationContext";
|
||||
import InternalTemplate from "../../templates/InternalTemplate";
|
||||
import Button from "../../../components/elements/Button";
|
||||
import InputGroup from "../../../components/elements/InputGroup";
|
||||
import Label from "../../../components/elements/Label";
|
||||
import Select from "../../../components/elements/Select";
|
||||
import AntdTable from "../../../components/blocks/AntdTable";
|
||||
import handleErrors from "../../../helpers/handleErrors";
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
& > span {
|
||||
height: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const AlertsIndex = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const jwt = localStorage.getItem("token");
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Table props
|
||||
const [alertsData, setAlertsData] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowCount, setRowCount] = useState(25);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Filter values
|
||||
const [account, setAccount] = useState("");
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
const [attemptedAt, setAttemptedAt] = useState("today");
|
||||
|
||||
//=============================================================================
|
||||
// Define Table props
|
||||
//=============================================================================
|
||||
const Columns = [
|
||||
{
|
||||
title: "Date",
|
||||
dataIndex: "time",
|
||||
key: "time",
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
title: "Message",
|
||||
dataIndex: "message",
|
||||
key: "message",
|
||||
width: 250,
|
||||
},
|
||||
];
|
||||
const { height } = window.screen;
|
||||
|
||||
const renderPagination = (page, type, originElement) => {
|
||||
let node = originElement;
|
||||
|
||||
switch (type) {
|
||||
case "page":
|
||||
node = <StyledButton gray={currentPage !== page}>{page}</StyledButton>;
|
||||
break;
|
||||
case "prev":
|
||||
node = <StyledButton>{`<`}</StyledButton>;
|
||||
break;
|
||||
case "next":
|
||||
node = <StyledButton>{`>`}</StyledButton>;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
//=============================================================================
|
||||
// Get alerts
|
||||
//=============================================================================
|
||||
const getAlerts = async () => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
let filter = {
|
||||
page: currentPage,
|
||||
count: rowCount,
|
||||
};
|
||||
|
||||
if (!account) {
|
||||
setAlertsData([]);
|
||||
setTotalCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
switch (attemptedAt) {
|
||||
case "today":
|
||||
filter.start = moment().startOf("date").toISOString();
|
||||
break;
|
||||
case "7d":
|
||||
filter.days = 7;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
const alerts = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${account}/Alerts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
params: {
|
||||
...filter,
|
||||
},
|
||||
});
|
||||
|
||||
if (isMounted) {
|
||||
const { total, data } = alerts.data;
|
||||
const simplififedAlerts = data.map((alert, index) => ({
|
||||
...alert,
|
||||
id: index,
|
||||
time: alert.time
|
||||
? moment(alert.time).format("YYYY MM.DD hh:mm a")
|
||||
: "",
|
||||
message: alert.message,
|
||||
}));
|
||||
|
||||
setAlertsData(simplififedAlerts);
|
||||
setTotalCount(total);
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage === 1) {
|
||||
getAlerts();
|
||||
} else {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [account, attemptedAt]);
|
||||
|
||||
useEffect(() => {
|
||||
getAlerts();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPage, rowCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
let isMounted = true;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
if (accountResponse.data.length > 0) {
|
||||
setAccount(accountResponse.data[0].account_sid);
|
||||
} else {
|
||||
setAccount("");
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
return (
|
||||
<InternalTemplate title="Alerts">
|
||||
<StyledInputGroup flexEnd space>
|
||||
<Label indented htmlFor="account">
|
||||
Account
|
||||
</Label>
|
||||
<AccountSelect
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={(e) => setAccount(e.target.value)}
|
||||
>
|
||||
{accountList.map((acc) => (
|
||||
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
|
||||
))}
|
||||
</AccountSelect>
|
||||
<Label middle htmlFor="daterange">
|
||||
Date
|
||||
</Label>
|
||||
<Select
|
||||
name="daterange"
|
||||
id="daterange"
|
||||
value={attemptedAt}
|
||||
onChange={(e) => setAttemptedAt(e.target.value)}
|
||||
>
|
||||
<option value="today">today</option>
|
||||
<option value="7d">last 7d</option>
|
||||
</Select>
|
||||
</StyledInputGroup>
|
||||
<AntdTable
|
||||
dataSource={alertsData}
|
||||
columns={Columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: ["bottomCenter"],
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(page);
|
||||
setRowCount(size);
|
||||
},
|
||||
showTotal: (total) => `Total: ${total} records`,
|
||||
current: currentPage,
|
||||
total: totalCount,
|
||||
pageSize: rowCount,
|
||||
pageSizeOptions: [25, 50, 100],
|
||||
showSizeChanger: true,
|
||||
itemRender: renderPagination,
|
||||
showLessItems: true,
|
||||
}}
|
||||
scroll={{ y: Math.max(height - 660, 200) }}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertsIndex;
|
||||
@@ -1,21 +1,92 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useEffect, useContext, useState, useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import styled from "styled-components/macro";
|
||||
import Select from "../../../components/elements/Select";
|
||||
import InputGroup from "../../../components/elements/InputGroup";
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import handleErrors from "../../../helpers/handleErrors";
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const FilterLabel = styled.span`
|
||||
color: #231f20;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
margin-left: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const ApplicationsList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const jwt = localStorage.getItem('token');
|
||||
const [account, setAccount] = useState("");
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Applications | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
try {
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
if (accountResponse.data.length > 0) {
|
||||
setAccount(accountResponse.data[0].account_sid);
|
||||
} else {
|
||||
setAccount("");
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
//=============================================================================
|
||||
// Get applications
|
||||
//=============================================================================
|
||||
const getApplications = async () => {
|
||||
const getApplications = useCallback(async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
@@ -26,18 +97,13 @@ const ApplicationsList = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!account) {
|
||||
return [];
|
||||
}
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${account}/Applications`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
@@ -45,14 +111,11 @@ const ApplicationsList = () => {
|
||||
|
||||
const promiseAllValues = await Promise.all([
|
||||
applicationsPromise,
|
||||
accountsPromise,
|
||||
]);
|
||||
|
||||
const applications = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[1].data;
|
||||
|
||||
const simplifiedApplications = applications.map(app => {
|
||||
const account = accounts.filter(acc => acc.account_sid === app.account_sid);
|
||||
return {
|
||||
sid: app.application_sid,
|
||||
name: app.name,
|
||||
@@ -60,7 +123,7 @@ const ApplicationsList = () => {
|
||||
call_hook_url: app.call_hook && app.call_hook.url,
|
||||
status_hook_url: app.call_status_hook && app.call_status_hook.url,
|
||||
messaging_hook_url: app.messaging_hook && app.messaging_hook.url,
|
||||
account: account[0].name,
|
||||
account: app.account
|
||||
};
|
||||
});
|
||||
return(simplifiedApplications);
|
||||
@@ -83,7 +146,7 @@ const ApplicationsList = () => {
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [account]);
|
||||
|
||||
//=============================================================================
|
||||
// Delete application
|
||||
@@ -112,7 +175,7 @@ const ApplicationsList = () => {
|
||||
// check if any account or Microsoft Teams Tenant uses this application
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -120,7 +183,7 @@ const ApplicationsList = () => {
|
||||
});
|
||||
const msTeamsTenantsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/MicrosoftTeamsTenants',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -165,7 +228,7 @@ const ApplicationsList = () => {
|
||||
// Delete application
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Applications/${applicationToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -198,6 +261,19 @@ const ApplicationsList = () => {
|
||||
addButtonText="Add an Application"
|
||||
addButtonLink="/internal/applications/add"
|
||||
>
|
||||
<StyledInputGroup flexEnd space>
|
||||
<FilterLabel htmlFor="account">Account:</FilterLabel>
|
||||
<AccountSelect
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={(e) => setAccount(e.target.value)}
|
||||
>
|
||||
{accountList.map((acc) => (
|
||||
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
|
||||
))}
|
||||
</AccountSelect>
|
||||
</StyledInputGroup>
|
||||
<TableContent
|
||||
name="application"
|
||||
urlParam="applications"
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import SipTrunkForm from '../../forms/SipTrunkForm';
|
||||
import Sbcs from '../../blocks/Sbcs';
|
||||
import CarrierForm from '../../forms/CarrierForm';
|
||||
|
||||
const SipTrunksAddEdit = () => {
|
||||
const CarriersAddEdit = () => {
|
||||
let { voip_carrier_sid } = useParams();
|
||||
const pageTitle = voip_carrier_sid ? 'Edit SIP Trunk' : 'Add SIP Trunk';
|
||||
const pageTitle = voip_carrier_sid ? 'Edit Carrier' : 'Add Carrier';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
subtitle={<Sbcs />}
|
||||
breadcrumbs={[
|
||||
{ name: 'SIP Trunks', url: '/internal/sip-trunks' },
|
||||
{ name: 'Carriers', url: '/internal/carriers' },
|
||||
{ name: pageTitle },
|
||||
]}
|
||||
>
|
||||
<SipTrunkForm
|
||||
<CarrierForm
|
||||
type={voip_carrier_sid ? 'edit' : 'add'}
|
||||
voip_carrier_sid={voip_carrier_sid}
|
||||
/>
|
||||
@@ -28,4 +27,4 @@ const SipTrunksAddEdit = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SipTrunksAddEdit;
|
||||
export default CarriersAddEdit;
|
||||
283
src/components/pages/internal/CarriersList.js
Normal file
283
src/components/pages/internal/CarriersList.js
Normal file
@@ -0,0 +1,283 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useEffect, useContext, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import sortSipGateways from '../../../helpers/sortSipGateways';
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import InputGroup from '../../../components/elements/InputGroup';
|
||||
import Select from '../../../components/elements/Select';
|
||||
import handleErrors from '../../../helpers/handleErrors';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const FilterLabel = styled.span`
|
||||
color: #231f20;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
margin-left: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const CarriersList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const location = useLocation();
|
||||
const locationAccountSid = new URLSearchParams(location.search).get('account_sid');
|
||||
|
||||
const [accountSid, setAccountSid] = useState('');
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Carriers | Jambonz | Open Source CPAAS`;
|
||||
}, []);
|
||||
|
||||
//=============================================================================
|
||||
// Get accounts
|
||||
//=============================================================================
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
try {
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (locationAccountSid) {
|
||||
setAccountSid(locationAccountSid);
|
||||
}
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
//=============================================================================
|
||||
// Get sip trunks
|
||||
//=============================================================================
|
||||
const getCarriers = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(!currentServiceProvider) return [];
|
||||
if (!accountList.length) return [];
|
||||
// Get all SIP trunks
|
||||
const trunkResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/VoipCarriers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const trunkResultsFiltered = accountSid ?
|
||||
trunkResults.data.filter(t => t.account_sid === accountSid) :
|
||||
trunkResults.data.filter(t => t.account_sid === null);
|
||||
|
||||
// Add appropriate gateways to each trunk
|
||||
const trunkMap = {};
|
||||
for (const t of trunkResultsFiltered) {
|
||||
const gws = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/SipGateways?voip_carrier_sid=${t.voip_carrier_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
trunkMap[t.voip_carrier_sid] = gws.data;
|
||||
}
|
||||
|
||||
const trunksWithGateways = trunkResultsFiltered.map(t => {
|
||||
const gateways = trunkMap[t.voip_carrier_sid] || [];
|
||||
sortSipGateways(gateways);
|
||||
return {
|
||||
...t,
|
||||
gateways,
|
||||
};
|
||||
});
|
||||
|
||||
const simplifiedCarriers = trunksWithGateways.map(t => ({
|
||||
sid: t.voip_carrier_sid,
|
||||
name: t.name,
|
||||
status: t.is_active === 1 ? "active" : "inactive",
|
||||
gatewaysConcat: `${
|
||||
t.gateways.filter((item) => item.inbound === 1).length
|
||||
} inbound, ${
|
||||
t.gateways.filter((item) => item.outbound === 1).length
|
||||
} outbound`,
|
||||
gatewaysList: t.gateways.map(g => `${g.ipv4}:${g.port}`),
|
||||
gatewaysSid: t.gateways.map(g => g.sip_gateway_sid),
|
||||
}));
|
||||
return(simplifiedCarriers);
|
||||
} 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 {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get SIP trunk data',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Delete sip trunk
|
||||
//=============================================================================
|
||||
const formatCarrierToDelete = trunk => {
|
||||
const gatewayName = trunk.gatewaysList.length > 1
|
||||
? 'SIP Gateways:'
|
||||
: 'SIP Gateway:';
|
||||
|
||||
return [
|
||||
{ name: 'Name:', content: trunk.name || '[none]' },
|
||||
{ name: 'Status:', content: trunk.status || '[none]' },
|
||||
{ name: gatewayName, content: trunk.gatewaysConcat || '[none]' },
|
||||
];
|
||||
};
|
||||
const deleteCarrier = async carrierToDelete => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
// delete associated gateways
|
||||
for (const sid of carrierToDelete.gatewaysSid) {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/SipGateways/${sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
// delete sip trunk
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/VoipCarriers/${carrierToDelete.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 SIP trunk');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate
|
||||
title="Carriers"
|
||||
addButtonText="Add a Carrier"
|
||||
addButtonLink="/internal/carriers/add"
|
||||
>
|
||||
<StyledInputGroup flexEnd space>
|
||||
<FilterLabel htmlFor="account">Used By:</FilterLabel>
|
||||
<AccountSelect
|
||||
name="account"
|
||||
id="account"
|
||||
value={accountSid}
|
||||
onChange={e => setAccountSid(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
All accounts
|
||||
</option>
|
||||
{accountList.map((acc) => (
|
||||
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
|
||||
))}
|
||||
</AccountSelect>
|
||||
</StyledInputGroup>
|
||||
<TableContent
|
||||
name="Carrier"
|
||||
urlParam="carriers"
|
||||
getContent={getCarriers}
|
||||
columns={[
|
||||
{ header: 'Name', key: 'name' },
|
||||
{ header: 'Status', key: 'status' },
|
||||
{ header: 'Gateways', key: 'gatewaysConcat' },
|
||||
]}
|
||||
formatContentToDelete={formatCarrierToDelete}
|
||||
deleteContent={deleteCarrier}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default CarriersList;
|
||||
@@ -1,9 +1,11 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const MsTeamsTenantsList = () => {
|
||||
let history = useHistory();
|
||||
@@ -28,7 +30,7 @@ const MsTeamsTenantsList = () => {
|
||||
}
|
||||
const msTeamsTenantsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/MicrosoftTeamsTenants',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -36,7 +38,7 @@ const MsTeamsTenantsList = () => {
|
||||
});
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -44,7 +46,7 @@ const MsTeamsTenantsList = () => {
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -114,7 +116,7 @@ const MsTeamsTenantsList = () => {
|
||||
}
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/MicrosoftTeamsTenants/${tenant.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import phoneNumberFormat from '../../../helpers/phoneNumberFormat';
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const PhoneNumbersList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Phone Number Routing | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
}, []);
|
||||
|
||||
//=============================================================================
|
||||
// Get phone numbers
|
||||
//=============================================================================
|
||||
const getPhoneNumbers = async () => {
|
||||
const getPhoneNumbers = useCallback(async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
@@ -27,34 +32,35 @@ const PhoneNumbersList = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(!currentServiceProvider) return [];
|
||||
const phoneNumbersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/PhoneNumbers',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/PhoneNumbers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Applications`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const sipTrunksPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/VoipCarriers',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/VoipCarriers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
@@ -120,7 +126,7 @@ const PhoneNumbersList = () => {
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [currentServiceProvider, dispatch, history]);
|
||||
|
||||
//=============================================================================
|
||||
// Delete phone number
|
||||
@@ -146,7 +152,7 @@ const PhoneNumbersList = () => {
|
||||
}
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/PhoneNumbers/${phoneNumber.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -188,7 +194,7 @@ const PhoneNumbersList = () => {
|
||||
for (const sid of phoneNumberSids) {
|
||||
await axios({
|
||||
method: 'put',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/PhoneNumbers/${sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
|
||||
494
src/components/pages/internal/RecentCallsList.js
Normal file
494
src/components/pages/internal/RecentCallsList.js
Normal file
@@ -0,0 +1,494 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
import styled from "styled-components/macro";
|
||||
import { NotificationDispatchContext } from "../../../contexts/NotificationContext";
|
||||
import InternalTemplate from "../../templates/InternalTemplate";
|
||||
import AntdTable from "../../../components/blocks/AntdTable";
|
||||
import phoneNumberFormat from "../../../helpers/phoneNumberFormat";
|
||||
import timeFormat from "../../../helpers/timeFormat";
|
||||
import Label from "../../../components/elements/Label";
|
||||
import Button from "../../../components/elements/Button";
|
||||
import InputGroup from "../../../components/elements/InputGroup";
|
||||
import Select from "../../../components/elements/Select";
|
||||
import handleErrors from "../../../helpers/handleErrors";
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const FilterLabel = styled.span`
|
||||
color: #231f20;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
margin-left: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const ExpandedSection = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
& > span {
|
||||
height: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const StyledPcapLink = styled.a`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
outline: 0;
|
||||
height: 36px;
|
||||
padding: 10px 26px 8px;
|
||||
border-radius: 0.25rem;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
background: #BD164E;
|
||||
}
|
||||
`;
|
||||
|
||||
const PcapButton = ({call_data, account_sid, jwt_token}) => {
|
||||
const [pcap, setPcap] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${account_sid}/RecentCalls/${call_data.sip_callid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt_token}`,
|
||||
},
|
||||
}).then((result_1) => {
|
||||
if (result_1.status === 200 && result_1.data.total > 0) {
|
||||
axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${account_sid}/RecentCalls/${call_data.sip_callid}/pcap`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt_token}`,
|
||||
},
|
||||
responseType: "blob",
|
||||
}).then((result_2) => {
|
||||
setPcap({
|
||||
dataUrl: URL.createObjectURL(result_2.data),
|
||||
fileName: `callid-${call_data.sip_callid}.pcap`,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [call_data, account_sid, jwt_token, setPcap]);
|
||||
|
||||
if (pcap) {
|
||||
return (
|
||||
<Label>
|
||||
<StyledPcapLink
|
||||
href={pcap.dataUrl}
|
||||
download={pcap.fileName}
|
||||
>
|
||||
Download pcap
|
||||
</StyledPcapLink>
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const RecentCallsIndex = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const jwt = localStorage.getItem("token");
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Table props
|
||||
const [recentCallsData, setRecentCallsData] = useState([]);
|
||||
const [rawData, setRawData] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowCount, setRowCount] = useState(25);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
|
||||
|
||||
// Filter values
|
||||
const [account, setAccount] = useState("");
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
const [attemptedAt, setAttemptedAt] = useState("today");
|
||||
const [dirFilter, setDirFilter] = useState("io");
|
||||
const [answered, setAnswered] = useState("all");
|
||||
// width
|
||||
const { height } = window.screen;
|
||||
|
||||
//=============================================================================
|
||||
// Define Table props
|
||||
//=============================================================================
|
||||
const Columns = [
|
||||
{
|
||||
title: "Date",
|
||||
dataIndex: "attempted_at",
|
||||
key: "attempted_at",
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
title: "Direction",
|
||||
dataIndex: "direction",
|
||||
key: "direction",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "From",
|
||||
dataIndex: "from",
|
||||
key: "from",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "To",
|
||||
dataIndex: "to",
|
||||
key: "to",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "Trunk",
|
||||
dataIndex: "trunk",
|
||||
key: "trunk",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "Duration",
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
width: 150,
|
||||
},
|
||||
];
|
||||
|
||||
//=============================================================================
|
||||
// Get recent calls
|
||||
//=============================================================================
|
||||
const handleFilterChange = () => {
|
||||
let filter = {
|
||||
page: currentPage,
|
||||
count: rowCount,
|
||||
};
|
||||
if (attemptedAt) {
|
||||
switch (attemptedAt) {
|
||||
case "today":
|
||||
filter.start = moment().startOf("date").toISOString();
|
||||
break;
|
||||
case "7d":
|
||||
filter.days = 7;
|
||||
break;
|
||||
case "14d":
|
||||
filter.days = 14;
|
||||
break;
|
||||
case "30d":
|
||||
filter.days = 30;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if (dirFilter === "inbound") {
|
||||
filter.direction = "inbound";
|
||||
} else if (dirFilter === "outbound") {
|
||||
filter.direction = "outbound";
|
||||
}
|
||||
|
||||
if (answered && answered !== "all") {
|
||||
filter.answered = answered === "answered" ? "true" : "false";
|
||||
}
|
||||
|
||||
getRecentCallsData(filter);
|
||||
};
|
||||
|
||||
const getRecentCallsData = async (filter = {}) => {
|
||||
let isMounted = true;
|
||||
|
||||
if (!account) {
|
||||
setRecentCallsData([]);
|
||||
setTotalCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${account}/RecentCalls`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
params: {
|
||||
...filter,
|
||||
},
|
||||
});
|
||||
|
||||
if (isMounted) {
|
||||
const { total, data } = result.data;
|
||||
|
||||
setRawData([...data]);
|
||||
|
||||
const recentCalls = data.map((item, index) => ({
|
||||
key: index,
|
||||
...item,
|
||||
attempted_at: item.attempted_at
|
||||
? moment(item.attempted_at).format("YYYY MM.DD hh:mm a")
|
||||
: "",
|
||||
from: phoneNumberFormat(item.from),
|
||||
to: phoneNumberFormat(item.to),
|
||||
status: item.answered ? "answered" : item.termination_reason,
|
||||
duration: timeFormat(item.duration),
|
||||
trace_id: item.trace_id
|
||||
}));
|
||||
|
||||
setRecentCallsData(recentCalls);
|
||||
setTotalCount(total);
|
||||
setExpandedRowKeys([]);
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderExpandedRow = (data) => {
|
||||
const fields = [
|
||||
"direction",
|
||||
"attempted_at",
|
||||
"answered_at",
|
||||
"terminated_at",
|
||||
"duration",
|
||||
"answered",
|
||||
"from",
|
||||
"to",
|
||||
"termination_reason",
|
||||
"call_sid",
|
||||
"sip_callid",
|
||||
"host",
|
||||
"remote_host",
|
||||
"sip_status",
|
||||
"trunk",
|
||||
"trace_id"
|
||||
];
|
||||
|
||||
return (
|
||||
<ExpandedSection>
|
||||
{fields.map((field, index) => {
|
||||
if (!rawData || !rawData[data.key]) {
|
||||
return null;
|
||||
}
|
||||
let label = rawData[data.key][field];
|
||||
|
||||
if (typeof label === "boolean") {
|
||||
label = label ? "true" : "false";
|
||||
}
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<Label>{`${field}:`}</Label>
|
||||
<Label>{label}</Label>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<PcapButton call_data={data} account_sid={account} jwt_token={jwt} />
|
||||
</ExpandedSection>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPagination = (page, type, originElement) => {
|
||||
let node = originElement;
|
||||
|
||||
switch (type) {
|
||||
case "page":
|
||||
node = <StyledButton gray={currentPage !== page}>{page}</StyledButton>;
|
||||
break;
|
||||
case "prev":
|
||||
node = <StyledButton>{`<`}</StyledButton>;
|
||||
break;
|
||||
case "next":
|
||||
node = <StyledButton>{`>`}</StyledButton>;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
const handleExpandChange = (expanded, record) => {
|
||||
if (expanded) {
|
||||
setExpandedRowKeys((prev) => [...prev, record.key]);
|
||||
} else {
|
||||
setExpandedRowKeys((prev) => [
|
||||
...prev.filter((item) => item !== record.key),
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage === 1) {
|
||||
handleFilterChange();
|
||||
} else {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [account, attemptedAt, dirFilter, answered]);
|
||||
|
||||
useEffect(() => {
|
||||
handleFilterChange();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPage, rowCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
let isMounted = true;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
if (accountResponse.data.length > 0) {
|
||||
setAccount(accountResponse.data[0].account_sid);
|
||||
} else {
|
||||
setAccount("");
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate title="Recent Calls">
|
||||
<StyledInputGroup flexEnd space>
|
||||
<FilterLabel htmlFor="account">Account:</FilterLabel>
|
||||
<AccountSelect
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={(e) => setAccount(e.target.value)}
|
||||
>
|
||||
{accountList.map((acc) => (
|
||||
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
|
||||
))}
|
||||
</AccountSelect>
|
||||
<FilterLabel htmlFor="daterange">Date:</FilterLabel>
|
||||
<Select
|
||||
name="daterange"
|
||||
id="daterange"
|
||||
value={attemptedAt}
|
||||
onChange={(e) => setAttemptedAt(e.target.value)}
|
||||
>
|
||||
<option value="today">today</option>
|
||||
<option value="7d">last 7d</option>
|
||||
<option value="14d">last 14d</option>
|
||||
<option value="30d">last 30d</option>
|
||||
</Select>
|
||||
<FilterLabel htmlFor="direction">Direction:</FilterLabel>
|
||||
<Select
|
||||
name="direction"
|
||||
id="direction"
|
||||
value={dirFilter}
|
||||
onChange={(e) => setDirFilter(e.target.value)}
|
||||
>
|
||||
<option value="io">either</option>
|
||||
<option value="inbound">inbound only</option>
|
||||
<option value="outbound">outbound only</option>
|
||||
</Select>
|
||||
<FilterLabel htmlFor="status">Status:</FilterLabel>
|
||||
<Select
|
||||
name="status"
|
||||
id="status"
|
||||
value={answered}
|
||||
onChange={(e) => setAnswered(e.target.value)}
|
||||
>
|
||||
<option value="all">all</option>
|
||||
<option value="answered">answered</option>
|
||||
<option value="not-answered">not answered</option>
|
||||
</Select>
|
||||
</StyledInputGroup>
|
||||
<AntdTable
|
||||
dataSource={recentCallsData}
|
||||
columns={Columns}
|
||||
rowKey="key"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: ["bottomCenter"],
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(page);
|
||||
setRowCount(size);
|
||||
},
|
||||
showTotal: (total) => `Total: ${total} records`,
|
||||
current: currentPage,
|
||||
total: totalCount,
|
||||
pageSize: rowCount,
|
||||
pageSizeOptions: [25, 50, 100],
|
||||
showSizeChanger: true,
|
||||
itemRender: renderPagination,
|
||||
showLessItems: true,
|
||||
}}
|
||||
scroll={{ y: Math.max(height - 580, 200) }}
|
||||
expandable={{
|
||||
expandedRowRender: renderExpandedRow,
|
||||
}}
|
||||
expandedRowKeys={expandedRowKeys}
|
||||
onExpand={handleExpandChange}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecentCallsIndex;
|
||||
@@ -1,184 +0,0 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import Sbcs from '../../blocks/Sbcs';
|
||||
import sortSipGateways from '../../../helpers/sortSipGateways';
|
||||
|
||||
const SipTrunksList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
useEffect(() => {
|
||||
document.title = `SIP Trunks | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
//=============================================================================
|
||||
// Get sip trunks
|
||||
//=============================================================================
|
||||
const getSipTrunks = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all SIP trunks
|
||||
const trunkResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/VoipCarriers',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Get all SIP gateways
|
||||
const gatewayResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/SipGateways',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Add appropriate gateways to each trunk
|
||||
const trunksWithGateways = trunkResults.data.map(t => {
|
||||
const gateways = gatewayResults.data.filter(g => t.voip_carrier_sid === g.voip_carrier_sid);
|
||||
sortSipGateways(gateways);
|
||||
return {
|
||||
...t,
|
||||
gateways,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
const simplifiedSipTrunks = trunksWithGateways.map(t => ({
|
||||
sid: t.voip_carrier_sid,
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
gatewaysConcat: t.gateways.map(g => `${g.ipv4}:${g.port}`).join(', '),
|
||||
gatewaysList: t.gateways.map(g => `${g.ipv4}:${g.port}`),
|
||||
gatewaysSid: t.gateways.map(g => g.sip_gateway_sid),
|
||||
}));
|
||||
return(simplifiedSipTrunks);
|
||||
} 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 {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get SIP trunk data',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Delete sip trunk
|
||||
//=============================================================================
|
||||
const formatSipTrunkToDelete = trunk => {
|
||||
const gatewayName = trunk.gatewaysList.length > 1
|
||||
? 'SIP Gateways:'
|
||||
: 'SIP Gateway:';
|
||||
const gatewayContent = trunk.gatewaysList.length > 1
|
||||
? trunk.gatewaysList
|
||||
: trunk.gatewaysList[0];
|
||||
return [
|
||||
{ name: 'Name:', content: trunk.name || '[none]' },
|
||||
{ name: 'Description:', content: trunk.description || '[none]' },
|
||||
{ name: gatewayName, content: gatewayContent || '[none]' },
|
||||
];
|
||||
};
|
||||
const deleteSipTrunk = async sipTrunkToDelete => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
// delete associated gateways
|
||||
for (const sid of sipTrunkToDelete.gatewaysSid) {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/SipGateways/${sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
// delete sip trunk
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/VoipCarriers/${sipTrunkToDelete.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 SIP trunk');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate
|
||||
title="SIP Trunks"
|
||||
addButtonText="Add a SIP Trunk"
|
||||
addButtonLink="/internal/sip-trunks/add"
|
||||
subtitle={<Sbcs />}
|
||||
>
|
||||
<TableContent
|
||||
name="SIP trunk"
|
||||
urlParam="sip-trunks"
|
||||
getContent={getSipTrunks}
|
||||
columns={[
|
||||
{ header: 'Name', key: 'name' },
|
||||
{ header: 'Description', key: 'description' },
|
||||
{ header: 'SIP Gateways', key: 'gatewaysConcat' },
|
||||
]}
|
||||
formatContentToDelete={formatSipTrunkToDelete}
|
||||
deleteContent={deleteSipTrunk}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default SipTrunksList;
|
||||
29
src/components/pages/internal/SpeechServicesAddEdit.js
Normal file
29
src/components/pages/internal/SpeechServicesAddEdit.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import SpeechForm from '../../forms/SpeechForm';
|
||||
|
||||
const SpeechServicesAddEdit = () => {
|
||||
let { speech_service_sid } = useParams();
|
||||
const pageTitle = speech_service_sid ? 'Edit Speech Service' : 'Add Speech Service';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
breadcrumbs={[
|
||||
{ name: 'Speech Services', url: '/internal/speech-services' },
|
||||
{ name: pageTitle },
|
||||
]}
|
||||
>
|
||||
<SpeechForm
|
||||
type={speech_service_sid ? 'edit' : 'add'}
|
||||
speech_service_sid={speech_service_sid}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeechServicesAddEdit;
|
||||
309
src/components/pages/internal/SpeechServicesList.js
Normal file
309
src/components/pages/internal/SpeechServicesList.js
Normal file
@@ -0,0 +1,309 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import handleErrors from '../../../helpers/handleErrors';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../../components/blocks/TableContent';
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import InputGroup from '../../../components/elements/InputGroup';
|
||||
import Select from '../../../components/elements/Select';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const FilterLabel = styled.span`
|
||||
color: #231f20;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
margin-left: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const SpeechServicesList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const jwt = localStorage.getItem('token');
|
||||
const location = useLocation();
|
||||
const locationAccountSid = new URLSearchParams(location.search).get('account_sid');
|
||||
|
||||
const [accountSid, setAccountSid] = useState('');
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
|
||||
//=============================================================================
|
||||
// Get accounts
|
||||
//=============================================================================
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
try {
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
if (locationAccountSid) {
|
||||
setAccountSid(locationAccountSid);
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
//=============================================================================
|
||||
// Get speech services
|
||||
//=============================================================================
|
||||
const getSpeechServices = async () => {
|
||||
try {
|
||||
if (!jwt) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!currentServiceProvider) return [];
|
||||
|
||||
const speechApiUrl = accountSid ?
|
||||
`/Accounts/${accountSid}/SpeechCredentials` :
|
||||
`/ServiceProviders/${currentServiceProvider}/SpeechCredentials`;
|
||||
const speechServices = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: speechApiUrl,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
const credentialTestPromises = speechServices.data.map(s => {
|
||||
if (s.use_for_stt || s.use_for_tts) {
|
||||
return axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${s.speech_credential_sid}/test`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const testResposes = await Promise.all(credentialTestPromises);
|
||||
|
||||
const cleanedUpSpeechServices = speechServices.data.map((s, i) => {
|
||||
const testResults = testResposes[i] && testResposes[i].data;
|
||||
|
||||
let content = null;
|
||||
let title = null;
|
||||
|
||||
if (s.use_for_tts && s.use_for_stt) {
|
||||
|
||||
if (testResults.tts.status === 'ok' && testResults.stt.status === 'ok') {
|
||||
content = 'ok';
|
||||
title = 'Connection test successful';
|
||||
} else {
|
||||
content = 'fail';
|
||||
|
||||
if (testResults.tts.reason && testResults.stt.reason) {
|
||||
|
||||
if (testResults.tts.reason === testResults.stt.reason) {
|
||||
title = testResults.tts.reason;
|
||||
} else {
|
||||
title = `TTS: ${testResults.tts.reason}. STT: ${testResults.stt.reason}`;
|
||||
}
|
||||
|
||||
} else if (testResults.tts.reason) {
|
||||
title = `TTS: ${testResults.tts.reason}`;
|
||||
|
||||
} else if (testResults.stt.reason) {
|
||||
title = `STT: ${testResults.stt.reason}`;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (s.use_for_tts) {
|
||||
|
||||
content = testResults.tts.status;
|
||||
title = testResults.tts.status === 'ok'
|
||||
? 'Connection test successful'
|
||||
: testResults.tts.reason;
|
||||
|
||||
} else if (s.use_for_stt) {
|
||||
|
||||
content = testResults.stt.status;
|
||||
title = testResults.stt.status === 'ok'
|
||||
? 'Connection test successful'
|
||||
: testResults.stt.reason;
|
||||
|
||||
}
|
||||
|
||||
const { last_used } = s;
|
||||
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: s.speech_credential_sid,
|
||||
vendor: s.vendor,
|
||||
usage: (s.use_for_tts && s.use_for_stt) ? 'TTS/STT'
|
||||
: s.use_for_tts ? 'TTS'
|
||||
: s.use_for_stt ? 'STT'
|
||||
: 'Not in use',
|
||||
last_used: lastUsedString,
|
||||
status: {
|
||||
type: 'status',
|
||||
content,
|
||||
title,
|
||||
},
|
||||
};
|
||||
});
|
||||
return(cleanedUpSpeechServices);
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch, fallbackMessage: 'Unable to get speech services' });
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Delete speech service
|
||||
//=============================================================================
|
||||
const formatSpeechServiceToDelete = s => {
|
||||
return [
|
||||
{ name: 'Vendor', content: s.vendor || '[none]' },
|
||||
{ name: 'Usage', content: s.usage || '[none]' },
|
||||
{ name: 'Last Used', content: s.last_used || 'Never' },
|
||||
];
|
||||
};
|
||||
const deleteSpeechService = async speechServiceToDelete => {
|
||||
try {
|
||||
if (!jwt) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete speech service
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speechServiceToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.error(err.response || err);
|
||||
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete speech service');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="normalTable"
|
||||
title="Speech Services"
|
||||
addButtonText="Add Speech Service"
|
||||
addButtonLink="/internal/speech-services/add"
|
||||
>
|
||||
<StyledInputGroup flexEnd space>
|
||||
<FilterLabel htmlFor="account">Used By:</FilterLabel>
|
||||
<AccountSelect
|
||||
name="account"
|
||||
id="account"
|
||||
value={accountSid}
|
||||
onChange={e => setAccountSid(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
All accounts
|
||||
</option>
|
||||
{accountList.map((acc) => (
|
||||
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
|
||||
))}
|
||||
</AccountSelect>
|
||||
</StyledInputGroup>
|
||||
<TableContent
|
||||
normalTable
|
||||
name="speech service"
|
||||
urlParam="speech-services"
|
||||
getContent={getSpeechServices}
|
||||
columns={[
|
||||
{ header: 'Vendor', key: 'vendor', bold: true },
|
||||
{ header: 'Usage', key: 'usage', },
|
||||
{ header: 'Last Used', key: 'last_used', },
|
||||
{ header: 'Status', key: 'status', textAlign: 'center' },
|
||||
]}
|
||||
formatContentToDelete={formatSpeechServiceToDelete}
|
||||
deleteContent={deleteSpeechService}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeechServicesList;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import SetupTemplate from '../../templates/SetupTemplate';
|
||||
import SipTrunkForm from '../../forms/SipTrunkForm';
|
||||
import CarrierForm from '../../forms/CarrierForm';
|
||||
import Sbcs from '../../blocks/Sbcs';
|
||||
|
||||
const ConfigureSipTrunk = () => {
|
||||
@@ -14,7 +14,7 @@ const ConfigureSipTrunk = () => {
|
||||
subtitle={<Sbcs centered />}
|
||||
progress={3}
|
||||
>
|
||||
<SipTrunkForm
|
||||
<CarrierForm
|
||||
type="setup"
|
||||
/>
|
||||
</SetupTemplate>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
@@ -8,6 +9,7 @@ import Button from '../../elements/Button';
|
||||
import Input from '../../elements/Input';
|
||||
import FormError from '../../blocks/FormError';
|
||||
import Loader from '../../blocks/Loader';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const CreatePassword = () => {
|
||||
let history = useHistory();
|
||||
@@ -63,7 +65,7 @@ const CreatePassword = () => {
|
||||
//-----------------------------------------------------------------------------
|
||||
const serviceProvidersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/serviceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -72,7 +74,7 @@ const CreatePassword = () => {
|
||||
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -81,7 +83,7 @@ const CreatePassword = () => {
|
||||
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -90,7 +92,7 @@ const CreatePassword = () => {
|
||||
|
||||
const voipCarriersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/voipCarriers',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -123,25 +125,25 @@ const CreatePassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sip_realm, registration_hook } = accounts[0];
|
||||
// const { sip_realm, registration_hook } = accounts[0];
|
||||
|
||||
if (
|
||||
(!sip_realm || !registration_hook) &&
|
||||
!applications.length
|
||||
) {
|
||||
history.push('/configure-account');
|
||||
return;
|
||||
}
|
||||
// if (
|
||||
// (!sip_realm || !registration_hook) &&
|
||||
// !applications.length
|
||||
// ) {
|
||||
// history.push('/configure-account');
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!applications.length) {
|
||||
history.push('/create-application');
|
||||
return;
|
||||
}
|
||||
// if (!applications.length) {
|
||||
// history.push('/create-application');
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!voipCarriers.length) {
|
||||
history.push('/configure-sip-trunk');
|
||||
return;
|
||||
}
|
||||
// if (!voipCarriers.length) {
|
||||
// history.push('/configure-sip-trunk');
|
||||
// return;
|
||||
// }
|
||||
|
||||
history.push('/internal/accounts');
|
||||
}
|
||||
@@ -235,12 +237,15 @@ const CreatePassword = () => {
|
||||
|
||||
const response = await axios({
|
||||
method: 'put',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Users/${user_sid}`,
|
||||
data: {
|
||||
old_password,
|
||||
new_password: password,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
}
|
||||
});
|
||||
|
||||
sessionStorage.removeItem('user_sid');
|
||||
@@ -250,7 +255,8 @@ const CreatePassword = () => {
|
||||
localStorage.setItem('token', response.data.token);
|
||||
}
|
||||
|
||||
history.push('/configure-account');
|
||||
// history.push('/configure-account');
|
||||
history.push('/internal/accounts');
|
||||
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
|
||||
2
src/constants.js
Normal file
2
src/constants.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const { REACT_APP_API_BASE_URL } = process.env;
|
||||
export const APP_API_BASE_URL = (window.JAMBONZ) ? window.JAMBONZ.APP_API_BASE_URL : REACT_APP_API_BASE_URL;
|
||||
16
src/contexts/ServiceProviderContext.js
Normal file
16
src/contexts/ServiceProviderContext.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React, { createContext, useState } from "react";
|
||||
|
||||
export const ServiceProviderValueContext = createContext();
|
||||
export const ServiceProviderMethodContext = createContext();
|
||||
|
||||
export function ServiceProvider(props) {
|
||||
const [currentServiceProvider, setCurrentServiceProvider] = useState("");
|
||||
|
||||
return (
|
||||
<ServiceProviderValueContext.Provider value={currentServiceProvider}>
|
||||
<ServiceProviderMethodContext.Provider value={setCurrentServiceProvider}>
|
||||
{props.children}
|
||||
</ServiceProviderMethodContext.Provider>
|
||||
</ServiceProviderValueContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, createContext, useContext } from 'react';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from './NotificationContext';
|
||||
import { APP_API_BASE_URL } from "../constants";
|
||||
|
||||
export const ShowMsTeamsStateContext = createContext();
|
||||
export const ShowMsTeamsDispatchContext = createContext();
|
||||
@@ -13,7 +14,7 @@ export function ShowMsTeamsProvider(props) {
|
||||
try {
|
||||
const serviceProvidersResponse = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/ServiceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
|
||||
60
src/data/AwsRegions.js
Normal file
60
src/data/AwsRegions.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const regions = [
|
||||
{
|
||||
name: 'US East (N. Virginia)',
|
||||
value: 'us-east-1'
|
||||
},
|
||||
{
|
||||
name: 'US East (Ohio)',
|
||||
value: 'us-east-2'
|
||||
},
|
||||
{
|
||||
name: 'US West (N. California)',
|
||||
value: 'us-west-1'
|
||||
},
|
||||
{
|
||||
name: 'US West (Oregon)',
|
||||
value: 'us-west-2'
|
||||
},
|
||||
{
|
||||
name: 'Africa (Cape Town)',
|
||||
value: 'af-south-1'
|
||||
},
|
||||
{
|
||||
name: 'Asia Pacific (Hong Kong)',
|
||||
value: 'ap-east-1'
|
||||
},
|
||||
{
|
||||
name: 'Asia Pacific (Jakarta)',
|
||||
value: 'ap-southeast-3'
|
||||
},
|
||||
{
|
||||
name: 'Asia Pacific (Mumbai)',
|
||||
value: 'ap-south-1'
|
||||
},
|
||||
{
|
||||
name: 'Asia Pacific (Osaka)',
|
||||
value: 'ap-northeast-3'
|
||||
},
|
||||
{
|
||||
name: 'Asia Pacific (Seoul)',
|
||||
value: 'ap-northeast-2'
|
||||
},
|
||||
{
|
||||
name: 'Asia Pacific (Singapore)',
|
||||
value: 'ap-southeast-1'
|
||||
},
|
||||
{
|
||||
name: 'Asia Pacific (Sydney)',
|
||||
value: 'ap-southeast-2'
|
||||
},
|
||||
{
|
||||
name: 'Asia Pacific (Tokyo)',
|
||||
value: 'ap-northeast-1'
|
||||
},
|
||||
{
|
||||
name: 'Canada (Central)',
|
||||
value: 'ca-central-1'
|
||||
}
|
||||
];
|
||||
|
||||
export default regions;
|
||||
96
src/data/MicrosoftAzureRegions.js
Normal file
96
src/data/MicrosoftAzureRegions.js
Normal file
@@ -0,0 +1,96 @@
|
||||
const regions = [
|
||||
{
|
||||
name: 'Asia (East)',
|
||||
value: 'eastasia'
|
||||
},
|
||||
{
|
||||
name: 'Asia (Southeast)',
|
||||
value: 'southeastasia'
|
||||
},
|
||||
{
|
||||
name: 'Australia (East)',
|
||||
value: 'australiaeast'
|
||||
},
|
||||
{
|
||||
name: 'Brazil (South)',
|
||||
value: 'brazilsouth'
|
||||
},
|
||||
{
|
||||
name: 'Canada (Central)',
|
||||
value: 'canadacentral'
|
||||
},
|
||||
{
|
||||
name: 'Europe (North)',
|
||||
value: 'northeurope'
|
||||
},
|
||||
{
|
||||
name: 'Europe (West)',
|
||||
value: 'westeurope'
|
||||
},
|
||||
{
|
||||
name: 'France (Central)',
|
||||
value: 'francecentral'
|
||||
},
|
||||
{
|
||||
name: 'Switzerland (North)',
|
||||
value: 'switzerlandnorth'
|
||||
},
|
||||
{
|
||||
name: 'India (Central)',
|
||||
value: 'centralindia'
|
||||
},
|
||||
{
|
||||
name: 'Japan (West)',
|
||||
value: 'japanwest'
|
||||
},
|
||||
{
|
||||
name: 'Japan (East)',
|
||||
value: 'japaneast'
|
||||
},
|
||||
{
|
||||
name: 'Korea (Central)',
|
||||
value: 'koreacentral'
|
||||
},
|
||||
{
|
||||
name: 'South Africa (North)',
|
||||
value: 'southafricanorth'
|
||||
},
|
||||
{
|
||||
name: 'UK (South)',
|
||||
value: 'uksouth'
|
||||
},
|
||||
{
|
||||
name: 'US (Cental)',
|
||||
value: 'centralus'
|
||||
},
|
||||
{
|
||||
name: 'US (West Central)',
|
||||
value: 'westcentralus'
|
||||
},
|
||||
{
|
||||
name: 'US (East)',
|
||||
value: 'eastus'
|
||||
},
|
||||
{
|
||||
name: 'US (East 2)',
|
||||
value: 'eastus2'
|
||||
},
|
||||
{
|
||||
name: 'US (North Central)',
|
||||
value: 'northcentralus'
|
||||
},
|
||||
{
|
||||
name: 'US (South Central)',
|
||||
value: 'southcentralus'
|
||||
},
|
||||
{
|
||||
name: 'US (West)',
|
||||
value: 'westus'
|
||||
},
|
||||
{
|
||||
name: 'US (West 2)',
|
||||
value: 'westus2'
|
||||
},
|
||||
];
|
||||
|
||||
export default regions;
|
||||
12
src/data/SpeechRecognizerLanguageAws.js
Normal file
12
src/data/SpeechRecognizerLanguageAws.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const languages = [
|
||||
{ name: 'Australian English', 'code': 'en-AU' },
|
||||
{ name: 'British English', 'code': 'en-GB' },
|
||||
{ name: 'US English', 'code': 'en-US' },
|
||||
{ name: 'French', 'code': 'fr-FR' },
|
||||
{ name: 'Canadian French', 'code': 'fr-CA' },
|
||||
{ name: 'German', 'code': 'de-DE' },
|
||||
{ name: 'Italian', 'code': 'it-IT' },
|
||||
{ name: 'US Spanish', 'code': 'es-US' },
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,4 +1,4 @@
|
||||
export default [
|
||||
const languages = [
|
||||
{ name: 'Afrikaans (South Africa)', code: 'af-ZA', },
|
||||
{ name: 'Albanian (Albania)', code: 'sq-AL', },
|
||||
{ name: 'Amharic (Ethiopia)', code: 'am-ET', },
|
||||
@@ -128,3 +128,5 @@ export default [
|
||||
{ name: 'Vietnamese (Vietnam)', code: 'vi-VN', },
|
||||
{ name: 'Zulu (South Africa)', code: 'zu-ZA', },
|
||||
];
|
||||
|
||||
export default languages;
|
||||
|
||||
492
src/data/SpeechRecognizerLanguageMicrosoft.js
Normal file
492
src/data/SpeechRecognizerLanguageMicrosoft.js
Normal file
@@ -0,0 +1,492 @@
|
||||
const languages = [
|
||||
{
|
||||
name: 'Afrikaans (South Africa)',
|
||||
code: 'af-ZA'
|
||||
},
|
||||
{
|
||||
name: 'Amharic (Ethiopia)',
|
||||
code: 'am-ET'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Algeria)',
|
||||
code: 'ar-DZ'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Bahrain)',
|
||||
code: 'ar-BH'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Egypt)',
|
||||
code: 'ar-EG'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Iraq)',
|
||||
code: 'ar-IQ'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Israel)',
|
||||
code: 'ar-IL'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Jordan)',
|
||||
code: 'ar-JO'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Kuwait)',
|
||||
code: 'ar-KW'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Lebanon)',
|
||||
code: 'ar-LB'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Libya)',
|
||||
code: 'ar-LY'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Morocco)',
|
||||
code: 'ar-MA'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Oman)',
|
||||
code: 'ar-OM'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Qatar)',
|
||||
code: 'ar-QA'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Saudi Arabia)',
|
||||
code: 'ar-SA'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Palestinian Authority)',
|
||||
code: 'ar-PS'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Syria)',
|
||||
code: 'ar-SY'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Tunisia)',
|
||||
code: 'ar-TN'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (United Arab Emirates)',
|
||||
code: 'ar-AE'
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Yemen)',
|
||||
code: 'ar-YE'
|
||||
},
|
||||
{
|
||||
name: 'Bulgarian (Bulgaria)',
|
||||
code: 'bg-BG'
|
||||
},
|
||||
{
|
||||
name: 'Bengali (India)',
|
||||
code: 'bn-IN'
|
||||
},
|
||||
{
|
||||
name: 'Catalan (Spain)',
|
||||
code: 'ca-ES'
|
||||
},
|
||||
{
|
||||
name: 'Chinese (Cantonese, Traditional)',
|
||||
code: 'zh-HK'
|
||||
},
|
||||
{
|
||||
name: 'Chinese (Mandarin, Simplified)',
|
||||
code: 'zh-CN'
|
||||
},
|
||||
{
|
||||
name: 'Chinese (Taiwanese Mandarin)',
|
||||
code: 'zh-TW'
|
||||
},
|
||||
{
|
||||
name: 'Croatian (Croatia)',
|
||||
code: 'hr-HR'
|
||||
},
|
||||
{
|
||||
name: 'Czech (Czech)',
|
||||
code: 'cs-CZ'
|
||||
},
|
||||
{
|
||||
name: 'Danish (Denmark)',
|
||||
code: 'da-DK'
|
||||
},
|
||||
{
|
||||
name: 'Dutch (Netherlands)',
|
||||
code: 'nl-NL'
|
||||
},
|
||||
{
|
||||
name: 'Dutch (Belgium)',
|
||||
code: 'nl-BE'
|
||||
},
|
||||
{
|
||||
name: 'English (Australia)',
|
||||
code: 'en-AU'
|
||||
},
|
||||
{
|
||||
name: 'English (Canada)',
|
||||
code: 'en-CA'
|
||||
},
|
||||
{
|
||||
name: 'English (Ghana)',
|
||||
code: 'en-GH'
|
||||
},
|
||||
{
|
||||
name: 'English (Hong Kong)',
|
||||
code: 'en-HK'
|
||||
},
|
||||
{
|
||||
name: 'English (India)',
|
||||
code: 'en-IN'
|
||||
},
|
||||
{
|
||||
name: 'English (Ireland)',
|
||||
code: 'en-IE'
|
||||
},
|
||||
{
|
||||
name: 'English (Kenya)',
|
||||
code: 'en-KE'
|
||||
},
|
||||
{
|
||||
name: 'English (New Zealand)',
|
||||
code: 'en-NZ'
|
||||
},
|
||||
{
|
||||
name: 'English (Nigeria)',
|
||||
code: 'en-NG'
|
||||
},
|
||||
{
|
||||
name: 'English (Philippines)',
|
||||
code: 'en-PH'
|
||||
},
|
||||
{
|
||||
name: 'English (Singapore)',
|
||||
code: 'en-SG'
|
||||
},
|
||||
{
|
||||
name: 'English (South Africa)',
|
||||
code: 'en-ZA'
|
||||
},
|
||||
{
|
||||
name: 'English (Tanzania)',
|
||||
code: 'en-TZ'
|
||||
},
|
||||
{
|
||||
name: 'English (United Kingdom)',
|
||||
code: 'en-GB'
|
||||
},
|
||||
{
|
||||
name: 'English (United States)',
|
||||
code: 'en-US'
|
||||
},
|
||||
{
|
||||
name: 'Estonian(Estonia)',
|
||||
code: 'et-EE'
|
||||
},
|
||||
{
|
||||
name: 'Filipino (Philippines)',
|
||||
code: 'fil-PH'
|
||||
},
|
||||
{
|
||||
name: 'Finnish (Finland)',
|
||||
code: 'fi-FI'
|
||||
},
|
||||
{
|
||||
name: 'French (Belgium)',
|
||||
code: 'fr-BE'
|
||||
},
|
||||
{
|
||||
name: 'French (Canada)',
|
||||
code: 'fr-CA'
|
||||
},
|
||||
{
|
||||
name: 'French (France)',
|
||||
code: 'fr-FR'
|
||||
},
|
||||
{
|
||||
name: 'French (Switzerland)',
|
||||
code: 'fr-CH'
|
||||
},
|
||||
{
|
||||
name: 'German (Austria)',
|
||||
code: 'de-AT'
|
||||
},
|
||||
{
|
||||
name: 'German (Switzerland)',
|
||||
code: 'de-CH'
|
||||
},
|
||||
{
|
||||
name: 'German (Germany)',
|
||||
code: 'de-DE'
|
||||
},
|
||||
{
|
||||
name: 'Greek (Greece)',
|
||||
code: 'el-GR'
|
||||
},
|
||||
{
|
||||
name: 'Gujarati (Indian)',
|
||||
code: 'gu-IN'
|
||||
},
|
||||
{
|
||||
name: 'Hebrew (Israel)',
|
||||
code: 'he-IL'
|
||||
},
|
||||
{
|
||||
name: 'Hindi (India)',
|
||||
code: 'hi-IN'
|
||||
},
|
||||
{
|
||||
name: 'Hungarian (Hungary)',
|
||||
code: 'hu-HU'
|
||||
},
|
||||
{
|
||||
name: 'Indonesian (Indonesia)',
|
||||
code: 'id-ID'
|
||||
},
|
||||
{
|
||||
name: 'Icelandic (Iceland)',
|
||||
code: 'is-IS'
|
||||
},
|
||||
{
|
||||
name: 'Irish (Ireland)',
|
||||
code: 'ga-IE'
|
||||
},
|
||||
{
|
||||
name: 'Italian (Italy)',
|
||||
code: 'it-IT'
|
||||
},
|
||||
{
|
||||
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'
|
||||
},
|
||||
{
|
||||
name: 'Latvian (Latvia)',
|
||||
code: 'lv-LV'
|
||||
},
|
||||
{
|
||||
name: 'Lao (Laos)',
|
||||
code: 'lo-LA'
|
||||
},
|
||||
{
|
||||
name: 'Lithuanian (Lithuania)',
|
||||
code: 'lt-LT'
|
||||
},
|
||||
{
|
||||
name: 'Malay (Malaysia)',
|
||||
code: 'ms-MY'
|
||||
},
|
||||
{
|
||||
name: 'Macedonian (North Macedonia)',
|
||||
code: 'mk-MK'
|
||||
},
|
||||
{
|
||||
name: 'Maltese (Malta)',
|
||||
code: 'mt-MT'
|
||||
},
|
||||
{
|
||||
name: 'Marathi (India)',
|
||||
code: 'mr-IN'
|
||||
},
|
||||
{
|
||||
name: 'Burmese (Myanmar)',
|
||||
code: 'my-MM'
|
||||
},
|
||||
{
|
||||
name: 'Norwegian (Bokmål, Norway)',
|
||||
code: 'nb-NO'
|
||||
},
|
||||
{
|
||||
name: 'Persian (Iran)',
|
||||
code: 'fa-IR'
|
||||
},
|
||||
{
|
||||
name: 'Polish (Poland)',
|
||||
code: 'pl-PL'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Brazil)',
|
||||
code: 'pt-BR'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Portugal)',
|
||||
code: 'pt-PT'
|
||||
},
|
||||
{
|
||||
name: 'Romanian (Romania)',
|
||||
code: 'ro-RO'
|
||||
},
|
||||
{
|
||||
name: 'Russian (Russia)',
|
||||
code: 'ru-RU'
|
||||
},
|
||||
{
|
||||
name: 'Slovak (Slovakia)',
|
||||
code: 'sk-SK'
|
||||
},
|
||||
{
|
||||
name: 'Slovenian (Slovenia)',
|
||||
code: 'sl-SI'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Argentina)',
|
||||
code: 'es-AR'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Bolivia)',
|
||||
code: 'es-BO'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Chile)',
|
||||
code: 'es-CL'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Colombia)',
|
||||
code: 'es-CO'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Costa Rica)',
|
||||
code: 'es-CR'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Cuba)',
|
||||
code: 'es-CU'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Dominican Republic)',
|
||||
code: 'es-DO'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Ecuador)',
|
||||
code: 'es-EC'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (El Salvador)',
|
||||
code: 'es-SV'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Equatorial Guinea)',
|
||||
code: 'es-GQ'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Guatemala)',
|
||||
code: 'es-GT'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Honduras)',
|
||||
code: 'es-HN'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Mexico)',
|
||||
code: 'es-MX'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Nicaragua)',
|
||||
code: 'es-NI'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Panama)',
|
||||
code: 'es-PA'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Paraguay)',
|
||||
code: 'es-PY'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Peru)',
|
||||
code: 'es-PE'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Puerto Rico)',
|
||||
code: 'es-PR'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Spain)',
|
||||
code: 'es-ES'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Uruguay)',
|
||||
code: 'es-UY'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (USA)',
|
||||
code: 'es-US'
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Venezuela)',
|
||||
code: 'es-VE'
|
||||
},
|
||||
{
|
||||
name: 'Swahili (Kenya)',
|
||||
code: 'sw-KE'
|
||||
},
|
||||
{
|
||||
name: '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'
|
||||
},
|
||||
{
|
||||
name: 'Telugu (India)',
|
||||
code: 'te-IN'
|
||||
},
|
||||
{
|
||||
name: 'Thai (Thailand)',
|
||||
code: 'th-TH'
|
||||
},
|
||||
{
|
||||
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'
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,4 +1,4 @@
|
||||
export default [
|
||||
const languages = [
|
||||
{
|
||||
code: 'arb',
|
||||
name: 'Arabic',
|
||||
@@ -235,3 +235,5 @@ export default [
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default [
|
||||
const languages = [
|
||||
{
|
||||
code: 'ar-XA',
|
||||
name: 'Arabic',
|
||||
@@ -408,3 +408,6 @@ export default [
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export default languages;
|
||||
|
||||
4451
src/data/SpeechSynthesisLanguageMicrosoft.js
Normal file
4451
src/data/SpeechSynthesisLanguageMicrosoft.js
Normal file
File diff suppressed because it is too large
Load Diff
41
src/data/SpeechSynthesisLanguageWellSaid.js
Normal file
41
src/data/SpeechSynthesisLanguageWellSaid.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const languages = [
|
||||
{
|
||||
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)' }
|
||||
],
|
||||
}
|
||||
];
|
||||
|
||||
export default languages;
|
||||
41
src/helpers/handleErrors.js
Normal file
41
src/helpers/handleErrors.js
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
const handleErrors = ({ err, history, dispatch, redirect, setErrorMessage, fallbackMessage, preferFallback }) => {
|
||||
|
||||
const errorMessage = (err.response && err.response.data && err.response.data.msg)
|
||||
|| (preferFallback && fallbackMessage)
|
||||
|| err.message
|
||||
|| fallbackMessage
|
||||
|| 'Something went wrong, please try again.';
|
||||
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please sign in and try again.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (setErrorMessage) {
|
||||
setErrorMessage(errorMessage);
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error(err.response || err);
|
||||
}
|
||||
|
||||
if (redirect) {
|
||||
history.push(redirect);
|
||||
}
|
||||
};
|
||||
|
||||
export default handleErrors;
|
||||
12
src/helpers/timeFormat.js
Normal file
12
src/helpers/timeFormat.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
const timeFormat = seconds => {
|
||||
if (seconds < 60) {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
};
|
||||
|
||||
export default timeFormat;
|
||||
3
src/images/AlertsIcon.svg
Normal file
3
src/images/AlertsIcon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="21" height="19" viewBox="0 0 21 19" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.749 0.737865C11.1939 -0.245954 9.80608 -0.245955 9.25095 0.737863L0.19538 16.7864C-0.359751 17.7702 0.334161 19 1.44442 19H19.5556C20.6658 19 21.3598 17.7702 20.8046 16.7864L11.749 0.737865ZM11.5307 12.8922L12 10.367V7H9V10.367L9.5 12.8922H11.5307ZM11.5307 14.1799C11.2736 13.9958 10.9225 13.9037 10.4774 13.9037C10.0324 13.9037 9.68129 13.9958 9.42415 14.1799C9.17691 14.364 9.05328 14.6234 9.05328 14.9581C9.05328 15.2845 9.17691 15.5397 9.42415 15.7238C9.68129 15.9079 10.0324 16 10.4774 16C10.9225 16 11.2736 15.9079 11.5307 15.7238C11.7878 15.5397 11.9164 15.2845 11.9164 14.9581C11.9164 14.6234 11.7878 14.364 11.5307 14.1799Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 781 B |
|
Before Width: | Height: | Size: 827 B After Width: | Height: | Size: 827 B |
12
src/images/LogoJambong.svg
Normal file
12
src/images/LogoJambong.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="43" viewBox="0 0 128 43">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#231F20" fill-rule="nonzero" d="M3.285 3.867c-.254.454-.352.98-.278 1.496.06.42.23.817.495 1.148.264.332.612.587 1.007.738.485.185 1.017.205 1.514.056.498-.149.933-.457 1.238-.879.305-.42.465-.931.453-1.452-.012-.521-.194-1.024-.518-1.43-.324-.408-.772-.696-1.276-.822-.503-.126-1.034-.082-1.51.125-.477.207-.872.565-1.125 1.02zM3.177 23.187c0 1.132-.642 1.833-1.555 1.833H0v3.36c.338.238 1.25.442 2.163.442 2.873 0 5.306-2.24 5.306-5.126V9.676H3.177v13.511zM25.671 19.722v3.53c-.36.17-.751.262-1.149.271-.833.014-1.655-.193-2.384-.597-.73-.405-1.34-.995-1.773-1.71-.535.726-1.234 1.315-2.04 1.716-.807.402-1.697.604-2.597.591-3.756 0-6.692-3.123-6.692-7.06 0-3.938 2.936-7.06 6.692-7.06 1.422-.023 2.8.499 3.852 1.46V9.673h4.293v8.724c0 1.131.608 1.324 1.183 1.324h.615zm-5.88-3.259c-.003-.557-.148-1.104-.422-1.588-.273-.485-.666-.89-1.14-1.179-.475-.288-1.016-.449-1.57-.467-.554-.018-1.104.107-1.596.364-.353.171-.667.412-.925.708s-.455.642-.577 1.015c-.165.444-.23.918-.192 1.389.037.471.178.929.41 1.34.234.41.554.765.938 1.039.384.273.824.458 1.287.542.463.084.94.065 1.395-.055.455-.121.878-.341 1.24-.644.361-.303.652-.682.852-1.11.2-.429.303-.896.303-1.37l-.003.016zM27.611 23.252V9.674h4.326v1.528c.462-.56 1.04-1.011 1.694-1.322.654-.31 1.368-.474 2.091-.477 1.851 0 3.44 1.04 4.274 2.635.188-.275.416-.593.626-.836.466-.54 1.04-1.011 1.694-1.322.654-.31 1.368-.474 2.092-.477 2.737 0 4.9 2.274 4.9 5.257v8.588h-4.326v-7.871c.012-.278-.033-.556-.132-.817-.098-.26-.248-.497-.441-.697-.193-.2-.425-.359-.68-.466-.256-.108-.531-.162-.809-.159-.61.014-1.192.266-1.621.703-.421.43-.662 1.003-.676 1.604v7.703h-4.326v-7.871c.012-.278-.033-.556-.132-.817-.098-.26-.248-.497-.442-.697-.192-.2-.424-.359-.68-.466-.255-.108-.53-.162-.808-.159-.61.014-1.192.266-1.621.703-.43.438-.672 1.026-.677 1.64v7.67h-4.326z" transform="translate(-656 -16) translate(656 16)"/>
|
||||
<path fill="#231F20" d="M55.95 22.081v1.17h-4.326V0h4.326v10.86c1.052-.959 2.429-1.48 3.85-1.457 3.756 0 6.692 3.122 6.692 7.06 0 3.937-2.936 7.06-6.692 7.06-.9.013-1.79-.19-2.596-.59-.456-.228-.877-.514-1.254-.852zm.209-7.206c-.274.484-.419 1.031-.422 1.588l-.003-.015c0 .473.104.94.303 1.368.2.429.49.808.852 1.11.362.304.785.524 1.24.645.456.12.932.14 1.395.055.463-.084.903-.269 1.287-.542.384-.274.705-.628.937-1.04.234-.41.374-.868.412-1.339.037-.471-.028-.945-.193-1.389-.122-.373-.319-.719-.577-1.015-.258-.296-.572-.537-.925-.708-.492-.257-1.042-.382-1.596-.364-.554.018-1.095.18-1.57.467-.474.288-.867.694-1.14 1.179z" transform="translate(-656 -16) translate(656 16)"/>
|
||||
<path fill="#231F20" fill-rule="nonzero" d="M68.057 16.463c0-4.073 3.004-7.06 7.536-7.06 4.533 0 7.51 2.987 7.51 7.06 0 4.073-2.974 7.06-7.51 7.06s-7.536-2.987-7.536-7.06zm10.747 0c0-.638-.189-1.261-.541-1.792-.353-.53-.854-.943-1.441-1.187-.587-.244-1.232-.308-1.855-.184-.623.125-1.195.432-1.644.883-.45.45-.755 1.025-.879 1.65-.124.626-.06 1.275.183 1.864.243.59.654 1.093 1.182 1.447.528.355 1.15.544 1.784.544.423.004.842-.076 1.233-.237.39-.16.746-.398 1.044-.698.298-.3.533-.658.692-1.051.158-.394.237-.815.23-1.239h.012zM84.668 9.673v13.579h4.326V15.58c.005-.613.247-1.201.676-1.639.43-.437 1.011-.689 1.622-.703.277-.003.552.051.808.159.256.107.488.266.68.466.193.2.344.437.442.697.099.26.144.539.132.817v7.871h4.326V14.66c0-2.983-2.163-5.257-4.9-5.257-.724.003-1.438.166-2.092.477-.654.311-1.232.762-1.694 1.322v-1.53h-4.326z" transform="translate(-656 -16) translate(656 16)"/>
|
||||
<path fill="#DA1C5C" d="M108.45 13.255c-.344-.377-.977-.762-2.123-.722-2.3.081-3.477 2.276-3.208 3.556.214 1.019-.435 2.02-1.45 2.234-1.014.216-2.01-.436-2.225-1.455-.729-3.47 2.017-7.937 6.751-8.104 2.146-.076 3.872.682 5.022 1.94 1.11 1.214 1.596 2.805 1.465 4.255-.26 2.87-2.463 4.395-4.03 5.389l-.072.045c1.532.41 2.903 1.164 4.066 2.184 1.606 1.409 2.763 3.276 3.432 5.333 2.25.886 4.468 2.098 6.487 3.668.28-1.03.958-2.396 1.58-2.91 1.685 3.234 2.775 7.954 3.16 11.524-2.345-1.133-6.738-2.564-9.686-2.982.566-1.1 1.718-2.167 2.61-2.678-1.105-.855-2.285-1.588-3.501-2.204-.132 2.544-1.31 4.774-2.904 6.418-1.813 1.87-4.31 3.135-6.802 3.251-2.319.108-4.506-.512-6.137-1.86-1.663-1.376-2.615-3.408-2.556-5.78.057-2.289.995-4.49 2.728-6.064 1.748-1.587 4.19-2.432 7.064-2.222.983.071 2.002.206 3.041.408-.298-.392-.628-.748-.986-1.062-1.525-1.337-3.668-2.024-6.418-1.326-.064.016-.128.029-.192.038-.961.192-1.828.149-2.165-.381-.416-.654-.437-1.518.518-2.809.637-.913 1.447-1.609 2.22-2.168.6-.434 1.28-.853 1.876-1.22.226-.139.44-.27.633-.393 1.624-1.03 2.216-1.68 2.294-2.54.037-.405-.106-.942-.491-1.363zm4.451 17.537c-1.726-.527-3.445-.842-5.052-.96-1.963-.143-3.365.434-4.274 1.26-.925.839-1.459 2.041-1.492 3.36-.03 1.237.437 2.15 1.19 2.773.783.648 2.005 1.078 3.575 1.004 1.397-.064 3.03-.819 4.286-2.114 1.237-1.276 1.965-2.925 1.836-4.669-.016-.22-.04-.438-.069-.654z" transform="translate(-656 -16) translate(656 16)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
10
src/images/RecentCallsIcon.svg
Normal file
10
src/images/RecentCallsIcon.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="20" height="18" viewBox="0 0 20 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 0H0V3H5V0Z" />
|
||||
<path d="M5 10H0V13H5V10Z" />
|
||||
<path d="M0 5H5V8H0V5Z" />
|
||||
<path d="M5 15H0V18H5V15Z" />
|
||||
<path d="M7 0H20V3H7V0Z" />
|
||||
<path d="M20 10H7V13H20V10Z" />
|
||||
<path d="M7 5H20V8H7V5Z" />
|
||||
<path d="M20 15H7V18H20V15Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
6
src/images/SpeechIcon.svg
Normal file
6
src/images/SpeechIcon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C11.1712 18 12.281 17.749 13.2809 17.2987C13.5009 17.1997 13.7493 17.1835 13.9802 17.2531C15.0582 17.5782 16.1613 17.7632 17.0663 17.8677C16.7653 17.2512 16.4715 16.4797 16.2688 15.5535C16.2071 15.272 16.2699 14.9776 16.4411 14.7457C17.4211 13.4182 18 11.7781 18 10C18 5.58172 14.4183 2 10 2ZM19 19C19 20 18.9997 20 18.9997 20L18.9957 20L18.9875 20L18.9597 19.9998C18.9361 19.9995 18.9025 19.9991 18.8597 19.9983C18.7741 19.9966 18.6512 19.9933 18.4967 19.9868C18.1878 19.9738 17.7507 19.9479 17.2293 19.8965C16.2798 19.8028 15.0295 19.6221 13.7566 19.2701C12.5956 19.741 11.3269 20 10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10C20 12.0489 19.3828 13.9562 18.3244 15.5429C18.5644 16.4315 18.897 17.105 19.1771 17.5645C19.3383 17.8291 19.482 18.0224 19.5801 18.1443C19.6291 18.2052 19.6665 18.248 19.6889 18.2729C19.7001 18.2853 19.7076 18.2932 19.7107 18.2965L19.709 18.2948L19.7081 18.2939C19.7096 18.2954 19.7105 18.2963 19.7119 18.2977C19.9941 18.5837 20.0778 19.0111 19.9239 19.3827C19.7691 19.7564 19.4041 20 18.9997 20L19 19ZM19.7119 18.2977V18.2977Z" />
|
||||
<path d="M6 5H14V7H6V5Z" />
|
||||
<path d="M4 9H16V11H4V9Z" />
|
||||
<path d="M6 13H14V15H6V13Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -38,6 +38,7 @@ body {
|
||||
font-family: WorkSans, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import { NotificationProvider } from './contexts/NotificationContext';
|
||||
import { ModalProvider } from './contexts/ModalContext';
|
||||
import { ShowMsTeamsProvider } from './contexts/ShowMsTeamsContext';
|
||||
import { ServiceProvider } from './contexts/ServiceProviderContext';
|
||||
import App from './App';
|
||||
|
||||
import "antd/dist/antd.css";
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.render(
|
||||
<NotificationProvider>
|
||||
<ModalProvider>
|
||||
<ShowMsTeamsProvider>
|
||||
<App />
|
||||
<ServiceProvider>
|
||||
<App />
|
||||
</ServiceProvider>
|
||||
</ShowMsTeamsProvider>
|
||||
</ModalProvider>
|
||||
</NotificationProvider>,
|
||||
|
||||
Reference in New Issue
Block a user