Compare commits
2 Commits
main
...
v0.7.7-rc5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b9d206f4f | ||
|
|
68d88decb5 |
@@ -1,17 +0,0 @@
|
||||
# Node / env / dist
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
|
||||
# Jambonz
|
||||
.dockerignore
|
||||
.git*
|
||||
.husky
|
||||
.vscode
|
||||
docs
|
||||
/public/fonts
|
||||
server
|
||||
CODEOWNERS
|
||||
Dockerfile
|
||||
LICENSE
|
||||
README.md
|
||||
38
.env
@@ -1,36 +1,2 @@
|
||||
# VITE_API_BASE_URL=http://127.0.0.1:3000/v1
|
||||
#VITE_DEV_BASE_URL=http://127.0.0.1:3000/v1
|
||||
|
||||
## enables choosing units and lisenced account call limits
|
||||
# VITE_APP_ENABLE_ACCOUNT_LIMITS_ALL=true
|
||||
|
||||
# disables controls for default application routing to carrier for SP and account level users
|
||||
#VITE_APP_DISABLE_DEFAULT_TRUNK_ROUTING=true
|
||||
## disables Least cost routing feature
|
||||
#VITE_APP_LCR_DISABLED=true
|
||||
## disables Jaeger Tracing feature
|
||||
#VITE_APP_JAEGER_TRACING_DISABLED=true
|
||||
## enable record All Calls feature
|
||||
#VITE_APP_DISABLE_CALL_RECORDING=true
|
||||
## enable Forgot password
|
||||
#VITE_APP_ENABLE_FORGOT_PASSWORD=true
|
||||
## enable hosted system
|
||||
#VITE_APP_ENABLE_HOSTED_SYSTEM=true
|
||||
## Google Client ID
|
||||
#VITE_APP_GOOGLE_CLIENT_ID=
|
||||
## Github Client ID
|
||||
#VITE_APP_GITHUB_CLIENT_ID=
|
||||
## Default jambonz service provider SID
|
||||
#VITE_APP_DEFAULT_SERVICE_PROVIDER_SID=
|
||||
## Base url for jambomz webapp
|
||||
#VITE_APP_BASE_URL="http://jambonz.one"
|
||||
## Strip publishable key
|
||||
#VITE_APP_STRIPE_PUBLISHABLE_KEY="pk_test_EChRaX9Tjk8csZZVSeoGqNvu00lsJzjaU1"
|
||||
## ignore some specific speech vendors, defined by ADDITIONAL_SPEECH_VENDORS constant
|
||||
# VITE_APP_DISABLE_ADDITIONAL_SPEECH_VENDORS=true
|
||||
## AWS region for enabling Recent Call Feature server logs
|
||||
#VITE_APP_AWS_REGION=us-west-2
|
||||
## enable lazy loading for phone numbers (improves performance when managing large quantities)
|
||||
# VITE_APP_ENABLE_PHONE_NUMBER_LAZY_LOAD=true
|
||||
# hides controlls to add Carrier and Phone number from non Admin/SP Users (also need to set flag on API server to block API calls)
|
||||
#VITE_ADMIN_CARRIER=1
|
||||
REACT_APP_API_BASE_URL=http://127.0.0.1:3002/v1
|
||||
GENERATE_SOURCEMAP=false
|
||||
13
.eslintrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "react-app",
|
||||
"rules": {
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
48
.github/workflows/docker-publish.yml
vendored
@@ -2,22 +2,38 @@ 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:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: prepare tag
|
||||
id: prepare_tag
|
||||
- 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=jambonz/webapp
|
||||
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,')
|
||||
@@ -31,21 +47,5 @@ jobs:
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
|
||||
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
|
||||
build-args: |
|
||||
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
|
||||
GITHUB_REF=$GITHUB_REF
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
44
.github/workflows/pr-checks.yml
vendored
@@ -1,44 +0,0 @@
|
||||
name: PR Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
jobs:
|
||||
pr-checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cache node_modules
|
||||
id: node-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Cache cypress binary
|
||||
id: cypress-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /home/runner/.cache/Cypress
|
||||
key: cypress-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install node_modules
|
||||
if: steps.node-cache.outputs.cache-hit != 'true'
|
||||
run: npm install
|
||||
|
||||
- name: Install cypress
|
||||
if: steps.cypress-cache.outputs.cache-hit != 'true'
|
||||
run: npx cypress install
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
npm run format
|
||||
npm run lint
|
||||
npm run test
|
||||
npm run build
|
||||
45
.gitignore
vendored
@@ -1,30 +1,21 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Node / env / dist
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
yarn.lock
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Jambonz
|
||||
/public/fonts
|
||||
|
||||
# Cypress
|
||||
cypress/videos
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# lint, prettier etc...
|
||||
npx lint-staged
|
||||
|
||||
# run tests
|
||||
# npm run test
|
||||
|
||||
# run build -- tsc
|
||||
# npm run build
|
||||
|
||||
# run tsc
|
||||
npx tsc
|
||||
@@ -1,27 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Node / env / dist
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
yarn.lock
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Jambonz
|
||||
/public/fonts
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
7
.vscode/extensions.json
vendored
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-vscode.vscode-typescript-next"
|
||||
]
|
||||
}
|
||||
9
.vscode/settings.json
vendored
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"editor.detectIndentation": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"prettier.configPath": ".prettierrc.json",
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"typescript.preferences.quoteStyle": "double"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
# see https://bit.ly/3t2LLMR for CODEOWNERS syntax
|
||||
|
||||
# all files owned by:
|
||||
* @davehorton
|
||||
@@ -1,17 +1,18 @@
|
||||
FROM node:20-alpine AS builder
|
||||
FROM node:18.8.0-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:20-alpine AS webapp
|
||||
FROM node:18.6.0-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/dist ./dist
|
||||
COPY --from=builder /opt/app/build ./build
|
||||
COPY ./entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
4
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2024 FirstFive8, Inc.
|
||||
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
|
||||
@@ -18,4 +18,4 @@ 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.
|
||||
SOFTWARE.
|
||||
75
README.md
@@ -1,66 +1,37 @@
|
||||
<p align="center">
|
||||
<a href="https://jambonz.org">
|
||||
<img src="./public/icon192.png" height="128">
|
||||
<h1 align="center">jambonz</h1>
|
||||
</a>
|
||||
</p>
|
||||
# Jambonz Web Application
|
||||
|
||||
<p align="center">
|
||||
<a aria-label="GitHub CI" href="https://github.com/jambonz/jambonz-webapp/actions/workflows/main.yml">
|
||||
<img alt="" src="https://github.com/jambonz/jambonz-webapp/actions/workflows/main.yml/badge.svg">
|
||||
</a>
|
||||
</p>
|
||||
## Deploy to Production
|
||||
|
||||
> A simple provisioning webapp for jambonz
|
||||
1. Install `pm2` globally on the server hosting this application.
|
||||
2. Copy `.env` to `.env.local`
|
||||
3. In `.env.local`, replace `[ip]:[port]` with the API's IP and port
|
||||
4. Run `npm run deploy`
|
||||
5. Access the web app via port 3001
|
||||
|
||||
## OSS Developers
|
||||
NOTE: Here is what `npm run deploy` does:
|
||||
|
||||
If you're here to contribute to the jambonz web app source code
|
||||
you can view our [contributor readme](./docs/contributors.md).
|
||||
|
||||
## Webapp deployment
|
||||
|
||||
### Deploy to production
|
||||
|
||||
1. Install `pm2` globally on the server hosting this application.
|
||||
2. Copy `.env` to `.env.local`
|
||||
3. In `.env.local`, replace `[ip]:[port]` with the API's IP and port
|
||||
4. Run `npm run deploy`
|
||||
5. Access the web app via port 3001
|
||||
|
||||
_NOTE: Here is what `npm run deploy` does:_
|
||||
|
||||
- Install all dependencies (`npm i`)
|
||||
- Build the production React application (`npm run build`)
|
||||
- Launch the app with pm2 (`pm2 start npm --name "jambonz-webapp" -- run serve`)
|
||||
- Install all dependencies (`npm i`)
|
||||
- Build the production React application (`npm run build`)
|
||||
- Launch the app with pm2 (`pm2 start npm --name "jambonz-webapp" -- run serve`)
|
||||
|
||||
Alternatively, you can serve the app manually (without pm2) with `npm run serve`.
|
||||
|
||||
### Update production
|
||||
## Updates
|
||||
|
||||
If there is an update to this code base, you can update the code without re-deploying.
|
||||
|
||||
1. run `git pull origin main --rebase`
|
||||
2. run `npm i`
|
||||
3. run `npm run build`
|
||||
1. run `git pull`
|
||||
2. run `npm run build`
|
||||
|
||||
### With docker
|
||||
## Development
|
||||
|
||||
You can pull the public docker image for the web app:
|
||||
### 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.
|
||||
|
||||
```sh
|
||||
docker pull ghcr.io/jambonz/webapp:latest
|
||||
```
|
||||
### 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.
|
||||
|
||||
You can run the docker image for the webapp and expose the serve port to the host:
|
||||
|
||||
```sh
|
||||
docker run --publish=3001:3001 ghcr.io/jambonz/webapp:latest
|
||||
```
|
||||
|
||||
You can build and run the docker image from the source, for example:
|
||||
|
||||
```sh
|
||||
docker build . --tag jambonz-webapp:local
|
||||
docker run --publish=3001:3001 jambonz-webapp:local
|
||||
```
|
||||
1. Copy `.env` to `.env.local`
|
||||
2. In `.env.local`, replace `[ip]:[port]` with the API's IP and port
|
||||
3. `npm start`
|
||||
4. Access the web app via http://localhost:3001
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
video: false,
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "vite",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "default account",
|
||||
"account_sid": "9351f46a-678c-43f5-b8a6-d4eb58d131af"
|
||||
},
|
||||
{
|
||||
"name": "custom account",
|
||||
"account_sid": "04e36b64-a4e5-4221-be7e-db80da39234d"
|
||||
}
|
||||
]
|
||||
@@ -1,10 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "dial time",
|
||||
"application_sid": "4ca2fb6a-8636-4f2e-96ff-8966c5e26f8e"
|
||||
},
|
||||
{
|
||||
"name": "hello world",
|
||||
"application_sid": "7087fe50-8acb-4f3b-b820-97b573723aab"
|
||||
}
|
||||
]
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3NpZCI6IjFjNTc4MWQyLTY5MGItNDIwYy1iZDUzLTVkN2Y1NjMwMDVjOCIsInNjb3BlIjoiYWRtaW4iLCJmb3JjZV9jaGFuZ2UiOnRydWUsInBlcm1pc3Npb25zIjpbIlBST1ZJU0lPTl9VU0VSUyIsIlBST1ZJU0lPTl9TRVJWSUNFUyIsIlZJRVdfT05MWSJdLCJpYXQiOjE2NjY3OTgzMTEsImV4cCI6MTY2NjgwMTkxMX0.ZV3KnRit8WGpipfiiMAZ2AVLQ25csWje1-K6hdqxktE",
|
||||
"user_sid": "78131ad5-f041-4d5d-821c-47b2d8c6d015",
|
||||
"force_change": false,
|
||||
"scope": "admin",
|
||||
"permissions": ["VIEW_ONLY", "PROVISION_SERVICES", "PROVISION_USERS"]
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
export {};
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Components App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
<div id="modal"></div>
|
||||
<div id="toast"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,65 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/component.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import jambonz global styles
|
||||
import "src/styles/index.scss";
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import "./commands";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { mount } from "cypress/react18";
|
||||
|
||||
import { TestProvider } from "src/test";
|
||||
|
||||
import type { TestProviderProps } from "src/test";
|
||||
import type { MountOptions, MountReturn } from "cypress/react";
|
||||
|
||||
// Augment the Cypress namespace to include type definitions for
|
||||
// your custom command.
|
||||
// Alternatively, can be defined in cypress/support/component.d.ts
|
||||
declare global {
|
||||
// Disabling, but: https://typescript-eslint.io/rules/no-namespace/...
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
mount: typeof mount;
|
||||
/**
|
||||
* Mounts a React node
|
||||
* @param component React Node to mount
|
||||
* @param options Additional options to pass into mount
|
||||
*/
|
||||
mountTestProvider(
|
||||
component: React.ReactNode,
|
||||
options?: MountOptions & { authProps?: TestProviderProps["authProps"] },
|
||||
): Cypress.Chainable<MountReturn>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("mount", mount);
|
||||
|
||||
// This gives us access to dispatch inside of our test wrappers...
|
||||
Cypress.Commands.add("mountTestProvider", (component, options = {}) => {
|
||||
const { authProps, ...mountOptions } = options;
|
||||
const wrapper = (
|
||||
<TestProvider authProps={authProps}>{component}</TestProvider>
|
||||
);
|
||||
return mount(wrapper, mountOptions);
|
||||
});
|
||||
|
||||
// Example use:
|
||||
// cy.mount(<MyComponent />)
|
||||
@@ -1,197 +0,0 @@
|
||||
<p align="center">
|
||||
<a href="https://jambonz.org">
|
||||
<img src="../public/icon192.png" height="128">
|
||||
<h1 align="center">jambonz</h1>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a aria-label="GitHub CI" href="https://github.com/jambonz/jambonz-webapp/actions/workflows/main.yml">
|
||||
<img alt="" src="https://github.com/jambonz/jambonz-webapp/actions/workflows/main.yml/badge.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
> Contributing to the web app source code
|
||||
|
||||
## :rocket: Getting started
|
||||
|
||||
In order to run the web app you'll need your local environment setup which you can do
|
||||
following instructions in our [environment readme](./environment.md).
|
||||
|
||||
Once your environment is setup you can fork or clone this repo. To start the web app
|
||||
just run `npm install` and then `npm start`.
|
||||
|
||||
## :pancakes: Dev stack
|
||||
|
||||
We're using [vite](https://vitejs.dev/) for development and
|
||||
the main application is [react](https://reactjs.org/docs/getting-started.html)
|
||||
with [typescript](https://www.typescriptlang.org/),
|
||||
[prettier](https://prettier.io/), [eslint](https://eslint.org/),
|
||||
[husky](https://typicode.github.io/husky/#/)
|
||||
and [lint-staged](https://www.npmjs.com/package/lint-staged).
|
||||
For testing we're using [cypress](https://docs.cypress.io/guides/component-testing/writing-your-first-component-test).
|
||||
|
||||
## :lock: Auth middleware
|
||||
|
||||
We have auth middleware that was initially based on this [useAuth](https://usehooks.com/useAuth/)
|
||||
example but has been typed and modified to include a `RequireAuth` component for wrapping internal Routes.
|
||||
The main hook you'll use here is `useAuth`. This hook provides the following:
|
||||
|
||||
- `token`
|
||||
- `signin(user, pass)`
|
||||
- `signout()`
|
||||
- `authorized`
|
||||
|
||||
### A note on our ACL implementation
|
||||
|
||||
We have some simple ACL utilities for managing access to UI/routes based on conditions.
|
||||
There is a basic `AccessControl` component and a handy `withAccessControl`
|
||||
HOC for route containers with redirect. There is also a `useAccessControl` hook for
|
||||
use at the component-level.
|
||||
|
||||
## :joystick: Application state
|
||||
|
||||
The state for the application has two parts: the local state and the remote server state.
|
||||
The server state is the source of truth. We keep only the minimal amount of local state
|
||||
necessary: the current logged in user, the list of service providers and the actively selected
|
||||
current service provider. We also use local state for a basic permissions matrix and for the
|
||||
toast notifications.
|
||||
|
||||
- `useSelectState(key)`: returns just the piece of state desired
|
||||
- `useDispatch()`: returns global dispatch method
|
||||
- `useAccessControl(acl)`: returns true/false for select ACL permissions
|
||||
- `useFeatureFlag(flag)`: returns true/false for available feature flags
|
||||
- `withSelectState([...keys])(Component)`: like redux connect it maps state to props
|
||||
- `toastError(msg)`: helper for dispatching error toasts
|
||||
- `toastSuccess(msg)`: helper for dispatching success toasts
|
||||
|
||||
## :wales: API implementation
|
||||
|
||||
We have a centralized API implementation that uses our normalized `fetchTransport` method
|
||||
under the hood. We have `use` hooks for general `GET` fetching that return the `data` fetched,
|
||||
a `refetcher` function that, when called, will update the data in the hook and therefore your
|
||||
component will render the new data, and a possible `error` if the fetch failed. The general
|
||||
consensus on when to use the hooks vs using a `getFetch` directly are dictated by whether the
|
||||
API response data needs to be refetched locally based on some user action, such as deleting
|
||||
an item from a list. In that case use the hooks, otherwise a `getFetch` pattern should work.
|
||||
|
||||
The hooks are:
|
||||
|
||||
- `useApiData(path)`: returns `[data, refetcher, error]`
|
||||
- `useServiceProviderData(path)`: returns `[data, refetcher, error]`
|
||||
|
||||
All API requests are piped through the `fetchTransport` method which receives a generic type
|
||||
and returns it as the type of response data resolved. Any `POST`, `PUT` or `DELETE` calls should
|
||||
have a wrapper method that calls our more generic methods under the hood, which are:
|
||||
|
||||
- `getFetch(url)`
|
||||
- `postFetch(url, payload)`
|
||||
- `putFetch(url, payload)`
|
||||
- `deleteFetch(url)`
|
||||
- `getBlob(url)`
|
||||
|
||||
Example of wrapper API methods to `:POST` and `:PUT` for the `Account` type:
|
||||
|
||||
```ts
|
||||
export const postAccount = (payload: Partial<Account>) => {
|
||||
return postFetch<SidResponse, Partial<Account>>(API_ACCOUNTS, payload);
|
||||
};
|
||||
|
||||
export const putAccount = (sid: string, payload: Partial<Account>) => {
|
||||
return putFetch<EmptyResponse, Partial<Account>>(
|
||||
`${API_ACCOUNTS}/${sid}`,
|
||||
payload,
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Local dev mock API server
|
||||
|
||||
There are two views that rely on call detail records (CDRs) that don't exist in the local
|
||||
developer db when running the docker stack. For these views we have a local node express
|
||||
server that replicates functional parity of the backend APIs in question so you can work
|
||||
on the UI. The views are `Recent Calls` and `Alerts`. They are simple table views with
|
||||
filter and pagination functionalities. You can view the implementation at `server/dev.server.ts`.
|
||||
The approach is to replicate the pattern of how test data is seeded for the API server and
|
||||
add the filtering on top of it with simple JavaScript functions. To run the dev server:
|
||||
|
||||
```shell
|
||||
npm run dev:server
|
||||
```
|
||||
|
||||
## :file_folder: Vendor data modules
|
||||
|
||||
Large data modules are used for menu options on the Applications and Speech Services
|
||||
forms. These modules are loaded lazily and set to local state in the context in which
|
||||
they are used. You can find the data modules and their type definitions in the `src/vendor`
|
||||
directory.
|
||||
|
||||
## :sunrise: Component composition
|
||||
|
||||
All components that are used as Route elements are considered `containers`.
|
||||
Containers are organized by `login` and `internal`, the latter of which requires
|
||||
the user to be authorized via our auth middleware layer. Reusable components are
|
||||
small with specific pieces of functionality and their own local state. We have
|
||||
plenty of examples including `toast`, `modal` and so forth. You should review some
|
||||
of each category of component to get an idea of how the patterns are put into practice.
|
||||
|
||||
## :art: UI and styling
|
||||
|
||||
We have a UI design system called [@jambonz/ui-kit](https://github.com/jambonz/jambonz-ui).
|
||||
It's public on `npm` and is being used for this project. It's still small and simple
|
||||
but provides the foundational package content for building jambonz UIs. You can view
|
||||
the storybook for it [here](https://jambonz-ui.vercel.app/) as well as view the docs
|
||||
for it [here](https://www.jambonz.org/docs/@jambonz/ui-kit/).
|
||||
|
||||
### A note on styles
|
||||
|
||||
While we use [sass](https://sass-lang.com/) with `scss` syntax it should be stated that the
|
||||
primary objective is to simply write generally pure `css`. We take advantage of a few nice
|
||||
features of `sass` like nesting for [BEM](http://getbem.com/naming/) module style etc. We
|
||||
also take advantage of loading the source `sass` from the UI library. Here's an example of
|
||||
the `BEM` style we use:
|
||||
|
||||
```scss
|
||||
.example {
|
||||
// This is the block
|
||||
|
||||
&--modifier {
|
||||
// This is a modifier of the block
|
||||
}
|
||||
|
||||
&__item {
|
||||
// This is an element
|
||||
|
||||
&--modifer {
|
||||
// This a modifer of the element
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## :robot: Testing
|
||||
|
||||
We're using [cypress](https://docs.cypress.io/guides/component-testing/writing-your-first-component-test)
|
||||
for component testing. Cypress is already configured and we're actively working on backfilling complete
|
||||
test coverage of the application. There are some issues open for this so you may refer to those to check
|
||||
out the progress.
|
||||
|
||||
All new components should have tests written alongside them. For example, if you component is called
|
||||
`my-component.tsx` then you would also have a `my-component.cy.tsx` test file to go with it. You can
|
||||
refer to existing tests for some common patterns we use to write tests.
|
||||
|
||||
## :heart: Contributing
|
||||
|
||||
If you would like to contribute to this project please follow these simple guidelines:
|
||||
|
||||
- Be excellent to each other!
|
||||
- Follow the best practices and coding standards outlined here.
|
||||
- Clone or fork this repo, write code and open a PR :+1:
|
||||
- All code must pass the `pr-checks` and be reviewed by a code owner.
|
||||
|
||||
That's it!
|
||||
|
||||
## :beetle: Bugs?
|
||||
|
||||
If you find a bug please file an issue on this repository with as much information as
|
||||
possible regarding replication etc :pray:.
|
||||
@@ -1,25 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This is where the frontend dist is located
|
||||
cd /opt/app/
|
||||
|
||||
# Build the backend API URL for frontend web app
|
||||
PUBLIC_IPV4="$(curl --fail -qs whatismyip.akamai.com)"
|
||||
API_PORT="${API_PORT:-3000}"
|
||||
API_VERSION="${API_VERSION:-v1}"
|
||||
API_BASE_URL=${API_BASE_URL:-http://$PUBLIC_IPV4:$API_PORT/$API_VERSION}
|
||||
|
||||
# Default to "false" if not set
|
||||
DISABLE_LCR=${DISABLE_LCR:-false}
|
||||
DISABLE_JAEGER_TRACING=${DISABLE_JAEGER_TRACING:-false}
|
||||
DISABLE_CUSTOM_SPEECH=${DISABLE_CUSTOM_SPEECH:-false}
|
||||
ENABLE_FORGOT_PASSWORD=${ENABLE_FORGOT_PASSWORD:-false}
|
||||
DISABLE_CALL_RECORDING=${DISABLE_CALL_RECORDING:-false}
|
||||
|
||||
# Serialize window global to provide the API URL to static frontend dist
|
||||
# This is declared and utilized in the web app: src/api/constants.ts
|
||||
SCRIPT_TAG="<script>window.JAMBONZ = {API_BASE_URL: \"${API_BASE_URL}\",DISABLE_LCR: \"${DISABLE_LCR}\",DISABLE_JAEGER_TRACING: \"${DISABLE_JAEGER_TRACING}\",DISABLE_CUSTOM_SPEECH: \"${DISABLE_CUSTOM_SPEECH}\",ENABLE_FORGOT_PASSWORD: \"${ENABLE_FORGOT_PASSWORD}\",DISABLE_CALL_RECORDING: \"${DISABLE_CALL_RECORDING}\",ADMIN_CARRIER: \"${ADMIN_CARRIER}\"};</script>"
|
||||
sed -i -e "\@</head>@i\ $SCRIPT_TAG" ./dist/index.html
|
||||
|
||||
# Start the frontend web app static server
|
||||
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
|
||||
|
||||
@@ -1,40 +1,19 @@
|
||||
<p align="center">
|
||||
<a href="https://jambonz.org">
|
||||
<img src="../public/icon192.png" height="128">
|
||||
<h1 align="center">jambonz</h1>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a aria-label="GitHub CI" href="https://github.com/jambonz/jambonz-webapp/actions/workflows/main.yml">
|
||||
<img alt="" src="https://github.com/jambonz/jambonz-webapp/actions/workflows/main.yml/badge.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
> 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 web app UI locally.
|
||||
# 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.
|
||||
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:
|
||||
|
||||
This will take a few minutes to start, but eventually a successfull startup will eventually look something like this:
|
||||
```bash
|
||||
$ npm run integration-test
|
||||
|
||||
@@ -59,7 +38,6 @@ ready for testing!
|
||||
```
|
||||
|
||||
This starts the a docker-compose network running the following containers:
|
||||
|
||||
- mysql
|
||||
- redis
|
||||
- influxdb
|
||||
@@ -67,29 +45,15 @@ This starts the a docker-compose network running the following containers:
|
||||
- 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:
|
||||
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 i
|
||||
npm start
|
||||
```
|
||||
This will start the react UI and open a browser page to http://localhost:3001.
|
||||
|
||||
This will start the web app UI on 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.
|
||||
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.
|
||||
|
||||
## Testing with 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
|
||||
3. `npm start`
|
||||
4. Access the web app via http://localhost:3001
|
||||
57
index.html
@@ -1,57 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Build innovative voice and collaboration services with jambonz, the open-source communication platform for conversational AI providers and CSPs."
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/objectivity-medium-webfont.woff2"
|
||||
crossorigin="anonymous"
|
||||
as="font"
|
||||
type="font/woff"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/objectivity-bold-webfont.woff2"
|
||||
crossorigin="anonymous"
|
||||
as="font"
|
||||
type="font/woff"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/objectivity-regular-webfont.woff2"
|
||||
crossorigin="anonymous"
|
||||
as="font"
|
||||
type="font/woff"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/objectivity-boldslanted-webfont.woff2"
|
||||
crossorigin="anonymous"
|
||||
as="font"
|
||||
type="font/woff"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/objectivity-regularslanted-webfont.woff2"
|
||||
crossorigin="anonymous"
|
||||
as="font"
|
||||
type="font/woff"
|
||||
/>
|
||||
<title>Jambonz Portal | Jambonz CPaaS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="modal"></div>
|
||||
<div id="toast"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
32012
package-lock.json
generated
147
package.json
@@ -1,128 +1,43 @@
|
||||
{
|
||||
"name": "jambonz-webapp",
|
||||
"description": "A simple provisioning web app for jambonz",
|
||||
"version": "0.9.5",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"version": "v0.7.6",
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"antd": "^4.21.0",
|
||||
"axios": "^0.21.1",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "^5.0.0",
|
||||
"serve": "^13.0.2",
|
||||
"styled-components": "^5.0.1"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Brandon Lee Kitajchuk",
|
||||
"email": "bk@kitajchuk.com",
|
||||
"url": "https://www.kitajchuk.com"
|
||||
},
|
||||
{
|
||||
"name": "Lê Hàn Minh Khang",
|
||||
"email": "mkhangle20@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Dave Horton",
|
||||
"email": "daveh@drachtio.org",
|
||||
"url": "https://drachtio.org"
|
||||
}
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"postinstall": "rm -rf public/fonts && cp -R node_modules/@jambonz/ui-kit/public/fonts public/fonts",
|
||||
"start": "npm run dev",
|
||||
"dev": "vite --port 3001",
|
||||
"dev:server": "ts-node --esm server/dev.server.ts",
|
||||
"prebuild": "rm -rf ./dist",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext ts,tsx --max-warnings=0",
|
||||
"format": "prettier --check .",
|
||||
"test": "cypress run --component --headless",
|
||||
"test:open": "cypress open --component",
|
||||
"serve": "serve -s dist -l ${HTTP_PORT:-3001}",
|
||||
"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 ${HTTP_PORT:-3001}",
|
||||
"pm2": "pm2 start npm --name \"jambonz-webapp\" -- run serve",
|
||||
"deploy": "npm i && npm run build && npm run pm2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/ui-kit": "^0.0.22",
|
||||
"@stripe/react-stripe-js": "^2.6.2",
|
||||
"@stripe/stripe-js": "^3.2.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"wavesurfer.js": "^7.7.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.12.5",
|
||||
"@types/react": "^18.2.74",
|
||||
"@types/react-dom": "^18.2.24",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"cypress": "^13.7.2",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"express": "^5.1.0",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^16.1.2",
|
||||
"nanoid": "^5.1.5",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.89.2",
|
||||
"serve": "^14.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.4",
|
||||
"vite": "^6.4.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": "eslint --max-warnings=0",
|
||||
"*.{ts,tsx,json,md,html,scss,css,yml}": "prettier --write"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:jsx-a11y/recommended",
|
||||
"prettier"
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"react-hooks",
|
||||
"jsx-a11y",
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-key": [
|
||||
"error",
|
||||
{
|
||||
"checkFragmentShorthand": true
|
||||
}
|
||||
],
|
||||
"react/prop-types": [
|
||||
0
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"dist",
|
||||
"node_modules"
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,14 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 420">
|
||||
<rect width="420" height="420"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" fill="#DA1C5C" d="M179.9,126.8c-2.4-2.6-6.7-5.2-14.6-4.9
|
||||
c-15.8,0.6-24,15.6-22.1,24.4c1.5,7-3,13.8-10,15.3c-7,1.5-13.8-3-15.3-10c-5-23.8,13.9-54.4,46.5-55.6c14.8-0.5,26.7,4.7,34.6,13.3
|
||||
c7.6,8.3,11,19.2,10.1,29.2c-1.8,19.7-17,30.1-27.8,36.9l-0.5,0.3c10.6,2.8,20,8,28,15c11.1,9.7,19,22.5,23.6,36.6
|
||||
c15.5,6.1,30.8,14.4,44.7,25.2c1.9-7.1,6.6-16.4,10.9-20c11.6,22.2,19.1,54.5,21.8,79c-16.1-7.8-46.4-17.6-66.7-20.4
|
||||
c3.9-7.5,11.8-14.9,18-18.4c-7.6-5.9-15.7-10.9-24.1-15.1c-0.9,17.4-9,32.7-20,44c-12.5,12.8-29.7,21.5-46.8,22.3
|
||||
c-16,0.7-31-3.5-42.3-12.8c-11.5-9.4-18-23.4-17.6-39.6c0.4-15.7,6.8-30.8,18.8-41.6c12-10.9,28.9-16.7,48.7-15.2
|
||||
c6.8,0.5,13.8,1.4,20.9,2.8c-2.1-2.7-4.3-5.1-6.8-7.3c-10.5-9.2-25.3-13.9-44.2-9.1c-0.4,0.1-0.9,0.2-1.3,0.3
|
||||
c-6.6,1.3-12.6,1-14.9-2.6c-2.9-4.5-3-10.4,3.6-19.3c4.4-6.3,10-11,15.3-14.9c4.1-3,8.8-5.8,12.9-8.4c1.6-1,3-1.9,4.4-2.7
|
||||
c11.2-7.1,15.3-11.5,15.8-17.4C183.6,133.4,182.6,129.7,179.9,126.8z M210.6,247.1c-11.9-3.6-23.7-5.8-34.8-6.6
|
||||
c-13.5-1-23.2,3-29.4,8.6c-6.4,5.8-10,14-10.3,23c-0.2,8.5,3,14.7,8.2,19c5.4,4.4,13.8,7.4,24.6,6.9c9.6-0.4,20.9-5.6,29.5-14.5
|
||||
c8.5-8.7,13.5-20.1,12.6-32C211,250.1,210.8,248.6,210.6,247.1z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
BIN
public/icon.png
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB |
14
public/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#565656" />
|
||||
<meta name="description" content="Jambonz is an open source Communications Platform as a Service (CPaaS) built on drachtio, the open source project for creating SIP server applications." />
|
||||
<title>Jambonz | Open Source CPAAS</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>Please enable JavaScript in order to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"short_name": "jambonz-webapp",
|
||||
"name": "jambonz-webapp",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "icon192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "icon384.png",
|
||||
"type": "image/png",
|
||||
"sizes": "384x384"
|
||||
},
|
||||
{
|
||||
"src": "icon512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icon1024.png",
|
||||
"type": "image/png",
|
||||
"sizes": "1024x1024"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
Disallow:
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="649px" height="213px" viewBox="0 0 649 213" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Logo">
|
||||
<path d="M16.7269,19.605 C15.4359,21.9104 14.9386,24.5764 15.3117,27.1923 C15.6177,29.3235 16.4884,31.3341 17.8334,33.0154 C19.1785,34.6967 20.9489,35.9875 22.961,36.7539 C25.4312,37.6921 28.1412,37.7922 30.6738,37.0387 C33.2065,36.2853 35.4212,34.7202 36.9769,32.5843 C38.5326,30.4485 39.3429,27.8605 39.2832,25.2189 C39.2234,22.5772 38.2969,20.0284 36.6462,17.9651 C34.9956,15.9018 32.7124,14.4384 30.1483,13.8002 C27.5842,13.1621 24.8814,13.3846 22.4562,14.4335 C20.031,15.4824 18.0179,17.2995 16.7269,19.605 Z" id="Path" fill="#ffffff" fill-rule="nonzero"></path>
|
||||
<path d="M16.1782,117.568 C16.1782,123.305 12.9082,126.862 8.26124,126.862 L0,126.862 L0,143.9 C1.72109,145.105 6.36805,146.138 11.015,146.138 C25.6442,146.138 38.036,134.779 38.036,120.149 L38.036,49.0583 L16.1782,49.0583 L16.1782,117.568 Z" id="Path" fill="#ffffff" fill-rule="nonzero"></path>
|
||||
<path d="M130.732,99.9951 L130.732,117.894 C128.897,118.753 126.905,119.222 124.88,119.271 C120.637,119.338 116.45,118.293 112.736,116.24 C109.022,114.187 105.91,111.197 103.711,107.568 C100.986,111.254 97.4234,114.238 93.3164,116.274 C89.2094,118.309 84.6767,119.336 80.0935,119.271 C60.9703,119.271 46.0159,103.437 46.0159,83.4727 C46.0159,63.508 60.9703,47.674 80.0935,47.674 C87.3363,47.559 94.3513,50.205 99.7139,55.0747 L99.7139,49.0509 L121.572,49.0509 L121.572,93.2829 C121.572,99.0198 124.67,99.9951 127.596,99.9951 L130.732,99.9951 Z M100.785,83.4727 C100.771,80.6486 100.032,77.8754 98.6391,75.4189 C97.2458,72.9625 95.2448,70.9052 92.8279,69.4443 C90.4111,67.9835 87.6594,67.168 84.8368,67.0762 C82.0143,66.9843 79.2154,67.6192 76.7087,68.9199 C74.9128,69.7893 73.3102,71.0109 71.9961,72.5123 C70.682,74.0137 69.6833,75.764 69.0594,77.6592 C68.2216,79.9061 67.8872,82.3096 68.0796,84.6999 C68.2721,87.0902 68.9867,89.4091 70.1732,91.4931 C71.3597,93.5771 72.989,95.3753 74.9462,96.7609 C76.9035,98.1464 79.141,99.0856 81.5008,99.512 C83.8606,99.9385 86.2852,99.8419 88.6036,99.2291 C90.9221,98.6162 93.0778,97.502 94.9186,95.9651 C96.7594,94.4283 98.2405,92.5062 99.2574,90.3344 C100.274,88.1627 100.802,85.7942 100.804,83.3961 L100.785,83.4727 Z" id="Shape" fill="#ffffff" fill-rule="nonzero"></path>
|
||||
<path d="M140.612,117.895 L140.612,49.0486 L162.641,49.0486 L162.641,56.7958 C164.992,53.9589 167.937,51.6718 171.267,50.0958 C174.598,48.5198 178.233,47.6932 181.918,47.6741 C191.344,47.6741 199.432,52.946 203.683,61.0366 C204.639,59.6404 205.802,58.0306 206.871,56.7958 C209.242,54.0553 212.166,51.6718 215.497,50.0958 C218.827,48.5198 222.462,47.6932 226.147,47.6741 C240.088,47.6741 251.103,59.2054 251.103,74.3318 L251.103,117.875 L229.073,117.875 L229.073,77.9652 C229.133,76.5544 228.904,75.1462 228.402,73.8264 C227.9,72.5066 227.135,71.3028 226.153,70.2884 C225.171,69.274 223.992,68.4701 222.689,67.9258 C221.386,67.3815 219.986,67.1082 218.574,67.1224 C215.462,67.1919 212.5,68.4707 210.315,70.6877 C208.172,72.862 206.943,75.7711 206.873,78.8186 L206.873,117.875 L184.844,117.875 L184.844,77.9652 C184.903,76.5544 184.675,75.1462 184.173,73.8264 C183.671,72.5066 182.906,71.3028 181.923,70.2884 C180.941,69.274 179.763,68.4701 178.46,67.9258 C177.157,67.3815 175.757,67.1082 174.345,67.1224 C171.233,67.1919 168.271,68.4707 166.086,70.6877 C163.901,72.9047 162.666,75.8854 162.641,78.9979 L162.641,117.895 L140.612,117.895 Z" id="Path" fill="#ffffff" fill-rule="nonzero"></path>
|
||||
<path d="M284.925,111.959 L284.925,117.895 L262.895,117.895 L262.895,0 L284.925,0 L284.925,55.0664 C290.286,50.2021 297.297,47.5592 304.536,47.6741 C323.659,47.6741 338.613,63.5081 338.613,83.4728 C338.613,103.437 323.659,119.271 304.536,119.271 C299.953,119.337 295.42,118.309 291.313,116.274 C288.992,115.124 286.845,113.671 284.925,111.959 Z M285.99,75.4191 C284.597,77.8755 283.858,80.6487 283.845,83.4728 L283.825,83.3963 C283.827,85.7943 284.355,88.1628 285.372,90.3346 C286.389,92.5063 287.87,94.4284 289.711,95.9652 C291.552,97.5021 293.707,98.6163 296.026,99.2292 C298.344,99.842 300.769,99.9387 303.129,99.5122 C305.488,99.0857 307.726,98.1465 309.683,96.761 C311.64,95.3754 313.27,93.5772 314.456,91.4932 C315.643,89.4093 316.357,87.0903 316.55,84.7 C316.742,82.3097 316.408,79.9063 315.57,77.6593 C314.946,75.7641 313.947,74.0138 312.633,72.5124 C311.319,71.0111 309.717,69.7894 307.921,68.92 C305.414,67.6194 302.615,66.9845 299.793,67.0763 C296.97,67.1681 294.218,67.9836 291.801,69.4445 C289.385,70.9053 287.384,72.9626 285.99,75.4191 Z" id="Shape" fill="#ffffff"></path>
|
||||
<path d="M346.581,83.4727 C346.581,62.8197 361.879,47.6741 384.961,47.6741 C408.043,47.6741 423.207,62.8197 423.207,83.4727 C423.207,104.126 408.062,119.271 384.961,119.271 C361.86,119.271 346.581,104.126 346.581,83.4727 Z M401.311,83.4727 C401.311,80.2389 400.352,77.0778 398.556,74.389 C396.759,71.7002 394.206,69.6045 391.218,68.367 C388.23,67.1295 384.943,66.8057 381.771,67.4366 C378.6,68.0674 375.686,69.6246 373.4,71.9113 C371.113,74.1979 369.556,77.1113 368.925,80.2829 C368.294,83.4546 368.618,86.7421 369.855,89.7297 C371.093,92.7174 373.188,95.271 375.877,97.0676 C378.566,98.8641 381.727,99.823 384.961,99.823 C387.112,99.8462 389.246,99.4388 391.237,98.6246 C393.228,97.8104 395.036,96.606 396.554,95.0823 C398.072,93.5586 399.27,91.7464 400.078,89.7526 C400.885,87.7588 401.285,85.6235 401.254,83.4727 L401.311,83.4727 Z" id="Shape" fill="#ffffff" fill-rule="nonzero"></path>
|
||||
<path d="M431.175,49.0431 L431.175,117.895 L453.205,117.895 L453.205,78.9979 C453.229,75.8854 454.464,72.9047 456.649,70.6877 C458.834,68.4707 461.796,67.1919 464.908,67.1224 C466.32,67.1082 467.72,67.3815 469.023,67.9258 C470.326,68.4701 471.505,69.274 472.487,70.2884 C473.469,71.3028 474.235,72.5066 474.737,73.8264 C475.239,75.1462 475.467,76.5544 475.407,77.9652 L475.407,117.875 L497.437,117.875 L497.437,74.3318 C497.437,59.2054 486.422,47.6741 472.481,47.6741 C468.797,47.6932 465.161,48.5198 461.831,50.0958 C458.5,51.6718 455.556,53.9589 453.205,56.7958 L453.205,49.0431 L431.175,49.0431 Z" id="Path" fill="#ffffff" fill-rule="nonzero"></path>
|
||||
<path d="M552.288,67.2054 C550.533,65.2934 547.308,63.3419 541.476,63.5469 C529.761,63.9587 523.766,75.0885 525.135,81.5759 C526.226,86.7428 522.921,91.8154 517.754,92.9059 C512.587,93.9964 507.515,90.6918 506.424,85.5249 C502.712,67.9368 516.694,45.283 540.804,44.4355 C551.73,44.0515 560.519,47.8936 566.376,54.2733 C572.026,60.4288 574.505,68.4947 573.837,75.8471 C572.514,90.4014 561.294,98.132 553.314,103.169 L552.947,103.401 C560.751,105.477 567.732,109.3 573.654,114.473 C581.83,121.615 587.724,131.083 591.132,141.515 C602.593,146.006 613.885,152.148 624.164,160.113 C625.588,154.886 629.044,147.961 632.213,145.361 C640.793,161.753 646.346,185.684 648.305,203.786 C636.364,198.042 613.993,190.787 598.981,188.666 C601.859,183.086 607.729,177.682 612.267,175.088 C606.644,170.752 600.635,167.037 594.441,163.914 C593.768,176.812 587.773,188.12 579.654,196.456 C570.419,205.94 557.705,212.351 545.014,212.939 C533.206,213.486 522.067,210.344 513.76,203.503 C505.292,196.53 500.444,186.23 500.745,174.206 C501.035,162.596 505.809,151.435 514.635,143.457 C523.54,135.409 535.977,131.123 550.61,132.187 C555.615,132.551 560.807,133.234 566.097,134.255 C564.577,132.272 562.896,130.467 561.073,128.875 C553.309,122.093 542.394,118.61 528.39,122.148 C528.065,122.23 527.739,122.295 527.412,122.342 C522.518,123.318 518.103,123.096 516.388,120.409 C514.27,117.092 514.163,112.712 519.024,106.168 C522.272,101.538 526.394,98.0112 530.331,95.1763 C533.391,92.973 536.851,90.8513 539.885,88.9914 C541.035,88.2863 542.124,87.6186 543.106,86.9986 C551.38,81.7756 554.396,78.4765 554.792,74.1157 C554.978,72.0653 554.249,69.3415 552.288,67.2054 Z M574.952,156.124 C566.16,153.452 557.408,151.855 549.223,151.26 C539.229,150.533 532.089,153.458 527.458,157.644 C522.749,161.9 520.029,167.996 519.862,174.684 C519.705,180.957 522.088,185.588 525.917,188.741 C529.907,192.027 536.131,194.207 544.128,193.836 C551.243,193.507 559.559,189.682 565.954,183.114 C572.254,176.645 575.96,168.286 575.305,159.444 C575.222,158.326 575.104,157.219 574.952,156.124 Z" id="Shape" fill="#DA1C5C"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 8.3 KiB |
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="649px" height="213px" viewBox="0 0 649 213" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Logo">
|
||||
<path d="M16.7269,19.605 C15.4359,21.9104 14.9386,24.5764 15.3117,27.1923 C15.6177,29.3235 16.4884,31.3341 17.8334,33.0154 C19.1785,34.6967 20.9489,35.9875 22.961,36.7539 C25.4312,37.6921 28.1412,37.7922 30.6738,37.0387 C33.2065,36.2853 35.4212,34.7202 36.9769,32.5843 C38.5326,30.4485 39.3429,27.8605 39.2832,25.2189 C39.2234,22.5772 38.2969,20.0284 36.6462,17.9651 C34.9956,15.9018 32.7124,14.4384 30.1483,13.8002 C27.5842,13.1621 24.8814,13.3846 22.4562,14.4335 C20.031,15.4824 18.0179,17.2995 16.7269,19.605 Z" id="Path" fill="#231F20" fill-rule="nonzero"></path>
|
||||
<path d="M16.1782,117.568 C16.1782,123.305 12.9082,126.862 8.26124,126.862 L0,126.862 L0,143.9 C1.72109,145.105 6.36805,146.138 11.015,146.138 C25.6442,146.138 38.036,134.779 38.036,120.149 L38.036,49.0583 L16.1782,49.0583 L16.1782,117.568 Z" id="Path" fill="#231F20" fill-rule="nonzero"></path>
|
||||
<path d="M130.732,99.9951 L130.732,117.894 C128.897,118.753 126.905,119.222 124.88,119.271 C120.637,119.338 116.45,118.293 112.736,116.24 C109.022,114.187 105.91,111.197 103.711,107.568 C100.986,111.254 97.4234,114.238 93.3164,116.274 C89.2094,118.309 84.6767,119.336 80.0935,119.271 C60.9703,119.271 46.0159,103.437 46.0159,83.4727 C46.0159,63.508 60.9703,47.674 80.0935,47.674 C87.3363,47.559 94.3513,50.205 99.7139,55.0747 L99.7139,49.0509 L121.572,49.0509 L121.572,93.2829 C121.572,99.0198 124.67,99.9951 127.596,99.9951 L130.732,99.9951 Z M100.785,83.4727 C100.771,80.6486 100.032,77.8754 98.6391,75.4189 C97.2458,72.9625 95.2448,70.9052 92.8279,69.4443 C90.4111,67.9835 87.6594,67.168 84.8368,67.0762 C82.0143,66.9843 79.2154,67.6192 76.7087,68.9199 C74.9128,69.7893 73.3102,71.0109 71.9961,72.5123 C70.682,74.0137 69.6833,75.764 69.0594,77.6592 C68.2216,79.9061 67.8872,82.3096 68.0796,84.6999 C68.2721,87.0902 68.9867,89.4091 70.1732,91.4931 C71.3597,93.5771 72.989,95.3753 74.9462,96.7609 C76.9035,98.1464 79.141,99.0856 81.5008,99.512 C83.8606,99.9385 86.2852,99.8419 88.6036,99.2291 C90.9221,98.6162 93.0778,97.502 94.9186,95.9651 C96.7594,94.4283 98.2405,92.5062 99.2574,90.3344 C100.274,88.1627 100.802,85.7942 100.804,83.3961 L100.785,83.4727 Z" id="Shape" fill="#231F20" fill-rule="nonzero"></path>
|
||||
<path d="M140.612,117.895 L140.612,49.0486 L162.641,49.0486 L162.641,56.7958 C164.992,53.9589 167.937,51.6718 171.267,50.0958 C174.598,48.5198 178.233,47.6932 181.918,47.6741 C191.344,47.6741 199.432,52.946 203.683,61.0366 C204.639,59.6404 205.802,58.0306 206.871,56.7958 C209.242,54.0553 212.166,51.6718 215.497,50.0958 C218.827,48.5198 222.462,47.6932 226.147,47.6741 C240.088,47.6741 251.103,59.2054 251.103,74.3318 L251.103,117.875 L229.073,117.875 L229.073,77.9652 C229.133,76.5544 228.904,75.1462 228.402,73.8264 C227.9,72.5066 227.135,71.3028 226.153,70.2884 C225.171,69.274 223.992,68.4701 222.689,67.9258 C221.386,67.3815 219.986,67.1082 218.574,67.1224 C215.462,67.1919 212.5,68.4707 210.315,70.6877 C208.172,72.862 206.943,75.7711 206.873,78.8186 L206.873,117.875 L184.844,117.875 L184.844,77.9652 C184.903,76.5544 184.675,75.1462 184.173,73.8264 C183.671,72.5066 182.906,71.3028 181.923,70.2884 C180.941,69.274 179.763,68.4701 178.46,67.9258 C177.157,67.3815 175.757,67.1082 174.345,67.1224 C171.233,67.1919 168.271,68.4707 166.086,70.6877 C163.901,72.9047 162.666,75.8854 162.641,78.9979 L162.641,117.895 L140.612,117.895 Z" id="Path" fill="#231F20" fill-rule="nonzero"></path>
|
||||
<path d="M284.925,111.959 L284.925,117.895 L262.895,117.895 L262.895,0 L284.925,0 L284.925,55.0664 C290.286,50.2021 297.297,47.5592 304.536,47.6741 C323.659,47.6741 338.613,63.5081 338.613,83.4728 C338.613,103.437 323.659,119.271 304.536,119.271 C299.953,119.337 295.42,118.309 291.313,116.274 C288.992,115.124 286.845,113.671 284.925,111.959 Z M285.99,75.4191 C284.597,77.8755 283.858,80.6487 283.845,83.4728 L283.825,83.3963 C283.827,85.7943 284.355,88.1628 285.372,90.3346 C286.389,92.5063 287.87,94.4284 289.711,95.9652 C291.552,97.5021 293.707,98.6163 296.026,99.2292 C298.344,99.842 300.769,99.9387 303.129,99.5122 C305.488,99.0857 307.726,98.1465 309.683,96.761 C311.64,95.3754 313.27,93.5772 314.456,91.4932 C315.643,89.4093 316.357,87.0903 316.55,84.7 C316.742,82.3097 316.408,79.9063 315.57,77.6593 C314.946,75.7641 313.947,74.0138 312.633,72.5124 C311.319,71.0111 309.717,69.7894 307.921,68.92 C305.414,67.6194 302.615,66.9845 299.793,67.0763 C296.97,67.1681 294.218,67.9836 291.801,69.4445 C289.385,70.9053 287.384,72.9626 285.99,75.4191 Z" id="Shape" fill="#231F20"></path>
|
||||
<path d="M346.581,83.4727 C346.581,62.8197 361.879,47.6741 384.961,47.6741 C408.043,47.6741 423.207,62.8197 423.207,83.4727 C423.207,104.126 408.062,119.271 384.961,119.271 C361.86,119.271 346.581,104.126 346.581,83.4727 Z M401.311,83.4727 C401.311,80.2389 400.352,77.0778 398.556,74.389 C396.759,71.7002 394.206,69.6045 391.218,68.367 C388.23,67.1295 384.943,66.8057 381.771,67.4366 C378.6,68.0674 375.686,69.6246 373.4,71.9113 C371.113,74.1979 369.556,77.1113 368.925,80.2829 C368.294,83.4546 368.618,86.7421 369.855,89.7297 C371.093,92.7174 373.188,95.271 375.877,97.0676 C378.566,98.8641 381.727,99.823 384.961,99.823 C387.112,99.8462 389.246,99.4388 391.237,98.6246 C393.228,97.8104 395.036,96.606 396.554,95.0823 C398.072,93.5586 399.27,91.7464 400.078,89.7526 C400.885,87.7588 401.285,85.6235 401.254,83.4727 L401.311,83.4727 Z" id="Shape" fill="#231F20" fill-rule="nonzero"></path>
|
||||
<path d="M431.175,49.0431 L431.175,117.895 L453.205,117.895 L453.205,78.9979 C453.229,75.8854 454.464,72.9047 456.649,70.6877 C458.834,68.4707 461.796,67.1919 464.908,67.1224 C466.32,67.1082 467.72,67.3815 469.023,67.9258 C470.326,68.4701 471.505,69.274 472.487,70.2884 C473.469,71.3028 474.235,72.5066 474.737,73.8264 C475.239,75.1462 475.467,76.5544 475.407,77.9652 L475.407,117.875 L497.437,117.875 L497.437,74.3318 C497.437,59.2054 486.422,47.6741 472.481,47.6741 C468.797,47.6932 465.161,48.5198 461.831,50.0958 C458.5,51.6718 455.556,53.9589 453.205,56.7958 L453.205,49.0431 L431.175,49.0431 Z" id="Path" fill="#231F20" fill-rule="nonzero"></path>
|
||||
<path d="M552.288,67.2054 C550.533,65.2934 547.308,63.3419 541.476,63.5469 C529.761,63.9587 523.766,75.0885 525.135,81.5759 C526.226,86.7428 522.921,91.8154 517.754,92.9059 C512.587,93.9964 507.515,90.6918 506.424,85.5249 C502.712,67.9368 516.694,45.283 540.804,44.4355 C551.73,44.0515 560.519,47.8936 566.376,54.2733 C572.026,60.4288 574.505,68.4947 573.837,75.8471 C572.514,90.4014 561.294,98.132 553.314,103.169 L552.947,103.401 C560.751,105.477 567.732,109.3 573.654,114.473 C581.83,121.615 587.724,131.083 591.132,141.515 C602.593,146.006 613.885,152.148 624.164,160.113 C625.588,154.886 629.044,147.961 632.213,145.361 C640.793,161.753 646.346,185.684 648.305,203.786 C636.364,198.042 613.993,190.787 598.981,188.666 C601.859,183.086 607.729,177.682 612.267,175.088 C606.644,170.752 600.635,167.037 594.441,163.914 C593.768,176.812 587.773,188.12 579.654,196.456 C570.419,205.94 557.705,212.351 545.014,212.939 C533.206,213.486 522.067,210.344 513.76,203.503 C505.292,196.53 500.444,186.23 500.745,174.206 C501.035,162.596 505.809,151.435 514.635,143.457 C523.54,135.409 535.977,131.123 550.61,132.187 C555.615,132.551 560.807,133.234 566.097,134.255 C564.577,132.272 562.896,130.467 561.073,128.875 C553.309,122.093 542.394,118.61 528.39,122.148 C528.065,122.23 527.739,122.295 527.412,122.342 C522.518,123.318 518.103,123.096 516.388,120.409 C514.27,117.092 514.163,112.712 519.024,106.168 C522.272,101.538 526.394,98.0112 530.331,95.1763 C533.391,92.973 536.851,90.8513 539.885,88.9914 C541.035,88.2863 542.124,87.6186 543.106,86.9986 C551.38,81.7756 554.396,78.4765 554.792,74.1157 C554.978,72.0653 554.249,69.3415 552.288,67.2054 Z M574.952,156.124 C566.16,153.452 557.408,151.855 549.223,151.26 C539.229,150.533 532.089,153.458 527.458,157.644 C522.749,161.9 520.029,167.996 519.862,174.684 C519.705,180.957 522.088,185.588 525.917,188.741 C529.907,192.027 536.131,194.207 544.128,193.836 C551.243,193.507 559.559,189.682 565.954,183.114 C572.254,176.645 575.96,168.286 575.305,159.444 C575.222,158.326 575.104,157.219 574.952,156.124 Z" id="Shape" fill="#DA1C5C"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 8.3 KiB |
@@ -1,267 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import cors from "cors";
|
||||
import express from "express";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type {
|
||||
Alert,
|
||||
RecentCall,
|
||||
PageQuery,
|
||||
CallQuery,
|
||||
PagedResponse,
|
||||
} from "../src/api/types";
|
||||
|
||||
const app = express();
|
||||
const port = 3002;
|
||||
|
||||
app.use(cors());
|
||||
|
||||
/** RecentCalls mock API responses for local dev */
|
||||
app.get(
|
||||
"/api/Accounts/:account_sid/RecentCalls",
|
||||
(req: Request, res: Response) => {
|
||||
const data: RecentCall[] = [];
|
||||
const points = 500;
|
||||
const start = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
const now = new Date();
|
||||
const increment = (now.getTime() - start.getTime()) / points;
|
||||
|
||||
for (let i = 0; i < 500; i++) {
|
||||
const attempted_at = new Date(start.getTime() + i * increment);
|
||||
const failed = 0 === i % 5;
|
||||
const call_sid = nanoid();
|
||||
const call: RecentCall = {
|
||||
account_sid: req.params.account_sid,
|
||||
call_sid,
|
||||
from: "15083084809",
|
||||
to: "18882349999",
|
||||
answered: !failed,
|
||||
sip_callid: `${nanoid()}@192.168.1.100`,
|
||||
sip_status: 200,
|
||||
duration: failed ? 0 : 45,
|
||||
attempted_at: attempted_at.getTime(),
|
||||
answered_at: attempted_at.getTime() + 3000,
|
||||
terminated_at: attempted_at.getTime() + 45000,
|
||||
termination_reason: "caller hungup",
|
||||
host: "192.168.1.100",
|
||||
remote_host: "3.55.24.34",
|
||||
direction: 0 === i % 2 ? "inbound" : "outbound",
|
||||
trunk: 0 === i % 2 ? "twilio" : "user",
|
||||
trace_id: nanoid(),
|
||||
recording_url: `http://127.0.0.1:3002/api/Accounts/${req.params.account_sid}/RecentCalls/${call_sid}/record`,
|
||||
};
|
||||
data.push(call);
|
||||
}
|
||||
|
||||
const query: CallQuery = {
|
||||
...req.query,
|
||||
page: Number(req.query.page),
|
||||
count: Number(req.query.count),
|
||||
};
|
||||
|
||||
let filtered = data;
|
||||
|
||||
if (query.start) {
|
||||
filtered = filtered.filter((call) => {
|
||||
return call.attempted_at >= new Date(query.start!).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
console.log("RecentCalls: filtered", filtered.length);
|
||||
|
||||
if (query.days) {
|
||||
filtered = filtered.filter((call) => {
|
||||
return (
|
||||
call.attempted_at >=
|
||||
new Date(Date.now() - query.days! * 24 * 60 * 60 * 1000).getTime()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
console.log("RecentCalls: filtered", filtered.length);
|
||||
|
||||
if (query.direction) {
|
||||
filtered = filtered.filter((call) => {
|
||||
return call.direction === query.direction;
|
||||
});
|
||||
}
|
||||
|
||||
console.log("RecentCalls: filtered", filtered.length);
|
||||
|
||||
if (query.answered) {
|
||||
filtered = filtered.filter((call) => {
|
||||
return call.answered.toString() === query.answered;
|
||||
});
|
||||
}
|
||||
|
||||
console.log("RecentCalls: filtered", filtered.length);
|
||||
|
||||
const begin = (query.page - 1) * query.count;
|
||||
const end = begin + query.count;
|
||||
const paged = filtered.slice(begin, end);
|
||||
|
||||
console.log("RecentCalls: paged", paged.length);
|
||||
console.log("---");
|
||||
|
||||
res.status(200).json(<PagedResponse<RecentCall>>{
|
||||
page_size: query.count,
|
||||
total: filtered.length,
|
||||
page: query.page,
|
||||
data: paged,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/Accounts/:account_sid/RecentCalls/:call_sid",
|
||||
(req: Request, res: Response) => {
|
||||
res.status(200).json({ total: Math.random() > 0.5 ? 1 : 0 });
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/Accounts/:account_sid/RecentCalls/:call_sid/pcap",
|
||||
(req: Request, res: Response) => {
|
||||
/** Sample pcap file from: https://wiki.wireshark.org/SampleCaptures#sip-and-rtp */
|
||||
const pcap: Buffer = fs.readFileSync(
|
||||
path.resolve(process.cwd(), "server", "sample-sip-rtp-traffic.pcap"),
|
||||
);
|
||||
|
||||
res
|
||||
.status(200)
|
||||
.set({
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Disposition": "attachment",
|
||||
})
|
||||
.send(pcap); // server: Buffer => client: Blob
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/Accounts/:account_sid/RecentCalls/:call_sid/record",
|
||||
(req: Request, res: Response) => {
|
||||
/** Sample pcap file from: https://wiki.wireshark.org/SampleCaptures#sip-and-rtp */
|
||||
const wav: Buffer = fs.readFileSync(
|
||||
path.resolve(process.cwd(), "server", "example.mp3"),
|
||||
);
|
||||
|
||||
res
|
||||
.status(200)
|
||||
.set({
|
||||
"Content-Type": "audio/wav",
|
||||
"Content-Disposition": "attachment",
|
||||
})
|
||||
.send(wav); // server: Buffer => client: Blob
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/Accounts/:account_sid/RecentCalls/trace/:trace_id",
|
||||
(req: Request, res: Response) => {
|
||||
const json = fs.readFileSync(
|
||||
path.resolve(process.cwd(), "server", "sample-jaeger.json"),
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
res.status(200).json(JSON.parse(json));
|
||||
},
|
||||
);
|
||||
|
||||
/** Alerts mock API responses for local dev */
|
||||
app.get("/api/Accounts/:account_sid/Alerts", (req: Request, res: Response) => {
|
||||
const data: Alert[] = [];
|
||||
const points = 500;
|
||||
const start = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
const now = new Date();
|
||||
const increment = (now.getTime() - start.getTime()) / points;
|
||||
const url = "http://foo.bar";
|
||||
const vendor = "google";
|
||||
const count = 500;
|
||||
|
||||
for (let i = 0; i < 500; i++) {
|
||||
const time = new Date(start.getTime() + i * increment);
|
||||
const scenario = i % 5;
|
||||
let alert_type = "";
|
||||
let message = "";
|
||||
|
||||
switch (scenario) {
|
||||
case 0:
|
||||
alert_type = "webhook-failure";
|
||||
message = `${url} returned 404`;
|
||||
break;
|
||||
case 1:
|
||||
alert_type = "webhook-connection-failure";
|
||||
message = `failed to connect to ${url}`;
|
||||
break;
|
||||
case 2:
|
||||
alert_type = "no-tts";
|
||||
message = `text to speech credentials for ${vendor} have not been provisioned`;
|
||||
break;
|
||||
case 3:
|
||||
alert_type = "no-carrier";
|
||||
message = "outbound call failure: no carriers have been provisioned";
|
||||
break;
|
||||
case 4:
|
||||
alert_type = "call-limit";
|
||||
message = `you have exceeded your provisioned call limit of ${count}; please consider upgrading your plan`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const alert: Alert = {
|
||||
account_sid: req.params.account_sid,
|
||||
time: time.getTime(),
|
||||
alert_type,
|
||||
message,
|
||||
detail: "",
|
||||
};
|
||||
data.push(alert);
|
||||
}
|
||||
|
||||
const query: PageQuery = {
|
||||
...req.query,
|
||||
page: Number(req.query.page),
|
||||
count: Number(req.query.count),
|
||||
};
|
||||
|
||||
let filtered = data;
|
||||
|
||||
if (query.start) {
|
||||
filtered = filtered.filter((call) => {
|
||||
return call.time >= new Date(query.start!).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Alerts: filtered", filtered.length);
|
||||
|
||||
if (query.days) {
|
||||
filtered = filtered.filter((call) => {
|
||||
return (
|
||||
call.time >=
|
||||
new Date(Date.now() - query.days! * 24 * 60 * 60 * 1000).getTime()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Alerts: filtered", filtered.length);
|
||||
|
||||
const begin = (query.page - 1) * query.count;
|
||||
const end = begin + query.count;
|
||||
const paged = filtered.slice(begin, end);
|
||||
|
||||
console.log("Alerts: paged", paged.length);
|
||||
console.log("---");
|
||||
|
||||
res.status(200).json(<PagedResponse<Alert>>{
|
||||
page_size: query.count,
|
||||
total: filtered.length,
|
||||
page: query.page,
|
||||
data: paged,
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`express server listening on port ${port}`);
|
||||
});
|
||||
111
src/App.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
|
||||
import { NotificationStateContext } from './contexts/NotificationContext';
|
||||
|
||||
import Login from './components/pages/Login';
|
||||
import CreatePassword from './components/pages/setup/CreatePassword';
|
||||
import ConfigureAccount from './components/pages/setup/ConfigureAccount';
|
||||
import CreateApplication from './components/pages/setup/CreateApplication';
|
||||
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 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 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';
|
||||
import SideMenu from './components/blocks/SideMenu';
|
||||
|
||||
function App() {
|
||||
const notifications = useContext(NotificationStateContext);
|
||||
return (
|
||||
<Router>
|
||||
<Notification notifications={notifications} />
|
||||
<Nav />
|
||||
<Switch>
|
||||
<Route exact path="/"><Login /></Route>
|
||||
<Route exact path="/create-password"><CreatePassword /></Route>
|
||||
<Route exact path="/configure-account"><ConfigureAccount /></Route>
|
||||
<Route exact path="/create-application"><CreateApplication /></Route>
|
||||
<Route exact path="/configure-sip-trunk"><ConfigureSipTrunk /></Route>
|
||||
<Route exact path="/setup-complete"><SetupComplete /></Route>
|
||||
|
||||
<Route path="/internal">
|
||||
<div style={{ display: "flex" }}>
|
||||
<SideMenu />
|
||||
<Route exact path="/internal/accounts"><AccountsList /></Route>
|
||||
<Route exact path="/internal/applications"><ApplicationsList /></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>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/accounts/add",
|
||||
"/internal/accounts/:account_sid/edit"
|
||||
]}>
|
||||
<AccountsAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/applications/add",
|
||||
"/internal/applications/:application_sid/edit"
|
||||
]}>
|
||||
<ApplicationsAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/carriers/add",
|
||||
"/internal/carriers/:voip_carrier_sid/edit"
|
||||
]}>
|
||||
<CarriersAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/speech-services/add",
|
||||
"/internal/speech-services/:speech_service_sid/edit"
|
||||
]}>
|
||||
<SpeechServicesAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/phone-numbers/add",
|
||||
"/internal/phone-numbers/:phone_number_sid/edit"
|
||||
]}>
|
||||
<PhoneNumbersAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/ms-teams-tenants/add",
|
||||
"/internal/ms-teams-tenants/:ms_teams_tenant_sid/edit"
|
||||
]}>
|
||||
<MsTeamsTenantsAddEdit />
|
||||
</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>
|
||||
|
||||
<Route><InvalidRoute /></Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,489 +0,0 @@
|
||||
import type {
|
||||
CartesiaOptions,
|
||||
Currency,
|
||||
ElevenLabsOptions,
|
||||
GoogleCustomVoice,
|
||||
InworldOptions,
|
||||
LimitField,
|
||||
LimitUnitOption,
|
||||
PasswordSettings,
|
||||
PlayHTOptions,
|
||||
RimelabsOptions,
|
||||
SelectorOptions,
|
||||
SipGateway,
|
||||
SmppGateway,
|
||||
WebHook,
|
||||
WebhookOption,
|
||||
} from "./types";
|
||||
import { Vendor } from "src/vendor/types";
|
||||
|
||||
/** This window object is serialized and injected at docker runtime */
|
||||
/** The API url is constructed with the docker containers `ip:port` */
|
||||
interface JambonzWindowObject {
|
||||
API_BASE_URL: string;
|
||||
DISABLE_LCR: string;
|
||||
DISABLE_JAEGER_TRACING: string;
|
||||
DISABLE_CUSTOM_SPEECH: string;
|
||||
ENABLE_FORGOT_PASSWORD: string;
|
||||
ENABLE_HOSTED_SYSTEM: string;
|
||||
DISABLE_CALL_RECORDING: string;
|
||||
GITHUB_CLIENT_ID: string;
|
||||
GOOGLE_CLIENT_ID: string;
|
||||
BASE_URL: string;
|
||||
DEFAULT_SERVICE_PROVIDER_SID: string;
|
||||
STRIPE_PUBLISHABLE_KEY: string;
|
||||
DISABLE_ADDITIONAL_SPEECH_VENDORS: string;
|
||||
AWS_REGION: string;
|
||||
ENABLE_PHONE_NUMBER_LAZY_LOAD: string;
|
||||
ADMIN_CARRIER: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
JAMBONZ: JambonzWindowObject;
|
||||
}
|
||||
}
|
||||
|
||||
/** https://vitejs.dev/guide/env-and-mode.html#env-files */
|
||||
const CONFIGURED_API_BASE_URL =
|
||||
window.JAMBONZ?.API_BASE_URL || import.meta.env.VITE_API_BASE_URL;
|
||||
export const API_BASE_URL =
|
||||
CONFIGURED_API_BASE_URL && CONFIGURED_API_BASE_URL.length !== 0
|
||||
? CONFIGURED_API_BASE_URL
|
||||
: `${window.location.protocol}//${window.location.hostname}/api/v1`;
|
||||
|
||||
/** Serves mock API responses from a local dev API server */
|
||||
export const DEV_BASE_URL = import.meta.env.VITE_DEV_BASE_URL;
|
||||
|
||||
/** Disable custom speech vendor*/
|
||||
export const DISABLE_CUSTOM_SPEECH: boolean =
|
||||
window.JAMBONZ?.DISABLE_CUSTOM_SPEECH === "true" ||
|
||||
JSON.parse(import.meta.env.VITE_DISABLE_CUSTOM_SPEECH || "false");
|
||||
|
||||
/** Enable Forgot Password */
|
||||
export const ENABLE_FORGOT_PASSWORD: boolean =
|
||||
window.JAMBONZ?.ENABLE_FORGOT_PASSWORD === "true" ||
|
||||
JSON.parse(import.meta.env.VITE_APP_ENABLE_FORGOT_PASSWORD || "false");
|
||||
|
||||
/** Enable Cloud version */
|
||||
export const ENABLE_HOSTED_SYSTEM: boolean =
|
||||
window.JAMBONZ?.ENABLE_HOSTED_SYSTEM === "true" ||
|
||||
JSON.parse(import.meta.env.VITE_APP_ENABLE_HOSTED_SYSTEM || "false");
|
||||
/** Disable Lcr */
|
||||
export const DISABLE_LCR: boolean =
|
||||
window.JAMBONZ?.DISABLE_LCR === "true" ||
|
||||
JSON.parse(import.meta.env.VITE_APP_LCR_DISABLED || "false");
|
||||
|
||||
/** Disable jaeger tracing */
|
||||
export const DISABLE_JAEGER_TRACING: boolean =
|
||||
window.JAMBONZ?.DISABLE_JAEGER_TRACING === "true" ||
|
||||
JSON.parse(import.meta.env.VITE_APP_JAEGER_TRACING_DISABLED || "false");
|
||||
|
||||
/** Enable Record All Call Feature */
|
||||
export const DISABLE_CALL_RECORDING: boolean =
|
||||
window.JAMBONZ?.DISABLE_CALL_RECORDING === "true" ||
|
||||
JSON.parse(import.meta.env.VITE_APP_DISABLE_CALL_RECORDING || "false");
|
||||
|
||||
/** Disable additional speech vendors */
|
||||
export const DISABLE_ADDITIONAL_SPEECH_VENDORS: boolean =
|
||||
window.JAMBONZ?.DISABLE_ADDITIONAL_SPEECH_VENDORS === "true" ||
|
||||
JSON.parse(
|
||||
import.meta.env.VITE_APP_DISABLE_ADDITIONAL_SPEECH_VENDORS || "false",
|
||||
);
|
||||
|
||||
export const AWS_REGION: string =
|
||||
window.JAMBONZ?.AWS_REGION || import.meta.env.VITE_APP_AWS_REGION;
|
||||
|
||||
export const DEFAULT_SERVICE_PROVIDER_SID: string =
|
||||
window.JAMBONZ?.DEFAULT_SERVICE_PROVIDER_SID ||
|
||||
import.meta.env.VITE_APP_DEFAULT_SERVICE_PROVIDER_SID;
|
||||
|
||||
export const GITHUB_CLIENT_ID: string =
|
||||
window.JAMBONZ?.GITHUB_CLIENT_ID || import.meta.env.VITE_APP_GITHUB_CLIENT_ID;
|
||||
|
||||
export const BASE_URL: string =
|
||||
window.JAMBONZ?.BASE_URL || import.meta.env.VITE_APP_BASE_URL;
|
||||
|
||||
export const GOOGLE_CLIENT_ID: string =
|
||||
window.JAMBONZ?.GOOGLE_CLIENT_ID || import.meta.env.VITE_APP_GOOGLE_CLIENT_ID;
|
||||
|
||||
export const STRIPE_PUBLISHABLE_KEY: string =
|
||||
window.JAMBONZ?.STRIPE_PUBLISHABLE_KEY ||
|
||||
import.meta.env.VITE_APP_STRIPE_PUBLISHABLE_KEY;
|
||||
|
||||
export const ADMIN_CARRIER: string =
|
||||
window.JAMBONZ?.ADMIN_CARRIER || import.meta.env.VITE_ADMIN_CARRIER || "0";
|
||||
/** TCP Max Port */
|
||||
export const TCP_MAX_PORT = 65535;
|
||||
|
||||
/** Tech Prefix minlength */
|
||||
export const TECH_PREFIX_MINLENGTH = 3;
|
||||
|
||||
/** IP Types for validations */
|
||||
export const IP = "ip";
|
||||
export const FQDN = "fqdn";
|
||||
export const FQDN_TOP_LEVEL = "fqdn-top-level";
|
||||
export const INVALID = "invalid";
|
||||
|
||||
/** Default API object models */
|
||||
export const DEFAULT_WEBHOOK: WebHook = {
|
||||
url: "",
|
||||
method: "POST",
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
/** Default SIP/SMPP Gateways */
|
||||
export const DEFAULT_SIP_INBOUND_GATEWAY: SipGateway = {
|
||||
voip_carrier_sid: "",
|
||||
ipv4: "",
|
||||
port: 5060,
|
||||
netmask: 32,
|
||||
is_active: true,
|
||||
inbound: 1,
|
||||
outbound: 0,
|
||||
};
|
||||
|
||||
export const DEFAULT_SMPP_GATEWAY: SmppGateway = {
|
||||
voip_carrier_sid: "",
|
||||
ipv4: "",
|
||||
port: 2775,
|
||||
is_primary: false,
|
||||
use_tls: false,
|
||||
netmask: 32,
|
||||
inbound: 1,
|
||||
outbound: 1,
|
||||
};
|
||||
/** Netmask Bits */
|
||||
export const NETMASK_BITS = Array(32)
|
||||
.fill(0)
|
||||
.map((_, index) => index + 1)
|
||||
.reverse();
|
||||
|
||||
export const NETMASK_OPTIONS = NETMASK_BITS.map((bit) => ({
|
||||
name: bit.toString(),
|
||||
value: bit.toString(),
|
||||
}));
|
||||
|
||||
/** SIP Gateway Protocol */
|
||||
export const SIP_GATEWAY_PROTOCOL_OPTIONS = [
|
||||
{
|
||||
name: "UDP",
|
||||
value: "udp",
|
||||
},
|
||||
{
|
||||
name: "TCP",
|
||||
value: "tcp",
|
||||
},
|
||||
{
|
||||
name: "TLS",
|
||||
value: "tls",
|
||||
},
|
||||
{
|
||||
name: "TLS/SRTP",
|
||||
value: "tls/srtp",
|
||||
},
|
||||
];
|
||||
/**
|
||||
* Record bucket type
|
||||
*/
|
||||
export const BUCKET_VENDOR_AWS = "aws_s3";
|
||||
export const BUCKET_VENDOR_S3_COMPATIBLE = "s3_compatible";
|
||||
export const BUCKET_VENDOR_GOOGLE = "google";
|
||||
export const BUCKET_VENDOR_AZURE = "azure";
|
||||
export const BUCKET_VENDOR_OPTIONS = [
|
||||
{
|
||||
name: "NONE",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "AWS S3",
|
||||
value: BUCKET_VENDOR_AWS,
|
||||
},
|
||||
{
|
||||
name: "AWS S3 Compatible",
|
||||
value: BUCKET_VENDOR_S3_COMPATIBLE,
|
||||
},
|
||||
{
|
||||
name: "Azure Cloud Storage",
|
||||
value: BUCKET_VENDOR_AZURE,
|
||||
},
|
||||
{
|
||||
name: "Google Cloud Storage",
|
||||
value: BUCKET_VENDOR_GOOGLE,
|
||||
},
|
||||
];
|
||||
|
||||
export const AUDIO_FORMAT_OPTIONS = [
|
||||
{
|
||||
name: "mp3",
|
||||
value: "mp3",
|
||||
},
|
||||
{
|
||||
name: "wav",
|
||||
value: "wav",
|
||||
},
|
||||
];
|
||||
|
||||
export const LOG_LEVEL_OPTIONS = [
|
||||
{
|
||||
name: "Info",
|
||||
value: "info",
|
||||
},
|
||||
{
|
||||
name: "Debug",
|
||||
value: "debug",
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_ELEVENLABS_MODEL = "eleven_multilingual_v2";
|
||||
|
||||
export const DEFAULT_WHISPER_MODEL = "tts-1";
|
||||
|
||||
// VERBIO
|
||||
export const VERBIO_STT_MODELS = [
|
||||
{ name: "V1", value: "V1" },
|
||||
{ name: "V2", value: "V2" },
|
||||
];
|
||||
|
||||
export const DEFAULT_VERBIO_MODEL = "V1";
|
||||
|
||||
// ASSEMBLYAI
|
||||
export const ASSEMBLYAI_STT_VERSIONS = [
|
||||
{ name: "V2", value: "v2" },
|
||||
{ name: "V3", value: "v3" },
|
||||
];
|
||||
|
||||
export const DEFAULT_ASSEMBLYAI_STT_VERSION = "v2";
|
||||
|
||||
export const ADDITIONAL_SPEECH_VENDORS: Lowercase<Vendor>[] = ["speechmatics"];
|
||||
|
||||
// Google Custom Voice reported usage options
|
||||
|
||||
export const DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE = "REALTIME";
|
||||
export const GOOGLE_CUSTOM_VOICES_REPORTED_USAGE = [
|
||||
{ name: "REPORTED_USAGE_UNSPECIFIED", value: "REPORTED_USAGE_UNSPECIFIED" },
|
||||
{ name: "REALTIME", value: "REALTIME" },
|
||||
{ name: "OFFLINE", value: "OFFLINE" },
|
||||
];
|
||||
export const DEFAULT_GOOGLE_CUSTOM_VOICE: GoogleCustomVoice = {
|
||||
name: "",
|
||||
reported_usage: DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
||||
model: "",
|
||||
use_voice_cloning_key: 0,
|
||||
voice_cloning_key_file: null,
|
||||
};
|
||||
// ElevenLabs options
|
||||
export const DEFAULT_ELEVENLABS_OPTIONS: Partial<ElevenLabsOptions> = {
|
||||
optimize_streaming_latency: 3,
|
||||
voice_settings: {
|
||||
stability: 0.5,
|
||||
similarity_boost: 0.5,
|
||||
use_speaker_boost: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Rimelabs options
|
||||
export const DEFAULT_RIMELABS_OPTIONS: Partial<RimelabsOptions> = {
|
||||
speedAlpha: 1.0,
|
||||
reduceLatency: true,
|
||||
};
|
||||
|
||||
export const DEFAULT_INWORLD_OPTIONS: Partial<InworldOptions> = {
|
||||
audioConfig: {
|
||||
pitch: 0.0,
|
||||
speakingRate: 1.0,
|
||||
},
|
||||
temperature: 0.8,
|
||||
};
|
||||
|
||||
// PlayHT options
|
||||
export const DEFAULT_PLAYHT_OPTIONS: Partial<PlayHTOptions> = {
|
||||
quality: "medium",
|
||||
speed: 1,
|
||||
seed: 1,
|
||||
temperature: 1,
|
||||
emotion: "female_happy",
|
||||
voice_guidance: 3,
|
||||
style_guidance: 20,
|
||||
text_guidance: 1,
|
||||
};
|
||||
|
||||
// Cartesia options
|
||||
export const DEFAULT_CARTESIA_OPTIONS: Partial<CartesiaOptions> = {
|
||||
speed: 0.0,
|
||||
emotion: "positivity:high",
|
||||
};
|
||||
/** Password Length options */
|
||||
|
||||
export const PASSWORD_MIN = 8;
|
||||
export const PASSWORD_LENGTHS_OPTIONS = Array(13)
|
||||
.fill(PASSWORD_MIN)
|
||||
.map((i, j) => ({
|
||||
name: (i + j).toString(),
|
||||
value: (i + j).toString(),
|
||||
}));
|
||||
|
||||
/** List view filters */
|
||||
export const DATE_SELECTION = [
|
||||
{ name: "today", value: "today" },
|
||||
{ name: "yesterday", value: "yesterday" },
|
||||
{ name: "last 7d", value: "7" },
|
||||
{ name: "last 14d", value: "14" },
|
||||
{ name: "last 30d", value: "30" },
|
||||
];
|
||||
|
||||
export const PER_PAGE_SELECTION = [
|
||||
{ name: "25 / page", value: "25" },
|
||||
{ name: "50 / page", value: "50" },
|
||||
{ name: "100 / page", value: "100" },
|
||||
];
|
||||
|
||||
export const USER_SCOPE_SELECTION: SelectorOptions[] = [
|
||||
{ name: "All scopes", value: "all" },
|
||||
{ name: "Admin", value: "admin" },
|
||||
{ name: "Service provider", value: "service_provider" },
|
||||
{ name: "Account", value: "account" },
|
||||
];
|
||||
|
||||
export const DTMF_TYPE_SELECTION: SelectorOptions[] = [
|
||||
{ name: "RFC 2833", value: "rfc2833" },
|
||||
{ name: "Tones", value: "tones" },
|
||||
];
|
||||
|
||||
export const TRUNK_TYPE_SELECTION: SelectorOptions[] = [
|
||||
{ name: "IP Trunk", value: "static_ip" },
|
||||
{ name: "Auth Trunk", value: "auth" },
|
||||
{ name: "Registration Trunk", value: "reg" },
|
||||
];
|
||||
|
||||
/** Available webhook methods */
|
||||
export const WEBHOOK_METHODS: WebhookOption[] = [
|
||||
{
|
||||
name: "POST",
|
||||
value: "POST",
|
||||
},
|
||||
{
|
||||
name: "GET",
|
||||
value: "GET",
|
||||
},
|
||||
];
|
||||
|
||||
/** Various system limits */
|
||||
export const LIMITS: LimitField[] = [
|
||||
{
|
||||
label: "Max calls",
|
||||
category: "voice_call_session",
|
||||
},
|
||||
// {
|
||||
// label: "Max registered devices (0=unlimited)",
|
||||
// category: "device",
|
||||
// },
|
||||
// {
|
||||
// label: "Max api calls per minute (0=unlimited)",
|
||||
// category: "api_rate",
|
||||
// },
|
||||
{
|
||||
label: "Licensed calls",
|
||||
category: "voice_call_session_license",
|
||||
},
|
||||
{
|
||||
label: "Max minutes",
|
||||
category: "voice_call_minutes",
|
||||
},
|
||||
{
|
||||
label: "Licensed minutes",
|
||||
category: "voice_call_minutes_license",
|
||||
},
|
||||
];
|
||||
|
||||
export const LIMIT_MIN = "minute";
|
||||
export const LIMIT_SESS = "session";
|
||||
|
||||
export const LIMIT_UNITS: LimitUnitOption[] = [
|
||||
{
|
||||
name: "Session",
|
||||
value: LIMIT_SESS,
|
||||
},
|
||||
{
|
||||
name: "Minute",
|
||||
value: LIMIT_MIN,
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_PSWD_SETTINGS: PasswordSettings = {
|
||||
min_password_length: 6,
|
||||
require_digit: 0,
|
||||
require_special_character: 0,
|
||||
};
|
||||
|
||||
export const PlanType = {
|
||||
PAID: "paid",
|
||||
TRIAL: "trial",
|
||||
FREE: "free",
|
||||
};
|
||||
|
||||
export const CurrencySymbol: Currency = {
|
||||
usd: "$",
|
||||
};
|
||||
|
||||
export const DEEPGRAM_STT_ENPOINT = [
|
||||
{ name: "US (Default)", value: "" },
|
||||
{ name: "EU-hosted", value: "api.eu.deepgram.com" },
|
||||
];
|
||||
|
||||
// ElevenLabs API URI options
|
||||
export const ELEVENLABS_API_URI_OPTIONS = [
|
||||
{ name: "US", value: "api.elevenlabs.io" },
|
||||
{ name: "EU", value: "api.eu.residency.elevenlabs.io" },
|
||||
{ name: "IN", value: "api.in.residency.elevenlabs.io" },
|
||||
];
|
||||
|
||||
/** User scope values values */
|
||||
export const USER_ADMIN = "admin";
|
||||
export const USER_SP = "service_provider";
|
||||
export const USER_ACCOUNT = "account";
|
||||
|
||||
/** Speech credential test result status values */
|
||||
export const CRED_OK = "ok";
|
||||
export const CRED_FAIL = "fail";
|
||||
export const CRED_NOT_TESTED = "not tested";
|
||||
|
||||
/** Voip Carrier Register result status values */
|
||||
export const CARRIER_REG_OK = "ok";
|
||||
export const CARRIER_REG_FAIL = "fail";
|
||||
|
||||
export const PRIVACY_POLICY = "https://jambonz.org/privacy";
|
||||
export const TERMS_OF_SERVICE = "https://jambonz.org/terms";
|
||||
|
||||
/** API base paths */
|
||||
export const API_LOGIN = `${API_BASE_URL}/login`;
|
||||
export const API_LOGOUT = `${API_BASE_URL}/logout`;
|
||||
export const API_SBCS = `${API_BASE_URL}/Sbcs`;
|
||||
export const API_USERS = `${API_BASE_URL}/Users`;
|
||||
export const API_API_KEYS = `${API_BASE_URL}/ApiKeys`;
|
||||
export const API_ACCOUNTS = `${API_BASE_URL}/Accounts`;
|
||||
export const API_APPLICATIONS = `${API_BASE_URL}/Applications`;
|
||||
export const API_PHONE_NUMBERS = `${API_BASE_URL}/PhoneNumbers`;
|
||||
export const API_MS_TEAMS_TENANTS = `${API_BASE_URL}/MicrosoftTeamsTenants`;
|
||||
export const API_SERVICE_PROVIDERS = `${API_BASE_URL}/ServiceProviders`;
|
||||
export const API_CARRIERS = `${API_BASE_URL}/VoipCarriers`;
|
||||
export const API_SMPP_GATEWAY = `${API_BASE_URL}/SmppGateways`;
|
||||
export const API_SIP_GATEWAY = `${API_BASE_URL}/SipGateways`;
|
||||
export const API_PASSWORD_SETTINGS = `${API_BASE_URL}/PasswordSettings`;
|
||||
export const API_FORGOT_PASSWORD = `${API_BASE_URL}/forgot-password`;
|
||||
export const API_SYSTEM_INFORMATION = `${API_BASE_URL}/SystemInformation`;
|
||||
export const API_LCRS = `${API_BASE_URL}/Lcrs`;
|
||||
export const API_LCR_ROUTES = `${API_BASE_URL}/LcrRoutes`;
|
||||
export const API_LCR_CARRIER_SET_ENTRIES = `${API_BASE_URL}/LcrCarrierSetEntries`;
|
||||
export const API_TTS_CACHE = `${API_BASE_URL}/TtsCache`;
|
||||
export const API_CLIENTS = `${API_BASE_URL}/Clients`;
|
||||
export const API_REGISTER = `${API_BASE_URL}/register`;
|
||||
export const API_ACTIVATION_CODE = `${API_BASE_URL}/ActivationCode`;
|
||||
export const API_AVAILABILITY = `${API_BASE_URL}/Availability`;
|
||||
export const API_PRICE = `${API_BASE_URL}/Prices`;
|
||||
export const API_SUBSCRIPTIONS = `${API_BASE_URL}/Subscriptions`;
|
||||
export const API_CHANGE_PASSWORD = `${API_BASE_URL}/change-password`;
|
||||
export const API_SIGNIN = `${API_BASE_URL}/signin`;
|
||||
export const API_GOOGLE_CUSTOM_VOICES = `${API_BASE_URL}/GoogleCustomVoices`;
|
||||
export const API_APP_ENV = `${API_BASE_URL}/AppEnv`;
|
||||
1029
src/api/index.ts
@@ -1,87 +0,0 @@
|
||||
export interface JaegerRoot {
|
||||
resourceSpans: JaegerResourceSpan[];
|
||||
}
|
||||
|
||||
export interface JaegerResourceSpan {
|
||||
resource: JaegerResource;
|
||||
instrumentationLibrarySpans: InstrumentationLibrarySpan[];
|
||||
}
|
||||
|
||||
export interface JaegerResource {
|
||||
attributes: JaegerAttribute[];
|
||||
}
|
||||
|
||||
export interface InstrumentationLibrarySpan {
|
||||
instrumentationLibrary: InstrumentationLibrary;
|
||||
spans: JaegerSpan[];
|
||||
}
|
||||
|
||||
export interface InstrumentationLibrary {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface JaegerSpan {
|
||||
traceId: string;
|
||||
spanId: string;
|
||||
parentSpanId: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
startTimeUnixNano: number;
|
||||
endTimeUnixNano: number;
|
||||
attributes: JaegerAttribute[];
|
||||
}
|
||||
|
||||
export interface JaegerAttribute {
|
||||
key: string;
|
||||
value: JaegerValue;
|
||||
}
|
||||
|
||||
export interface WaveSurferSttResult {
|
||||
vendor: string;
|
||||
transcript: string;
|
||||
confidence: number;
|
||||
language_code: string;
|
||||
latency?: number;
|
||||
}
|
||||
|
||||
export interface WaveSurferTtsLatencyResult {
|
||||
vendor: string;
|
||||
latency: string;
|
||||
isCached: string;
|
||||
}
|
||||
|
||||
export interface WaveSurferGatherSpeechVerbHookLatencyResult {
|
||||
statusCode: number;
|
||||
latency: string;
|
||||
}
|
||||
|
||||
export interface WaveSurferDtmfResult {
|
||||
dtmf: string;
|
||||
duration: string;
|
||||
}
|
||||
|
||||
export interface JaegerValue {
|
||||
stringValue: string;
|
||||
doubleValue: string;
|
||||
boolValue: string;
|
||||
}
|
||||
|
||||
export interface JaegerGroup {
|
||||
level: number;
|
||||
startPx: number;
|
||||
endPx: number;
|
||||
durationPx: number;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
durationMs: number;
|
||||
traceId: string;
|
||||
spanId: string;
|
||||
parentSpanId: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
startTimeUnixNano: number;
|
||||
endTimeUnixNano: number;
|
||||
attributes: JaegerAttribute[];
|
||||
children: JaegerGroup[];
|
||||
}
|
||||
846
src/api/types.ts
@@ -1,846 +0,0 @@
|
||||
import type {
|
||||
JambonzResourceOptions,
|
||||
Language,
|
||||
Model,
|
||||
Vendor,
|
||||
VoiceLanguage,
|
||||
} from "src/vendor/types";
|
||||
|
||||
/** Simple types */
|
||||
|
||||
export type WebhookMethod = "POST" | "GET";
|
||||
|
||||
export type CredentialStatus = "ok" | "fail" | "not tested";
|
||||
|
||||
export type IpType = "ip" | "fqdn" | "fqdn-top-level" | "invalid";
|
||||
|
||||
export type LimitCategories =
|
||||
| "api_rate"
|
||||
| "voice_call_session"
|
||||
| "device"
|
||||
| "voice_call_minutes_license"
|
||||
| "voice_call_minutes"
|
||||
| "voice_call_session_license";
|
||||
|
||||
export type LimitUnit = "Session" | "Minute";
|
||||
|
||||
export interface LimitUnitOption {
|
||||
name: LimitUnit;
|
||||
value: Lowercase<LimitUnit>;
|
||||
}
|
||||
|
||||
/** User roles / permissions */
|
||||
export type UserScopes = "admin" | "service_provider" | "account";
|
||||
|
||||
export type LogLevel = "info" | "debug";
|
||||
|
||||
export type UserPermissions =
|
||||
| "VIEW_ONLY"
|
||||
| "PROVISION_SERVICES"
|
||||
| "PROVISION_USERS";
|
||||
|
||||
/** Status codes */
|
||||
|
||||
export enum StatusCodes {
|
||||
OK = 200,
|
||||
CREATED = 201,
|
||||
ACCEPTED = 202,
|
||||
NO_CONTENT = 204,
|
||||
BAD_REQUEST = 400,
|
||||
UNAUTHORIZED = 401,
|
||||
FORBIDDEN = 403,
|
||||
NOT_FOUND = 404,
|
||||
UNPROCESSABLE_ENTITY = 422,
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
/** SMPP temporarily unavailable */
|
||||
TEMPORARILY_UNAVAILABLE = 480,
|
||||
}
|
||||
|
||||
/** Fetch transport interfaces */
|
||||
|
||||
export interface FetchTransport<Type> {
|
||||
headers: Headers;
|
||||
status: StatusCodes;
|
||||
json: Type;
|
||||
blob?: Blob;
|
||||
}
|
||||
|
||||
export interface FetchError {
|
||||
status: StatusCodes;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export interface UseApiData {
|
||||
<Type>(
|
||||
apiPath: string,
|
||||
): [Type | undefined, () => void, FetchError | undefined];
|
||||
}
|
||||
|
||||
/** API related interfaces */
|
||||
|
||||
export interface UseApiDataMap<Type> {
|
||||
data?: Type;
|
||||
error?: FetchError;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
export interface WebhookOption {
|
||||
name: WebhookMethod;
|
||||
value: WebhookMethod;
|
||||
}
|
||||
|
||||
export interface SelectorOptions {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface DownloadedBlob {
|
||||
data_url: string;
|
||||
file_name: string;
|
||||
}
|
||||
|
||||
export interface CredentialTest {
|
||||
status: CredentialStatus;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface CredentialTestResult {
|
||||
stt: CredentialTest;
|
||||
tts: CredentialTest;
|
||||
}
|
||||
|
||||
export interface BucketCredentialTestResult {
|
||||
status: CredentialStatus;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface LimitField {
|
||||
label: string;
|
||||
category: LimitCategories;
|
||||
}
|
||||
|
||||
export interface PasswordSettings {
|
||||
min_password_length: number;
|
||||
require_digit: number;
|
||||
require_special_character: number;
|
||||
}
|
||||
|
||||
export interface ForgotPassword {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface SystemInformation {
|
||||
domain_name: null | string;
|
||||
sip_domain_name: null | string;
|
||||
monitoring_domain_name: null | string;
|
||||
private_network_cidr: null | string;
|
||||
log_level: LogLevel;
|
||||
}
|
||||
|
||||
export interface TtsCache {
|
||||
size: number;
|
||||
}
|
||||
|
||||
/** API responses/payloads */
|
||||
|
||||
export interface User {
|
||||
scope: UserScopes;
|
||||
user_sid: string;
|
||||
name: string;
|
||||
email: string;
|
||||
is_active: boolean;
|
||||
is_view_only: boolean;
|
||||
force_change: boolean;
|
||||
account_sid: string | null;
|
||||
account_name?: string | null;
|
||||
service_provider_sid: string | null;
|
||||
service_provider_name?: string | null;
|
||||
initial_password?: string;
|
||||
permissions?: UserPermissions[];
|
||||
provider?: null | string;
|
||||
}
|
||||
|
||||
export interface UserLogin {
|
||||
token: string;
|
||||
force_change: boolean;
|
||||
scope: UserScopes;
|
||||
user_sid: string;
|
||||
permissions: UserPermissions[];
|
||||
}
|
||||
|
||||
export interface UserLoginPayload {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UserUpdatePayload {
|
||||
old_password?: string;
|
||||
new_password?: string;
|
||||
initial_password: string | null;
|
||||
email: string;
|
||||
name: string;
|
||||
force_change: boolean;
|
||||
is_active: boolean;
|
||||
is_view_only: boolean;
|
||||
service_provider_sid: string | null;
|
||||
account_sid: string | null;
|
||||
}
|
||||
|
||||
export interface UserJWT {
|
||||
scope: UserScopes;
|
||||
user_sid: string;
|
||||
account_sid?: string | null;
|
||||
service_provider_sid?: string | null;
|
||||
permissions: UserPermissions[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CurrentUserData {
|
||||
user: User;
|
||||
account?: Account;
|
||||
subscription?: null | Subscription;
|
||||
}
|
||||
|
||||
export interface ServiceProvider {
|
||||
name: string;
|
||||
ms_teams_fqdn: null | string;
|
||||
service_provider_sid: string;
|
||||
lcr_sid: null | string;
|
||||
}
|
||||
|
||||
export interface Limit {
|
||||
category: LimitCategories;
|
||||
/** Empty string signals :DELETE */
|
||||
/** @see src/components/forms/local-limits */
|
||||
/** @see src/containers/internal/views/accounts/form */
|
||||
/** @see src/containers/internal/views/settings/index */
|
||||
quantity: number | string;
|
||||
account_sid?: string;
|
||||
account_limits_sid?: string;
|
||||
service_provider_sid?: string;
|
||||
service_provider_limits_sid?: string;
|
||||
}
|
||||
|
||||
export interface ApiKey {
|
||||
token: string;
|
||||
last_used: null | string;
|
||||
expires_at: null | string;
|
||||
created_at: string;
|
||||
api_key_sid: string;
|
||||
account_sid: null | string;
|
||||
service_provider_sid: null | string;
|
||||
}
|
||||
|
||||
export interface WebHook {
|
||||
url: string;
|
||||
method: WebhookMethod;
|
||||
username: null | string;
|
||||
password: null | string;
|
||||
webhook_sid?: null | string;
|
||||
}
|
||||
|
||||
export interface Sbc {
|
||||
ipv4: string;
|
||||
port: number | string;
|
||||
sbc_address_sid: string;
|
||||
service_provider_sid: null | string;
|
||||
}
|
||||
|
||||
export interface Smpp {
|
||||
ipv4: string;
|
||||
port: number | string;
|
||||
use_tls: boolean;
|
||||
is_primary: boolean;
|
||||
smpp_address_sid: string;
|
||||
service_provider_sid: null | string;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
name: string;
|
||||
sip_realm: null | string;
|
||||
root_domain?: null | string;
|
||||
account_sid: string;
|
||||
webhook_secret: string;
|
||||
siprec_hook_sid: null | string;
|
||||
queue_event_hook: null | WebHook;
|
||||
registration_hook: null | WebHook;
|
||||
service_provider_sid: string;
|
||||
device_calling_application_sid: null | string;
|
||||
record_all_calls: number;
|
||||
record_format?: null | string;
|
||||
bucket_credential: null | BucketCredential;
|
||||
plan_type?: string;
|
||||
device_to_call_ratio?: number;
|
||||
trial_end_date?: null | string;
|
||||
is_active: boolean;
|
||||
enable_debug_log: boolean;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
price_id?: null | string;
|
||||
product_sid?: null | string;
|
||||
name?: string;
|
||||
quantity?: number;
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
action?: null | string;
|
||||
payment_method_id?: null | string;
|
||||
account_subscription_sid?: null | string;
|
||||
stripe_customer_id?: null | string;
|
||||
products?: null | Product[];
|
||||
start_date?: string;
|
||||
status?: string;
|
||||
client_secret?: null | string;
|
||||
last4?: null | string;
|
||||
exp_month?: null | string;
|
||||
exp_year?: null | string;
|
||||
card_type?: null | string;
|
||||
reason?: null | string;
|
||||
dry_run?: boolean;
|
||||
currency?: null | string;
|
||||
prorated_cost?: number;
|
||||
monthly_cost?: number;
|
||||
next_invoice_date?: null | string;
|
||||
}
|
||||
|
||||
export interface AwsTag {
|
||||
Key: string;
|
||||
Value: string;
|
||||
}
|
||||
|
||||
export interface BucketCredential {
|
||||
vendor: null | string;
|
||||
region?: null | string;
|
||||
name?: null | string;
|
||||
access_key_id?: null | string;
|
||||
secret_access_key?: null | string;
|
||||
tags?: null | AwsTag[];
|
||||
service_key?: null | string;
|
||||
connection_string?: null | string;
|
||||
endpoint?: null | string;
|
||||
}
|
||||
|
||||
export interface Application {
|
||||
name: string;
|
||||
app_json: null | string;
|
||||
call_hook: null | WebHook;
|
||||
account_sid: null | string;
|
||||
application_sid: string;
|
||||
call_status_hook: null | WebHook;
|
||||
speech_synthesis_voice: null | string;
|
||||
speech_synthesis_vendor: null | Lowercase<Vendor>;
|
||||
speech_synthesis_language: null | string;
|
||||
speech_synthesis_label: null | string;
|
||||
speech_recognizer_vendor: null | Lowercase<Vendor>;
|
||||
speech_recognizer_language: null | string;
|
||||
speech_recognizer_label: null | string;
|
||||
record_all_calls: number;
|
||||
use_for_fallback_speech: number;
|
||||
fallback_speech_synthesis_vendor: null | string;
|
||||
fallback_speech_synthesis_language: null | string;
|
||||
fallback_speech_synthesis_voice: null | string;
|
||||
fallback_speech_synthesis_label: null | string;
|
||||
fallback_speech_recognizer_vendor: null | string;
|
||||
fallback_speech_recognizer_language: null | string;
|
||||
fallback_speech_recognizer_label: null | string;
|
||||
env_vars: null | Record<string, string | number | boolean>;
|
||||
}
|
||||
|
||||
export interface PhoneNumber {
|
||||
number: string;
|
||||
account_sid: null | string;
|
||||
application_sid: null | string;
|
||||
phone_number_sid: string;
|
||||
voip_carrier_sid: null | string;
|
||||
}
|
||||
|
||||
export interface MSTeamsTenant {
|
||||
tenant_fqdn: string;
|
||||
ms_teams_tenant_sid: string;
|
||||
account_sid: null | string;
|
||||
application_sid: null | string;
|
||||
service_provider_sid: string;
|
||||
}
|
||||
|
||||
export interface RecentCall {
|
||||
account_sid: string;
|
||||
call_sid: string;
|
||||
from: string;
|
||||
to: string;
|
||||
answered: boolean;
|
||||
sip_callid: string;
|
||||
sip_status: number;
|
||||
duration: number;
|
||||
attempted_at: number;
|
||||
answered_at: number;
|
||||
terminated_at: number;
|
||||
termination_reason: string;
|
||||
host: string;
|
||||
remote_host: string;
|
||||
direction: string;
|
||||
trunk: string;
|
||||
trace_id: string;
|
||||
recording_url?: string;
|
||||
}
|
||||
|
||||
export interface GoogleCustomVoice {
|
||||
google_custom_voice_sid?: string;
|
||||
speech_credential_sid?: string;
|
||||
name: string;
|
||||
reported_usage: string;
|
||||
model?: string;
|
||||
use_voice_cloning_key: number;
|
||||
voice_cloning_key?: string | null;
|
||||
voice_cloning_key_file?: File | null;
|
||||
}
|
||||
|
||||
export interface SpeechCredential {
|
||||
speech_credential_sid: string;
|
||||
service_provider_sid: null | string;
|
||||
account_sid: null | string;
|
||||
vendor: Lowercase<Vendor>;
|
||||
use_for_tts: number;
|
||||
use_for_stt: number;
|
||||
last_used: null | string;
|
||||
region: null | string;
|
||||
aws_region: null | string;
|
||||
api_key: null | string;
|
||||
role_arn: null | string;
|
||||
user_id: null | string;
|
||||
access_key_id: null | string;
|
||||
secret_access_key: null | string;
|
||||
service_key: null | string;
|
||||
use_custom_tts: number;
|
||||
custom_tts_endpoint_url: null | string;
|
||||
custom_tts_endpoint: null | string;
|
||||
use_custom_stt: number;
|
||||
custom_stt_endpoint_url: null | string;
|
||||
custom_stt_endpoint: null | string;
|
||||
client_id: null | string;
|
||||
client_secret: null | string;
|
||||
client_key: null | string;
|
||||
secret: null | string;
|
||||
nuance_tts_uri: null | string;
|
||||
nuance_stt_uri: null | string;
|
||||
tts_api_key: null | string;
|
||||
tts_region: null | string;
|
||||
stt_api_key: null | string;
|
||||
stt_region: null | string;
|
||||
instance_id: null | string;
|
||||
riva_server_uri: null | string;
|
||||
auth_token: null | string;
|
||||
custom_stt_url: null | string;
|
||||
custom_tts_url: null | string;
|
||||
custom_tts_streaming_url: null | string;
|
||||
label: null | string;
|
||||
cobalt_server_uri: null | string;
|
||||
model_id: null | string;
|
||||
stt_model_id: null | string;
|
||||
voice_engine: null | string;
|
||||
engine_version: null | string;
|
||||
service_version: null | string;
|
||||
model: null | string;
|
||||
options: null | string;
|
||||
deepgram_stt_uri: null | string;
|
||||
deepgram_tts_uri: null | string;
|
||||
deepgram_stt_use_tls: number;
|
||||
speechmatics_stt_uri: null | string;
|
||||
playht_tts_uri: null | string;
|
||||
resemble_tts_uri: null | string;
|
||||
resemble_tts_use_tls: number;
|
||||
api_uri: null | string;
|
||||
houndify_server_uri: null | string;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
time: number;
|
||||
account_sid: string;
|
||||
alert_type: string;
|
||||
message: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface CarrierRegisterStatus {
|
||||
status: null | string;
|
||||
reason: null | string;
|
||||
cseq: null | string;
|
||||
callId: null | string;
|
||||
}
|
||||
|
||||
export type DtmfType = "rfc2833" | "tones" | "info";
|
||||
|
||||
export type TrunkType = "static_ip" | "auth" | "reg";
|
||||
|
||||
export interface Carrier {
|
||||
voip_carrier_sid: string;
|
||||
name: string;
|
||||
description: null | string;
|
||||
is_active: boolean;
|
||||
service_provider_sid: string;
|
||||
account_sid: null | string;
|
||||
application_sid: null | string;
|
||||
e164_leading_plus: boolean;
|
||||
requires_register: boolean;
|
||||
register_username: null | string;
|
||||
register_password: null | string;
|
||||
register_sip_realm: null | string;
|
||||
register_from_user: null | string;
|
||||
register_from_domain: null | string;
|
||||
register_public_ip_in_contact: boolean;
|
||||
tech_prefix: null | string;
|
||||
diversion: null | string;
|
||||
inbound_auth_username: string;
|
||||
inbound_auth_password: string;
|
||||
smpp_system_id: null | string;
|
||||
smpp_password: null | string;
|
||||
smpp_inbound_system_id: null | string;
|
||||
smpp_inbound_password: null | string;
|
||||
smpp_enquire_link_interval: number;
|
||||
register_status: CarrierRegisterStatus;
|
||||
dtmf_type: DtmfType;
|
||||
outbound_sip_proxy: string | null;
|
||||
trunk_type: TrunkType;
|
||||
}
|
||||
|
||||
export interface PredefinedCarrier extends Carrier {
|
||||
requires_static_ip: boolean;
|
||||
predefined_carrier_sid: string;
|
||||
}
|
||||
|
||||
export interface Gateway {
|
||||
voip_carrier_sid: string;
|
||||
ipv4: string;
|
||||
netmask: number;
|
||||
inbound: number;
|
||||
outbound: number;
|
||||
}
|
||||
|
||||
export interface SipGateway extends Gateway {
|
||||
sip_gateway_sid?: null | string;
|
||||
is_active: boolean;
|
||||
protocol?: string;
|
||||
port: number | null;
|
||||
pad_crypto?: boolean;
|
||||
send_options_ping?: boolean;
|
||||
use_sips_scheme?: boolean;
|
||||
}
|
||||
|
||||
export interface SmppGateway extends Gateway {
|
||||
smpp_gateway_sid?: null | string;
|
||||
is_primary: boolean;
|
||||
use_tls: boolean;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface Lcr {
|
||||
lcr_sid?: null | string;
|
||||
is_active: boolean;
|
||||
name: null | string;
|
||||
default_carrier_set_entry_sid?: null | string;
|
||||
account_sid: null | string;
|
||||
service_provider_sid: null | string;
|
||||
number_routes?: number;
|
||||
}
|
||||
|
||||
export interface LcrRoute {
|
||||
lcr_route_sid?: null | string;
|
||||
lcr_sid: null | string;
|
||||
regex: null | string;
|
||||
description?: null | string;
|
||||
priority: number;
|
||||
lcr_carrier_set_entries?: LcrCarrierSetEntry[];
|
||||
}
|
||||
|
||||
export interface LcrCarrierSetEntry {
|
||||
lcr_carrier_set_entry_sid?: null | string;
|
||||
workload?: number;
|
||||
lcr_route_sid: null | string;
|
||||
voip_carrier_sid: null | string;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
client_sid?: null | string;
|
||||
account_sid: null | string;
|
||||
username: null | string;
|
||||
password?: null | string;
|
||||
is_active: boolean;
|
||||
allow_direct_app_calling: boolean;
|
||||
allow_direct_queue_calling: boolean;
|
||||
allow_direct_user_calling: boolean;
|
||||
}
|
||||
|
||||
export interface PageQuery {
|
||||
page: number;
|
||||
page_size?: number;
|
||||
count: number;
|
||||
start?: string;
|
||||
days?: number;
|
||||
}
|
||||
|
||||
export interface PhoneNumberQuery extends PageQuery {
|
||||
service_provider_sid?: string;
|
||||
account_sid?: string;
|
||||
filter?: string;
|
||||
}
|
||||
|
||||
export interface CallQuery extends PageQuery {
|
||||
direction?: string;
|
||||
answered?: string;
|
||||
}
|
||||
|
||||
export interface ApplicationQuery extends PageQuery {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface VoipCarrierQuery extends PageQuery {
|
||||
name?: string;
|
||||
account_sid?: string;
|
||||
}
|
||||
|
||||
export interface GoogleCustomVoicesQuery {
|
||||
speech_credential_sid?: string;
|
||||
label?: string;
|
||||
account_sid?: string;
|
||||
service_provider_sid: string;
|
||||
}
|
||||
|
||||
export interface PagedResponse<Type> {
|
||||
page_size: number;
|
||||
total: number;
|
||||
page: number;
|
||||
data: Type[];
|
||||
}
|
||||
|
||||
export interface SidResponse {
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface UserSidResponse {
|
||||
user_sid: string;
|
||||
}
|
||||
|
||||
export interface TokenResponse extends SidResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface SecretResponse {
|
||||
webhook_secret: string;
|
||||
}
|
||||
|
||||
export interface EmptyResponse {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface TotalResponse {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
service_provider_sid: string;
|
||||
provider: string;
|
||||
oauth2_code?: string;
|
||||
oauth2_state?: string;
|
||||
oauth2_client_id?: string;
|
||||
oauth2_redirect_uri?: string;
|
||||
locationBeforeAuth?: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
email_activation_code?: string;
|
||||
inviteCode?: string;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
jwt: string;
|
||||
user_sid: string;
|
||||
account_sid: string;
|
||||
root_domain: string;
|
||||
}
|
||||
|
||||
export interface ActivationCode {
|
||||
user_sid: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Availability {
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export interface Invoice {
|
||||
total: number;
|
||||
currency: null | string;
|
||||
next_payment_attempt: null | string;
|
||||
}
|
||||
|
||||
export type Currency = {
|
||||
[key: string]: null | string;
|
||||
};
|
||||
|
||||
export interface Recurring {
|
||||
aggregate_usage: null | string;
|
||||
interval: null | string;
|
||||
interval_count: number;
|
||||
trial_period_days: null | string;
|
||||
usage_type: string;
|
||||
}
|
||||
|
||||
export interface Price {
|
||||
billing_scheme: string;
|
||||
currency: string;
|
||||
recurring: Recurring;
|
||||
stripe_price_id: null | string;
|
||||
tiers_mode: null | string;
|
||||
tiers?: null | Tier[];
|
||||
type: null | string;
|
||||
unit_amount: null | number;
|
||||
unit_amount_decimal: null | string;
|
||||
}
|
||||
|
||||
export interface PriceInfo {
|
||||
category: null | string;
|
||||
description: null | string;
|
||||
name: null | string;
|
||||
prices: Price[];
|
||||
product_sid: null | string;
|
||||
stripe_product_id: null | string;
|
||||
unit_label: null | string;
|
||||
}
|
||||
|
||||
export interface StripeCustomerId {
|
||||
stripe_customer_id: null | string;
|
||||
}
|
||||
|
||||
export interface Tier {
|
||||
up_to: null | number;
|
||||
flat_amount: null | number;
|
||||
unit_amount: null | number;
|
||||
flat_amount_decimal: null | string;
|
||||
unit_amount_decimal: null | string;
|
||||
}
|
||||
|
||||
export interface ServiceData {
|
||||
category: null | string;
|
||||
name: null | string;
|
||||
service: null | string;
|
||||
fees: number;
|
||||
feesLabel: null | string;
|
||||
cost: number;
|
||||
capacity: number;
|
||||
invalid: boolean;
|
||||
currency: null | string;
|
||||
min: number;
|
||||
max: number;
|
||||
dirty: boolean;
|
||||
visible: boolean;
|
||||
required: boolean;
|
||||
billing_scheme?: null | string;
|
||||
stripe_price_id?: null | string;
|
||||
unit_label?: null | string;
|
||||
product_sid?: null | string;
|
||||
stripe_product_id?: null | string;
|
||||
tiers?: Tier[];
|
||||
}
|
||||
|
||||
export interface DeleteAccount {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ChangePassword {
|
||||
old_password: null | string;
|
||||
new_password: null | string;
|
||||
}
|
||||
|
||||
export interface SignIn {
|
||||
link?: null | string;
|
||||
jwt?: null | string;
|
||||
account_sid?: null | string;
|
||||
}
|
||||
|
||||
export interface GetLanguagesAndVoices {
|
||||
vendor: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SpeechSupportedLanguagesAndVoices {
|
||||
tts: VoiceLanguage[];
|
||||
stt: Language[];
|
||||
models: Model[];
|
||||
sttModels: Model[];
|
||||
}
|
||||
|
||||
export interface ElevenLabsOptions {
|
||||
optimize_streaming_latency: number;
|
||||
voice_settings: Partial<{
|
||||
similarity_boost: number;
|
||||
stability: number;
|
||||
style: number;
|
||||
use_speaker_boost: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface PlayHTOptions {
|
||||
quality: string;
|
||||
speed: number;
|
||||
seed: number;
|
||||
temperature: number;
|
||||
emotion: string;
|
||||
voice_guidance: number;
|
||||
style_guidance: number;
|
||||
text_guidance: number;
|
||||
}
|
||||
|
||||
export interface RimelabsOptions {
|
||||
speedAlpha: number;
|
||||
reduceLatency: boolean;
|
||||
}
|
||||
|
||||
export interface InworldOptions {
|
||||
audioConfig: {
|
||||
bitRate?: number;
|
||||
sampleRateHertz?: number;
|
||||
pitch?: number;
|
||||
speakingRate?: number;
|
||||
};
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
export type CartesiaEmotions =
|
||||
| "anger:lowest"
|
||||
| "anger:low"
|
||||
| "anger:high"
|
||||
| "anger:highest"
|
||||
| "positivity:lowest"
|
||||
| "positivity:low"
|
||||
| "positivity:high"
|
||||
| "positivity:highest"
|
||||
| "surprise:lowest"
|
||||
| "surprise:high"
|
||||
| "surprise:highest"
|
||||
| "sadness:lowest"
|
||||
| "sadness:low"
|
||||
| "curiosity:low"
|
||||
| "curiosity:high"
|
||||
| "curiosity:highest";
|
||||
|
||||
export interface CartesiaOptions {
|
||||
speed: number;
|
||||
emotion: CartesiaEmotions;
|
||||
}
|
||||
|
||||
export interface AppEnvProperty {
|
||||
description: string;
|
||||
type: string;
|
||||
required?: boolean;
|
||||
default?: string | number | boolean;
|
||||
obscure?: boolean;
|
||||
uiHint?: "input" | "textarea" | "filepicker";
|
||||
enum?: string[];
|
||||
jambonzResource?: "carriers";
|
||||
jambonzResourceOptions?: JambonzResourceOptions[];
|
||||
}
|
||||
|
||||
export interface AppEnv {
|
||||
[key: string]: AppEnvProperty;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from "react";
|
||||
import { H1 } from "@jambonz/ui-kit";
|
||||
|
||||
import { AccessControl } from "./access-control";
|
||||
|
||||
import type { ACLProps } from "./access-control";
|
||||
|
||||
/** Wrapper to pass different ACLs */
|
||||
const AccessControlTestWrapper = (props: Partial<ACLProps>) => {
|
||||
return (
|
||||
<AccessControl acl={props.acl!}>
|
||||
<div className="acl-div">
|
||||
<H1>ACL: {props.acl}</H1>
|
||||
</div>
|
||||
</AccessControl>
|
||||
);
|
||||
};
|
||||
|
||||
describe("<AccessControl>", () => {
|
||||
it("mounts", () => {
|
||||
cy.mountTestProvider(<AccessControlTestWrapper acl="hasAdminAuth" />);
|
||||
});
|
||||
|
||||
it("doesn't have teams fqdn ACL", () => {
|
||||
cy.mountTestProvider(<AccessControlTestWrapper acl="hasMSTeamsFqdn" />);
|
||||
|
||||
/** Default ACL disables MS Teams FQDN */
|
||||
cy.get(".acl-div").should("not.exist");
|
||||
});
|
||||
|
||||
it("has admin ACL", () => {
|
||||
cy.mountTestProvider(<AccessControlTestWrapper acl="hasAdminAuth" />);
|
||||
|
||||
/** Default ACL applies admin auth -- the singleton admin user */
|
||||
cy.get(".acl-div").should("exist");
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { useSelectState } from "src/store";
|
||||
|
||||
import type { ACL } from "src/store/types";
|
||||
|
||||
export type ACLProps = {
|
||||
acl: keyof ACL;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const AccessControl = ({ acl, children }: ACLProps) => {
|
||||
const accessControl = useSelectState("accessControl");
|
||||
|
||||
if (accessControl[acl]) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,105 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { sortLocaleName } from "src/utils";
|
||||
|
||||
import { AccountFilter } from "./account-filter";
|
||||
|
||||
import type { AccountFilterProps } from "./account-filter";
|
||||
import type { Account } from "src/api/types";
|
||||
|
||||
/** Import fixture data directly so we don't use cy.fixture() ... */
|
||||
import accounts from "../../cypress/fixtures/accounts.json";
|
||||
|
||||
/** Wrapper to perform React state setup */
|
||||
const AccountFilterTestWrapper = (props: Partial<AccountFilterProps>) => {
|
||||
const [account, setAccount] = useState("");
|
||||
|
||||
return (
|
||||
<AccountFilter
|
||||
label="Test"
|
||||
accounts={accounts as Account[]}
|
||||
account={[account, setAccount]}
|
||||
defaultOption={props.defaultOption}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe("<AccountFilter>", () => {
|
||||
/** The AccountFilter uses sort with `localeCompare` */
|
||||
const accountsSorted = accounts.sort(sortLocaleName);
|
||||
|
||||
it("mounts", () => {
|
||||
cy.mount(<AccountFilterTestWrapper />);
|
||||
});
|
||||
|
||||
it("has label text", () => {
|
||||
cy.mount(<AccountFilterTestWrapper />);
|
||||
|
||||
/** Label text is properly set */
|
||||
cy.get("label").should("have.text", "Test:");
|
||||
});
|
||||
|
||||
it("has default value", () => {
|
||||
cy.mount(<AccountFilterTestWrapper />);
|
||||
|
||||
/** Default value is properly set to first option */
|
||||
cy.get("input").should("have.value", accountsSorted[0].name);
|
||||
});
|
||||
|
||||
it("updates value onChange", () => {
|
||||
cy.mount(<AccountFilterTestWrapper />);
|
||||
|
||||
/** Assert onChange value updates */
|
||||
cy.get("input").clear();
|
||||
cy.get("input").type(accountsSorted[1].name);
|
||||
cy.get("input").should("have.value", accountsSorted[1].name);
|
||||
});
|
||||
|
||||
it("manages the focused state", () => {
|
||||
cy.mount(<AccountFilterTestWrapper />);
|
||||
|
||||
/** Test the `focused` state className (applied onFocus) */
|
||||
cy.get("input").clear();
|
||||
cy.get("input").type(accountsSorted[1].name);
|
||||
cy.get("input").parent().should("have.class", "focused");
|
||||
cy.get("input").blur();
|
||||
cy.get("input").parent().should("not.have.class", "focused");
|
||||
});
|
||||
|
||||
it("renders with default option", () => {
|
||||
/** Test with the `defaultOption` prop */
|
||||
cy.mount(<AccountFilterTestWrapper defaultOption />);
|
||||
/** No default value is set when this prop is present */
|
||||
cy.get("input").should("have.value", "All accounts");
|
||||
});
|
||||
|
||||
it("verify the typeahead dropdown", () => {
|
||||
/** Test by typing cus then custom account is selected */
|
||||
cy.mount(<AccountFilterTestWrapper defaultOption />);
|
||||
cy.get("input").clear();
|
||||
cy.get("input").type("cus");
|
||||
cy.get("div#account_filter-option-1").should("have.text", "custom account");
|
||||
});
|
||||
it("handles Enter key press", () => {
|
||||
cy.mount(<AccountFilterTestWrapper />);
|
||||
|
||||
cy.get("input").clear();
|
||||
cy.get("input").type("cus{enter}");
|
||||
cy.get("input").should("have.value", "custom account");
|
||||
});
|
||||
it("navigates down and up with arrow keys", () => {
|
||||
cy.mount(<AccountFilterTestWrapper />);
|
||||
|
||||
cy.get("input").clear();
|
||||
// Press arrow down to move to the first option
|
||||
cy.get("input").type("{downarrow}");
|
||||
cy.get("input").type("{enter}");
|
||||
cy.get("input").should("have.value", "default account");
|
||||
|
||||
// Press up to move to the previous option
|
||||
cy.get("input").type("{uparrow}");
|
||||
cy.get("input").type("{uparrow}");
|
||||
cy.get("input").type("{enter}");
|
||||
cy.get("input").should("have.value", "custom account");
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { classNames } from "@jambonz/ui-kit";
|
||||
|
||||
import { TypeaheadSelector } from "src/components/forms";
|
||||
|
||||
import type { Account } from "src/api/types";
|
||||
import { hasLength, sortLocaleName } from "src/utils";
|
||||
import { setAccountFilter } from "src/store/localStore";
|
||||
|
||||
export type AccountFilterProps = {
|
||||
label?: string;
|
||||
account: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
accounts?: Account[];
|
||||
defaultOption?: boolean;
|
||||
};
|
||||
|
||||
/** This will apply the selected account SID so you can filter local data */
|
||||
/** Currently used by: Applications, Recent Calls, Alerts, Carriers and Speech index views */
|
||||
export const AccountFilter = ({
|
||||
label = "Account",
|
||||
account: [accountSid, setAccountSid],
|
||||
accounts,
|
||||
defaultOption,
|
||||
}: AccountFilterProps) => {
|
||||
const classes = {
|
||||
smsel: true,
|
||||
"smsel--filter": true,
|
||||
"account-filter": true,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasLength(accounts) && !defaultOption) {
|
||||
setAccountSid(accounts[0].account_sid);
|
||||
}
|
||||
}, [accounts, defaultOption, setAccountSid]);
|
||||
|
||||
const options = [
|
||||
...(defaultOption ? [{ name: "All accounts", value: "" }] : []),
|
||||
...(hasLength(accounts)
|
||||
? accounts.sort(sortLocaleName).map((acct) => ({
|
||||
name: acct.name,
|
||||
value: acct.account_sid,
|
||||
}))
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={classNames(classes)}>
|
||||
{label && <label htmlFor="account_filter">{label}:</label>}
|
||||
<TypeaheadSelector
|
||||
id="account_filter"
|
||||
name="account_filter"
|
||||
value={accountSid}
|
||||
options={options}
|
||||
className="small"
|
||||
onChange={(e) => {
|
||||
setAccountSid(e.target.value);
|
||||
setAccountFilter(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { sortLocaleName } from "src/utils";
|
||||
|
||||
import { ApplicationFilter } from "./application-filter";
|
||||
|
||||
import type { ApplicationFilterProps } from "./application-filter";
|
||||
import type { Application } from "src/api/types";
|
||||
|
||||
/** Import fixture data directly so we don't use cy.fixture() ... */
|
||||
import applications from "../../cypress/fixtures/applications.json";
|
||||
|
||||
/** Wrapper to perform React state setup */
|
||||
const ApplicationFilterTestWrapper = (
|
||||
props: Partial<ApplicationFilterProps>,
|
||||
) => {
|
||||
const [application, setApplication] = useState("");
|
||||
|
||||
return (
|
||||
<ApplicationFilter
|
||||
label="Test"
|
||||
applications={applications as Application[]}
|
||||
application={[application, setApplication]}
|
||||
defaultOption={props.defaultOption}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe("<ApplicationFilter>", () => {
|
||||
/** The AccountFilter uses sort with `localeCompare` */
|
||||
const applicationsSorted = applications.sort(sortLocaleName);
|
||||
|
||||
it("mounts", () => {
|
||||
cy.mount(<ApplicationFilterTestWrapper />);
|
||||
});
|
||||
|
||||
it("has label text", () => {
|
||||
cy.mount(<ApplicationFilterTestWrapper />);
|
||||
|
||||
/** Label text is properly set */
|
||||
cy.get("label").should("have.text", "Test:");
|
||||
});
|
||||
|
||||
it("has default value", () => {
|
||||
cy.mount(<ApplicationFilterTestWrapper />);
|
||||
|
||||
/** Default value is properly set to first option */
|
||||
cy.get("select").should(
|
||||
"have.value",
|
||||
applicationsSorted[0].application_sid,
|
||||
);
|
||||
});
|
||||
|
||||
it("updates value onChange", () => {
|
||||
cy.mount(<ApplicationFilterTestWrapper />);
|
||||
|
||||
/** Assert onChange value updates */
|
||||
cy.get("select").select(applicationsSorted[1].application_sid);
|
||||
cy.get("select").should(
|
||||
"have.value",
|
||||
applicationsSorted[1].application_sid,
|
||||
);
|
||||
});
|
||||
|
||||
it("manages focused state", () => {
|
||||
cy.mount(<ApplicationFilterTestWrapper />);
|
||||
|
||||
/** Test the `focused` state className (applied onFocus) */
|
||||
cy.get("select").select(applicationsSorted[1].application_sid);
|
||||
cy.get(".application-filter").should("have.class", "focused");
|
||||
cy.get("select").blur();
|
||||
cy.get(".application-filter").should("not.have.class", "focused");
|
||||
});
|
||||
|
||||
it("renders default option", () => {
|
||||
/** Test with the `defaultOption` prop */
|
||||
cy.mount(
|
||||
<ApplicationFilterTestWrapper defaultOption="Choose Application" />,
|
||||
);
|
||||
|
||||
/** No default value is set when this prop is present */
|
||||
cy.get("select").should("have.value", "");
|
||||
|
||||
/** Validate that our prop renders correct default option text */
|
||||
cy.get("option").first().should("have.text", "Choose Application");
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { classNames } from "@jambonz/ui-kit";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
import { sortLocaleName } from "src/utils";
|
||||
|
||||
import type { Application } from "src/api/types";
|
||||
|
||||
export type ApplicationFilterProps = JSX.IntrinsicElements["select"] & {
|
||||
label?: string;
|
||||
application: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
applications?: Application[];
|
||||
defaultOption?: string;
|
||||
};
|
||||
|
||||
export const ApplicationFilter = ({
|
||||
label = "Application",
|
||||
application: [applicationSid, setApplicationSid],
|
||||
applications,
|
||||
defaultOption,
|
||||
}: ApplicationFilterProps) => {
|
||||
const [focus, setFocus] = useState(false);
|
||||
const classes = {
|
||||
smsel: true,
|
||||
"smsel--filter": true,
|
||||
"application-filter": true,
|
||||
focused: focus,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(classes)}>
|
||||
<label htmlFor="application_filter">{label}:</label>
|
||||
<div>
|
||||
<select
|
||||
id="application_filter"
|
||||
name="application_filter"
|
||||
value={applicationSid}
|
||||
onChange={(e) => setApplicationSid(e.target.value)}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
>
|
||||
{defaultOption && <option value="">{defaultOption}</option>}
|
||||
{applications &&
|
||||
applications.sort(sortLocaleName).map((app) => {
|
||||
return (
|
||||
<option key={app.application_sid} value={app.application_sid}>
|
||||
{app.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span>
|
||||
<Icons.ChevronUp />
|
||||
<Icons.ChevronDown />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
89
src/components/blocks/AntdTable.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components/macro";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import Loader from "../../components/blocks/Loader";
|
||||
|
||||
import Table from "antd/lib/table";
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
width: 100%;
|
||||
margin-top: 1rem !important;
|
||||
|
||||
table {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
|
||||
tr,
|
||||
th,
|
||||
td {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
height: 32px;
|
||||
|
||||
.ant-pagination-simple-pager {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-item {
|
||||
border: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLoader = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const AntdTable = ({ dataSource, columns, loading, ...rest }) => {
|
||||
let props = {
|
||||
...rest,
|
||||
dataSource,
|
||||
columns,
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
props = {
|
||||
...props,
|
||||
loading: {
|
||||
spinning: true,
|
||||
indicator: (
|
||||
<StyledLoader>
|
||||
<Loader />
|
||||
</StyledLoader>
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return <StyledTable {...props} />;
|
||||
};
|
||||
|
||||
AntdTable.propTypes = {
|
||||
dataSource: PropTypes.array,
|
||||
loading: PropTypes.bool,
|
||||
columns: PropTypes.array,
|
||||
};
|
||||
|
||||
AntdTable.defaultProps = {
|
||||
dataSource: [],
|
||||
loading: false,
|
||||
columns: [],
|
||||
};
|
||||
|
||||
export default AntdTable;
|
||||
27
src/components/blocks/Breadcrumbs.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as Chevron } from '../../images/Chevron.svg';
|
||||
import Link from '../elements/Link';
|
||||
|
||||
const BreadcrumbsContainer = styled.div`
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Breadcrumbs = props => {
|
||||
return (
|
||||
<BreadcrumbsContainer>
|
||||
{props.breadcrumbs.map((b, i) => (
|
||||
b.url
|
||||
? <React.Fragment key={i}>
|
||||
<Link to={b.url}>{b.name}</Link>
|
||||
<Chevron style={{ margin: '0 0.75rem' }} />
|
||||
</React.Fragment>
|
||||
: <span key={i}>{b.name}</span>
|
||||
))}
|
||||
</BreadcrumbsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Breadcrumbs;
|
||||
45
src/components/blocks/FormError.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as ErrorIcon } from '../../images/ErrorIcon.svg';
|
||||
|
||||
const FormErrorContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
background: RGBA(217, 28, 92, 0.2);
|
||||
${props => !props.grid && `margin-bottom: 1rem;`}
|
||||
color: #76042A;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
${props => props.grid && `grid-column: 2;`}
|
||||
& > div {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
& ul {
|
||||
margin: 0.25rem 0 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
& li {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const FormError = props => (
|
||||
<FormErrorContainer {...props}>
|
||||
<ErrorIcon />
|
||||
<div>
|
||||
{typeof props.message === 'object' && props.message.length ? (
|
||||
<ul>
|
||||
{props.message.map((message, i) => (
|
||||
<li key={i}>{message}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
props.message
|
||||
)}
|
||||
</div>
|
||||
</FormErrorContainer>
|
||||
);
|
||||
|
||||
export default FormError;
|
||||
37
src/components/blocks/Loader.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 2rem;
|
||||
${props => props.height && `
|
||||
height: ${props.height};
|
||||
`}
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Spinner = styled.div`
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
border: 4px solid #E3E3E3;
|
||||
border-top-color: #D91C5C;
|
||||
border-radius: 50%;
|
||||
animation: spin 1.25s linear infinite;
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(-45deg); }
|
||||
100% { transform: rotate(315deg); }
|
||||
}
|
||||
`;
|
||||
|
||||
const Loader = props => {
|
||||
return (
|
||||
<Container height={props.height}>
|
||||
<Spinner />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
129
src/components/blocks/Modal.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { ModalDispatchContext } from '../../contexts/ModalContext';
|
||||
import styled from 'styled-components/macro';
|
||||
import Button from '../elements/Button';
|
||||
import Loader from '../blocks/Loader';
|
||||
|
||||
const Overlay = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 90;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ModalContainer = styled.div`
|
||||
max-height: calc(100% - 2rem);
|
||||
max-width: 700px;
|
||||
overflow: auto;
|
||||
margin: 1rem;
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
background: #FFF;
|
||||
text-align: left;
|
||||
|
||||
& h1 {
|
||||
margin-top: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const LoaderContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -1.5rem;
|
||||
height: 100%;
|
||||
width: calc(100% + 3rem);
|
||||
background: #FFF;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
${props => props.normalPadding ? `
|
||||
margin-top: 1rem;
|
||||
` : `
|
||||
margin: 1rem -0.5rem -0.5rem 0;
|
||||
`}
|
||||
& > * {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Modal = props => {
|
||||
|
||||
// Handle modal context, which tells other elements to be disabled while modal is open
|
||||
const setModalOpen = useContext(ModalDispatchContext);
|
||||
useEffect(() => {
|
||||
setModalOpen(true);
|
||||
return () => setModalOpen(false);
|
||||
});
|
||||
|
||||
// Lock scroll on desktop and Android
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => document.body.style.overflow = 'auto';
|
||||
});
|
||||
|
||||
// Lock scroll on iOS
|
||||
useEffect(() => {
|
||||
const stopTouchScroll = e => e.preventDefault();
|
||||
window.addEventListener('touchmove', stopTouchScroll);
|
||||
return () => window.removeEventListener('touchmove', stopTouchScroll);
|
||||
});
|
||||
|
||||
// Close modal on Escape
|
||||
useEffect(() => {
|
||||
const closeOnEsc = e => {
|
||||
if (e.key === 'Escape' || e.key === 'Esc') {
|
||||
props.handleCancel();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', closeOnEsc);
|
||||
return () => window.removeEventListener('keydown', closeOnEsc);
|
||||
});
|
||||
|
||||
return (
|
||||
<Overlay onClick={props.handleCancel}>
|
||||
<ModalContainer onClick={e => e.stopPropagation()}>
|
||||
<h1>{props.title}</h1>
|
||||
<ContentContainer>
|
||||
{props.content}
|
||||
<ButtonContainer normalPadding={props.normalButtonPadding}>
|
||||
<Button inModal gray onClick={props.handleCancel}>
|
||||
{props.closeText || "Cancel"}
|
||||
</Button>
|
||||
{props.actionText && (
|
||||
<Button
|
||||
inModal
|
||||
disabled={props.loader}
|
||||
onClick={props.handleSubmit}
|
||||
>
|
||||
{props.actionText}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonContainer>
|
||||
{props.loader && (
|
||||
<LoaderContainer>
|
||||
<Loader />
|
||||
</LoaderContainer>
|
||||
)}
|
||||
</ContentContainer>
|
||||
</ModalContainer>
|
||||
</Overlay>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
251
src/components/blocks/Nav.js
Normal file
@@ -0,0 +1,251 @@
|
||||
/* 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;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.12);
|
||||
`;
|
||||
|
||||
const LogOutContainer = styled.div`
|
||||
margin-right: 3rem;
|
||||
@media (max-width: 34rem) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
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');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: "You've successfully logged out",
|
||||
});
|
||||
};
|
||||
|
||||
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>
|
||||
<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
|
||||
large
|
||||
gray
|
||||
text
|
||||
onClick={logOut}
|
||||
>
|
||||
Log Out
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Nav;
|
||||
128
src/components/blocks/Notification.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import React, { useContext } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import { ReactComponent as CheckGreen } from '../../images/CheckGreen.svg';
|
||||
import { ReactComponent as ErrorIcon } from '../../images/ErrorIcon.svg';
|
||||
|
||||
const NotificationContainer = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
`;
|
||||
|
||||
const NotificationDiv = styled.div`
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 28rem;
|
||||
padding: 0.75rem;
|
||||
background: #fff;
|
||||
border: 1px solid ${props => (
|
||||
props.level === 'success'
|
||||
? '#61c43e'
|
||||
: props.level === 'error'
|
||||
? '#D91C5C'
|
||||
: '#949494'
|
||||
)};
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
pointer-events: auto;
|
||||
& svg {
|
||||
flex-shrink: 0;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 1.5rem;
|
||||
color: #767676;
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
padding: 0.25rem;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
border-color: #767676;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
color: #d91c5c;
|
||||
}
|
||||
`;
|
||||
|
||||
const InfoIcon = styled.span`
|
||||
flex-shrink: 0;
|
||||
margin-right: 0.75rem;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background: #949494;
|
||||
color: #FFF;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const Notification = props => {
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
return (
|
||||
<NotificationContainer>
|
||||
{props.notifications.map(n => (
|
||||
<NotificationDiv
|
||||
key={n.id}
|
||||
level={n.level}
|
||||
>
|
||||
{n.level === 'success'
|
||||
? <CheckGreen />
|
||||
: n.level === 'error'
|
||||
? <ErrorIcon />
|
||||
: <InfoIcon>i</InfoIcon>
|
||||
}
|
||||
<span>{n.message}</span>
|
||||
<CloseButton
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: 'REMOVE',
|
||||
id: n.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span tabIndex="-1">×</span>
|
||||
</CloseButton>
|
||||
</NotificationDiv>
|
||||
))}
|
||||
</NotificationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notification;
|
||||
113
src/components/blocks/ProgressVisualization.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Container = styled.div`
|
||||
height: 8rem;
|
||||
width: 45rem;
|
||||
max-width: 100%;
|
||||
padding: 3.5rem;
|
||||
@media (max-width: 30rem) {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Step = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
color: #FFF;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
border-radius: 50%;
|
||||
${props => props.incomplete ? `
|
||||
border: 0.25rem solid #A6A6A6;
|
||||
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.08),
|
||||
0 0 3px rgba(0, 0, 0, 0.08),
|
||||
inset 0 3px 3px rgba(0, 0, 0, 0.08),
|
||||
inset 0 0 3px rgba(0, 0, 0, 0.08);
|
||||
` : `
|
||||
background: #D91C5C;
|
||||
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.08),
|
||||
0 0 3px rgba(0, 0, 0, 0.08);
|
||||
`
|
||||
}
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
const Line = styled.div`
|
||||
width: 50%;
|
||||
height: 0.25rem;
|
||||
margin: -2px;
|
||||
background: ${props => props.incomplete
|
||||
? '#A6A6A6'
|
||||
: '#D91C5C'
|
||||
};
|
||||
`;
|
||||
|
||||
const Checkmark = styled.div`
|
||||
height: 6px;
|
||||
width: 11px;
|
||||
border-left: 2px solid #FFF;
|
||||
border-bottom: 2px solid #FFF;
|
||||
transform: rotate(-45deg);
|
||||
`;
|
||||
|
||||
const Title = styled.span`
|
||||
position: absolute;
|
||||
top: 2.5rem;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
@media (max-width: 30rem) {
|
||||
white-space: normal;
|
||||
}
|
||||
color: ${props => props.active ? '#D91C5C' : '#767676'};
|
||||
font-weight: ${props => props.active ? 'bold' : 'normal'};
|
||||
`;
|
||||
|
||||
const ProgressVisualization = props => (
|
||||
!props.progress
|
||||
? <Container />
|
||||
: <Container>
|
||||
<Step>
|
||||
{props.progress === 1
|
||||
? '1'
|
||||
: <Checkmark />
|
||||
}
|
||||
<Title active={props.progress === 1}>
|
||||
Configure Account
|
||||
</Title>
|
||||
</Step>
|
||||
<Line incomplete={props.progress < 2} />
|
||||
<Step incomplete={props.progress < 2} >
|
||||
{props.progress < 2
|
||||
? null
|
||||
: props.progress === 2
|
||||
? '2'
|
||||
: <Checkmark />
|
||||
}
|
||||
<Title active={props.progress === 2}>
|
||||
Create Application
|
||||
</Title>
|
||||
</Step>
|
||||
<Line incomplete={props.progress < 3} />
|
||||
<Step incomplete={props.progress < 3} >
|
||||
{props.progress < 3
|
||||
? null
|
||||
: props.progress === 3
|
||||
? '3'
|
||||
: <Checkmark />
|
||||
}
|
||||
<Title active={props.progress === 3}>
|
||||
Configure SIP Trunk
|
||||
</Title>
|
||||
</Step>
|
||||
</Container>
|
||||
);
|
||||
|
||||
export default ProgressVisualization;
|
||||
94
src/components/blocks/Sbcs.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
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;
|
||||
${props => props.centered && `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
& ul {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const Sbcs = props => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
// const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const [ sbcs, setSbcs ] = useState('');
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const sbcResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
// url: `/Sbcs?service_provider_sid=${currentServiceProvider}`,
|
||||
url: '/Sbcs',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
setSbcs(sbcResults.data);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const text = 'Have your SIP trunking provider(s) send calls to';
|
||||
return (
|
||||
sbcs.length > 1
|
||||
? <Container centered={props.centered}>
|
||||
{text}:
|
||||
<ul>
|
||||
{sbcs.map(sbc => (
|
||||
<li key={sbc.sbc_address_sid}>
|
||||
{`${sbc.ipv4}:${sbc.port}`}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Container>
|
||||
: sbcs.length === 1
|
||||
? <Container>
|
||||
{text} {sbcs[0].ipv4}:{sbcs[0].port}
|
||||
</Container>
|
||||
: null
|
||||
);
|
||||
};
|
||||
|
||||
export default Sbcs;
|
||||
122
src/components/blocks/SideMenu.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
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 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;
|
||||
flex-shrink: 0;
|
||||
height: calc(100vh - 4rem);
|
||||
overflow: auto;
|
||||
background: #FFF;
|
||||
padding-top: 3.25rem;
|
||||
`;
|
||||
|
||||
const activeClassName = 'nav-item-active';
|
||||
|
||||
const StyledNavLink = styled(NavLink).attrs({ activeClassName })`
|
||||
height: 2.75rem;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
color: #565656;
|
||||
fill: #565656;
|
||||
|
||||
&.${activeClassName} {
|
||||
color: #D91C5C;
|
||||
fill: #D91C5C;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: RGBA(217, 28, 92, 0.1);
|
||||
color: #C0134D;
|
||||
fill: #C0134D;
|
||||
}
|
||||
&.${activeClassName}:hover {
|
||||
color: #D91C5C;
|
||||
fill: #D91C5C;
|
||||
}
|
||||
`;
|
||||
|
||||
const IconContainer = styled.span`
|
||||
width: 3rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: 0;
|
||||
`;
|
||||
|
||||
const MenuText = styled.span`
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
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 (
|
||||
<StyledNavLink
|
||||
to={props.to}
|
||||
activeClassName={activeClassName}
|
||||
tabIndex={modalOpen ? '-1' : ''}
|
||||
>
|
||||
<IconContainer tabIndex="-1">
|
||||
{props.icon}
|
||||
</IconContainer>
|
||||
<MenuText tabIndex="-1">
|
||||
{props.name}
|
||||
</MenuText>
|
||||
</StyledNavLink>
|
||||
);
|
||||
};
|
||||
|
||||
const SideMenu = () => {
|
||||
const showMsTeams = useContext(ShowMsTeamsStateContext);
|
||||
const getMsTeamsData = useContext(ShowMsTeamsDispatchContext);
|
||||
useEffect(() => {
|
||||
getMsTeamsData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
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/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 />} />
|
||||
)}
|
||||
</StyledSideMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default SideMenu;
|
||||
463
src/components/blocks/TableContent.js
Normal file
@@ -0,0 +1,463 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import Table from '../elements/Table.js';
|
||||
import Button from '../elements/Button.js';
|
||||
import Checkbox from '../elements/Checkbox.js';
|
||||
import TableMenu from '../blocks/TableMenu.js';
|
||||
import Loader from '../blocks/Loader.js';
|
||||
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;
|
||||
&:first-child {
|
||||
font-weight: 500;
|
||||
padding-right: 1.5rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
& ul {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const TableContent = props => {
|
||||
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
const [ showTableLoader, setShowTableLoader ] = useState(true);
|
||||
const [ showModalLoader, setShowModalLoader ] = useState(false);
|
||||
const [ content, setContent ] = useState([]);
|
||||
const [ contentToDelete, setContentToDelete ] = useState({});
|
||||
|
||||
//=============================================================================
|
||||
// Get and sort content
|
||||
//=============================================================================
|
||||
const [ sort, setSort ] = useState({
|
||||
column: props.columns[0].key,
|
||||
order: 'asc',
|
||||
});
|
||||
const sortTableContent = ({ newContent, column }) => {
|
||||
const newSortOrder = sort.column === column
|
||||
? sort.order === 'asc'
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
: 'asc';
|
||||
column = column || sort.column;
|
||||
newContent = newContent || content;
|
||||
const sortedContent = [...newContent];
|
||||
sortedContent.sort((a, b) => {
|
||||
let valA;
|
||||
let valB;
|
||||
if (!a[column]) {
|
||||
valA = '';
|
||||
valB = '';
|
||||
} else if (typeof a[column] === 'object') {
|
||||
if (a[column].type === 'masked') {
|
||||
valA = a[column].masked;
|
||||
valB = b[column].masked;
|
||||
}
|
||||
if (a[column].type === 'normal') {
|
||||
valA = a[column].content;
|
||||
valB = b[column].content;
|
||||
}
|
||||
} else {
|
||||
valA = (a[column] && a[column].toLowerCase()) || '';
|
||||
valB = (b[column] && b[column].toLowerCase()) || '';
|
||||
}
|
||||
if (newSortOrder === 'asc') {
|
||||
return valA > valB ? 1 : valA < valB ? -1 : 0;
|
||||
} else {
|
||||
return valA < valB ? 1 : valA > valB ? -1 : 0;
|
||||
}
|
||||
});
|
||||
setContent(sortedContent);
|
||||
setSort({
|
||||
column,
|
||||
order: newSortOrder
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
const getNewContent = async () => {
|
||||
const newContent = await props.getContent();
|
||||
sortTableContent({ newContent });
|
||||
setShowTableLoader(false);
|
||||
};
|
||||
getNewContent();
|
||||
// eslint-disable-next-line
|
||||
}, [props.getContent]);
|
||||
|
||||
//=============================================================================
|
||||
// Handle checkboxes
|
||||
//=============================================================================
|
||||
const [ selected, setSelected ] = useState([]);
|
||||
const checkboxesToggleAll = e => {
|
||||
if (content.length === selected.length) {
|
||||
setSelected([]);
|
||||
} else {
|
||||
setSelected(content.map(c => c.sid));
|
||||
}
|
||||
};
|
||||
const checkboxesToggleOne = e => {
|
||||
const sid = e.target.value;
|
||||
setSelected(prev => {
|
||||
if (prev.includes(sid)) {
|
||||
return prev.filter(p => p !== sid);
|
||||
} else {
|
||||
return [...prev, sid];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleBulkAction = async (selected, i) => {
|
||||
setShowTableLoader(true);
|
||||
const success = await props.bulkAction(selected, i);
|
||||
if (success) {
|
||||
const newContent = await props.getContent();
|
||||
sortTableContent({ newContent });
|
||||
setSelected([]);
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: 'Number routing updated',
|
||||
});
|
||||
}
|
||||
setShowTableLoader(false);
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Handle Open Menus (i.e. bulk action menu or 3 dots on right of each row)
|
||||
//=============================================================================
|
||||
const [ menuOpen, setMenuOpen ] = useState(null);
|
||||
useEffect(() => {
|
||||
const hideMenu = () => setMenuOpen(null);
|
||||
window.addEventListener('click', hideMenu);
|
||||
return () => window.removeEventListener('click', hideMenu);
|
||||
}, []);
|
||||
const handleMenuOpen = sid => {
|
||||
if (menuOpen === sid) {
|
||||
setMenuOpen(null);
|
||||
} else {
|
||||
setMenuOpen(sid);
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Handle Adding content
|
||||
//=============================================================================
|
||||
const [ showNewContentModal, setShowNewContentModal ] = useState(false);
|
||||
const [ showNewContentLoader, setShowNewContentLoader ] = useState(false);
|
||||
const [ newItem, setNewItem ] = useState('');
|
||||
const addContent = async () => {
|
||||
setShowNewContentModal(true);
|
||||
setShowNewContentLoader(true);
|
||||
const result = await props.addContent();
|
||||
if (result !== 'error') {
|
||||
const newContent = await props.getContent();
|
||||
sortTableContent({ newContent });
|
||||
setNewItem(result);
|
||||
} else {
|
||||
setShowNewContentModal(false);
|
||||
}
|
||||
setShowNewContentLoader(false);
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Handle Deleting content
|
||||
//=============================================================================
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
const deleteContent = async () => {
|
||||
setShowModalLoader(true);
|
||||
const result = await props.deleteContent(contentToDelete);
|
||||
if (result === 'success') {
|
||||
const newContent = await props.getContent();
|
||||
sortTableContent({ newContent });
|
||||
setContentToDelete({});
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: `${props.name.charAt(0).toUpperCase()}${props.name.slice(1)} deleted successfully`,
|
||||
});
|
||||
} else {
|
||||
setErrorMessage(result);
|
||||
}
|
||||
setSelected([]);
|
||||
setShowModalLoader(false);
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<React.Fragment>
|
||||
{showNewContentModal && (
|
||||
<Modal
|
||||
title={`Here is your new ${props.name}`}
|
||||
closeText="Close"
|
||||
loader={showNewContentLoader}
|
||||
content={
|
||||
<CopyableText
|
||||
text={newItem}
|
||||
textType={props.name}
|
||||
inModal
|
||||
hasBorder
|
||||
/>
|
||||
}
|
||||
handleCancel={() => setShowNewContentModal(false)}
|
||||
normalButtonPadding
|
||||
/>
|
||||
)}
|
||||
|
||||
{contentToDelete && (
|
||||
contentToDelete.name ||
|
||||
contentToDelete.number ||
|
||||
contentToDelete.tenant_fqdn ||
|
||||
contentToDelete.token ||
|
||||
contentToDelete.vendor
|
||||
) && (
|
||||
<Modal
|
||||
title={`Are you sure you want to delete the following ${props.name}?`}
|
||||
loader={showModalLoader}
|
||||
content={
|
||||
<div>
|
||||
<table>
|
||||
<tbody>
|
||||
{props.formatContentToDelete(contentToDelete).map((d, i) => (
|
||||
<tr key={i}>
|
||||
<Td>{d.name}</Td>
|
||||
<Td>
|
||||
{typeof d.content === 'string'
|
||||
? d.content
|
||||
: <ul>
|
||||
{d.content.map((c, i) => (
|
||||
<li key={i}>{c}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
</Td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{errorMessage && (
|
||||
<FormError message={errorMessage} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
handleCancel={() => {
|
||||
setContentToDelete({});
|
||||
setErrorMessage('');
|
||||
}}
|
||||
handleSubmit={deleteContent}
|
||||
actionText="Delete"
|
||||
/>
|
||||
)}
|
||||
<Table
|
||||
withCheckboxes={props.withCheckboxes}
|
||||
rowsHaveDeleteButtons={props.rowsHaveDeleteButtons}
|
||||
>
|
||||
{/* colgroup is used to set the width of the last column because the
|
||||
last two <th> are combined in a colSpan="2", preventing the columns from
|
||||
being given an expicit width (`table-layout: fixed;` requires setting
|
||||
column width in the first row) */}
|
||||
{!props.rowsHaveDeleteButtons && (
|
||||
<colgroup>
|
||||
<col
|
||||
span={
|
||||
props.withCheckboxes
|
||||
? props.columns.length + 1
|
||||
: props.columns.length
|
||||
}
|
||||
/>
|
||||
<col style={{ width: '4rem' }}></col>
|
||||
</colgroup>
|
||||
)}
|
||||
<thead>
|
||||
<tr>
|
||||
{props.withCheckboxes && (
|
||||
<th>
|
||||
<Button
|
||||
checkbox={
|
||||
!selected.length
|
||||
? 'none'
|
||||
: content.length === selected.length
|
||||
? 'all'
|
||||
: 'partial'
|
||||
}
|
||||
onClick={checkboxesToggleAll}
|
||||
/>
|
||||
</th>
|
||||
)}
|
||||
{props.columns.map((c, i) => (
|
||||
<th
|
||||
key={c.key}
|
||||
style={{ width: c.width }}
|
||||
colSpan={!props.addContent && (i === props.columns.length - 1) ? '2' : null}
|
||||
>
|
||||
{selected.length && i === props.columns.length - 1 ? (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
marginLeft: '-1rem',
|
||||
}}
|
||||
>
|
||||
<TableMenu
|
||||
bulkEditMenu
|
||||
buttonText="Choose Application"
|
||||
sid="bulk-menu"
|
||||
open={menuOpen === 'bulk-menu'}
|
||||
handleMenuOpen={handleMenuOpen}
|
||||
disabled={modalOpen}
|
||||
menuItems={
|
||||
props.bulkMenuItems.map(i => ({
|
||||
name: i.name,
|
||||
type: 'button',
|
||||
action: () => handleBulkAction(selected, i),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
gray
|
||||
tableHeaderLink
|
||||
onClick={() => sortTableContent({ column: c.key })}
|
||||
>
|
||||
{c.header}
|
||||
{sort.column === c.key
|
||||
? sort.order === 'asc'
|
||||
? <span>▴</span>
|
||||
: <span>▾</span>
|
||||
: null
|
||||
}
|
||||
</Button>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
{props.addContent && (
|
||||
<th>
|
||||
<Button onClick={addContent}>+</Button>
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{showTableLoader ? (
|
||||
<tr>
|
||||
<td colSpan={props.withCheckboxes ? props.columns.length + 1 : props.columns.length}>
|
||||
<Loader height={'71px'} />
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
!content || !content.length ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={props.withCheckboxes ? props.columns.length + 1 : props.columns.length}
|
||||
style={{ textAlign: 'center' }}
|
||||
>
|
||||
No {props.name}s
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
content.map(a => (
|
||||
<tr key={a.sid}>
|
||||
{props.withCheckboxes && (
|
||||
<td>
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
id={a.sid}
|
||||
value={a.sid}
|
||||
onChange={checkboxesToggleOne}
|
||||
checked={selected.includes(a.sid)}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{props.columns.map((c, i) => {
|
||||
let columnContent = '';
|
||||
let columnTitle = null;
|
||||
if (a[c.key]) {
|
||||
if (typeof a[c.key] === 'object') {
|
||||
if (a[c.key].type === 'normal') {
|
||||
columnContent = a[c.key].content;
|
||||
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];
|
||||
columnTitle = columnContent;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<td key={c.key} style={{ fontWeight: c.fontWeight }}>
|
||||
{i === 0 && props.urlParam
|
||||
? <span>
|
||||
<Link
|
||||
to={`/internal/${props.urlParam}/${a.sid}/edit`}
|
||||
tabIndex={modalOpen ? '-1' : ''}
|
||||
>
|
||||
<span tabIndex="-1" title={columnTitle}>
|
||||
{columnContent}
|
||||
</span>
|
||||
</Link>
|
||||
</span>
|
||||
: <span title={columnTitle}>{columnContent}</span>
|
||||
}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td>
|
||||
{props.rowsHaveDeleteButtons ? (
|
||||
<Button
|
||||
gray
|
||||
onClick={() => setContentToDelete(a)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
) : (
|
||||
<TableMenu
|
||||
sid={a.sid}
|
||||
open={menuOpen === a.sid}
|
||||
handleMenuOpen={handleMenuOpen}
|
||||
disabled={modalOpen}
|
||||
menuItems={[
|
||||
{
|
||||
name: 'Edit',
|
||||
type: 'link',
|
||||
url: `/internal/${props.urlParam}/${a.sid}/edit`,
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
type: 'button',
|
||||
action: () => setContentToDelete(a),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableContent;
|
||||
104
src/components/blocks/TableMenu.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
import { ReactComponent as MenuDots } from '../../images/MenuDots.svg';
|
||||
import Button from '../elements/Button';
|
||||
|
||||
const Container = styled.div`
|
||||
position: absolute;
|
||||
right: ${props => props.bulkEditMenu
|
||||
? '0'
|
||||
: '1.75rem'
|
||||
};
|
||||
top: ${props => props.bulkEditMenu
|
||||
? 'calc(100% + 0.25rem)'
|
||||
: '3rem'
|
||||
};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 0.5rem 0;
|
||||
border-radius: 0.25rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 0.5rem 0.5rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.5rem rgba(0, 0, 0, 0.12);
|
||||
z-index: 70;
|
||||
`;
|
||||
|
||||
const buttonLink = css`
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
color: #565656;
|
||||
outline: none;
|
||||
|
||||
& > span {
|
||||
outline: none;
|
||||
line-height: 1rem;
|
||||
padding: 1rem;
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
box-shadow: inset 0 0 0 0.125rem #D91C5C;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
background: #EEE;
|
||||
}
|
||||
`;
|
||||
|
||||
const MenuLink = styled(Link)`
|
||||
${buttonLink}
|
||||
`;
|
||||
|
||||
const MenuButton = styled.button`
|
||||
${buttonLink}
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const TableMenu = props => (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
bulkEditMenu={props.bulkEditMenu}
|
||||
tableMenu={!props.bulkEditMenu}
|
||||
selected={props.open}
|
||||
disabled={props.disabled}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
props.handleMenuOpen(props.sid);
|
||||
}}
|
||||
>
|
||||
{props.buttonText || <MenuDots />}
|
||||
</Button>
|
||||
{props.open && (
|
||||
<Container
|
||||
bulkEditMenu={props.bulkEditMenu}
|
||||
>
|
||||
{props.menuItems.map((m, i) => (
|
||||
m.type === 'link'
|
||||
? <MenuLink key={i} to={m.url}>
|
||||
<span tabIndex="-1">
|
||||
{m.name}
|
||||
</span>
|
||||
</MenuLink>
|
||||
: <MenuButton key={i}
|
||||
onClick={m.action}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
{m.name}
|
||||
</span>
|
||||
</MenuButton>
|
||||
))}
|
||||
</Container>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
export default TableMenu;
|
||||
60
src/components/blocks/ToggleText.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as ViewPassword } from '../../images/ViewPassword.svg';
|
||||
import { ReactComponent as HidePassword } from '../../images/HidePassword.svg';
|
||||
|
||||
const Container = styled.span`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const ToggleVisibilityButton = styled.button`
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
left: 22rem;
|
||||
width: 2.5rem;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
padding: 0;
|
||||
|
||||
& > span {
|
||||
height: 2.25rem;
|
||||
width: 2.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: 0;
|
||||
border-radius: 0.25rem;
|
||||
fill: #767676;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
fill: #565656;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
box-shadow: inset 0 0 0 0.125rem #767676;
|
||||
}
|
||||
`;
|
||||
|
||||
const ToggleText = props => {
|
||||
const [ mode, setMode ] = useState('masked');
|
||||
return (
|
||||
<Container>
|
||||
{mode === 'masked' ? props.masked : props.revealed}
|
||||
<ToggleVisibilityButton
|
||||
onClick={() => setMode(mode === 'masked' ? 'revealed' : 'masked')}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
{mode === 'masked'
|
||||
? <ViewPassword />
|
||||
: <HidePassword />
|
||||
}
|
||||
</span>
|
||||
</ToggleVisibilityButton>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleText;
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
import { useToast } from "../toast/toast-provider";
|
||||
|
||||
type ClipBoardProps = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
/** Clipboard support...? */
|
||||
const hasClipboard = typeof navigator.clipboard !== "undefined";
|
||||
|
||||
export const ClipBoard = ({ text, id = "", name = "" }: ClipBoardProps) => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const handleClick = () => {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
toastSuccess(
|
||||
<>
|
||||
<strong>{text}</strong> copied to clipboard
|
||||
</>,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toastError(
|
||||
<>
|
||||
Unable to copy <strong>{text}</strong>, please select the text and
|
||||
right click to copy
|
||||
</>,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="clipboard inpbtn">
|
||||
<input id={id} name={name} type="text" readOnly value={text} />
|
||||
{hasClipboard && (
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
title="Copy to clipboard"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Icons.Clipboard />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from "react";
|
||||
import { Icons } from "../icons";
|
||||
import "./styles.scss";
|
||||
|
||||
type DomainInputProbs = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
value: string;
|
||||
setValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
root_domain: string;
|
||||
placeholder?: string;
|
||||
is_valid: boolean;
|
||||
};
|
||||
|
||||
export const DomainInput = ({
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
setValue,
|
||||
root_domain,
|
||||
is_valid,
|
||||
placeholder,
|
||||
}: DomainInputProbs) => {
|
||||
return (
|
||||
<>
|
||||
<div className="clipboard clipboard-domain">
|
||||
<div className="input-container">
|
||||
<input
|
||||
id={id}
|
||||
name={name}
|
||||
type="text"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<div className={`input-icon txt--${is_valid ? "teal" : "red"}`}>
|
||||
{is_valid ? <Icons.CheckCircle /> : <Icons.XCircle />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="root-domain">
|
||||
<p>{root_domain}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainInput;
|
||||
@@ -1,55 +0,0 @@
|
||||
@use "../../styles/vars";
|
||||
@use "../../styles/mixins";
|
||||
@use "@jambonz/ui-kit/src/styles/vars" as ui-vars;
|
||||
@use "@jambonz/ui-kit/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.clipboard-domain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
width: 100%;
|
||||
height: vars.$clipheight;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.internal form & {
|
||||
max-width: calc(#{vars.$widthinput} - #{vars.$clipheight});
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.root-domain {
|
||||
height: vars.$clipheight;
|
||||
border-bottom-right-radius: ui-vars.$px01;
|
||||
border-top-right-radius: ui-vars.$px01;
|
||||
border: 2px solid ui-vars.$grey;
|
||||
border-left: 0;
|
||||
background-color: ui-vars.$pink;
|
||||
padding: ui-vars.$px01;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&[disabled] {
|
||||
@include mixins.disabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
type EditBoardProps = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
text: string;
|
||||
path: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export const EditBoard = ({
|
||||
text,
|
||||
id = "",
|
||||
name = "",
|
||||
path,
|
||||
title,
|
||||
}: EditBoardProps) => {
|
||||
const navigate = useNavigate();
|
||||
const handleClick = () => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="clipboard inpbtn">
|
||||
<input id={id} name={name} type="text" readOnly value={text} />
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
title={title ? title : "Edit"}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Icons.Edit />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
87
src/components/elements/AddButton.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link } 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)`
|
||||
position: absolute;
|
||||
top: 7rem;
|
||||
right: 3rem;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
grid-column: 2;
|
||||
border-radius: 50%;
|
||||
text-decoration: none;
|
||||
color: #565656;
|
||||
z-index: 1;
|
||||
|
||||
& > span:first-child {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: 3.5rem;
|
||||
width: 3.5rem;
|
||||
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;
|
||||
right: calc(100% + 0.75rem);
|
||||
top: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
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 AddButton = props => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
return (
|
||||
<StyledLink
|
||||
{...props}
|
||||
tabIndex={modalOpen ? '-1' : ''}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
+
|
||||
</span>
|
||||
<Tooltip>{props.addButtonText}</Tooltip>
|
||||
</StyledLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButton;
|
||||
84
src/components/elements/AddModalButton.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
|
||||
const FilteredLink = ({ addButtonText, ...props }) => (
|
||||
<Link {...props}>{props.children}</Link>
|
||||
);
|
||||
|
||||
const StyledLink = styled(FilteredLink)`
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
grid-column: 2;
|
||||
border-radius: 50%;
|
||||
text-decoration: none;
|
||||
color: #565656;
|
||||
margin-left: 1rem;
|
||||
position: relative;
|
||||
& > span:first-child {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
border-radius: 50%;
|
||||
outline: 0;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
font-size: 2.5rem;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
&:focus > span:first-child {
|
||||
border: 0.25rem solid #890934;
|
||||
}
|
||||
|
||||
&:hover > span:first-child {
|
||||
}
|
||||
|
||||
&:active > span:first-child {
|
||||
}
|
||||
`;
|
||||
|
||||
const Tooltip = styled.span`
|
||||
display: none;
|
||||
color: #767676;
|
||||
a:focus > &,
|
||||
a:hover > & {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
left: calc(100% + 0.75rem);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #FFF;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
z-index: 60;
|
||||
}
|
||||
`;
|
||||
|
||||
const AddModalButton = props => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
const history = useHistory();
|
||||
return (
|
||||
<StyledLink
|
||||
{...props}
|
||||
to={history.location.pathname}
|
||||
tabIndex={modalOpen ? '-1' : ''}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
+
|
||||
</span>
|
||||
<Tooltip>{props.addButtonText}</Tooltip>
|
||||
</StyledLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddModalButton;
|
||||
272
src/components/elements/Button.js
Normal file
@@ -0,0 +1,272 @@
|
||||
import React, { useContext, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const StyledButton = styled.button`
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
grid-column: 2;
|
||||
${props => props.fullWidth
|
||||
? `width: 100%;`
|
||||
: `justify-self: start;`
|
||||
}
|
||||
${props => props.bottomGap && `
|
||||
margin-bottom: 1rem;
|
||||
`}
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
outline: 0;
|
||||
height: ${
|
||||
props => props.large
|
||||
? '3rem'
|
||||
: '2.25rem'
|
||||
};
|
||||
${props => props.fullWidth && `
|
||||
width: 100%;
|
||||
`}
|
||||
${props => props.square
|
||||
? `width: 2.25rem;`
|
||||
: `padding: 0 1rem;`
|
||||
}
|
||||
border-radius: 0.25rem;
|
||||
background: ${
|
||||
props => props.text
|
||||
? 'none'
|
||||
: props.gray
|
||||
? '#E3E3E3'
|
||||
: '#D91C5C'
|
||||
};
|
||||
color: ${
|
||||
props => props.gray
|
||||
? '#565656'
|
||||
: props.text
|
||||
? '#D91C5C'
|
||||
: '#FFF'
|
||||
};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
box-shadow: ${props => props.text
|
||||
? ''
|
||||
: '0 0.125rem 0.25rem rgba(0,0,0,0.12),'
|
||||
}
|
||||
inset 0 0 0
|
||||
${props => props.text
|
||||
? '0.125rem'
|
||||
: '0.25rem'
|
||||
}
|
||||
${props => props.gray
|
||||
? '#767676'
|
||||
: '#890934'
|
||||
};
|
||||
}
|
||||
|
||||
&:hover:not([disabled]) > span {
|
||||
background: ${props => props.text
|
||||
? '#E3E3E3'
|
||||
: props.gray
|
||||
? '#C6C6C6'
|
||||
: '#BD164E'
|
||||
};
|
||||
}
|
||||
|
||||
&:active:not([disabled]) > span {
|
||||
background: ${props => props.text
|
||||
? '#D5D5D5'
|
||||
: props.gray
|
||||
? '#B6B6B6'
|
||||
: '#A40D40'
|
||||
};
|
||||
}
|
||||
|
||||
${props => props.formLink && `
|
||||
justify-self: start;
|
||||
|
||||
& > span {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
padding: 0.25rem;
|
||||
margin: -0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0 0 0.125rem #D91C5C;
|
||||
}
|
||||
|
||||
&:hover:not([disabled]) > span,
|
||||
&:active:not([disabled]) > span {
|
||||
background: none;
|
||||
box-shadow: 0 0.125rem 0 #D91C5C;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:active > span {
|
||||
background: none;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.tableHeaderLink && `
|
||||
& > span {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:focus > span,
|
||||
&:hover:not([disabled]) > span,
|
||||
&:active:not([disabled]) > span {
|
||||
padding: 0.625rem;
|
||||
margin: -0.625rem;
|
||||
}
|
||||
|
||||
& > span > *:last-child {
|
||||
color: #8F8F8F;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.right && `
|
||||
justify-self: end;
|
||||
`}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Table Menu (3 dots on right of each row)
|
||||
//=============================================================================
|
||||
${props => props.tableMenu && `
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
& > span {
|
||||
background: ${props.selected
|
||||
? '#E3E3E3'
|
||||
: 'none'
|
||||
};
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
border-radius: 50%;
|
||||
outline: 0;
|
||||
fill: #767676;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
border: 2px solid #D91C5C;
|
||||
background: ${props.selected
|
||||
? 'RGBA(217, 28, 92, 0.15)'
|
||||
: 'none'
|
||||
};
|
||||
fill: #D91C5C;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover:not([disabled]) > span,
|
||||
&:active:not([disabled]) > span {
|
||||
background: ${props.selected
|
||||
? 'RGBA(217, 28, 92, 0.15)'
|
||||
: 'none'
|
||||
};
|
||||
fill: #D91C5C;
|
||||
}
|
||||
`}
|
||||
|
||||
//=============================================================================
|
||||
// "Check All" button for bulk editing in table
|
||||
//=============================================================================
|
||||
${props => props.checkbox && `
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
& > span {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 1px solid #A5A5A5;
|
||||
border-radius: 0.125rem;
|
||||
background: #FFF;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
border-color: #565656;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
&:hover:not([disabled]) > span,
|
||||
&:active:not([disabled]) > span {
|
||||
background: none;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => (props.checkbox === 'all' || props.checkbox === 'partial') && `
|
||||
& > span,
|
||||
&:hover:not([disabled]) > span {
|
||||
background: #D91C5C;
|
||||
border-color: #D91C5C;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
border: 3px solid #890934;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.checkbox === 'all' && `
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.35rem;
|
||||
left: 0.25rem;
|
||||
height: 8px;
|
||||
width: 15px;
|
||||
border-left: 2px solid #FFF;
|
||||
border-bottom: 2px solid #FFF;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.checkbox === 'partial' && `
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.6875rem;
|
||||
left: 0.1875rem;
|
||||
height: 0.125rem;
|
||||
width: 1.125rem;
|
||||
background: #FFF;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const Button = (props, ref) => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
const buttonRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
buttonRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<StyledButton
|
||||
{...props}
|
||||
ref={buttonRef}
|
||||
disabled={(modalOpen && !props.inModal) || props.disabled}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
{props.children}
|
||||
</span>
|
||||
</StyledButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Button);
|
||||
138
src/components/elements/Checkbox.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import Label from './Label';
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
const CheckboxContainer = styled.div`
|
||||
margin-left: ${props => props.noLeftMargin
|
||||
? '0'
|
||||
: props.invalid
|
||||
? '0.5rem'
|
||||
: '1rem'
|
||||
};
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: ${props => props.large
|
||||
? '3rem'
|
||||
: '2.25rem'
|
||||
};
|
||||
${props => props.invalid && `
|
||||
margin-right: -0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
border: 1px solid #D91C5C;
|
||||
border-radius: 0.125rem;
|
||||
background: RGBA(217,28,92,0.2);
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledCheckbox = styled.input`
|
||||
margin: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled(Label)`
|
||||
margin-left: 0.5rem;
|
||||
cursor: pointer;
|
||||
${props => props.tooltip && `
|
||||
& > span {
|
||||
border-bottom: 1px dotted;
|
||||
border-left: 1px solid transparent;
|
||||
cursor: help;
|
||||
}
|
||||
`}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: ${props => props.large
|
||||
? '0.75rem'
|
||||
: '0.375rem'
|
||||
};
|
||||
left: ${props => props.invalid
|
||||
? '0.5rem'
|
||||
: '0'
|
||||
};
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 1px solid #A5A5A5;
|
||||
border-radius: 0.125rem;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
input:focus + &::before {
|
||||
border-color: #565656;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
input:checked + &::before {
|
||||
background: #D91C5C;
|
||||
border-color: #D91C5C;
|
||||
}
|
||||
|
||||
input:checked:focus + &::before {
|
||||
border: 3px solid #890934;
|
||||
}
|
||||
|
||||
input:checked + &::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: ${props => props.large
|
||||
? '1.1rem'
|
||||
: '0.725rem'
|
||||
};
|
||||
left: ${props => props.invalid
|
||||
? '0.75rem'
|
||||
: '0.25rem'
|
||||
};
|
||||
height: 8px;
|
||||
width: 15px;
|
||||
border-left: 2px solid #FFF;
|
||||
border-bottom: 2px solid #FFF;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const Checkbox = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<CheckboxContainer
|
||||
invalid={props.invalid}
|
||||
noLeftMargin={props.noLeftMargin}
|
||||
>
|
||||
<StyledCheckbox
|
||||
id={props.id}
|
||||
name={props.id}
|
||||
type="checkbox"
|
||||
checked={props.checked}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
value={props.value}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<StyledLabel
|
||||
htmlFor={props.id}
|
||||
tooltip={props.tooltip}
|
||||
invalid={props.invalid}
|
||||
>
|
||||
<span>
|
||||
{props.label}
|
||||
{
|
||||
props.tooltip &&
|
||||
<Tooltip>
|
||||
{props.tooltip}
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
</StyledLabel>
|
||||
</CheckboxContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Checkbox);
|
||||
14
src/components/elements/Code.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Code = styled.code`
|
||||
white-space: pre-wrap;
|
||||
text-align: left;
|
||||
background: #fbfbfb;
|
||||
padding: 0.5rem;
|
||||
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #B6B6B6;
|
||||
max-height: 120px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export default Code;
|
||||
62
src/components/elements/CopyableText.js
Normal file
@@ -0,0 +1,62 @@
|
||||
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 StyledButton = styled(Button)`
|
||||
margin-left: 1rem;
|
||||
`;
|
||||
|
||||
const CopyableText = props => {
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
const copyText = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(props.text);
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: `${props.textType} copied to clipboard`,
|
||||
});
|
||||
} catch (err) {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: `Unable to copy ${props.textType}, please select the text and right click to copy`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof navigator.clipboard === 'undefined') {
|
||||
return (
|
||||
<Span hasBorder={props.hasBorder}>
|
||||
{props.text}
|
||||
<StyledButton
|
||||
text
|
||||
formLink
|
||||
inModal={props.inModal}
|
||||
type="button"
|
||||
>
|
||||
</StyledButton>
|
||||
</Span>
|
||||
);
|
||||
}
|
||||
else return (
|
||||
<Span hasBorder={props.hasBorder}>
|
||||
{props.text}
|
||||
<StyledButton
|
||||
text
|
||||
formLink
|
||||
inModal={props.inModal}
|
||||
type="button"
|
||||
onClick={copyText}
|
||||
>
|
||||
copy
|
||||
</StyledButton>
|
||||
</Span>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyableText;
|
||||
97
src/components/elements/FileUpload.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
height: 2.25rem;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
border: 0;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
&:after {
|
||||
content: '${props => props.validFile ? 'Choose a Different File' : 'Choose File'}';
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2.25rem;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus:after {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12),
|
||||
inset 0 0 0 0.25rem #890934;
|
||||
}
|
||||
|
||||
&:hover:not([disabled]):after {
|
||||
background: #BD164E;
|
||||
}
|
||||
|
||||
&:active:not([disabled]):after {
|
||||
background: #A40D40;
|
||||
}
|
||||
|
||||
&::file-selector-button {
|
||||
content: '${props => props.validFile ? 'Choose a Different File' : 'Choose File'}';
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2.25rem;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&:focus::file-selector-button {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12),
|
||||
inset 0 0 0 0.25rem #890934;
|
||||
}
|
||||
|
||||
&:hover:not([disabled])::file-selector-button {
|
||||
background: #BD164E;
|
||||
}
|
||||
|
||||
&:active:not([disabled])::file-selector-button {
|
||||
background: #A40D40;
|
||||
}
|
||||
`;
|
||||
|
||||
const FileUpload = (props, ref) => {
|
||||
return (
|
||||
<Container>
|
||||
<StyledInput
|
||||
id={props.id}
|
||||
name={props.id}
|
||||
type="file"
|
||||
onChange={props.onChange}
|
||||
validFile={props.validFile}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(FileUpload);
|
||||
30
src/components/elements/Form.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Form = styled.form`
|
||||
text-align: right;
|
||||
padding: 2rem;
|
||||
${props => !props.large && `
|
||||
& input {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`}
|
||||
& hr {
|
||||
margin: 0 -2rem;
|
||||
background: none;
|
||||
border: 0;
|
||||
border-top: 1px solid #C6C6C6;
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
${props => props.large && `
|
||||
display: grid;
|
||||
grid-template-columns: ${props.wideLabel
|
||||
? '1.75fr'
|
||||
: '1.3fr'
|
||||
} 10fr;
|
||||
grid-row-gap: 1rem;
|
||||
grid-column-gap: 0.75rem;
|
||||
align-items: center;
|
||||
`}
|
||||
`;
|
||||
|
||||
export default Form;
|
||||
9
src/components/elements/H1.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const H1 = styled.h1`
|
||||
font-size: 3rem;
|
||||
margin: 1rem 0;
|
||||
font-weight: normal;
|
||||
`;
|
||||
|
||||
export default H1;
|
||||
112
src/components/elements/Input.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as ViewPassword } from '../../images/ViewPassword.svg';
|
||||
import { ReactComponent as HidePassword } from '../../images/HidePassword.svg';
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
${props => props.width && `
|
||||
width: ${props.width};
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
height: ${props => props.large
|
||||
? '3rem'
|
||||
: '2.25rem'
|
||||
};
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
border: 1px solid #B6B6B6;
|
||||
${props => props.invalid && `
|
||||
background: RGBA(217,28,92,0.2);
|
||||
border-color: #D91C5C;
|
||||
`}
|
||||
border-radius: 0.125rem;
|
||||
color: inherit;
|
||||
&:focus {
|
||||
border-color: ${props => props.invalid
|
||||
? '#890934;'
|
||||
: '#565656;'
|
||||
}
|
||||
outline: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
&:disabled {
|
||||
background: #DDD;
|
||||
border: 1px solid #B6B6B6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
|
||||
const PasswordButton = styled.button`
|
||||
position: absolute;
|
||||
top: ${props => props.large ? '0.375rem' : '0.25rem'};
|
||||
right: 1rem;
|
||||
height: ${props => props.large ? '2.25rem' : '1.75rem'};
|
||||
width: 2.5rem;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
padding: 0;
|
||||
|
||||
& > span {
|
||||
height: ${props => props.large ? '2.25rem' : '1.75rem'};
|
||||
width: 2.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: 0;
|
||||
border-radius: 0.25rem;
|
||||
fill: #D91C5C;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
fill: #BD164E;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
box-shadow: inset 0 0 0 0.125rem #D91C5C;
|
||||
}
|
||||
`;
|
||||
|
||||
const Input = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<Container
|
||||
width={props.width}
|
||||
>
|
||||
<StyledInput
|
||||
{...props}
|
||||
ref={inputRef}
|
||||
/>
|
||||
{
|
||||
props.allowShowPassword &&
|
||||
<PasswordButton
|
||||
type="button"
|
||||
large={props.large}
|
||||
onClick={props.toggleShowPassword}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
{
|
||||
props.showPassword
|
||||
? <HidePassword />
|
||||
: <ViewPassword />
|
||||
}
|
||||
</span>
|
||||
</PasswordButton>
|
||||
}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Input);
|
||||
18
src/components/elements/InputGroup.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const InputGroup = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-column: 2;
|
||||
${props => props.flexEnd && 'justify-content: flex-end;'}
|
||||
${props => props.spaced && `
|
||||
& > * {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
& > *:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export default InputGroup;
|
||||
16
src/components/elements/Label.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Label = styled.label`
|
||||
color: #767676;
|
||||
${props => props.indented && `margin-right: 0.5rem;`}
|
||||
${props => props.middle && `margin: 0 0.5rem 0 1rem;`}
|
||||
${props => props.tooltip && `
|
||||
& > span {
|
||||
border-bottom: 1px dotted;
|
||||
border-left: 1px solid transparent;
|
||||
cursor: help;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export default Label;
|
||||
72
src/components/elements/Link.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link as ReactRouterLink } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
|
||||
const FilteredLink = ({ formLink, right, inModal, ...props }) => (
|
||||
<ReactRouterLink {...props}>{props.children}</ReactRouterLink>
|
||||
);
|
||||
|
||||
const StyledReactRouterLink = styled(FilteredLink)`
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #D91C5C;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
outline: 0;
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
padding: 0.25rem;
|
||||
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 {
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
${props => props.formLink && `
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
`}
|
||||
|
||||
${props => props.right && `
|
||||
justify-self: end;
|
||||
`}
|
||||
`;
|
||||
|
||||
const Link = props => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
return (
|
||||
<StyledReactRouterLink
|
||||
{...props}
|
||||
tabIndex={modalOpen && !props.inModal ? '-1' : ''}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
{props.children}
|
||||
</span>
|
||||
</StyledReactRouterLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default Link;
|
||||
35
src/components/elements/PasswordInput.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import Input from '../elements/Input';
|
||||
|
||||
const PasswordInput = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
const [ showPassword, setShowPassword ] = useState(false);
|
||||
const toggleShowPassword = () => setShowPassword(!showPassword);
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
ref={inputRef}
|
||||
showPassword={showPassword}
|
||||
toggleShowPassword={toggleShowPassword}
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={props.password}
|
||||
onChange={e => props.setPassword(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (!showPassword && e.getModifierState('CapsLock')) {
|
||||
props.setErrorMessage('CAPSLOCK is enabled!');
|
||||
} else {
|
||||
props.setErrorMessage('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(PasswordInput);
|
||||
119
src/components/elements/Radio.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import Label from './Label';
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
const RadioContainer = styled.div`
|
||||
margin-left: ${props => props.noLeftMargin
|
||||
? '-0.5rem'
|
||||
: '0.5rem'
|
||||
};
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.25rem;
|
||||
padding: 0 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.125rem;
|
||||
${props => props.invalid && `
|
||||
border-color: #D91C5C;
|
||||
background: RGBA(217,28,92,0.2);
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledRadio = styled.input`
|
||||
outline: none;
|
||||
margin: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled(Label)`
|
||||
padding-left: 0.5rem;
|
||||
cursor: ${props => props.disabled
|
||||
? 'not-allowed'
|
||||
: 'pointer'
|
||||
};
|
||||
${props => props.tooltip && `
|
||||
& > span {
|
||||
border-bottom: 1px dotted;
|
||||
border-left: 1px solid transparent;
|
||||
cursor: help;
|
||||
}
|
||||
`}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.375rem;
|
||||
left: 0.5rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 1px solid #A5A5A5;
|
||||
border-radius: 50%;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
input:focus + &::before {
|
||||
border-color: #565656;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
input:checked + &::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0.75rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 50%;
|
||||
background: ${props => props.disabled
|
||||
? '#959595'
|
||||
: '#707070'
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
const Radio = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<RadioContainer
|
||||
invalid={props.invalid}
|
||||
noLeftMargin={props.noLeftMargin}
|
||||
>
|
||||
<StyledRadio
|
||||
name={props.name}
|
||||
id={props.id}
|
||||
value={props.id}
|
||||
type="radio"
|
||||
checked={props.checked}
|
||||
onChange={props.onChange}
|
||||
ref={inputRef}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
<StyledLabel
|
||||
htmlFor={props.id}
|
||||
tooltip={props.tooltip}
|
||||
invalid={props.invalid}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
<span>
|
||||
{props.label}
|
||||
{
|
||||
props.tooltip &&
|
||||
<Tooltip>
|
||||
{props.tooltip}
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
</StyledLabel>
|
||||
</RadioContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Radio);
|
||||
33
src/components/elements/Select.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Select = styled.select`
|
||||
height: ${props => props.large
|
||||
? '3rem'
|
||||
: '2.25rem'
|
||||
};
|
||||
padding: 0 0.75rem;
|
||||
border: 1px solid #B6B6B6;
|
||||
border-radius: 0.125rem;
|
||||
background: #fff;
|
||||
color: inherit;
|
||||
max-width: 230px;
|
||||
&:focus {
|
||||
border-color: #565656;
|
||||
outline: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
${props => props.invalid && `
|
||||
background: RGBA(217,28,92,0.2);
|
||||
border-color: #D91C5C;
|
||||
&:focus {
|
||||
border-color: #890934;
|
||||
}
|
||||
`}
|
||||
|
||||
&:disabled {
|
||||
background: #DDD;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Select;
|
||||
16
src/components/elements/Span.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Span = styled.span`
|
||||
text-align: left;
|
||||
${props => props.hasBorder ? `
|
||||
height: 2.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
border: 1px solid #B6B6B6;
|
||||
border-radius: 0.125rem;
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
export default Span;
|
||||
103
src/components/elements/Table.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Table = styled.table`
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
min-width: 38rem;
|
||||
|
||||
& > thead {
|
||||
background: #F7F7F7;
|
||||
}
|
||||
|
||||
& tr {
|
||||
border-bottom: 1px solid #E0E0E0;
|
||||
}
|
||||
|
||||
& thead tr {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
& tbody tr {
|
||||
height: 5.5rem;
|
||||
}
|
||||
|
||||
& tbody tr:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
& th {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
color: #717171;
|
||||
}
|
||||
|
||||
& th,
|
||||
& td {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
& td > span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
& td > span > a {
|
||||
outline: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
& td > span > a > span {
|
||||
outline: 0;
|
||||
color: #565656;
|
||||
}
|
||||
|
||||
& td > span > a:hover > span {
|
||||
box-shadow: 0 0.125rem 0 #565656;
|
||||
}
|
||||
|
||||
& td > span > a:focus > span {
|
||||
padding: 0.625rem;
|
||||
margin: -0.625rem;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: inset 0 0 0 0.125rem #D91C5C;
|
||||
}
|
||||
|
||||
& td:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
& td:last-child {
|
||||
overflow: inherit;
|
||||
position: relative;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
${props => props.withCheckboxes && `
|
||||
& th:first-child,
|
||||
& td:first-child {
|
||||
width: 3rem;
|
||||
padding: 1.25rem 0 1.25rem 1.25rem;
|
||||
}
|
||||
& td:nth-child(2) {
|
||||
font-weight: bold;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.rowsHaveDeleteButtons && `
|
||||
& th:last-child {
|
||||
width: 9rem;
|
||||
text-align: right;
|
||||
}
|
||||
& td:last-child {
|
||||
padding: 0 1.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export default Table;
|
||||
122
src/components/elements/Tooltip.js
Normal file
@@ -0,0 +1,122 @@
|
||||
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;
|
||||
bottom: 100%;
|
||||
right: calc(50% - 1rem);
|
||||
transform: translateX(50%);
|
||||
padding: 0.75rem 1rem;
|
||||
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: 80;
|
||||
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;
|
||||
55
src/components/elements/TrashButton.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as TrashIcon } from '../../images/TrashIcon.svg';
|
||||
|
||||
const StyledButton = styled.button`
|
||||
display: flex;
|
||||
background: none;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: 0.5rem;
|
||||
fill: #949494;
|
||||
& > span {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 2.2rem;
|
||||
width: 1.9rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0.25rem;
|
||||
outline: 0;
|
||||
}
|
||||
&:focus > span {
|
||||
box-shadow: inset 0 0 0 0.125rem #D91C5C;
|
||||
fill: #D91C5C;
|
||||
}
|
||||
&:hover > span {
|
||||
fill: #D91C5C;
|
||||
}
|
||||
&:active > span {
|
||||
}
|
||||
`;
|
||||
|
||||
const TrashButton = (props, ref) => {
|
||||
const buttonRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
buttonRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<StyledButton
|
||||
type="button"
|
||||
{...props}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
<TrashIcon />
|
||||
</span>
|
||||
</StyledButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(TrashButton);
|
||||
1148
src/components/forms/AccountForm.js
Normal file
977
src/components/forms/ApplicationForm.js
Normal file
@@ -0,0 +1,977 @@
|
||||
/* 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 Select from '../elements/Select';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import FormError from '../blocks/FormError';
|
||||
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);
|
||||
const refAccount = useRef(null);
|
||||
const refCallWebhook = useRef(null);
|
||||
const refCallWebhookUser = useRef(null);
|
||||
const refCallWebhookPass = useRef(null);
|
||||
const refStatusWebhook = useRef(null);
|
||||
const refStatusWebhookUser = useRef(null);
|
||||
const refStatusWebhookPass = useRef(null);
|
||||
const refMessagingWebhookUser = useRef(null);
|
||||
const refMessagingWebhookPass = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ name, setName ] = useState('');
|
||||
const [ accountSid, setAccountSid ] = useState('');
|
||||
const [ callWebhook, setCallWebhook ] = useState('');
|
||||
const [ callWebhookMethod, setCallWebhookMethod ] = useState('POST');
|
||||
const [ callWebhookUser, setCallWebhookUser ] = useState('');
|
||||
const [ callWebhookPass, setCallWebhookPass ] = useState('');
|
||||
const [ statusWebhook, setStatusWebhook ] = useState('');
|
||||
const [ statusWebhookMethod, setStatusWebhookMethod ] = useState('POST');
|
||||
const [ statusWebhookUser, setStatusWebhookUser ] = useState('');
|
||||
const [ statusWebhookPass, setStatusWebhookPass ] = useState('');
|
||||
const [ messagingWebhook, setMessagingWebhook ] = useState('');
|
||||
const [ messagingWebhookMethod, setMessagingWebhookMethod ] = useState('POST');
|
||||
const [ messagingWebhookUser, setMessagingWebhookUser ] = useState('');
|
||||
const [ messagingWebhookPass, setMessagingWebhookPass ] = useState('');
|
||||
const [ speechSynthesisVendor, setSpeechSynthesisVendor ] = useState('google');
|
||||
const [ speechSynthesisLanguage, setSpeechSynthesisLanguage ] = useState('en-US');
|
||||
const [ speechSynthesisVoice, setSpeechSynthesisVoice ] = useState('en-US-Standard-C');
|
||||
const [ speechRecognizerVendor, setSpeechRecognizerVendor ] = useState('google');
|
||||
const [ speechRecognizerLanguage, setSpeechRecognizerLanguage ] = useState('en-US');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidName, setInvalidName ] = useState(false);
|
||||
const [ invalidAccount, setInvalidAccount ] = useState(false);
|
||||
const [ invalidCallWebhook, setInvalidCallWebhook ] = useState(false);
|
||||
const [ invalidCallWebhookUser, setInvalidCallWebhookUser ] = useState(false);
|
||||
const [ invalidCallWebhookPass, setInvalidCallWebhookPass ] = useState(false);
|
||||
const [ invalidStatusWebhook, setInvalidStatusWebhook ] = useState(false);
|
||||
const [ invalidStatusWebhookUser, setInvalidStatusWebhookUser ] = useState(false);
|
||||
const [ invalidStatusWebhookPass, setInvalidStatusWebhookPass ] = useState(false);
|
||||
const [ invalidMessagingWebhookUser, setInvalidMessagingWebhookUser ] = useState(false);
|
||||
const [ invalidMessagingWebhookPass, setInvalidMessagingWebhookPass ] = useState(false);
|
||||
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
const [ showCallAuth, setShowCallAuth ] = useState(false);
|
||||
const toggleCallAuth = () => setShowCallAuth(!showCallAuth);
|
||||
|
||||
const [ showStatusAuth, setShowStatusAuth ] = useState(false);
|
||||
const toggleStatusAuth = () => setShowStatusAuth(!showStatusAuth);
|
||||
|
||||
const [ showMessagingAuth, setShowMessagingAuth ] = useState(false);
|
||||
const toggleMessagingAuth = () => setShowMessagingAuth(!showMessagingAuth);
|
||||
|
||||
const [ accounts, setAccounts ] = useState([]);
|
||||
const [ applications, setApplications ] = useState([]);
|
||||
const [ applicationSid, setApplicationSid ] = useState([]);
|
||||
|
||||
// See if user logged in
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const promiseAllValues = await Promise.all([
|
||||
accountsPromise,
|
||||
applicationsPromise,
|
||||
]);
|
||||
|
||||
const accounts = promiseAllValues[0].data.filter(a => a.service_provider_sid === currentServiceProvider);
|
||||
const applications = promiseAllValues[1].data;
|
||||
|
||||
setAccounts(accounts);
|
||||
setApplications(applications);
|
||||
|
||||
if (accounts.length === 0) {
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must create an account before you can create an application.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(props.type === 'setup' && accounts.length > 1) ||
|
||||
(props.type === 'setup' && applications.length > 1)
|
||||
) {
|
||||
history.push('/internal/applications');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'That page is only accessible during setup.',
|
||||
});
|
||||
}
|
||||
|
||||
if (props.type === 'add' && accounts.length === 1) {
|
||||
setAccountSid(accounts[0].account_sid);
|
||||
}
|
||||
|
||||
if (props.type === 'setup' || props.type === 'edit') {
|
||||
const currentApplication = props.type === 'edit'
|
||||
? applications.filter(a => a.application_sid === props.application_sid)
|
||||
: applications;
|
||||
|
||||
if (props.type === 'edit' && !currentApplication.length) {
|
||||
history.push('/internal/applications');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'That application does not exist.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentApplication.length) {
|
||||
setName('default application');
|
||||
setAccountSid(accounts[0].account_sid);
|
||||
} else {
|
||||
const app = currentApplication[0];
|
||||
setName( app.name || '');
|
||||
setCallWebhook( (app.call_hook && app.call_hook.url) || '');
|
||||
setCallWebhookMethod( (app.call_hook && app.call_hook.method) || 'post');
|
||||
setCallWebhookUser( (app.call_hook && app.call_hook.username) || '');
|
||||
setCallWebhookPass( (app.call_hook && app.call_hook.password) || '');
|
||||
setStatusWebhook( (app.call_status_hook && app.call_status_hook.url) || '');
|
||||
setStatusWebhookMethod( (app.call_status_hook && app.call_status_hook.method) || 'post');
|
||||
setStatusWebhookUser( (app.call_status_hook && app.call_status_hook.username) || '');
|
||||
setStatusWebhookPass( (app.call_status_hook && app.call_status_hook.password) || '');
|
||||
setMessagingWebhook( (app.messaging_hook && app.messaging_hook.url) || '');
|
||||
setMessagingWebhookMethod( (app.messaging_hook && app.messaging_hook.method) || 'post');
|
||||
setMessagingWebhookUser( (app.messaging_hook && app.messaging_hook.username) || '');
|
||||
setMessagingWebhookPass( (app.messaging_hook && app.messaging_hook.password) || '');
|
||||
setSpeechSynthesisVendor( app.speech_synthesis_vendor || '');
|
||||
setSpeechSynthesisLanguage( app.speech_synthesis_language || '');
|
||||
setSpeechSynthesisVoice( app.speech_synthesis_voice || '');
|
||||
setSpeechRecognizerVendor( app.speech_recognizer_vendor || '');
|
||||
setSpeechRecognizerLanguage( app.speech_recognizer_language || '');
|
||||
setAccountSid( app.account_sid || '');
|
||||
setApplicationSid( app.application_sid);
|
||||
if (
|
||||
(app.call_hook && app.call_hook.username) ||
|
||||
(app.call_hook && app.call_hook.password)
|
||||
) {
|
||||
setShowCallAuth(true);
|
||||
}
|
||||
|
||||
if (
|
||||
(app.call_status_hook && app.call_status_hook.username) ||
|
||||
(app.call_status_hook && app.call_status_hook.password)
|
||||
) {
|
||||
setShowStatusAuth(true);
|
||||
}
|
||||
|
||||
if (
|
||||
(app.messaging_hook && app.messaging_hook.username) ||
|
||||
(app.messaging_hook && app.messaging_hook.password)
|
||||
) {
|
||||
setShowMessagingAuth(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) || 'Unable to get accounts',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidName(false);
|
||||
setInvalidAccount(false);
|
||||
setInvalidCallWebhook(false);
|
||||
setInvalidCallWebhookUser(false);
|
||||
setInvalidCallWebhookPass(false);
|
||||
setInvalidStatusWebhook(false);
|
||||
setInvalidStatusWebhookUser(false);
|
||||
setInvalidStatusWebhookPass(false);
|
||||
setInvalidMessagingWebhookUser(false);
|
||||
setInvalidMessagingWebhookPass(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if ((props.type === 'add' || props.type === 'edit') && !name) {
|
||||
errorMessages.push('Please provide a name.');
|
||||
setInvalidName(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
// check if name is already in use
|
||||
|
||||
if ((props.type === 'add' || props.type === 'edit') && !accountSid) {
|
||||
errorMessages.push('Please choose an account for this application to be associated with.');
|
||||
setInvalidAccount(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refAccount.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!callWebhook) {
|
||||
errorMessages.push('Please enter a Calling Webhook.');
|
||||
setInvalidCallWebhook(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refCallWebhook.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!statusWebhook) {
|
||||
errorMessages.push('Please enter a Call Status Webhook.');
|
||||
setInvalidStatusWebhook(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refStatusWebhook.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((callWebhookUser && !callWebhookPass) || (!callWebhookUser && callWebhookPass)) {
|
||||
errorMessages.push('Calling Webhook username and password must be either both filled out or both empty.');
|
||||
setInvalidCallWebhookUser(true);
|
||||
setInvalidCallWebhookPass(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!callWebhookUser) {
|
||||
refCallWebhookUser.current.focus();
|
||||
} else {
|
||||
refCallWebhookPass.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((statusWebhookUser && !statusWebhookPass) || (!statusWebhookUser && statusWebhookPass)) {
|
||||
errorMessages.push('Call Status Webhook username and password must be either both filled out or both empty.');
|
||||
setInvalidStatusWebhookUser(true);
|
||||
setInvalidStatusWebhookPass(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!statusWebhookUser) {
|
||||
refStatusWebhookUser.current.focus();
|
||||
} else {
|
||||
refStatusWebhookPass.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((messagingWebhookUser && !messagingWebhookPass) || (!messagingWebhookUser && messagingWebhookPass)) {
|
||||
errorMessages.push('Messaging Webhook username and password must be either both filled out or both empty.');
|
||||
setInvalidMessagingWebhookUser(true);
|
||||
setInvalidMessagingWebhookPass(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!messagingWebhookUser) {
|
||||
refMessagingWebhookUser.current.focus();
|
||||
} else {
|
||||
refMessagingWebhookPass.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Submit
|
||||
//=============================================================================
|
||||
const shouldCreateNew = props.type === 'add' || (props.type === 'setup' && !applications.length);
|
||||
|
||||
const method = shouldCreateNew
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
const url = shouldCreateNew
|
||||
? '/Applications'
|
||||
: `/Applications/${applicationSid}`;
|
||||
|
||||
const data = {
|
||||
account_sid: accountSid,
|
||||
name: name.trim(),
|
||||
call_hook: {
|
||||
url: callWebhook.trim(),
|
||||
method: callWebhookMethod,
|
||||
username: callWebhookUser.trim() || null,
|
||||
password: callWebhookPass || null,
|
||||
},
|
||||
call_status_hook: {
|
||||
url: statusWebhook.trim(),
|
||||
method: statusWebhookMethod,
|
||||
username: statusWebhookUser.trim() || null,
|
||||
password: statusWebhookPass || null,
|
||||
},
|
||||
messaging_hook: {
|
||||
url: messagingWebhook.trim(),
|
||||
method: messagingWebhookMethod,
|
||||
username: messagingWebhookUser.trim() || null,
|
||||
password: messagingWebhookPass || null,
|
||||
},
|
||||
speech_synthesis_vendor: speechSynthesisVendor,
|
||||
speech_synthesis_language: speechSynthesisLanguage,
|
||||
speech_synthesis_voice: speechSynthesisVoice,
|
||||
speech_recognizer_vendor: speechRecognizerVendor,
|
||||
speech_recognizer_language: speechRecognizerLanguage,
|
||||
};
|
||||
|
||||
await axios({
|
||||
method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
if (props.type === 'setup') {
|
||||
isMounted = false;
|
||||
history.push('/configure-sip-trunk');
|
||||
} else {
|
||||
isMounted = false;
|
||||
history.push('/internal/applications');
|
||||
const dispatchMessage = props.type === 'add'
|
||||
? 'Application created successfully'
|
||||
: 'Application 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'
|
||||
? '505px'
|
||||
: props.type === 'edit'
|
||||
? '646px'
|
||||
: '611px'
|
||||
}
|
||||
/>
|
||||
: <Form
|
||||
large
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
|
||||
{props.type === 'edit' && (
|
||||
<React.Fragment>
|
||||
<Label>ApplicationSid</Label>
|
||||
<CopyableText text={applicationSid} textType="ApplicationSid" />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{(props.type === 'add' || props.type === 'edit') && (
|
||||
<React.Fragment>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="name"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="Application name"
|
||||
invalid={invalidName}
|
||||
autoFocus
|
||||
ref={refName}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="account">Account</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="account"
|
||||
id="account"
|
||||
value={accountSid}
|
||||
onChange={e => setAccountSid(e.target.value)}
|
||||
invalid={invalidAccount}
|
||||
ref={refAccount}
|
||||
>
|
||||
{(
|
||||
(accounts.length > 1) ||
|
||||
(props.type === 'edit' && accounts[0] && accountSid !== accounts[0].account_sid)
|
||||
) && (
|
||||
<option value="">
|
||||
-- Choose the account this application will be associated with --
|
||||
</option>
|
||||
)}
|
||||
{accounts.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
|
||||
<option
|
||||
key={a.account_sid}
|
||||
value={a.account_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<hr />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
<Label htmlFor="callWebhook">Calling Webhook</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="callWebhook"
|
||||
id="callWebhook"
|
||||
value={callWebhook}
|
||||
onChange={e => setCallWebhook(e.target.value)}
|
||||
placeholder="URL for your web application that will handle calls"
|
||||
invalid={invalidCallWebhook}
|
||||
ref={refCallWebhook}
|
||||
autoFocus={props.type === 'setup'}
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="callWebhookMethod"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="callWebhookMethod"
|
||||
id="callWebhookMethod"
|
||||
value={callWebhookMethod}
|
||||
onChange={e => setCallWebhookMethod(e.target.value)}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showCallAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="callWebhookUser">User</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="callWebhookUser"
|
||||
id="callWebhookUser"
|
||||
value={callWebhookUser}
|
||||
onChange={e => setCallWebhookUser(e.target.value)}
|
||||
placeholder="Optional"
|
||||
invalid={invalidCallWebhookUser}
|
||||
ref={refCallWebhookUser}
|
||||
/>
|
||||
<Label htmlFor="callWebhookPass" middle>Password</Label>
|
||||
<PasswordInput
|
||||
large={props.type === 'setup'}
|
||||
allowShowPassword
|
||||
name="callWebhookPass"
|
||||
id="callWebhookPass"
|
||||
password={callWebhookPass}
|
||||
setPassword={setCallWebhookPass}
|
||||
setErrorMessage={setErrorMessage}
|
||||
placeholder="Optional"
|
||||
invalid={invalidCallWebhookPass}
|
||||
ref={refCallWebhookPass}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleCallAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="statusWebhook">Call Status Webhook</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="statusWebhook"
|
||||
id="statusWebhook"
|
||||
value={statusWebhook}
|
||||
onChange={e => setStatusWebhook(e.target.value)}
|
||||
placeholder="URL for your web application that will receive call status"
|
||||
invalid={invalidStatusWebhook}
|
||||
ref={refStatusWebhook}
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="statusWebhookMethod"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="statusWebhookMethod"
|
||||
id="statusWebhookMethod"
|
||||
value={statusWebhookMethod}
|
||||
onChange={e => setStatusWebhookMethod(e.target.value)}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showStatusAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="statusWebhookUser">User</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="statusWebhookUser"
|
||||
id="statusWebhookUser"
|
||||
value={statusWebhookUser}
|
||||
onChange={e => setStatusWebhookUser(e.target.value)}
|
||||
placeholder="Optional"
|
||||
invalid={invalidStatusWebhookUser}
|
||||
ref={refStatusWebhookUser}
|
||||
/>
|
||||
<Label htmlFor="statusWebhookPass" middle>Password</Label>
|
||||
<PasswordInput
|
||||
large={props.type === 'setup'}
|
||||
allowShowPassword
|
||||
name="statusWebhookPass"
|
||||
id="statusWebhookPass"
|
||||
password={statusWebhookPass}
|
||||
setPassword={setStatusWebhookPass}
|
||||
setErrorMessage={setErrorMessage}
|
||||
placeholder="Optional"
|
||||
invalid={invalidStatusWebhookPass}
|
||||
ref={refStatusWebhookPass}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleStatusAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="messagingWebhook">Messaging Webhook</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="messagingWebhook"
|
||||
id="messagingWebhook"
|
||||
value={messagingWebhook}
|
||||
onChange={e => setMessagingWebhook(e.target.value)}
|
||||
placeholder="URL for your web application that will receive SMS"
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="messagingWebhookMethod"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="messagingWebhookMethod"
|
||||
id="messagingWebhookMethod"
|
||||
value={messagingWebhookMethod}
|
||||
onChange={e => setMessagingWebhookMethod(e.target.value)}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showMessagingAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="messagingWebhookUser">User</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="messagingWebhookUser"
|
||||
id="messagingWebhookUser"
|
||||
value={messagingWebhookUser}
|
||||
onChange={e => setMessagingWebhookUser(e.target.value)}
|
||||
placeholder="Optional"
|
||||
invalid={invalidMessagingWebhookUser}
|
||||
ref={refMessagingWebhookUser}
|
||||
/>
|
||||
<Label htmlFor="messagingWebhookPass" middle>Password</Label>
|
||||
<PasswordInput
|
||||
large={props.type === 'setup'}
|
||||
allowShowPassword
|
||||
name="messagingWebhookPass"
|
||||
id="messagingWebhookPass"
|
||||
password={messagingWebhookPass}
|
||||
setPassword={setMessagingWebhookPass}
|
||||
setErrorMessage={setErrorMessage}
|
||||
placeholder="Optional"
|
||||
invalid={invalidMessagingWebhookPass}
|
||||
ref={refMessagingWebhookPass}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleMessagingAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="speechSynthesisVendor">Speech Synthesis Vendor</Label>
|
||||
<InputGroup>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="speechSynthesisVendor"
|
||||
id="speechSynthesisVendor"
|
||||
value={speechSynthesisVendor}
|
||||
onChange={e => {
|
||||
setSpeechSynthesisVendor(e.target.value);
|
||||
|
||||
// When using Google and en-US, ensure "Standard-C" is used as default
|
||||
if (
|
||||
e.target.value === 'google' &&
|
||||
speechSynthesisLanguage === 'en-US'
|
||||
) {
|
||||
setSpeechSynthesisVoice('en-US-Standard-C');
|
||||
return;
|
||||
}
|
||||
|
||||
// Google and AWS have different voice lists. See if the newly
|
||||
// chosen vendor has the same language as what was already in use.
|
||||
let newLang = e.target.value === 'google'
|
||||
? 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
|
||||
));
|
||||
|
||||
// if not, use en-US as fallback.
|
||||
if (!newLang) {
|
||||
setSpeechSynthesisLanguage('en-US');
|
||||
|
||||
if (e.target.value === 'google') {
|
||||
setSpeechSynthesisVoice('en-US-Standard-C');
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
setSpeechSynthesisVoice(newLang.voices[0].value);
|
||||
}}
|
||||
>
|
||||
<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
|
||||
large={props.type === 'setup'}
|
||||
name="speechSynthesisLanguage"
|
||||
id="speechSynthesisLanguage"
|
||||
value={speechSynthesisLanguage}
|
||||
onChange={e => {
|
||||
setSpeechSynthesisLanguage(e.target.value);
|
||||
|
||||
// When using Google and en-US, ensure "Standard-C" is used as default
|
||||
if (
|
||||
(speechSynthesisVendor === 'google')
|
||||
&& (e.target.value === 'en-US')
|
||||
) {
|
||||
setSpeechSynthesisVoice('en-US-Standard-C');
|
||||
return;
|
||||
}
|
||||
|
||||
const newLang = speechSynthesisVendor === 'google'
|
||||
? 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
|
||||
));
|
||||
|
||||
setSpeechSynthesisVoice(newLang.voices[0].value);
|
||||
|
||||
}}
|
||||
>
|
||||
{speechSynthesisVendor === 'google' ? (
|
||||
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>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
<Label middle htmlFor="speechSynthesisVoice">Voice</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="speechSynthesisVoice"
|
||||
id="speechSynthesisVoice"
|
||||
value={speechSynthesisVoice}
|
||||
onChange={e => setSpeechSynthesisVoice(e.target.value)}
|
||||
>
|
||||
{speechSynthesisVendor === 'google' ? (
|
||||
SpeechSynthesisLanguageGoogle
|
||||
.filter(l => l.code === speechSynthesisLanguage)
|
||||
.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)
|
||||
.map(m => m.voices.map(v => (
|
||||
<option key={v.value} value={v.value}>{v.name}</option>
|
||||
)))
|
||||
)}
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="speechRecognizerVendor">Speech Recognizer Vendor</Label>
|
||||
<InputGroup>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="speechRecognizerVendor"
|
||||
id="speechRecognizerVendor"
|
||||
value={speechRecognizerVendor}
|
||||
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
|
||||
large={props.type === 'setup'}
|
||||
name="speechRecognizerLanguage"
|
||||
id="speechRecognizerLanguage"
|
||||
value={speechRecognizerLanguage}
|
||||
onChange={e => setSpeechRecognizerLanguage(e.target.value)}
|
||||
>
|
||||
{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>
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
{props.type === 'edit' && (
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/applications');
|
||||
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 Application'
|
||||
: 'Save'
|
||||
}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplicationForm;
|
||||
1868
src/components/forms/CarrierForm.js
Normal file
389
src/components/forms/MsTeamsTenantForm.js
Normal file
@@ -0,0 +1,389 @@
|
||||
/* 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 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 FormError from '../blocks/FormError';
|
||||
import Loader from '../blocks/Loader';
|
||||
import Button from '../elements/Button';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const MsTeamsTenantForm = props => {
|
||||
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
// Refs
|
||||
const refDomainName = useRef(null);
|
||||
const refAccount = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ domainName, setDomainName ] = useState('');
|
||||
const [ account, setAccount ] = useState('');
|
||||
const [ application, setApplication ] = useState('');
|
||||
|
||||
// Select list values
|
||||
const [ accountValues, setAccountValues ] = useState('');
|
||||
const [ applicationValues, setApplicationValues ] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidDomainName, setInvalidDomainName ] = useState(false);
|
||||
const [ invalidAccount, setInvalidAccount ] = useState(false);
|
||||
|
||||
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
|
||||
const [ tenants, setTenants ] = useState('');
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
// Check if user is logged in
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tenantsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/MicrosoftTeamsTenants',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const promises = [
|
||||
tenantsPromise,
|
||||
accountsPromise,
|
||||
applicationsPromise,
|
||||
];
|
||||
|
||||
if (props.type === 'add') {
|
||||
promises.push(axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/ServiceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
const promiseAllValues = await Promise.all(promises);
|
||||
|
||||
const tenants = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[1].data;
|
||||
const applications = promiseAllValues[2].data;
|
||||
|
||||
setTenants(tenants);
|
||||
setAccountValues(accounts);
|
||||
setApplicationValues(applications);
|
||||
|
||||
if (props.type === 'add') {
|
||||
const serviceProviders = promiseAllValues[3].data;
|
||||
setServiceProviderSid(serviceProviders[0].service_provider_sid);
|
||||
}
|
||||
|
||||
if (!accounts.length) {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must create an account before you can create a Microsoft Teams Tenant.',
|
||||
});
|
||||
history.push('/internal/accounts');
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.type === 'edit') {
|
||||
const tenantData = tenants.filter(tenant => {
|
||||
return tenant.ms_teams_tenant_sid === props.ms_teams_tenant_sid;
|
||||
});
|
||||
|
||||
if (!tenantData.length) {
|
||||
history.push('/internal/ms-teams-tenants');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'That tenant does not exist.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setDomainName (( tenantData[0] && tenantData[0].tenant_fqdn ) || '');
|
||||
setAccount (( tenantData[0] && tenantData[0].account_sid ) || '');
|
||||
setApplication(( tenantData[0] && tenantData[0].application_sid) || '');
|
||||
}
|
||||
|
||||
if (props.type === 'add' && accounts.length === 1) {
|
||||
setAccount(accounts[0].account_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 {
|
||||
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);
|
||||
}
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async e => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidDomainName(false);
|
||||
setInvalidAccount(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if (!domainName) {
|
||||
errorMessages.push('Please provide a domain name');
|
||||
setInvalidDomainName(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refDomainName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
// check if domain name is already in use
|
||||
for (const tenant of tenants) {
|
||||
if (tenant.ms_teams_tenant_sid === props.ms_teams_tenant_sid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tenant.tenant_fqdn === domainName) {
|
||||
errorMessages.push(
|
||||
'The domain name you have entered is already in use.'
|
||||
);
|
||||
setInvalidDomainName(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refDomainName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!account) {
|
||||
errorMessages.push('Please select an account');
|
||||
setInvalidAccount(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refAccount.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Submit
|
||||
//=============================================================================
|
||||
const method = props.type === 'add'
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
const url = props.type === 'add'
|
||||
? `/MicrosoftTeamsTenants`
|
||||
: `/MicrosoftTeamsTenants/${props.ms_teams_tenant_sid}`;
|
||||
|
||||
const data = {
|
||||
tenant_fqdn: domainName.trim(),
|
||||
account_sid: account,
|
||||
application_sid: application || null,
|
||||
};
|
||||
|
||||
if (props.type === 'add') {
|
||||
data.service_provider_sid = serviceProviderSid;
|
||||
}
|
||||
|
||||
await axios({
|
||||
method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data
|
||||
});
|
||||
|
||||
const dispatchMessage = props.type === 'add'
|
||||
? 'Tenant created successfully'
|
||||
: 'Tenant updated successfully';
|
||||
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: dispatchMessage
|
||||
});
|
||||
|
||||
isMounted = false;
|
||||
history.push('/internal/ms-teams-tenants');
|
||||
} catch (err) {
|
||||
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={'258px'}/>
|
||||
: <Form
|
||||
large
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="domainName">Domain Name</Label>
|
||||
<Input
|
||||
name="domainName"
|
||||
id="domainName"
|
||||
value={domainName}
|
||||
onChange={e => setDomainName(e.target.value)}
|
||||
placeholder="Tenant's fully qualified domain name"
|
||||
invalid={invalidDomainName}
|
||||
autoFocus
|
||||
ref={refDomainName}
|
||||
/>
|
||||
|
||||
<Label htmlFor="account">Account</Label>
|
||||
<Select
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={e => setAccount(e.target.value)}
|
||||
invalid={invalidAccount}
|
||||
ref={refAccount}
|
||||
>
|
||||
{(
|
||||
(accountValues.length > 1) ||
|
||||
(props.type === 'edit' && account !== accountValues[0].account_sid)
|
||||
) && (
|
||||
<option value="">-- Choose the account that this tenant should be associated with --</option>
|
||||
)}
|
||||
{accountValues.map(a => (
|
||||
<option
|
||||
key={a.account_sid}
|
||||
value={a.account_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Label htmlFor="application">Application</Label>
|
||||
<Select
|
||||
name="application"
|
||||
id="application"
|
||||
value={application}
|
||||
onChange={e => setApplication(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
{props.type === 'add'
|
||||
? '-- OPTIONAL: Choose the application that this tenant should be associated with --'
|
||||
: '-- NONE --'
|
||||
}
|
||||
</option>
|
||||
{applicationValues.map(a => (
|
||||
<option
|
||||
key={a.application_sid}
|
||||
value={a.application_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
{props.type === 'edit' && (
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/ms-teams-tenants');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
grid
|
||||
fullWidth={props.type === 'add'}
|
||||
>
|
||||
{props.type === 'add'
|
||||
? 'Add Microsoft Teams Tenant'
|
||||
: 'Save'
|
||||
}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default MsTeamsTenantForm;
|
||||
457
src/components/forms/PhoneNumberForm.js
Normal file
@@ -0,0 +1,457 @@
|
||||
/* 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 Select from '../elements/Select';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
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);
|
||||
const refSipTrunk = useRef(null);
|
||||
const refAccount = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ phoneNumber, setPhoneNumber ] = useState('');
|
||||
const [ sipTrunk, setSipTrunk ] = useState('');
|
||||
const [ account, setAccount ] = useState('');
|
||||
const [ application, setApplication ] = useState('');
|
||||
|
||||
// Select list values
|
||||
const [ sipTrunkValues, setSipTrunkValues ] = useState('');
|
||||
const [ accountValues, setAccountValues ] = useState('');
|
||||
const [ applicationValues, setApplicationValues ] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidPhoneNumber, setInvalidPhoneNumber ] = useState(false);
|
||||
const [ invalidSipTrunk, setInvalidSipTrunk ] = useState(false);
|
||||
const [ invalidAccount, setInvalidAccount ] = useState(false);
|
||||
|
||||
const [ phoneNumbers, setPhoneNumbers ] = useState('');
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
// Check if user is logged in
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sipTrunksPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `ServiceProviders/${currentServiceProvider}/VoipCarriers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `ServiceProviders/${currentServiceProvider}/Applications`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const phoneNumbersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `ServiceProviders/${currentServiceProvider}/PhoneNumbers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const promises = [
|
||||
sipTrunksPromise,
|
||||
accountsPromise,
|
||||
applicationsPromise,
|
||||
phoneNumbersPromise,
|
||||
];
|
||||
|
||||
const promiseAllValues = await Promise.all(promises);
|
||||
|
||||
const sipTrunks = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[1].data;
|
||||
const applications = promiseAllValues[2].data;
|
||||
const phoneNumbers = promiseAllValues[3].data;
|
||||
|
||||
setSipTrunkValues(sipTrunks);
|
||||
setAccountValues(accounts);
|
||||
setApplicationValues(applications);
|
||||
setPhoneNumbers(phoneNumbers);
|
||||
|
||||
if (!accounts.length) {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must create an account before you can create a phone number.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!sipTrunks.length) {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must create a SIP trunk before you can create a phone number.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!accounts.length) {
|
||||
history.push('/internal/accounts');
|
||||
return;
|
||||
} else if (!sipTrunks.length) {
|
||||
history.push('/internal/carriers');
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.type === 'edit') {
|
||||
const phoneNumberData = promiseAllValues[3] && promiseAllValues[3].data.filter(p => {
|
||||
return p.phone_number_sid === props.phone_number_sid;
|
||||
});
|
||||
|
||||
if (!phoneNumberData.length) {
|
||||
history.push('/internal/phone-numbers');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'That phone number does not exist.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setPhoneNumber (( phoneNumberData[0] && phoneNumberFormat(phoneNumberData[0].number)) || '');
|
||||
setSipTrunk (( phoneNumberData[0] && phoneNumberData[0].voip_carrier_sid ) || '');
|
||||
setAccount (( phoneNumberData[0] && phoneNumberData[0].account_sid ) || '');
|
||||
setApplication (( phoneNumberData[0] && phoneNumberData[0].application_sid ) || '');
|
||||
}
|
||||
|
||||
if (props.type === 'add') {
|
||||
if (sipTrunks.length === 1) { setSipTrunk(sipTrunks[0].voip_carrier_sid); }
|
||||
if ( accounts.length === 1) { setAccount( accounts[0].account_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 {
|
||||
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);
|
||||
}
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async e => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidPhoneNumber(false);
|
||||
setInvalidSipTrunk(false);
|
||||
setInvalidAccount(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if (!phoneNumber) {
|
||||
errorMessages.push('Please provide a phone number');
|
||||
setInvalidPhoneNumber(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refPhoneNumber.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
// check if phone number is already in use
|
||||
for (const num of phoneNumbers) {
|
||||
if (num.phone_number_sid === props.phone_number_sid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (num.number === phoneNumber) {
|
||||
errorMessages.push(
|
||||
'The phone number you have entered is already in use.'
|
||||
);
|
||||
setInvalidPhoneNumber(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refPhoneNumber.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!sipTrunk) {
|
||||
errorMessages.push('Please select a SIP trunk');
|
||||
setInvalidSipTrunk(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refSipTrunk.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
errorMessages.push('Please select an account');
|
||||
setInvalidAccount(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refAccount.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Submit
|
||||
//=============================================================================
|
||||
const method = props.type === 'add'
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
const url = props.type === 'add'
|
||||
? `/PhoneNumbers`
|
||||
: `/PhoneNumbers/${props.phone_number_sid}`;
|
||||
|
||||
const data = {
|
||||
account_sid: account,
|
||||
application_sid: application || null,
|
||||
};
|
||||
|
||||
const cleanedUpNumber = phoneNumber.trim().replace(/[\s-()+]/g,'');
|
||||
|
||||
if (props.type === 'add') {
|
||||
data.number = cleanedUpNumber;
|
||||
data.voip_carrier_sid = sipTrunk;
|
||||
}
|
||||
|
||||
await axios({
|
||||
method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data
|
||||
});
|
||||
|
||||
const dispatchMessage = props.type === 'add'
|
||||
? 'Phone number created successfully'
|
||||
: 'Phone number updated successfully';
|
||||
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: dispatchMessage
|
||||
});
|
||||
|
||||
isMounted = false;
|
||||
history.push('/internal/phone-numbers');
|
||||
} catch (err) {
|
||||
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={'310px'}/>
|
||||
: <Form
|
||||
large
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="phoneNumber">Phone Number</Label>
|
||||
<Input
|
||||
name="phoneNumber"
|
||||
id="phoneNumber"
|
||||
value={phoneNumber}
|
||||
onChange={e => setPhoneNumber(e.target.value)}
|
||||
placeholder="Phone number that will be sending calls to this service"
|
||||
invalid={invalidPhoneNumber}
|
||||
autoFocus
|
||||
ref={refPhoneNumber}
|
||||
disabled={props.type === 'edit'}
|
||||
/>
|
||||
|
||||
<Label htmlFor="sipTrunk">SIP Trunk</Label>
|
||||
<Select
|
||||
name="sipTrunk"
|
||||
id="sipTrunk"
|
||||
value={sipTrunk}
|
||||
onChange={e => setSipTrunk(e.target.value)}
|
||||
invalid={invalidSipTrunk}
|
||||
ref={refSipTrunk}
|
||||
disabled={props.type === 'edit'}
|
||||
>
|
||||
{(
|
||||
(sipTrunkValues.length > 1) ||
|
||||
(props.type === 'edit' && sipTrunk !== sipTrunkValues[0].voip_carrier_sid)
|
||||
) && (
|
||||
<option value="">-- Choose the SIP trunk that this phone number belongs to --</option>
|
||||
)}
|
||||
{sipTrunkValues.map(s => (
|
||||
<option
|
||||
key={s.voip_carrier_sid}
|
||||
value={s.voip_carrier_sid}
|
||||
>
|
||||
{s.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Label htmlFor="account">Account</Label>
|
||||
<Select
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={(e) => {
|
||||
setAccount(e.target.value);
|
||||
setApplication('');
|
||||
}}
|
||||
invalid={invalidAccount}
|
||||
ref={refAccount}
|
||||
>
|
||||
{(
|
||||
(accountValues.length > 1) ||
|
||||
(props.type === 'edit' && account !== accountValues[0].account_sid)
|
||||
) && (
|
||||
<option value="">-- Choose the account that this phone number should be associated with --</option>
|
||||
)}
|
||||
{accountValues.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
|
||||
<option
|
||||
key={a.account_sid}
|
||||
value={a.account_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Label htmlFor="application">Application</Label>
|
||||
<Select
|
||||
name="application"
|
||||
id="application"
|
||||
value={application}
|
||||
onChange={e => setApplication(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
{props.type === 'add'
|
||||
? '-- OPTIONAL: Choose the application that will receive calls from this number --'
|
||||
: '-- NONE --'
|
||||
}
|
||||
</option>
|
||||
{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}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
{props.type === 'edit' && (
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/phone-numbers');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
grid
|
||||
fullWidth={props.type === 'add'}
|
||||
>
|
||||
{props.type === 'add'
|
||||
? 'Add Phone Number'
|
||||
: 'Save'
|
||||
}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhoneNumberForm;
|
||||
404
src/components/forms/SettingsForm.js
Normal file
@@ -0,0 +1,404 @@
|
||||
/* 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 Checkbox from '../elements/Checkbox';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
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, LIMITS } 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 refServiceProviderName = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [enableMsTeams, setEnableMsTeams] = useState(false);
|
||||
const [sbcDomainName, setSbcDomainName] = useState('');
|
||||
const [serviceProviderName, setServiceProviderName] = useState('');
|
||||
|
||||
// For when user has data in sbcDomainName and then taps the checkbox to disable MsTeams
|
||||
const [savedSbcDomainName, setSavedSbcDomainName] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [invalidEnableMsTeams, setInvalidEnableMsTeams] = useState(false);
|
||||
const [invalidSbcDomainName, setInvalidSbcDomainName] = useState(false);
|
||||
const [invalidServiceProviderName, setInvalidServiceProviderName] = useState(false);
|
||||
|
||||
const [showLoader, setShowLoader] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [serviceProviderSid, setServiceProviderSid] = useState('');
|
||||
const [serviceProviders, setServiceProviders] = useState([]);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
const [localLimits, setLocalLimits] = useState([]);
|
||||
|
||||
const callApi = async (path, method, data) => {
|
||||
return await axios({
|
||||
method: method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: path,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
...(data && {data})
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getSettingsData = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceProvidersResponse = await callApi(`/ServiceProviders`, 'get');
|
||||
|
||||
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 || '');
|
||||
|
||||
// Fetch Service provider Limits
|
||||
if (sp.service_provider_sid) {
|
||||
const serviceProvidersLimitsResponse = await callApi(`/ServiceProviders/${sp.service_provider_sid}/Limits`, 'get');
|
||||
setLocalLimits(serviceProvidersLimitsResponse.data);
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (currentServiceProvider) {
|
||||
getSettingsData();
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
const toggleMsTeams = (e) => {
|
||||
if (!e.target.checked && sbcDomainName) {
|
||||
setSavedSbcDomainName(sbcDomainName);
|
||||
setSbcDomainName('');
|
||||
}
|
||||
if (e.target.checked && savedSbcDomainName) {
|
||||
setSbcDomainName(savedSbcDomainName);
|
||||
setSavedSbcDomainName('');
|
||||
}
|
||||
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 {
|
||||
//=============================================================================
|
||||
// reset
|
||||
//=============================================================================
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidEnableMsTeams(false);
|
||||
setInvalidSbcDomainName(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'
|
||||
);
|
||||
setInvalidSbcDomainName(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refSbcDomainName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!enableMsTeams && sbcDomainName) {
|
||||
errorMessages.push(
|
||||
'You must check "Enable Microsoft Teams Direct Routing" to enable this feature, or remove the SBC Domain Name provided'
|
||||
);
|
||||
setInvalidEnableMsTeams(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refEnableMsTeams.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// submit data
|
||||
//=============================================================================
|
||||
const data = {
|
||||
ms_teams_fqdn: sbcDomainName.trim() || null,
|
||||
name: serviceProviderName.trim(),
|
||||
};
|
||||
|
||||
await callApi(`/ServiceProviders/${serviceProviderSid}`, 'put', data);
|
||||
await Promise.all(
|
||||
localLimits.map(l => l.quantity === "" ?
|
||||
callApi(`/ServiceProviders/${serviceProviderSid}/Limits?category=${l.category}`, 'delete') :
|
||||
callApi(`/ServiceProviders/${serviceProviderSid}/Limits`, 'post', l))
|
||||
);
|
||||
|
||||
refreshMsTeamsData();
|
||||
|
||||
//=============================================================================
|
||||
// redirect
|
||||
//=============================================================================
|
||||
isMounted = false;
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: 'Settings updated'
|
||||
});
|
||||
|
||||
} 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="365px" />
|
||||
: (
|
||||
<>
|
||||
<Form
|
||||
large
|
||||
wideLabel
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="serviceProviderName">Service Provider Name</Label>
|
||||
<Input
|
||||
name="serviceProviderName"
|
||||
id="serviceProviderName"
|
||||
value={serviceProviderName}
|
||||
onChange={e => setServiceProviderName(e.target.value)}
|
||||
invalid={invalidServiceProviderName}
|
||||
ref={refServiceProviderName}
|
||||
/>
|
||||
|
||||
<div>{/* needed for CSS grid layout */}</div>
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
id="enableMsTeams"
|
||||
label="Enable Microsoft Teams Direct Routing"
|
||||
checked={enableMsTeams}
|
||||
onChange={toggleMsTeams}
|
||||
invalid={invalidEnableMsTeams}
|
||||
ref={refEnableMsTeams}
|
||||
/>
|
||||
|
||||
<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") || ""}
|
||||
/>
|
||||
|
||||
{LIMITS.map(({ label, category }) => {
|
||||
const quantity = localLimits?.find(l => l.category === category)?.quantity;
|
||||
return <React.Fragment key={category}>
|
||||
<Label htmlFor={category}>{label}</Label>
|
||||
<Input
|
||||
name={category}
|
||||
id={category}
|
||||
type="number"
|
||||
placeholder="Enter Quantity (0=unlimited)"
|
||||
min="0"
|
||||
value={quantity >= 0 ? quantity : ""}
|
||||
onChange={e => {
|
||||
const limit = localLimits.find(l => l.category === category);
|
||||
const value = e.target.value ? Number(e.target.value) : "";
|
||||
if (limit) {
|
||||
setLocalLimits(localLimits.map(l => l.category === category ? {...l, quantity: value} : l));
|
||||
} else {
|
||||
setLocalLimits([...localLimits, {category, quantity: value}]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>;
|
||||
})}
|
||||
|
||||
{errorMessage && !confirmDelete && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
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>
|
||||
</Form>
|
||||
|
||||
{confirmDelete && serviceProviders.length > 1 && (
|
||||
<Modal
|
||||
title="Are you sure you want to delete the Service Provider?"
|
||||
loader={false}
|
||||
content={
|
||||
<div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<Td>Service Provider Name:</Td>
|
||||
<Td>{serviceProviderName}</Td>
|
||||
</tr>
|
||||
<tr>
|
||||
<Td>SBC Domain Name:</Td>
|
||||
<Td>{sbcDomainName || '[none]'}</Td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{errorMessage && (
|
||||
<FormError message={errorMessage} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
handleCancel={() => {
|
||||
setConfirmDelete(false);
|
||||
setErrorMessage('');
|
||||
}}
|
||||
handleSubmit={handleDelete}
|
||||
actionText="Delete"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsForm;
|
||||
821
src/components/forms/SpeechForm.js
Normal file
@@ -0,0 +1,821 @@
|
||||
/* 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);
|
||||
const refUseCustomTts = useRef(null);
|
||||
const refUseCustomStt = 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('');
|
||||
const [useCustomTts, setUseCustomTts] = useState(false);
|
||||
const [useCustomStt, setUseCustomStt] = useState(false);
|
||||
const [customTtsEndpoint, setCustomTtsEndpoint] = useState('');
|
||||
const [customSttEndpoint, setCustomSttEndpoint] = useState('');
|
||||
const [tmpCustomTtsEndpoint, setTmpCustomTtsEndpoint] = useState('');
|
||||
const [tmpCustomSttEndpoint, setTmpCustomSttEndpoint] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [invalidVendorGoogle, setInvalidVendorGoogle] = useState(false);
|
||||
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 [invalidUseCustomTts, setInvalidUseCustomTts] = useState(false);
|
||||
const [invalidUseCustomStt, setInvalidUseCustomStt] = 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);
|
||||
setUseCustomTts(speechCredential.data.use_custom_tts || false);
|
||||
setCustomTtsEndpoint(speechCredential.data.custom_tts_endpoint || '');
|
||||
setUseCustomStt(speechCredential.data.use_custom_stt || false);
|
||||
setCustomSttEndpoint(speechCredential.data.custom_stt_endpoint || '');
|
||||
}
|
||||
setShowLoader(false);
|
||||
} catch (err) {
|
||||
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);
|
||||
setInvalidUseCustomTts(false);
|
||||
setInvalidUseCustomStt(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 (useCustomTts && !customTtsEndpoint) {
|
||||
errorMessages.push('Please provide a custom voice endpoint.');
|
||||
setInvalidUseCustomTts(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refUseCustomTts.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (useCustomStt && !customSttEndpoint) {
|
||||
errorMessages.push('Please provide a custom speech endpoint.');
|
||||
setInvalidUseCustomStt(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refUseCustomStt.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} 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_provider_sid: accountSid ? null : currentServiceProvider,
|
||||
account_sid: accountSid || null,
|
||||
use_for_tts: useForTts,
|
||||
use_for_stt: useForStt,
|
||||
...(vendor === 'google' && method === 'post' && {
|
||||
service_key: serviceKey ? JSON.stringify(serviceKey) : null,
|
||||
}),
|
||||
...(vendor === 'aws' && {
|
||||
...(method === 'post' && {
|
||||
access_key_id: accessKeyId || null,
|
||||
secret_access_key: secretAccessKey || null}),
|
||||
aws_region: awsregion || null
|
||||
}),
|
||||
...(vendor === 'microsoft' && {
|
||||
region: region || null,
|
||||
use_custom_tts: useCustomTts ? 1 : 0,
|
||||
use_custom_stt: useCustomStt ? 1 : 0,
|
||||
custom_tts_endpoint: customTtsEndpoint || null,
|
||||
custom_stt_endpoint: customSttEndpoint || null,
|
||||
}),
|
||||
...(['wellsaid', 'microsoft'].includes(vendor) && method === 'post' && {
|
||||
api_key: apiKey || null,
|
||||
}),
|
||||
}
|
||||
});
|
||||
|
||||
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)}
|
||||
{...[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>
|
||||
<div />
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
name="useACustomVoice"
|
||||
id="useACustomVoice"
|
||||
label="Use a custom voice"
|
||||
checked={useCustomTts}
|
||||
onChange={e => {
|
||||
setUseCustomTts(e.target.checked);
|
||||
if (e.target.checked && tmpCustomTtsEndpoint) {
|
||||
setCustomTtsEndpoint(tmpCustomTtsEndpoint);
|
||||
}
|
||||
|
||||
if (!e.target.checked) {
|
||||
setTmpCustomTtsEndpoint(customTtsEndpoint);
|
||||
setCustomTtsEndpoint("");
|
||||
}
|
||||
}}
|
||||
invalid={invalidUseCustomTts}
|
||||
ref={refUseCustomTts}
|
||||
/>
|
||||
<Label htmlFor="customVoiceEndpoint">Custom voice endpoint</Label>
|
||||
<Input
|
||||
name="customVoiceEndpoint"
|
||||
id="customVoiceEndpoint"
|
||||
value={customTtsEndpoint}
|
||||
onChange={e => setCustomTtsEndpoint(e.target.value)}
|
||||
placeholder="Custom voice endpoint"
|
||||
invalid={invalidUseCustomTts}
|
||||
ref={refUseCustomTts}
|
||||
disabled={!useCustomTts}
|
||||
/>
|
||||
<div />
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
name="useACustomSpeechModel"
|
||||
id="useACustomSpeechModel"
|
||||
label="Use a custom speech model"
|
||||
checked={useCustomStt}
|
||||
onChange={e => {
|
||||
setUseCustomStt(e.target.checked);
|
||||
if(e.target.checked && tmpCustomSttEndpoint) {
|
||||
setCustomSttEndpoint(tmpCustomSttEndpoint);
|
||||
}
|
||||
|
||||
if(!e.target.checked) {
|
||||
setTmpCustomSttEndpoint(customSttEndpoint);
|
||||
setCustomSttEndpoint("");
|
||||
}
|
||||
}}
|
||||
invalid={invalidUseCustomStt}
|
||||
ref={refUseCustomStt}
|
||||
/>
|
||||
<Label htmlFor="customSpeechEndpoint">Custom speech endpoint</Label>
|
||||
<Input
|
||||
name="customSpeechEndpoint"
|
||||
id="customSpeechEndpoint"
|
||||
value={customSttEndpoint}
|
||||
onChange={e => setCustomSttEndpoint(e.target.value)}
|
||||
placeholder="Custom speech endpoint"
|
||||
invalid={invalidUseCustomStt}
|
||||
ref={refUseCustomStt}
|
||||
disabled={!useCustomStt}
|
||||
/>
|
||||
</>
|
||||
) : vendor === 'wellsaid' ? (
|
||||
<>
|
||||
<Label htmlFor="apiKey">API Key</Label>
|
||||
<Input
|
||||
name="apiKey"
|
||||
id="apiKey"
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
placeholder=""
|
||||
invalid={invalidApiKey}
|
||||
ref={refApiKey}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<StyledButtonGroup flexEnd spaced type={type}>
|
||||
<Button
|
||||
rounded="true"
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/speech-services');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: type === 'add' ? 'New speech service canceled' : 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button rounded="true" disabled={!vendor}>
|
||||
{type === 'add'
|
||||
? 'Add Speech Service'
|
||||
: 'Save'
|
||||
}
|
||||
</Button>
|
||||
</StyledButtonGroup>
|
||||
</Form>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeechServicesAddEdit;
|
||||
@@ -1,69 +0,0 @@
|
||||
import React, { useEffect, forwardRef } from "react";
|
||||
|
||||
import { TypeaheadSelector } from "src/components/forms";
|
||||
|
||||
import type { Account } from "src/api/types";
|
||||
import { hasLength } from "src/utils";
|
||||
|
||||
type AccountSelectProps = {
|
||||
label?: string;
|
||||
account: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
accounts?: Account[];
|
||||
defaultOption?: boolean;
|
||||
|
||||
/** Native select element attributes we support */
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type SelectorRef = HTMLInputElement;
|
||||
|
||||
export const AccountSelect = forwardRef<SelectorRef, AccountSelectProps>(
|
||||
(
|
||||
{
|
||||
label = "Account",
|
||||
account: [accountSid, setAccountSid],
|
||||
accounts,
|
||||
required = true,
|
||||
defaultOption,
|
||||
...restProps
|
||||
}: AccountSelectProps,
|
||||
ref,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (hasLength(accounts) && !accountSid && !defaultOption) {
|
||||
setAccountSid(accounts[0].account_sid);
|
||||
}
|
||||
}, [accounts, accountSid, defaultOption]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor="account_sid">
|
||||
{label} {required && <span>*</span>}
|
||||
</label>
|
||||
<TypeaheadSelector
|
||||
ref={ref}
|
||||
id="account_sid"
|
||||
name="account_sid"
|
||||
required={required}
|
||||
value={accountSid}
|
||||
options={(defaultOption
|
||||
? [{ name: "All accounts", value: "" }]
|
||||
: []
|
||||
).concat(
|
||||
hasLength(accounts)
|
||||
? accounts.map((account) => ({
|
||||
name: account.name,
|
||||
value: account.account_sid,
|
||||
}))
|
||||
: [],
|
||||
)}
|
||||
onChange={(e) => setAccountSid(e.target.value)}
|
||||
{...restProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
AccountSelect.displayName = "AccountSelect";
|
||||