Compare commits

...

64 Commits

Author SHA1 Message Date
Dave Horton
38d26dddc8 new Microsoft languages added in 1.21.0 2022-05-12 19:34:49 -04:00
Conner Luzier
c0d531c63f removed radio buttons, replaced with dropdown (#53)
* removed radio buttons, replaced with dropdown

* disabled button if no vendor, moved checkboxes up
2022-05-06 20:37:05 -04:00
Kieron Lawson
420080ba84 Updater FQDN regex to accept numeric characters (#42) 2022-04-27 14:39:06 -04:00
Conner Luzier
c40fb9cc01 fix add account error message (#51)
Co-authored-by: Conner Luzier <connerluzier@outlook.com>
2022-04-27 14:35:40 -04:00
Dave Horton
40143ae79d update package-lock.json 2022-04-22 12:39:50 -04:00
Conner Luzier
536b183535 regions field added to aws, default to us-east-1 (#50)
* regions field added to aws, default to us-east-1

* added aws_region to post request

* no default setting on aws region

* aws_region required before saving

* no longer showing default on dropdown

Co-authored-by: Conner Luzier <connerluzier@outlook.com>
2022-04-21 13:33:04 -04:00
Dave Horton
bf88a27330 add data file for aws regions 2022-04-18 10:10:50 -04:00
Dave Horton
831450306d docs 2022-04-14 14:18:28 -04:00
Dave Horton
c8d1034dc9 testing instructions 2022-04-14 14:14:44 -04:00
dependabot[bot]
37af9522aa Bump node-forge from 1.2.1 to 1.3.1 (#46)
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.2.1 to 1.3.1.
- [Release notes](https://github.com/digitalbazaar/forge/releases)
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.2.1...v1.3.1)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-06 08:22:25 -04:00
Dave Horton
6bb81a499b bump version 2022-04-06 08:19:56 -04:00
Dave Horton
58f97dcfb2 show trace_id in recent calls detail display 2022-03-28 19:35:43 -04:00
Dave Horton
23a067b6dd bump version 2022-03-08 20:20:46 -05:00
Andrew
92db20965e moved building to Dockefile instead of entrypoint (#39)
* moved building to Dockefile instead of entrypoint

* moved enviroment variable to gloabl

* updated to use process env during build to allow use of window global var

* added new line

* updated constants for window.jambonz

* removed NODE_ENV in favor of just window.JAMBONZ

* removed unrequired and change const per pull request
2022-02-23 07:40:05 -05:00
akirilyuk
8f8d635bd3 fix all security vulnerabilities (#41)
Co-authored-by: akirilyuk <a.kirilyuk@cognigy.com>
2022-02-17 07:33:00 -05:00
dependabot[bot]
b075028b7b Bump follow-redirects from 1.14.7 to 1.14.8 (#40)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-14 21:25:01 -05:00
Dave Horton
b5f2e5fc25 bump version 2022-02-09 15:43:14 -05:00
Dave Horton
6390cc6b81 add missing Azure regions 2022-02-03 08:59:47 -05:00
Dave Horton
d7db92f0c7 update version to 0.7.2 2022-01-31 07:32:10 -05:00
Dave Horton
f5201d2d69 Feature/wellsaid tts (#38)
* initial changes to support WellSaid TTS

* disable stt choice for WellSaid since they dont provide
2022-01-27 08:07:22 -05:00
akirilyuk
128ca045b0 update depenecies and fix security vulnerabilities (#37)
Co-authored-by: akirilyuk <a.kirilyuk@cognigy.com>
2022-01-14 08:00:35 -05:00
Dave Horton
3403996946 bump version 2021-12-21 09:43:00 -05:00
Dave Horton
87dbb461e0 added docker publish 2021-12-13 14:18:43 -05:00
Dave Horton
9bce9c5510 version bump 2021-12-13 09:55:03 -05:00
Brandon Lee Kitajchuk
2db5f26dbf Subspace (#35)
* pushing up what ive got from laptop

* beginnings of a UI for setting up subspace on a jambonz account

* enable the env flag and move content to right place

* changes to support subspace (thanks to nimbleape)

* fix column names

* Implement SIP realm selection for Subspace API calls

* Hook up Subspace disable method

* Finish up Subspace API handling

Co-authored-by: Dan Jenkins <dan@nimblea.pe>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2021-12-06 17:58:42 -05:00
Dave Horton
bfc7cc971c version bump 2021-12-02 19:34:28 -05:00
Dave Horton
35f353c905 bugfix: azure tts needs to be referenced by ShortName (#33) 2021-11-19 14:31:30 -05:00
Brandon Lee Kitajchuk
922d664bf8 Add Microsoft vendor to Jambonz Webapp (#32)
* Add Microsoft vendor to Speech Form

Add Microsoft vendor to Application Form

Clean up UI for Speech and Application forms

* Remove Sbcs from SpeechServiceAddEdit Form
2021-11-17 20:49:31 -05:00
Dave Horton
ff4d6b6e11 version bump 2021-11-03 13:53:11 -04:00
Dave Horton
70387ff4f1 bump version 2021-10-21 13:09:07 -04:00
Dave Horton
7a4c583345 bump version 2021-10-21 13:01:30 -04:00
Brandon Lee Kitajchuk
d54fbc4782 Update IP whitelist tooltips for carrier form (#29) 2021-10-21 11:41:19 -04:00
Brandon Lee Kitajchuk
14dd1319d9 SMS for Carrier Form (#28) 2021-10-17 12:29:26 -04:00
Brandon Lee Kitajchuk
8538d40696 Add account sid selector to speech form (#24)
* Add account sid selector to speech form

* Add account selector to speech service list view

* Add account selector to carrier list view

* Remove client-side check for existing speech services
2021-09-01 12:47:32 -04:00
Brandon Lee Kitajchuk
eda1fa0dc4 Chore/carrier apps accounts (#23)
* Scope accounts and applications to current service provider for Phone Numbers

* Scope accounts to current service provider when adding or editing an Application

* Implement account and application logic for add or edit Carrier form

* Implement delete action for service providers
2021-08-28 09:20:49 -04:00
Brandon Lee Kitajchuk
0174315a68 Only show accounts for current service provider when adding a new application (#21) 2021-08-26 12:25:26 -04:00
Brandon Lee Kitajchuk
b86bf0c403 User service provider context when adding an account (#20) 2021-08-26 12:21:32 -04:00
Brandon Lee Kitajchuk
3fc1c800ac Add queue event webhook to accounts list (#19) 2021-08-25 19:30:52 -04:00
Brandon Lee Kitajchuk
ee4483288d Adding pcap file download button to RecentCalls view (#17) 2021-08-01 21:02:01 -04:00
Dave Horton
d3f1dbf332 LICENSE 2021-07-21 12:37:41 -04:00
Brandon Lee Kitajchuk
5141989bb5 Add status UI to speech list (#16) 2021-06-22 10:22:51 -04:00
Dave Horton
35889ba122 fix loop in phone numbers page 2021-06-21 13:33:43 -04:00
Dave Horton
09b0bc8dde query entities by current service provider id on phone number list page 2021-06-18 13:33:43 -04:00
Dave Horton
4f1b928f8c another instance of querying gateways without voip_carrier_sid 2021-06-18 12:13:05 -04:00
Dave Horton
94c0fc88c1 bugfix: GET /SipGateways must include query arg for voip_carrier_sid 2021-06-18 10:42:05 -04:00
Dave Horton
b5a559bd08 merge of features from hosted fork (#15)
merge of recent features from hosted system
2021-06-17 22:01:04 -04:00
Dave Horton
24cb269379 Merge pull request #12 from mattddowney/master
use python3 package
2021-05-07 12:17:26 -04:00
mattddowney
c73ca9f46a use python3 package 2021-05-07 10:13:46 -06:00
Dave Horton
25a93edeac Merge pull request #10 from jambonz/ip_cidr
allow cidr ip range in sip gateway
2021-03-11 07:20:28 -05:00
Michal Tesar
7e2488a9c3 allow cidr ip range in sip gateway 2021-03-11 12:08:02 +00:00
Dave Horton
42872e9878 Merge pull request #9 from jambonz/tesarm-allow-alphanumeric-fqdn
fix - allow alphanumeric FQDN
2021-03-04 07:49:02 -05:00
Michal Tesar
809e1ae30f fix - allow alphanumeric FQDN 2021-03-04 12:34:15 +00:00
Dave Horton
04c8d05266 Merge pull request #8 from jambonz/implement-aws-speech-recognition
Implement AWS Speech Recognition
2021-01-31 14:21:07 -05:00
James Nuanez
e3d384158f Implement AWS Speech Recognition 2021-01-30 14:49:57 -08:00
Dave Horton
01c0aa321e fix react base url composition in entrypoint.sh 2021-01-12 09:44:39 -05:00
Dave Horton
30bcf9414f Merge pull request #7 from jambonz/sip_trunk_register_2
SIP Trunk registration: separate user/pass from sip realm
2021-01-12 07:51:13 -05:00
James Nuanez
fb2880b465 SIP Trunk registration: separate user/pass from sip realm 2021-01-11 19:55:15 -08:00
Dave Horton
f5f92e58e1 Merge pull request #5 from radicaldrew/master
created Dockerfile and entrypoint with default http port
2021-01-08 21:16:36 -05:00
Dave Horton
33734c91f6 Merge pull request #6 from jambonz/dependabot/npm_and_yarn/axios-0.21.1
Bump axios from 0.19.2 to 0.21.1
2021-01-05 20:00:31 -05:00
dependabot[bot]
478949ef73 Bump axios from 0.19.2 to 0.21.1
Bumps [axios](https://github.com/axios/axios) from 0.19.2 to 0.21.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.21.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.19.2...v0.21.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-06 00:52:47 +00:00
Andrew Karp
3ede7d5077 created Dockerfile and entrypoint with default http port 2020-12-28 12:02:26 +02:00
Dave Horton
d36a291543 Merge pull request #4 from jambonz/sip_trunk_register
SIP trunk registration
2020-12-12 12:17:34 -05:00
James Nuanez
630b555fe7 Add support for SIP trunks that require registration 2020-12-12 08:42:23 -08:00
James Nuanez
f16063ba44 Code clean-up 2020-12-12 08:06:11 -08:00
73 changed files with 34237 additions and 11354 deletions

3
.env
View File

@@ -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

View File

@@ -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
View 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
View 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
View 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.

View File

@@ -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
View 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

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
},

View File

@@ -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>

View 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;

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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')}`,

View File

@@ -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>
);
};

View File

@@ -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];

View File

@@ -70,6 +70,7 @@ const TableMenu = props => (
selected={props.open}
disabled={props.disabled}
onClick={e => {
e.preventDefault();
e.stopPropagation();
props.handleMenuOpen(props.sid);
}}

View File

@@ -51,6 +51,7 @@ const StyledLink = styled(FilteredLink)`
const Tooltip = styled.span`
display: none;
color: #767676;
a:focus > &,
a:hover > & {
display: inline;

View 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;

View File

@@ -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}

View 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;

View File

@@ -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;

View 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);

View File

@@ -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;

View 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);

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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&nbsp;</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 &mdash; 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>
);
};

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View File

@@ -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')}`,

View File

@@ -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}

View File

@@ -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"
/>
)}
</>
)
);
};

View File

@@ -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 &mdash; I'll complete later
</Link>
)}
</Form>
);
};
export default SipTrunkForm;

View 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;

View File

@@ -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');

View File

@@ -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')}`,

View File

@@ -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}

View 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;

View File

@@ -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"

View File

@@ -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;

View 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;

View File

@@ -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')}`,

View File

@@ -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')}`,

View 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;

View File

@@ -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;

View 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;

View 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;

View File

@@ -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>

View File

@@ -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
View 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;

View 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>
);
}

View File

@@ -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
View 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;

View 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;

View 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;

View File

@@ -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;

View 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;

View File

@@ -1,4 +1,4 @@
export default [
const languages = [
{
code: 'arb',
name: 'Arabic',
@@ -235,3 +235,5 @@ export default [
],
},
];
export default languages;

View File

@@ -1,4 +1,4 @@
export default [
const languages = [
{
code: 'ar-XA',
name: 'Arabic',
@@ -408,3 +408,6 @@ export default [
],
},
];
export default languages;

File diff suppressed because it is too large Load Diff

View 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;

View 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
View 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;

View 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

View File

Before

Width:  |  Height:  |  Size: 827 B

After

Width:  |  Height:  |  Size: 827 B

View 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

View 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

View 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

View File

@@ -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;
}

View File

@@ -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>,