jambonz webapp refresh
initial scaffold switch to preact/compat add feather icons dep jambonz-ui, index.html stub auth and store readme tweaks alias preact in vite config more readme tweaks Update README.md lots of things login flow... add notes on apis by route lots of work... readmes constants Update login.tsx Update index.ts Update index.ts Update create-password.tsx Update actions.ts Update index.tsx Update index.tsx Update actions.ts Update index.ts react version for eslint some refactor and cleanup Update api.ts Update create-password.tsx fetch transport wrapper api util toast time -- oops msg constants img path for docs/readmes global dispatch, generic actions etc... unreachable and stuff properly wrap require-auth routes support promise chain and async/await for api fetch transport initial responsive navi menu Update navi-data.ts Update navi.tsx Update styles.scss Rename navi-data.ts to navi-items.ts Update navi.tsx Update index.ts Update layout.tsx Update index.ts Update layout.tsx Update index.tsx Update index.tsx Update actions.ts Update index.tsx Update index.tsx Update create-password.tsx Update login.tsx Update create-password.tsx move things around access control interface Update index.tsx acl component etc working on settings form.. more settings, forms, HOCs service providers workflow button up modals and toasts mobile navi and toast timeout Update index.tsx Update index.ts Update and rename index.ts to index.tsx Update create-password.tsx Update create-password.tsx Update Dockerfile Update entrypoint.sh Update Dockerfile Update navi.tsx Update auth.tsx Update auth.tsx Update layout.tsx Update layout.tsx Update login.tsx Update login.tsx Update settings.tsx Update index.tsx Update index.ts better lint-staged fix sp undefined toast dispatch helpers sass vars -- no magic numbers Update index.ts Update create-password.tsx Update login.tsx Update index.ts Update settings.tsx Update accounts.tsx working on settings... Update index.ts Update settings.tsx Update index.tsx more settings view... get rid of most any usage Update index.tsx better api hook get strong with types obscured text component HOC for dispatch type-safety tweak api types github icon on login layout responsive grid -- api keys better fetch transport with resolve/reject fix generic action/dispatch typings prefer interface for GlobalDispatch Update index.ts Update auth.tsx Update auth.tsx Update create-password.tsx checkzones wrap up checkzones move styles around... alias src stub internal views stub not found container contrib readme and codeowners Update README.md Update and rename setup.md to environment.md Update environment.md Update environment.md Update contrib.md Update contrib.md Update contrib.md Update and rename contrib.md to contributors.md Update contributors.md Update index.ts use api data hook accounts stub, generic apikeys container account edit form Update edit.tsx Update edit.tsx add/edit for account form lots of good refactors check current sp on settings grid stuff Update index.scss Update styles.scss Update contributors.md Update constants.ts stubbing accounts as card view Update types.ts Update types.ts Update auth.tsx Update create-password.tsx Update index.ts Update index.tsx fix enum status codes component cleanup delete account flow Update types.ts Update delete.tsx Update use-mobile-media.ts acl hoc Update types.ts Update index.ts Update types.ts fix generic useapidata Update types.ts Update types.ts Update types.ts Create index.tsx Create types.ts Update types.ts Create types.ts Update index.tsx button up acl, feature flags and docs subspace initial feature stub fix some things wrap up subspace feature tooltip Update subspace.tsx Delete styles.scss Update types.ts Update auth.tsx Update index.ts some more type stuff add react/jsx-key error for missing shorthand frag keys basic spinner... no accounts data files for regions and speech vendor selector logic tighten up vendor stuff bit more cleanup Update types.ts Update index.tsx Update index.tsx Update subspace.tsx fix some type things stub mock dev server implementation add parity for account siprec_hook_sid latest jambonz-ui update cleanup package.json fix docker stuff docker notes in readme adding github actions package lock version remove unused jest deps update jambonz-ui new new jambonz-ui list view vs cards view fix no accounts list view fix prettier config
38
.dockerignore
Normal file
@@ -0,0 +1,38 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Lock files
|
||||
yarn.lock
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# App specifics
|
||||
/public/fonts
|
||||
.dockerignore
|
||||
.git*
|
||||
.husky
|
||||
.vscode
|
||||
CODEOWNERS
|
||||
Dockerfile
|
||||
LICENSE
|
||||
README.md
|
||||
dev.server.ts
|
||||
docs
|
||||
4
.env
@@ -1,2 +1,2 @@
|
||||
REACT_APP_API_BASE_URL=http://127.0.0.1:3002/v1
|
||||
GENERATE_SOURCEMAP=false
|
||||
VITE_API_BASE_URL=http://127.0.0.1:3000/v1
|
||||
VITE_DEV_BASE_URL=http://127.0.0.1:3002/api
|
||||
13
.eslintrc
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"extends": "react-app",
|
||||
"rules": {
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
3
.github/workflows/docker-publish.yml
vendored
@@ -15,7 +15,6 @@ env:
|
||||
|
||||
jobs:
|
||||
push:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
|
||||
@@ -48,4 +47,4 @@ jobs:
|
||||
echo VERSION=$VERSION
|
||||
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
|
||||
35
.github/workflows/pr-checks.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: PR Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# switch to "main" when we're ready
|
||||
- refresh
|
||||
pull_request:
|
||||
branches:
|
||||
# switch to "**" when we're ready
|
||||
- "refresh/**"
|
||||
|
||||
jobs:
|
||||
pr-checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Cache node_modules
|
||||
id: node-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install node_modules
|
||||
if: steps.node-cache.outputs.cache-hit != 'true'
|
||||
run: npm install
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
npm run format
|
||||
npm run lint
|
||||
npm run test
|
||||
npm run build
|
||||
43
.gitignore
vendored
@@ -1,21 +1,28 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Lock files
|
||||
yarn.lock
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# App specifics
|
||||
/public/fonts
|
||||
11
.husky/pre-commit
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/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
|
||||
28
.prettierignore
Normal file
@@ -0,0 +1,28 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Lock files
|
||||
yarn.lock
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# App specifics
|
||||
/public/fonts
|
||||
1
.prettierrc.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-vscode.vscode-typescript-next"
|
||||
]
|
||||
}
|
||||
9
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
4
CODEOWNERS
Normal file
@@ -0,0 +1,4 @@
|
||||
# see https://bit.ly/3t2LLMR for CODEOWNERS syntax
|
||||
|
||||
# all files owned by:
|
||||
* @davehorton @kitajchuk
|
||||
@@ -2,7 +2,6 @@ FROM node:18.6.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
|
||||
@@ -12,7 +11,7 @@ RUN apk add curl
|
||||
WORKDIR /opt/app
|
||||
COPY . /opt/app
|
||||
COPY --from=builder /opt/app/node_modules ./node_modules
|
||||
COPY --from=builder /opt/app/build ./build
|
||||
COPY --from=builder /opt/app/dist ./dist
|
||||
COPY ./entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Drachtio Communications Services, LLC
|
||||
Copyright (c) 2022 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
|
||||
|
||||
75
README.md
@@ -1,37 +1,66 @@
|
||||
# Jambonz Web Application
|
||||
<p align="center">
|
||||
<a href="https://jambonz.org">
|
||||
<img src="./public/icon192.png" height="128">
|
||||
<h1 align="center">jambonz</h1>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Deploy to Production
|
||||
<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>
|
||||
|
||||
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
|
||||
> A simple provisioning webapp for jambonz
|
||||
|
||||
NOTE: Here is what `npm run deploy` does:
|
||||
## OSS Developers
|
||||
|
||||
- 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`)
|
||||
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`)
|
||||
|
||||
Alternatively, you can serve the app manually (without pm2) with `npm run serve`.
|
||||
|
||||
## Updates
|
||||
### Update production
|
||||
|
||||
If there is an update to this code base, you can update the code without re-deploying.
|
||||
|
||||
1. run `git pull`
|
||||
2. run `npm run build`
|
||||
1. run `git pull origin main --rebase`
|
||||
2. run `npm i`
|
||||
3. run `npm run build`
|
||||
|
||||
## Development
|
||||
### With docker
|
||||
|
||||
### 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.
|
||||
You can pull the public docker image for the web app:
|
||||
|
||||
### 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.
|
||||
```sh
|
||||
docker pull ghcr.io/jambonz/webapp:latest
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
50
dev.server.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import cors from "cors";
|
||||
import express from "express";
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { RecentCall, PagedResponse } from "./src/api/types";
|
||||
|
||||
const app = express();
|
||||
const port = 3002;
|
||||
|
||||
app.use(cors());
|
||||
|
||||
/** Example of a local dev server that can serve mock responses for certain APIs */
|
||||
|
||||
app.get(
|
||||
"/api/Accounts/:account_sid/RecentCalls",
|
||||
(req: Request, res: Response) => {
|
||||
const call: RecentCall = {
|
||||
account_sid: req.params.account_sid,
|
||||
call_sid: "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
from: "string",
|
||||
to: "string",
|
||||
answered: true,
|
||||
sip_call_id: "string",
|
||||
sip_status: 0,
|
||||
duration: 0,
|
||||
attempted_at: 0,
|
||||
answered_at: 0,
|
||||
terminated_at: 0,
|
||||
termination_reason: "string",
|
||||
host: "string",
|
||||
remote_host: "string",
|
||||
direction: "inbound",
|
||||
trunk: "string",
|
||||
};
|
||||
const total = 50;
|
||||
/** Simple dumb hack to populate mock data for responses... */
|
||||
const data = new Array(total).fill(call, 0, total);
|
||||
|
||||
res.status(200).json(<PagedResponse<RecentCall>>{
|
||||
total,
|
||||
batch: 0,
|
||||
page: 0,
|
||||
data,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`express server listening on port ${port}`);
|
||||
});
|
||||
189
docs/contributors.md
Normal file
@@ -0,0 +1,189 @@
|
||||
<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
|
||||
|
||||
Before running the web app you'll need to complete 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 i` and then `npm start`.
|
||||
|
||||
## :pancakes: Dev stack
|
||||
|
||||
We're using [vite](https://vitejs.dev/) for development. The main application is
|
||||
[react](https://reactjs.org/docs/getting-started.html) aliasing [preact/compat](https://preactjs.com/guide/v10/switching-to-preact#setting-up-compat)
|
||||
for `production` builds. For code sanity we're using [typescript](https://www.typescriptlang.org/),
|
||||
[prettier](https://prettier.io/) and [eslint](https://eslint.org/). For code guarding we've implemented [husky](https://typicode.github.io/husky/#/)
|
||||
and [lint-staged](https://www.npmjs.com/package/lint-staged) to check your changes before committing.
|
||||
|
||||
## :lock: Auth middleware
|
||||
|
||||
We have auth middleware implemented at the router level. It was initially based on this
|
||||
[useAuth](https://usehooks.com/useAuth/) example but has been typed as well as modified to include
|
||||
a `RequireAuth` component for wrapping internal `Routes`. The main hook you'll use here is simply `useAuth()`.
|
||||
This hook provides the `AuthStateContext` which has the following:
|
||||
|
||||
- `token`: current auth token
|
||||
- `signin(user, pass)`: function to log the user in
|
||||
- `signout()`: function to log the user out
|
||||
- `authorized`: boolean dictating authorized status
|
||||
|
||||
#### A note on internal ACL implementation
|
||||
|
||||
We have some simple ACL utilities for managing access to UI/routes based on criteria. There is a basic
|
||||
`AccessControl` component and a handy `withAccessControl(acl)(Component)` HOC for route containers with
|
||||
redirect. There is also a `useAccessControl(acl)` hook for use at the component-level.
|
||||
|
||||
## :joystick: Application state
|
||||
|
||||
We're following the model `ui = fn(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 `serviceProviders` and the actively
|
||||
selected `currentServiceProvider`. We also use local state for a basic `ACL` permissions matrix and for the
|
||||
global `toast` notifications. That's it! Local state is easy.
|
||||
|
||||
Because of this limited scope for local state we're **not using a third-party state manager**. We have a
|
||||
custom store implementation using vanilla React `useReducer` and `useContext` to provide the state to our
|
||||
application. There are many useful functions and hooks for working with state which include the following:
|
||||
|
||||
- `useSelectState(key)`: returns just the piece of state desired
|
||||
- `useDispatch()`: returns global `dispatch({ type, payload })` 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
|
||||
|
||||
#### A note on feature flags
|
||||
|
||||
Feature flags are managed via [Vite's environment variables](https://vitejs.dev/guide/env-and-mode.html#env-files).
|
||||
They should be implemented as `VITE_FEATURE_{feature}`. They then need to be added to the `FeatureFlag` store interface
|
||||
and their implementation should be added to the initial state object. See the `subspace` feature flag implementation
|
||||
for an example of how to do this.
|
||||
|
||||
#### A note here on type-safety
|
||||
|
||||
Our action interface receives a generic type argument that is mapped to the type parameter for the action.
|
||||
This generic type is also used for indexed access to the state interface for the action payload parameter.
|
||||
We use a global dispatch middleware that also receives this type argument and passes it to the action.
|
||||
With this setup our app dispatch enforces that the type of payload maps to the type parameter received.
|
||||
|
||||
## :wales: API implementation
|
||||
|
||||
We have a normalized API implementation. It should be adhered to at all times. Always use API helpers
|
||||
that use `fetchTransport` under the hood and make use of our generic `use` hooks for most general fetch,
|
||||
e.g. `GET`, requests. Here are a couple useful hooks for general `GET` fetching. Both methods the `data` fetched,
|
||||
a `refetcher` function that, when called, will update the data in the hook and update your component with
|
||||
the newly fetched data and a possible `error` if the fetch failed.
|
||||
|
||||
- `getFetch(url)`: returns `fetchTransport` for an `:GET` api request
|
||||
- `useApiData(path)`: returns `[data, refetcher, error]`
|
||||
- `useServiceProviderData(path)`: returns `[data, refetcher, error]`
|
||||
|
||||
#### A note here on type-safety
|
||||
|
||||
All API requests are piped through the `fetchTransport` method. Most `GET` requests can be done with our
|
||||
normalized `use` hooks. Any `POST`, `PUT` or `DELETE` calls should have a helper method that uses `fetchTransport`
|
||||
under the hood. Examples are `putUser` or `deleteApiKey` etc. The `fetchTransport` function receives a generic
|
||||
type and returns it as the type of response data. This ensures type-safety when using API data at the component level.
|
||||
|
||||
## :file_folder: Vendor data modules
|
||||
|
||||
Large data modules are used for menu options on the `Applications` and `Speech Services` forms. These modules should be
|
||||
loaded lazily and set to local state in the context in which they are used. This pattern should be followed for
|
||||
any new data modules implemented in the application. You can find the data modules and their type definitions in
|
||||
the `src/vendor` directory. A basic example of how this is done on the speech form:
|
||||
|
||||
```jsx
|
||||
/** Assume setup code for a component... */
|
||||
|
||||
/** Lazy-load large data schemas -- e.g. code-splitting */
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
Promise.all([
|
||||
import("src/vendor/regions/aws-regions"),
|
||||
import("src/vendor/regions/ms-azure-regions"),
|
||||
]).then(([{ default: awsRegions }, { default: msRegions }]) => {
|
||||
if (!ignore) {
|
||||
setRegions({
|
||||
ms: msRegions,
|
||||
aws: awsRegions,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return function cleanup() {
|
||||
ignore = true;
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
## :sunrise: Component composition
|
||||
|
||||
All components that are used as `Route` elements are considered `containers`. You'll notice that all containers
|
||||
are lazy loaded at the router level, other than the root Login container, and this practice should always be
|
||||
followed.
|
||||
|
||||
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 models are put into practice.
|
||||
|
||||
## :art: UI and styling
|
||||
|
||||
We have a UI design system called [jambonz-ui](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 see an example page of elements [here](https://www.jambonz.org/jambonz-ui/) as well
|
||||
as view the docs for it [here](https://www.jambonz.org/docs/jambonz-ui/).
|
||||
|
||||
We're using the `scss` syntax for [sass](https://sass-lang.com/) and a [BEM](http://getbem.com/introduction/)
|
||||
style of authorship. You can find the naming guidelines for our implementation [here](http://getbem.com/naming/).
|
||||
An example of a module authored in this way would look like the following but you can check any of the `styles`
|
||||
files in the `src` code to get more of an understanding. One thing: never use `@import`! Always use `@use` for
|
||||
sass modules.
|
||||
|
||||
```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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## :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:
|
||||
|
||||
That's it. Peer review will be done by codeowners and all we ask is that you please be patient!
|
||||
|
||||
## :beetle: Bugs?
|
||||
|
||||
If you find a bug please file an issue on this repository :pray:.
|
||||
@@ -1,19 +1,40 @@
|
||||
# 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.
|
||||
<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.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -38,6 +59,7 @@ ready for testing!
|
||||
```
|
||||
|
||||
This starts the a docker-compose network running the following containers:
|
||||
|
||||
- mysql
|
||||
- redis
|
||||
- influxdb
|
||||
@@ -45,15 +67,29 @@ 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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -1,10 +1,18 @@
|
||||
#!/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}"
|
||||
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
|
||||
API_BASE_URL=${API_BASE_URL:-http://$PUBLIC_IPV4:$API_PORT/$API_VERSION}
|
||||
|
||||
# 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}\" };</script>"
|
||||
sed -i -e "\@</head>@i\ $SCRIPT_TAG" ./dist/index.html
|
||||
|
||||
# Start the frontend web app static server
|
||||
npm run serve
|
||||
|
||||
55
index.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!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="Simple provisioning webapp for jambonz."
|
||||
/>
|
||||
<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 Web App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
16681
package-lock.json
generated
128
package.json
@@ -1,43 +1,111 @@
|
||||
{
|
||||
"name": "jambonz-webapp",
|
||||
"description": "Simple provisioning webapp for jambonz",
|
||||
"version": "v0.7.5",
|
||||
"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.1",
|
||||
"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"
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Brandon Lee Kitajchuk",
|
||||
"email": "bk@kitajchuk.com",
|
||||
"url": "https://www.kitajchuk.com"
|
||||
}
|
||||
],
|
||||
"scripts": {
|
||||
"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}",
|
||||
"prepare": "husky install",
|
||||
"postinstall": "rm -rf public/fonts && cp -R ./node_modules/jambonz-ui/public/fonts ./public/fonts",
|
||||
"start": "npm run dev",
|
||||
"dev": "vite --port 3001",
|
||||
"dev:server": "ts-node --esm ./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": "echo 'need to setup testing'",
|
||||
"serve": "serve -s dist -l ${HTTP_PORT:-3001}",
|
||||
"pm2": "pm2 start npm --name \"jambonz-webapp\" -- run serve",
|
||||
"deploy": "npm i && npm run build && npm run pm2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
"dependencies": {
|
||||
"jambonz-ui": "^0.0.17",
|
||||
"preact": "^10.10.0",
|
||||
"react": "^18.0.0",
|
||||
"react-blockies": "^1.4.1",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-router-dom": "^6.3.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/node": "^18.6.1",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-blockies": "^1.4.1",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||
"@typescript-eslint/parser": "^5.30.6",
|
||||
"@vitejs/plugin-react": "^1.3.0",
|
||||
"cors": "^2.8.5",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.0",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"express": "^4.18.1",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.53.0",
|
||||
"serve": "^14.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.6.3",
|
||||
"vite": "^3.0.0"
|
||||
},
|
||||
"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"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.8 KiB |
14
public/favicon.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/icon.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/icon1024.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/icon192.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/icon384.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/icon512.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -1,14 +0,0 @@
|
||||
<!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>
|
||||
36
public/manifest.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"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:
|
||||
15
public/svg/jambonz--light.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
15
public/svg/jambonz.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
111
src/App.js
@@ -1,111 +0,0 @@
|
||||
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;
|
||||
193
src/api/constants.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import type { WebHook } from "./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;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
JAMBONZ: JambonzWindowObject;
|
||||
}
|
||||
}
|
||||
|
||||
// https://vitejs.dev/guide/env-and-mode.html#env-files
|
||||
export const API_BASE_URL =
|
||||
window.JAMBONZ?.API_BASE_URL || import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
/** Serves mock API responses from a local dev API server */
|
||||
export const DEV_BASE_URL = import.meta.env.VITE_DEV_BASE_URL;
|
||||
|
||||
/** Default API object models */
|
||||
export const DEFAULT_WEBHOOK: WebHook = {
|
||||
url: "",
|
||||
method: "POST",
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
/** API base paths */
|
||||
export const API_LOGIN = `${API_BASE_URL}/login`;
|
||||
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`;
|
||||
|
||||
/* Reference: API usage by internal route
|
||||
|
||||
/internal/*
|
||||
:POST /ServiceProviders
|
||||
|
||||
/internal/settings
|
||||
:GET /ServiceProviders
|
||||
:GET /ServiceProviders/:service_provider_sid/ApiKeys
|
||||
:POST /ApiKeys
|
||||
:PUT /ServiceProviders/:service_provider_sid
|
||||
:DELETE /ServiceProviders/:service_provider_sid
|
||||
|
||||
/internal/accounts
|
||||
:GET /ServiceProviders
|
||||
:GET /ServiceProviders/:service_provider_sid/Accounts
|
||||
|
||||
/internal/accounts/add
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /Sbcs
|
||||
:GET /Accounts/WebhookSecret?regenerate=true
|
||||
:POST /Accounts
|
||||
|
||||
/internal/accounts/edit
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /Applications
|
||||
:GET /Sbcs
|
||||
:GET /Accounts/:account_sid/ApiKeys
|
||||
:GET /Accounts/WebhookSecret?regenerate=true
|
||||
:PUT /Accounts/:account_sid
|
||||
|
||||
|
||||
/internal/applications
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /Applications
|
||||
|
||||
/internal/applications/add
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /Applications
|
||||
:POST /Applications
|
||||
|
||||
/internal/applications/edit
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /Applications
|
||||
:PUT /Applications/:application_sid
|
||||
|
||||
/internal/recent-calls
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /RecentCalls?page=1&count=25&start=2022-07-14T07:00:00.000Z
|
||||
|
||||
???
|
||||
|
||||
/internal/alerts
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /Alerts?page=1&count=25&start=2022-07-14T07:00:00.000Z
|
||||
|
||||
???
|
||||
|
||||
/internal/carriers
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /VoipCarriers
|
||||
:GET /SipGateways?voip_carrier_sid=:voip_carrier_sid
|
||||
:GET /SmppGateways?voip_carrier_sid=:voip_carrier_sid
|
||||
|
||||
/internal/carriers/add
|
||||
:GET /ServiceProviders
|
||||
:GET /Applications
|
||||
:GET /Accounts
|
||||
:GET /Smpps
|
||||
:GET /Sbcs
|
||||
:GET /PredefinedCarriers
|
||||
:POST /ServiceProviders/:service_provider_sid/VoipCarriers
|
||||
:POST /SipGateways
|
||||
:POST /SmppGateways (inbound / outbound)
|
||||
|
||||
/internal/carriers/edit
|
||||
:GET /ServiceProviders
|
||||
:GET /Applications
|
||||
:GET /Accounts
|
||||
:GET /Smpps
|
||||
:GET /Sbcs
|
||||
:GET /VoipCarriers/:voip_carrier_sid
|
||||
:GET /SipGateways?voip_carrier_sid=:voip_carrier_sid
|
||||
:GET /SmppGateways?voip_carrier_sid=:voip_carrier_sid
|
||||
:PUT /ServiceProviders/:service_provider_sid/VoipCarriers/:voip_carrier_sid
|
||||
:PUT /SipGateways/sip_gateway_sid
|
||||
:PUT /SmppGateways/smpp_gateway_sid (inbound / outbound)
|
||||
|
||||
/internal/speech-services
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /SpeechCredentials
|
||||
|
||||
/internal/speech-services/add
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:POST /ServiceProviders/:service_provider_sid/SpeechCredentials
|
||||
|
||||
/internal/speech-services/edit
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:PUT /ServiceProviders/:service_provider_sid/SpeechCredentials/:speech_service_sid
|
||||
|
||||
/internal/phone-numbers
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /PhoneNumbers
|
||||
:GET /Applications
|
||||
:GET /VoipCarriers
|
||||
|
||||
/internal/phone-numbers/add
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /PhoneNumbers
|
||||
:GET /Applications
|
||||
:GET /VoipCarriers
|
||||
:POST /PhoneNumbers
|
||||
|
||||
/internal/phone-numbers/edit
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /PhoneNumbers
|
||||
:GET /Applications
|
||||
:GET /VoipCarriers
|
||||
:PUT /PhoneNumbers/:phone_number_sid
|
||||
|
||||
/internal/ms-teams-tenants
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /Applications
|
||||
:GET /MicrosoftTeamsTenants
|
||||
|
||||
/internal/ms-teams-tenants/add
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /Applications
|
||||
:GET /MicrosoftTeamsTenants
|
||||
:POST /MicrosoftTeamsTenants
|
||||
|
||||
/internal/ms-teams-tenants/edit
|
||||
:GET /ServiceProviders
|
||||
:GET /Accounts
|
||||
:GET /Applications
|
||||
:GET /MicrosoftTeamsTenants
|
||||
:PUT /MicrosoftTeamsTenants/:ms_teams_tenants_sid
|
||||
*/
|
||||
346
src/api/index.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useSelectState } from "src/store";
|
||||
import { getToken } from "src/router/auth";
|
||||
import {
|
||||
DEV_BASE_URL,
|
||||
API_BASE_URL,
|
||||
API_LOGIN,
|
||||
API_USERS,
|
||||
API_SERVICE_PROVIDERS,
|
||||
API_API_KEYS,
|
||||
API_ACCOUNTS,
|
||||
} from "./constants";
|
||||
import { ROUTE_LOGIN } from "src/router/routes";
|
||||
import {
|
||||
SESS_UNAUTHORIZED,
|
||||
MSG_SESS_EXPIRED,
|
||||
MSG_SERVER_DOWN,
|
||||
MSG_SOMETHING_WRONG,
|
||||
} from "src/constants";
|
||||
import { PagedResponse, RecentCall, StatusCodes } from "./types";
|
||||
|
||||
import type {
|
||||
FetchError,
|
||||
FetchTransport,
|
||||
Payload,
|
||||
User,
|
||||
UserLogin,
|
||||
ServiceProvider,
|
||||
SidResponse,
|
||||
TokenResponse,
|
||||
EmptyResponse,
|
||||
SecretResponse,
|
||||
UseApiData,
|
||||
} from "./types";
|
||||
|
||||
/** Wrap all requests to normalize response handling */
|
||||
const fetchTransport = <Type>(
|
||||
url: string,
|
||||
options: RequestInit
|
||||
): Promise<FetchTransport<Type>> => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const transport = {
|
||||
status: response.status,
|
||||
json: <Type>{},
|
||||
};
|
||||
|
||||
// Redirect unauthorized
|
||||
if (response.status === StatusCodes.UNAUTHORIZED) {
|
||||
handleUnauthorized();
|
||||
reject();
|
||||
}
|
||||
|
||||
// API error handling returns { msg: string; }
|
||||
// See @type StatusJSON and StatusEmpty in ./types
|
||||
if (
|
||||
response.status >= StatusCodes.BAD_REQUEST &&
|
||||
response.status <= StatusCodes.INTERNAL_SERVER_ERROR
|
||||
) {
|
||||
try {
|
||||
const errJson = await response.json();
|
||||
reject(<FetchError>{
|
||||
status: response.status,
|
||||
...errJson,
|
||||
});
|
||||
} catch (error) {
|
||||
reject(<FetchError>{
|
||||
status: response.status,
|
||||
msg: MSG_SOMETHING_WRONG,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// API success handling returns a valid JSON response
|
||||
// This could either be a DTO object or a generic response
|
||||
// See types for various responses in ./types
|
||||
if (
|
||||
response.status === StatusCodes.OK ||
|
||||
response.status === StatusCodes.CREATED
|
||||
) {
|
||||
const json: Type = await response.json();
|
||||
|
||||
transport.json = json;
|
||||
}
|
||||
|
||||
resolve(transport);
|
||||
// TypeError "Failed to fetch"
|
||||
// net::ERR_CONNECTION_REFUSED
|
||||
// This is the case if the server is unreachable...
|
||||
} catch (error: unknown) {
|
||||
// Caveat -- we don't kill the app if this is a bad request on local dev server
|
||||
if (!url.includes(DEV_BASE_URL)) {
|
||||
handleUnreachable();
|
||||
}
|
||||
|
||||
reject(<FetchError>{
|
||||
status: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
msg: (error as TypeError).message,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const token = getToken();
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
};
|
||||
|
||||
/** Hard boot on 401 status code for unauthorized users */
|
||||
/** Since you're unauthorized there's no harm just reloading the app from "/" */
|
||||
/** We set a storage item for the dispatch message that is captured in the Login container */
|
||||
const handleBadRequest = (msg: string) => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
if (window.location.pathname !== ROUTE_LOGIN) {
|
||||
sessionStorage.setItem(SESS_UNAUTHORIZED, msg);
|
||||
window.location.href = ROUTE_LOGIN;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnauthorized = () => {
|
||||
handleBadRequest(MSG_SESS_EXPIRED);
|
||||
};
|
||||
|
||||
const handleUnreachable = () => {
|
||||
handleBadRequest(MSG_SERVER_DOWN);
|
||||
};
|
||||
|
||||
/** All APIs need a wrapper utility that uses the FetchTransport */
|
||||
|
||||
export const postLogin = (payload: Payload) => {
|
||||
return fetchTransport<UserLogin>(API_LOGIN, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const postServiceProviders = (payload: Payload) => {
|
||||
return fetchTransport<SidResponse>(API_SERVICE_PROVIDERS, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const postApiKey = (payload: Payload) => {
|
||||
return fetchTransport<TokenResponse>(API_API_KEYS, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const postAccount = (payload: Payload) => {
|
||||
return fetchTransport<SidResponse>(API_ACCOUNTS, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const postSubspace = (sid: string, payload: Payload) => {
|
||||
return fetchTransport<SidResponse>(
|
||||
`${API_ACCOUNTS}/${sid}/SubspaceTeleport`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
headers: getAuthHeaders(),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const putUser = (sid: string, payload: Payload) => {
|
||||
return fetchTransport<EmptyResponse>(`${API_USERS}/${sid}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload),
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const putServiceProvider = (sid: string, payload: Payload) => {
|
||||
return fetchTransport<EmptyResponse>(`${API_SERVICE_PROVIDERS}/${sid}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload),
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const putAccount = (sid: string, payload: Payload) => {
|
||||
return fetchTransport<EmptyResponse>(`${API_ACCOUNTS}/${sid}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload),
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteServiceProvider = (sid: string) => {
|
||||
return fetchTransport<EmptyResponse>(`${API_SERVICE_PROVIDERS}/${sid}`, {
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteApiKey = (sid: string) => {
|
||||
return fetchTransport<EmptyResponse>(`${API_API_KEYS}/${sid}`, {
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteAccount = (sid: string) => {
|
||||
return fetchTransport<EmptyResponse>(`${API_ACCOUNTS}/${sid}`, {
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteSubspace = (sid: string) => {
|
||||
return fetchTransport<EmptyResponse>(
|
||||
`${API_ACCOUNTS}/${sid}/SubspaceTeleport`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/** Simple wrapper for :GET fetchTransport calls to any API */
|
||||
|
||||
export const getFetch = <Type>(url: string) => {
|
||||
return fetchTransport<Type>(url, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
/** Specific use cases for wrapping `getFetch` */
|
||||
|
||||
export const getUser = (sid: string) => {
|
||||
return getFetch<User>(`${API_USERS}/${sid}`);
|
||||
};
|
||||
|
||||
export const getServiceProviders = () => {
|
||||
return getFetch<ServiceProvider[]>(API_SERVICE_PROVIDERS);
|
||||
};
|
||||
|
||||
export const getAccountWebhook = (sid: string) => {
|
||||
return getFetch<SecretResponse>(
|
||||
`${API_ACCOUNTS}/${sid}/WebhookSecret?regenerate=true`
|
||||
);
|
||||
};
|
||||
|
||||
/** Wrappers for APIs that can have a mock dev server response */
|
||||
|
||||
export const getRecentCalls = (sid: string) => {
|
||||
return getFetch<PagedResponse<RecentCall>>(
|
||||
import.meta.env.DEV
|
||||
? `${DEV_BASE_URL}/Accounts/${sid}/RecentCalls`
|
||||
: `${API_ACCOUNTS}/${sid}/RecentCalls`
|
||||
);
|
||||
};
|
||||
|
||||
/** Hooks for components to fetch data with refetch method */
|
||||
|
||||
/** :GET /{apiPath} -- this is generic for any fetch of data collections */
|
||||
export const useApiData: UseApiData = <Type>(apiPath: string) => {
|
||||
const [result, setResult] = useState<Type | null>(null);
|
||||
const [error, setError] = useState<FetchError | null>(null);
|
||||
const [refetch, setRefetch] = useState(0);
|
||||
|
||||
const refetcher = () => {
|
||||
setRefetch(refetch + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
getFetch<Type>(`${API_BASE_URL}/${apiPath}`)
|
||||
.then(({ json }) => {
|
||||
if (!ignore) {
|
||||
setResult(json!);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!ignore) {
|
||||
setError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return function cleanup() {
|
||||
ignore = true;
|
||||
};
|
||||
}, [refetch]);
|
||||
|
||||
return [result, refetcher, error];
|
||||
};
|
||||
|
||||
/** Only for a couple routes but makes these fetches nice at the component level */
|
||||
/** Wrapping up the currentServiceProvider logic here also streamlines component use */
|
||||
/** :GET /ServiceProviders/:service_provider_sid/ApiKeys */
|
||||
/** :GET /ServiceProviders/:service_provider_sid/Accounts */
|
||||
export const useServiceProviderData: UseApiData = <Type>(apiPath: string) => {
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [result, setResult] = useState<Type | null>(null);
|
||||
const [error, setError] = useState<FetchError | null>(null);
|
||||
const [refetch, setRefetch] = useState(0);
|
||||
|
||||
const refetcher = () => {
|
||||
setRefetch(refetch + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
if (currentServiceProvider) {
|
||||
getFetch<Type>(
|
||||
`${API_SERVICE_PROVIDERS}/${currentServiceProvider.service_provider_sid}/${apiPath}`
|
||||
)
|
||||
.then(({ json }) => {
|
||||
if (!ignore) {
|
||||
setResult(json!);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!ignore) {
|
||||
setError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return function cleanup() {
|
||||
ignore = true;
|
||||
};
|
||||
}, [currentServiceProvider, refetch]);
|
||||
|
||||
return [result, refetcher, error];
|
||||
};
|
||||
177
src/api/types.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
export type Payload = Record<string, unknown>;
|
||||
|
||||
export type EmptyResponse = Payload;
|
||||
|
||||
/** Status codes with JSON responses */
|
||||
/** Code 400,403 can be an empty response sometimes... */
|
||||
/** FYI: Code 480 is SMPP temporarily unavailable */
|
||||
// export type StatusJSON = 200 | 201 | 400 | 403 | 422 | 480 | 500;
|
||||
|
||||
/** Status codes with empty responses */
|
||||
/** Code 403 can have a JSON response sometimes... */
|
||||
// export type StatusEmpty = 202 | 204 | 400 | 403 | 404;
|
||||
|
||||
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,
|
||||
|
||||
/** Code 480 is SMPP temporarily unavailable */
|
||||
TEMPORARILY_UNAVAILABLE = 480,
|
||||
}
|
||||
|
||||
export interface FetchTransport<Type> {
|
||||
status: StatusCodes;
|
||||
json: Type;
|
||||
}
|
||||
|
||||
export interface FetchError {
|
||||
status: StatusCodes;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export interface UseApiData {
|
||||
<Type>(apiPath: string): [null | Type, () => void, null | FetchError];
|
||||
}
|
||||
|
||||
export interface User {
|
||||
user_sid: string;
|
||||
}
|
||||
|
||||
export interface UserLogin {
|
||||
token: string;
|
||||
user_sid: string;
|
||||
force_change: boolean;
|
||||
}
|
||||
|
||||
export interface ServiceProvider {
|
||||
name: string;
|
||||
ms_teams_fqdn: null | string;
|
||||
service_provider_sid: string;
|
||||
}
|
||||
|
||||
export interface ApiKey {
|
||||
token: string;
|
||||
last_used: null | string;
|
||||
expires_at: null | string;
|
||||
created_at: string;
|
||||
api_key_sid: string;
|
||||
service_provider_sid: null | string;
|
||||
}
|
||||
|
||||
export interface WebHook {
|
||||
url: string;
|
||||
method: string;
|
||||
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;
|
||||
}
|
||||
|
||||
/** Subspace is behind an ENV flag `VITE_FEATURE_SUBSPACE` */
|
||||
export interface SubspaceTeleport {
|
||||
destination: string;
|
||||
}
|
||||
|
||||
export interface SubspaceEntryPoint {
|
||||
address: string;
|
||||
transport_type: string;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
name: string;
|
||||
sip_realm: 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;
|
||||
|
||||
/** Subspace is behind an ENV flag `VITE_FEATURE_SUBSPACE` */
|
||||
subspace_client_id: null | string;
|
||||
subspace_client_secret: null | string;
|
||||
subspace_sip_teleport_id: null | string;
|
||||
/** Serialized JSON parsed as an array of SubspaceEntryPoint types */
|
||||
subspace_sip_teleport_destinations: null | string;
|
||||
}
|
||||
|
||||
export interface Application {
|
||||
name: string;
|
||||
call_hook: null | WebHook;
|
||||
account_sid: null | string;
|
||||
messaging_hook: null | WebHook;
|
||||
application_sid: string;
|
||||
call_status_hook: null | WebHook;
|
||||
speech_synthesis_voice: null | string;
|
||||
speech_synthesis_vendor: null | string;
|
||||
speech_recognizer_vendor: null | string;
|
||||
speech_recognizer_language: null | string;
|
||||
}
|
||||
|
||||
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;
|
||||
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_call_id: 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;
|
||||
}
|
||||
|
||||
export interface PagedResponse<Type> {
|
||||
total: number;
|
||||
batch: number;
|
||||
page: number;
|
||||
data: Type[];
|
||||
}
|
||||
|
||||
export interface SidResponse {
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface TokenResponse extends SidResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface SecretResponse {
|
||||
webhook_secret: string;
|
||||
}
|
||||
20
src/components/acl.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import { useSelectState } from "src/store";
|
||||
|
||||
import type { ACL } from "src/store/types";
|
||||
|
||||
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,89 +0,0 @@
|
||||
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;
|
||||
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
@@ -1,45 +0,0 @@
|
||||
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;
|
||||
@@ -1,37 +0,0 @@
|
||||
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;
|
||||
@@ -1,129 +0,0 @@
|
||||
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;
|
||||
@@ -1,251 +0,0 @@
|
||||
/* 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;
|
||||
@@ -1,128 +0,0 @@
|
||||
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;
|
||||
@@ -1,113 +0,0 @@
|
||||
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;
|
||||
@@ -1,94 +0,0 @@
|
||||
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;
|
||||
@@ -1,122 +0,0 @@
|
||||
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;
|
||||
@@ -1,463 +0,0 @@
|
||||
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;
|
||||
@@ -1,104 +0,0 @@
|
||||
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;
|
||||
@@ -1,60 +0,0 @@
|
||||
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;
|
||||
51
src/components/clipboard/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type ClipBoardProps = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const ClipBoard = ({ text, id = "", name = "" }: ClipBoardProps) => {
|
||||
const hasClipboard = typeof navigator.clipboard !== "undefined";
|
||||
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">
|
||||
<input id={id} name={name} type="text" readOnly value={text} />
|
||||
{hasClipboard && (
|
||||
<button
|
||||
className="btn--type"
|
||||
type="button"
|
||||
title="Copy to clipboard"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Icons.Clipboard />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
38
src/components/clipboard/styles.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
@use "src/styles/vars";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.clipboard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
input[type="text"] {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
width: 100%;
|
||||
height: vars.$clipheight;
|
||||
|
||||
.form & {
|
||||
max-width: calc(#{vars.$widthinput} - #{vars.$clipheight});
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
button[type="button"] {
|
||||
cursor: pointer;
|
||||
height: vars.$clipheight;
|
||||
width: vars.$clipheight;
|
||||
border-bottom-right-radius: vars.$px01;
|
||||
border-top-right-radius: vars.$px01;
|
||||
border: 2px solid ui-vars.$grey;
|
||||
border-left: 0;
|
||||
background-color: ui-vars.$pink;
|
||||
|
||||
svg {
|
||||
stroke: ui-vars.$jambonz;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
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;
|
||||
@@ -1,84 +0,0 @@
|
||||
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;
|
||||
@@ -1,272 +0,0 @@
|
||||
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);
|
||||
@@ -1,138 +0,0 @@
|
||||
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);
|
||||
@@ -1,14 +0,0 @@
|
||||
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;
|
||||
@@ -1,62 +0,0 @@
|
||||
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;
|
||||
@@ -1,97 +0,0 @@
|
||||
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);
|
||||
@@ -1,30 +0,0 @@
|
||||
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;
|
||||
@@ -1,9 +0,0 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const H1 = styled.h1`
|
||||
font-size: 3rem;
|
||||
margin: 1rem 0;
|
||||
font-weight: normal;
|
||||
`;
|
||||
|
||||
export default H1;
|
||||
@@ -1,112 +0,0 @@
|
||||
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);
|
||||
@@ -1,18 +0,0 @@
|
||||
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;
|
||||
@@ -1,16 +0,0 @@
|
||||
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;
|
||||
@@ -1,72 +0,0 @@
|
||||
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;
|
||||
@@ -1,35 +0,0 @@
|
||||
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);
|
||||
@@ -1,119 +0,0 @@
|
||||
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);
|
||||
@@ -1,33 +0,0 @@
|
||||
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;
|
||||
@@ -1,16 +0,0 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Span = styled.span`
|
||||
text-align: left;
|
||||
${props => props.hasBorder ? `
|
||||
height: 2.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
border: 1px solid #B6B6B6;
|
||||
border-radius: 0.125rem;
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
export default Span;
|
||||
@@ -1,103 +0,0 @@
|
||||
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;
|
||||
@@ -1,122 +0,0 @@
|
||||
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;
|
||||
@@ -1,55 +0,0 @@
|
||||
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);
|
||||
@@ -1,977 +0,0 @@
|
||||
/* 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;
|
||||
@@ -1,389 +0,0 @@
|
||||
/* 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;
|
||||
@@ -1,457 +0,0 @@
|
||||
/* 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;
|
||||
@@ -1,370 +0,0 @@
|
||||
/* 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 } 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);
|
||||
|
||||
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 axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
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 || '');
|
||||
} 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 axios({
|
||||
method: 'put',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${serviceProviderSid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
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") || ""}
|
||||
/>
|
||||
|
||||
{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;
|
||||
@@ -1,710 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import styled from "styled-components/macro";
|
||||
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import handleErrors from '../../helpers/handleErrors';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
import Select from '../elements/Select';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import Checkbox from '../elements/Checkbox';
|
||||
import FileUpload from '../elements/FileUpload';
|
||||
import Code from '../elements/Code';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Button from '../elements/Button';
|
||||
import Loader from '../blocks/Loader';
|
||||
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
|
||||
import AwsRegions from '../../data/AwsRegions';
|
||||
import MicrosoftAzureRegions from '../../data/MicrosoftAzureRegions';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const StyledButtonGroup = styled(InputGroup)`
|
||||
@media (max-width: 576.98px) {
|
||||
width: 100%;
|
||||
|
||||
& > *:first-child {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& > *:last-child {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
${props => props.type === 'add' ? `
|
||||
@media (max-width: 459.98px) {
|
||||
flex-direction: column;
|
||||
|
||||
& > *:first-child {
|
||||
width: 100%;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
}
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
const SpeechServicesAddEdit = (props) => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const jwt = localStorage.getItem('token');
|
||||
|
||||
let { speech_service_sid } = useParams();
|
||||
const type = speech_service_sid ? 'edit' : 'add';
|
||||
|
||||
// Refs
|
||||
const refVendorGoogle = useRef(null);
|
||||
const refVendorAws = useRef(null);
|
||||
const refVendorMs = useRef(null);
|
||||
const refVendorWellSaid = useRef(null);
|
||||
const refAccessKeyId = useRef(null);
|
||||
const refSecretAccessKey = useRef(null);
|
||||
const refUseForTts = useRef(null);
|
||||
const refUseForStt = useRef(null);
|
||||
const refApiKey = useRef(null);
|
||||
const refRegion = useRef(null);
|
||||
const refAwsRegion = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [vendor, setVendor] = useState('');
|
||||
const [serviceKey, setServiceKey] = useState('');
|
||||
const [displayedServiceKey, setDisplayedServiceKey] = useState('');
|
||||
const [accessKeyId, setAccessKeyId] = useState('');
|
||||
const [secretAccessKey, setSecretAccessKey] = useState('');
|
||||
const [useForTts, setUseForTts] = useState(false);
|
||||
const [useForStt, setUseForStt] = useState(false);
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
const [accountSid, setAccountSid] = useState('');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [region, setRegion] = useState('');
|
||||
const [awsregion, setAwsRegion] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [invalidVendorGoogle, setInvalidVendorGoogle] = useState(false);
|
||||
const [invalidVendorAws, setInvalidVendorAws] = useState(false);
|
||||
const [invalidVendorMs, setInvalidVendorMs] = useState(false);
|
||||
const [invalidVendorWellSaid, setInvalidVendorWellSaid] = useState(false);
|
||||
const [invalidAccessKeyId, setInvalidAccessKeyId] = useState(false);
|
||||
const [invalidSecretAccessKey, setInvalidSecretAccessKey] = useState(false);
|
||||
const [invalidUseForTts, setInvalidUseForTts] = useState(false);
|
||||
const [invalidUseForStt, setInvalidUseForStt] = useState(false);
|
||||
const [invalidApiKey, setInvalidApiKey] = useState(false);
|
||||
const [invalidRegion, setInvalidRegion] = useState(false);
|
||||
const [invalidAwsRegion, setInvalidAwsRegion] = useState(false);
|
||||
|
||||
const [originalTtsValue, setOriginalTtsValue] = useState(null);
|
||||
const [originalSttValue, setOriginalSttValue] = useState(null);
|
||||
|
||||
const [validServiceKey, setValidServiceKey] = useState(false);
|
||||
|
||||
const [showLoader, setShowLoader] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
const accountsResponse = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccounts(accountsResponse.data);
|
||||
|
||||
if (type === 'edit') {
|
||||
const speechCredential = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
let serviceKeyJson = '';
|
||||
let displayedServiceKeyJson = '';
|
||||
|
||||
try {
|
||||
serviceKeyJson = JSON.parse(speechCredential.data.service_key);
|
||||
displayedServiceKeyJson = JSON.stringify(serviceKeyJson, null, 2);
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
setAccountSid(speechCredential.data.account_sid || '');
|
||||
setVendor(speechCredential.data.vendor || undefined);
|
||||
setServiceKey(serviceKeyJson || '');
|
||||
setDisplayedServiceKey(displayedServiceKeyJson || '');
|
||||
setAccessKeyId(speechCredential.data.access_key_id || '');
|
||||
setSecretAccessKey(speechCredential.data.secret_access_key || '');
|
||||
setApiKey(speechCredential.data.api_key || '');
|
||||
setRegion(speechCredential.data.region || '');
|
||||
setAwsRegion(speechCredential.data.aws_region || '');
|
||||
setUseForTts(speechCredential.data.use_for_tts || false);
|
||||
setUseForStt(speechCredential.data.use_for_stt || false);
|
||||
setOriginalTtsValue(speechCredential.data.use_for_tts || false);
|
||||
setOriginalSttValue(speechCredential.data.use_for_stt || false);
|
||||
}
|
||||
setShowLoader(false);
|
||||
} catch (err) {
|
||||
isMounted = false;
|
||||
handleErrors({
|
||||
err,
|
||||
history,
|
||||
dispatch,
|
||||
redirect: '/internal/speech-services',
|
||||
fallbackMessage: 'That speech service does not exist',
|
||||
preferFallback: true,
|
||||
});
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const handleFileUpload = async (e) => {
|
||||
setErrorMessage('');
|
||||
setServiceKey('');
|
||||
setDisplayedServiceKey('');
|
||||
|
||||
const file = e.target.files[0];
|
||||
|
||||
if (!file) {
|
||||
setValidServiceKey(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileAsText = await file.text();
|
||||
|
||||
try {
|
||||
const fileJson = JSON.parse(fileAsText);
|
||||
|
||||
if (!fileJson.client_email || !fileJson.private_key) {
|
||||
setValidServiceKey(false);
|
||||
setErrorMessage('Invalid service key file, missing data.');
|
||||
return;
|
||||
}
|
||||
|
||||
setValidServiceKey(true);
|
||||
setServiceKey(fileJson);
|
||||
setDisplayedServiceKey(JSON.stringify(fileJson, null, 2));
|
||||
|
||||
} catch (err) {
|
||||
setValidServiceKey(false);
|
||||
setErrorMessage('Invalid service key file, could not parse as JSON.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidVendorGoogle(false);
|
||||
setInvalidVendorAws(false);
|
||||
setInvalidVendorMs(false);
|
||||
setInvalidVendorWellSaid(false);
|
||||
setInvalidAccessKeyId(false);
|
||||
setInvalidSecretAccessKey(false);
|
||||
setInvalidUseForTts(false);
|
||||
setInvalidUseForStt(false);
|
||||
setInvalidApiKey(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if (!vendor) {
|
||||
errorMessages.push('Please select a vendor.');
|
||||
setInvalidVendorGoogle(true);
|
||||
setInvalidVendorAws(true);
|
||||
setInvalidVendorMs(true);
|
||||
setInvalidVendorWellSaid(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refVendorGoogle.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'google' && !serviceKey) {
|
||||
errorMessages.push('Please upload a service key file.');
|
||||
}
|
||||
|
||||
if (vendor === 'aws' && !accessKeyId) {
|
||||
errorMessages.push('Please provide an access key ID.');
|
||||
setInvalidAccessKeyId(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refAccessKeyId.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'aws' && !secretAccessKey) {
|
||||
errorMessages.push('Please provide a secret access key.');
|
||||
setInvalidSecretAccessKey(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refSecretAccessKey.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'aws' && !awsregion) {
|
||||
errorMessages.push('Please select a region.');
|
||||
setInvalidAwsRegion(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refAwsRegion.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'microsoft' && !apiKey) {
|
||||
errorMessages.push('Please provide an API key.');
|
||||
setInvalidApiKey(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refApiKey.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'microsoft' && !region) {
|
||||
errorMessages.push('Please select a region.');
|
||||
setInvalidRegion(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refRegion.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'wellsaid' && !apiKey) {
|
||||
errorMessages.push('Please provide an API key.');
|
||||
setInvalidApiKey(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refApiKey.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//===============================================
|
||||
// Submit
|
||||
//===============================================
|
||||
const method = type === 'add'
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
const url = type === 'add'
|
||||
? `/ServiceProviders/${currentServiceProvider}/SpeechCredentials`
|
||||
: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}`;
|
||||
|
||||
const postResults = await axios({
|
||||
method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
data: {
|
||||
vendor,
|
||||
service_key: vendor === 'google' ? JSON.stringify(serviceKey) : null,
|
||||
access_key_id: vendor === 'aws' ? accessKeyId : null,
|
||||
secret_access_key: vendor === 'aws' ? secretAccessKey : null,
|
||||
aws_region: vendor === 'aws' ? awsregion : null,
|
||||
api_key: ['microsoft', 'wellsaid'].includes(vendor) ? apiKey : null,
|
||||
region: vendor === 'microsoft' ? region : null,
|
||||
use_for_tts: useForTts,
|
||||
use_for_stt: useForStt,
|
||||
service_provider_sid: accountSid ? null : currentServiceProvider,
|
||||
account_sid: accountSid || null,
|
||||
}
|
||||
});
|
||||
|
||||
if (type === 'add') {
|
||||
if (!postResults.data || !postResults.data.sid) {
|
||||
throw new Error('Error retrieving response data');
|
||||
}
|
||||
|
||||
speech_service_sid = postResults.data.sid;
|
||||
}
|
||||
|
||||
//===============================================
|
||||
// Test speech credentials
|
||||
//===============================================
|
||||
if (useForTts || useForStt) {
|
||||
const testResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}/test`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (useForTts && testResults.data.tts.status === 'not tested') {
|
||||
errorMessages.push('text-to-speech was not tested, please try again.');
|
||||
}
|
||||
|
||||
if (useForStt && testResults.data.stt.status === 'not tested') {
|
||||
errorMessages.push('speech-to-text was not tested, please try again.');
|
||||
}
|
||||
|
||||
const ttsReason = (useForTts && testResults.data.tts.status === 'fail')
|
||||
? testResults.data.tts.reason
|
||||
: null;
|
||||
|
||||
const sttReason = (useForStt && testResults.data.stt.status === 'fail')
|
||||
? testResults.data.stt.reason
|
||||
: null;
|
||||
|
||||
if (ttsReason && (ttsReason === sttReason)) {
|
||||
errorMessages.push(ttsReason);
|
||||
} else {
|
||||
if (ttsReason) {
|
||||
errorMessages.push(`Text-to-speech error: ${ttsReason}`);
|
||||
}
|
||||
|
||||
if (sttReason) {
|
||||
errorMessages.push(`Speech-to-text error: ${sttReason}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
}
|
||||
|
||||
if (errorMessages.length) {
|
||||
if (type === 'add') {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'edit') {
|
||||
await axios({
|
||||
method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
data: {
|
||||
use_for_tts: originalTtsValue,
|
||||
use_for_stt: originalSttValue,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//===============================================
|
||||
// If successful, go to speech services
|
||||
//===============================================
|
||||
isMounted = false;
|
||||
if (accountSid) {
|
||||
history.push(`/internal/speech-services?account_sid=${accountSid}`);
|
||||
} else {
|
||||
history.push('/internal/speech-services');
|
||||
}
|
||||
const dispatchMessage = type === 'add'
|
||||
? 'Speech service created successfully'
|
||||
: 'Speech service updated successfully';
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: dispatchMessage
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
isMounted = false;
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
setErrorMessage(
|
||||
(err.response && err.response.data && err.response.data.msg) ||
|
||||
err.message || 'Something went wrong, please try again.'
|
||||
);
|
||||
console.error(err.response || err);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
showLoader ? (
|
||||
<Loader height={props.type === 'add' ? '424px' : '376px'} />
|
||||
) : (
|
||||
<Form
|
||||
large
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="vendor">Vendor</Label>
|
||||
<Select
|
||||
name="vendor"
|
||||
id="vendor"
|
||||
value={vendor}
|
||||
onChange={e => setVendor(e.target.value)}
|
||||
{...[refVendorGoogle, refVendorAws, refVendorMs, refVendorWellSaid]}
|
||||
invalid={[invalidVendorGoogle, invalidVendorAws, invalidVendorMs, invalidVendorWellSaid].includes(true)}
|
||||
>
|
||||
<option value="">
|
||||
Select a Vendor
|
||||
</option>
|
||||
<option value="google">Google</option>
|
||||
<option value="aws">AWS</option>
|
||||
<option value="microsoft">Microsoft</option>
|
||||
<option value="wellsaid">WellSaid</option>
|
||||
</Select>
|
||||
|
||||
<Label htmlFor="account">Used by</Label>
|
||||
<Select
|
||||
name="account"
|
||||
id="account"
|
||||
value={accountSid}
|
||||
onChange={e => setAccountSid(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
All accounts
|
||||
</option>
|
||||
{accounts.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
|
||||
<option
|
||||
key={a.account_sid}
|
||||
value={a.account_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{['google', 'aws', 'microsoft', 'wellsaid'].includes(vendor) ? (
|
||||
<>
|
||||
<div />
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
name="useForTts"
|
||||
id="useForTts"
|
||||
label="Use for text-to-speech"
|
||||
checked={useForTts}
|
||||
onChange={e => setUseForTts(e.target.checked)}
|
||||
invalid={invalidUseForTts}
|
||||
ref={refUseForTts}
|
||||
/>
|
||||
<div />
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
name="useForStt"
|
||||
id="useForStt"
|
||||
label="Use for speech-to-text"
|
||||
disabled={'wellsaid' === vendor}
|
||||
checked={useForStt}
|
||||
onChange={e => setUseForStt(e.target.checked)}
|
||||
invalid={invalidUseForStt}
|
||||
ref={refUseForStt}
|
||||
/>
|
||||
</>
|
||||
) :
|
||||
(
|
||||
null
|
||||
)}
|
||||
|
||||
{vendor === 'google' ? (
|
||||
<>
|
||||
<Label htmlFor="serviceKey">Service Key</Label>
|
||||
{type === 'add' && (
|
||||
<FileUpload
|
||||
id="serviceKey"
|
||||
onChange={handleFileUpload}
|
||||
validFile={validServiceKey}
|
||||
/>
|
||||
)}
|
||||
{displayedServiceKey && (
|
||||
<>
|
||||
{type === 'add' && (
|
||||
<span></span>
|
||||
)}
|
||||
<Code>{displayedServiceKey}</Code>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : vendor === 'aws' ? (
|
||||
<>
|
||||
<Label htmlFor="accessKeyId">Access Key ID</Label>
|
||||
<Input
|
||||
name="accessKeyId"
|
||||
id="accessKeyId"
|
||||
value={accessKeyId}
|
||||
onChange={e => setAccessKeyId(e.target.value)}
|
||||
placeholder=""
|
||||
invalid={invalidAccessKeyId}
|
||||
ref={refAccessKeyId}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
|
||||
<Label htmlFor="secretAccessKey">Secret Access Key</Label>
|
||||
<PasswordInput
|
||||
allowShowPassword
|
||||
name="secretAccessKey"
|
||||
id="secretAccessKey"
|
||||
password={secretAccessKey}
|
||||
setPassword={setSecretAccessKey}
|
||||
setErrorMessage={setErrorMessage}
|
||||
invalid={invalidSecretAccessKey}
|
||||
ref={refSecretAccessKey}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
|
||||
<Label htmlFor="regions">Region</Label>
|
||||
<Select
|
||||
name="regions"
|
||||
id="regions"
|
||||
value={awsregion}
|
||||
onChange={e => setAwsRegion(e.target.value)}
|
||||
ref={refAwsRegion}
|
||||
invalid={invalidAwsRegion}
|
||||
>
|
||||
<option value="">
|
||||
Select a region
|
||||
</option>
|
||||
{AwsRegions.map(r => (
|
||||
<option
|
||||
key={r.value}
|
||||
value={r.value}
|
||||
>
|
||||
{r.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
) : vendor === 'microsoft' ? (
|
||||
<>
|
||||
<Label htmlFor="apiKey">API Key</Label>
|
||||
<Input
|
||||
name="apiKey"
|
||||
id="apiKey"
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
placeholder=""
|
||||
invalid={invalidApiKey}
|
||||
ref={refApiKey}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
|
||||
<Label htmlFor="region">Region</Label>
|
||||
<Select
|
||||
name="region"
|
||||
id="region"
|
||||
value={region}
|
||||
onChange={e => setRegion(e.target.value)}
|
||||
ref={refRegion}
|
||||
invalid={invalidRegion}
|
||||
>
|
||||
<option value="">
|
||||
All regions
|
||||
</option>
|
||||
{MicrosoftAzureRegions.map(r => (
|
||||
<option
|
||||
key={r.value}
|
||||
value={r.value}
|
||||
>
|
||||
{r.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
) : vendor === 'wellsaid' ? (
|
||||
<>
|
||||
<Label htmlFor="apiKey">API Key</Label>
|
||||
<Input
|
||||
name="apiKey"
|
||||
id="apiKey"
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
placeholder=""
|
||||
invalid={invalidApiKey}
|
||||
ref={refApiKey}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<StyledButtonGroup flexEnd spaced type={type}>
|
||||
<Button
|
||||
rounded="true"
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/speech-services');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: type === 'add' ? 'New speech service canceled' : 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button rounded="true" disabled={!vendor}>
|
||||
{type === 'add'
|
||||
? 'Add Speech Service'
|
||||
: 'Save'
|
||||
}
|
||||
</Button>
|
||||
</StyledButtonGroup>
|
||||
</Form>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeechServicesAddEdit;
|
||||
72
src/components/forms/checkzone/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { useState, useEffect, forwardRef } from "react";
|
||||
import { classNames } from "jambonz-ui";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type CheckzoneProps = {
|
||||
id?: string;
|
||||
name: string;
|
||||
label: string;
|
||||
hidden?: boolean;
|
||||
children: React.ReactNode;
|
||||
initialCheck: boolean;
|
||||
handleChecked?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
type CheckzoneRef = HTMLInputElement;
|
||||
|
||||
/** The forwarded ref is so forms can still focus() this checkbox field if necessary... */
|
||||
export const Checkzone = forwardRef<CheckzoneRef, CheckzoneProps>(
|
||||
(
|
||||
{
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
hidden = false,
|
||||
children,
|
||||
initialCheck,
|
||||
handleChecked,
|
||||
}: CheckzoneProps,
|
||||
ref
|
||||
) => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
const classesTop = classNames({
|
||||
checkzone: true,
|
||||
["checkzone--hidden"]: hidden,
|
||||
});
|
||||
const classesIn = classNames({
|
||||
checkzone__managed: true,
|
||||
active: checked,
|
||||
});
|
||||
|
||||
/** Handle initial checked condition */
|
||||
useEffect(() => {
|
||||
setChecked(initialCheck);
|
||||
}, [initialCheck]);
|
||||
|
||||
return (
|
||||
<div className={classesTop}>
|
||||
<label>
|
||||
<input
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
id={id || name}
|
||||
onChange={(e) => {
|
||||
setChecked(e.target.checked);
|
||||
|
||||
if (handleChecked) {
|
||||
handleChecked(e);
|
||||
}
|
||||
}}
|
||||
checked={checked}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
<div className={classesIn}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Checkzone.displayName = "Checkzone";
|
||||
57
src/components/forms/checkzone/styles.scss
Normal file
@@ -0,0 +1,57 @@
|
||||
@use "src/styles/vars";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.checkzone {
|
||||
padding: vars.$px02;
|
||||
border-radius: vars.$px01;
|
||||
border: 2px solid #888;
|
||||
width: 100%;
|
||||
max-width: vars.$widthinput;
|
||||
|
||||
> label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
margin-right: vars.$px02;
|
||||
}
|
||||
}
|
||||
|
||||
&__managed {
|
||||
margin-top: vars.$px02;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.25;
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-top: vars.$px02;
|
||||
}
|
||||
|
||||
> label + * {
|
||||
margin-top: vars.$px01;
|
||||
}
|
||||
|
||||
&.active {
|
||||
cursor: auto;
|
||||
opacity: 1;
|
||||
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
.checkzone__managed {
|
||||
display: none;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/components/forms/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Passwd } from "./passwd";
|
||||
import { Message } from "./message";
|
||||
import { Selector } from "./selector";
|
||||
import { Checkzone } from "./checkzone";
|
||||
|
||||
export { Passwd, Message, Selector, Checkzone };
|
||||
20
src/components/forms/message/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import type { IMessage } from "src/store/types";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type MsgProps = {
|
||||
message: IMessage;
|
||||
};
|
||||
|
||||
export const Message = ({ message }: MsgProps) => {
|
||||
return (
|
||||
<div className="msg">
|
||||
<Icons.AlertCircle />
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
28
src/components/forms/message/styles.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
@use "src/styles/vars";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.msg {
|
||||
@include ui-mixins.ms();
|
||||
padding: vars.$px01 vars.$px02 vars.$px01 56px;
|
||||
border-radius: vars.$px01;
|
||||
border: 2px solid ui-vars.$jambonz;
|
||||
background-color: ui-vars.$pink;
|
||||
color: ui-vars.$jambonz;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
> svg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
> svg:first-child {
|
||||
left: vars.$px02;
|
||||
}
|
||||
|
||||
> svg:last-child {
|
||||
right: vars.$px02;
|
||||
}
|
||||
}
|
||||
49
src/components/forms/passwd/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useState, forwardRef } from "react";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type PasswdProps = JSX.IntrinsicElements["input"] & {
|
||||
/** This is optional in case an onChange override is necessary... */
|
||||
setValue?: Dispatch<SetStateAction<string>>;
|
||||
};
|
||||
|
||||
type PasswdRef = HTMLInputElement;
|
||||
|
||||
/** The restProps spread at the end in case an onChange override is necessary... */
|
||||
/** The forwarded ref is so forms can still focus() this input field if necessary... */
|
||||
export const Passwd = forwardRef<PasswdRef, PasswdProps>(
|
||||
({ name, value, setValue, placeholder, ...restProps }: PasswdProps, ref) => {
|
||||
const [reveal, setReveal] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="passwd">
|
||||
<input
|
||||
ref={ref}
|
||||
type={reveal ? "text" : "password"}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
if (setValue) {
|
||||
setValue(e.target.value);
|
||||
}
|
||||
}}
|
||||
{...restProps}
|
||||
/>
|
||||
<button
|
||||
className="btn--type"
|
||||
type="button"
|
||||
onClick={() => setReveal(!reveal)}
|
||||
>
|
||||
{reveal ? <Icons.EyeOff /> : <Icons.Eye />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Passwd.displayName = "Passwd";
|
||||
23
src/components/forms/passwd/styles.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
@use "src/styles/vars";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
|
||||
.passwd {
|
||||
position: relative;
|
||||
|
||||
> input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> button[type="button"] {
|
||||
position: absolute;
|
||||
right: vars.$px02;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
stroke: ui-vars.$jambonz;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/components/forms/selector/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
interface SelectorOption {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
type SelectorProps = JSX.IntrinsicElements["select"] & {
|
||||
options: SelectorOption[];
|
||||
};
|
||||
|
||||
type SelectorRef = HTMLSelectElement;
|
||||
|
||||
/** The forwarded ref is so forms can still focus() this select menu if necessary... */
|
||||
export const Selector = forwardRef<SelectorRef, SelectorProps>(
|
||||
({ id, name, value, options, ...restProps }: SelectorProps, ref) => {
|
||||
return (
|
||||
<div className="selector">
|
||||
<select ref={ref} id={id} name={name} value={value} {...restProps}>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span>
|
||||
<Icons.ChevronUp />
|
||||
<Icons.ChevronDown />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Selector.displayName = "Selector";
|
||||
50
src/components/forms/selector/styles.scss
Normal file
@@ -0,0 +1,50 @@
|
||||
@use "src/styles/vars";
|
||||
@use "jambonz-ui/src/styles/index";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.selector {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: vars.$widthinput;
|
||||
|
||||
select {
|
||||
@include ui-mixins.m();
|
||||
appearance: none;
|
||||
padding: vars.$px01 vars.$px02;
|
||||
border-radius: vars.$px01;
|
||||
border: 2px solid ui-vars.$grey;
|
||||
background-color: ui-vars.$white;
|
||||
width: 100%;
|
||||
max-width: vars.$widthinput;
|
||||
}
|
||||
|
||||
span {
|
||||
height: 100%;
|
||||
width: 50px;
|
||||
background-color: ui-vars.$grey;
|
||||
border-radius: vars.$px01;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
|
||||
svg {
|
||||
stroke: ui-vars.$white;
|
||||
|
||||
&:first-child {
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/components/grid/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { classNames } from "jambonz-ui";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type CommonProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type GridProps = CommonProps & {
|
||||
col3?: boolean;
|
||||
col5?: boolean;
|
||||
};
|
||||
|
||||
type RowProps = CommonProps & {
|
||||
empty?: boolean;
|
||||
header?: boolean;
|
||||
};
|
||||
|
||||
export const GridRow = ({
|
||||
children,
|
||||
header = false,
|
||||
empty = false,
|
||||
}: RowProps) => {
|
||||
const classes = classNames({
|
||||
grid__row: true,
|
||||
grid__th: header,
|
||||
grid__empty: empty,
|
||||
});
|
||||
|
||||
return <div className={classes}>{children}</div>;
|
||||
};
|
||||
|
||||
export const Grid = ({ children, col3 = false, col5 = false }: GridProps) => {
|
||||
const classes = classNames({
|
||||
grid: true,
|
||||
["grid--col3"]: col3,
|
||||
["grid--col5"]: col5,
|
||||
});
|
||||
|
||||
return <div className={classes}>{children}</div>;
|
||||
};
|
||||
111
src/components/grid/styles.scss
Normal file
@@ -0,0 +1,111 @@
|
||||
@use "src/styles/vars";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
/** CSS magic numbers need to be normalized... */
|
||||
|
||||
@mixin grid-break {
|
||||
@media (max-width: 1070px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin grid-break2 {
|
||||
@media (max-width: 940px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
.grid {
|
||||
> * + * {
|
||||
border-top: 1px solid ui-vars.$grey;
|
||||
}
|
||||
|
||||
&__row {
|
||||
@include ui-mixins.m();
|
||||
display: grid;
|
||||
padding: vars.$px03;
|
||||
align-items: center;
|
||||
|
||||
> div {
|
||||
padding-right: vars.$px02;
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__th {
|
||||
@include ui-mixins.font-medium();
|
||||
}
|
||||
|
||||
&--col3 {
|
||||
.grid__row {
|
||||
grid-template-columns: [col] 60% [col] 20% [col] 20%;
|
||||
grid-template-rows: [row] auto [row] auto [row];
|
||||
|
||||
> div:nth-child(1) {
|
||||
@include grid-break() {
|
||||
grid-column: col / span 2;
|
||||
grid-row: row;
|
||||
}
|
||||
|
||||
@include grid-break2() {
|
||||
grid-column: col / span 3;
|
||||
grid-row: row;
|
||||
}
|
||||
}
|
||||
|
||||
> div:nth-child(2) {
|
||||
@include grid-break() {
|
||||
grid-column: col / span 2;
|
||||
grid-row: row 2;
|
||||
}
|
||||
|
||||
@include grid-break2() {
|
||||
grid-column: col;
|
||||
grid-row: row 2;
|
||||
margin-top: vars.$px02;
|
||||
}
|
||||
}
|
||||
|
||||
> div:nth-child(3) {
|
||||
text-align: right;
|
||||
|
||||
@include grid-break() {
|
||||
grid-column: col 3;
|
||||
grid-row: row / span 2;
|
||||
}
|
||||
|
||||
@include grid-break2() {
|
||||
grid-column: col 2 / span 2;
|
||||
grid-row: row 2;
|
||||
margin-top: vars.$px02;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid__th {
|
||||
> div:nth-child(2),
|
||||
> div:nth-child(3) {
|
||||
@include grid-break() {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--col5 {
|
||||
.grid__row {
|
||||
grid-template-columns: [col] 20% [col] 20% [col] 20% [col] 20% [col] 20%;
|
||||
grid-template-rows: [row] auto [row] auto [row];
|
||||
|
||||
> div {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/components/icons.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
X,
|
||||
Eye,
|
||||
Info,
|
||||
Menu,
|
||||
List,
|
||||
Edit,
|
||||
Plus,
|
||||
Grid,
|
||||
Phone,
|
||||
Users,
|
||||
Edit3,
|
||||
Trash,
|
||||
EyeOff,
|
||||
Server,
|
||||
Trash2,
|
||||
GitHub,
|
||||
XCircle,
|
||||
Settings,
|
||||
Activity,
|
||||
ChevronUp,
|
||||
Clipboard,
|
||||
RefreshCw,
|
||||
ArrowRight,
|
||||
PlusCircle,
|
||||
HelpCircle,
|
||||
ChevronDown,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
MessageCircle,
|
||||
} from "react-feather";
|
||||
|
||||
import type { Icon } from "react-feather";
|
||||
|
||||
export interface IconMap {
|
||||
[key: string]: Icon;
|
||||
}
|
||||
|
||||
export const Icons: IconMap = {
|
||||
X,
|
||||
Eye,
|
||||
Info,
|
||||
Menu,
|
||||
List,
|
||||
Edit,
|
||||
Plus,
|
||||
Grid,
|
||||
Phone,
|
||||
Users,
|
||||
Edit3,
|
||||
Trash,
|
||||
EyeOff,
|
||||
Server,
|
||||
Trash2,
|
||||
GitHub,
|
||||
XCircle,
|
||||
Settings,
|
||||
Activity,
|
||||
ChevronUp,
|
||||
Clipboard,
|
||||
RefreshCw,
|
||||
ArrowRight,
|
||||
PlusCircle,
|
||||
HelpCircle,
|
||||
ChevronDown,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
MessageCircle,
|
||||
};
|
||||
26
src/components/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Icons } from "./icons";
|
||||
import { Toast } from "./toast";
|
||||
import { Modal, ModalClose, ModalForm } from "./modal";
|
||||
import { AccessControl } from "./acl";
|
||||
import { Obscure } from "./obscure";
|
||||
import { ClipBoard } from "./clipboard";
|
||||
import { Section } from "./section";
|
||||
import { Grid, GridRow } from "./grid";
|
||||
import { Tooltip } from "./tooltip";
|
||||
import { Spinner } from "./spinner";
|
||||
|
||||
export {
|
||||
Icons,
|
||||
Toast,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalForm,
|
||||
AccessControl,
|
||||
Obscure,
|
||||
ClipBoard,
|
||||
Section,
|
||||
Grid,
|
||||
GridRow,
|
||||
Tooltip,
|
||||
Spinner,
|
||||
};
|
||||
99
src/components/modal/index.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React from "react";
|
||||
import { Button } from "jambonz-ui";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type ModalProps = {
|
||||
disabled?: boolean;
|
||||
children: React.ReactNode;
|
||||
handleSubmit: (e: React.FormEvent) => void;
|
||||
handleCancel: (e: React.FormEvent) => void;
|
||||
};
|
||||
|
||||
type CloseProps = {
|
||||
children: React.ReactNode;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const Modal = ({
|
||||
disabled,
|
||||
children,
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
}: ModalProps) => {
|
||||
return (
|
||||
<div className="modal">
|
||||
<div className="modal__box">
|
||||
<div className="modal__stuff">{children}</div>
|
||||
<div className="modal__btns">
|
||||
<Button
|
||||
type="button"
|
||||
small
|
||||
subStyle="grey"
|
||||
onClick={handleCancel}
|
||||
disabled={disabled}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
small
|
||||
onClick={handleSubmit}
|
||||
disabled={disabled}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalForm = ({
|
||||
disabled,
|
||||
children,
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
}: ModalProps) => {
|
||||
return (
|
||||
<div className="modal">
|
||||
<form
|
||||
className="modal__box form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit(e);
|
||||
}}
|
||||
>
|
||||
<div className="modal__stuff">{children}</div>
|
||||
<div className="modal__btns">
|
||||
<Button
|
||||
small
|
||||
subStyle="grey"
|
||||
onClick={handleCancel}
|
||||
disabled={disabled}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" small disabled={disabled}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalClose = ({ children, handleClose }: CloseProps) => {
|
||||
return (
|
||||
<div className="modal">
|
||||
<div className="modal__box">
|
||||
<div className="modal__stuff">{children}</div>
|
||||
<div className="modal__btns">
|
||||
<Button type="button" small subStyle="grey" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
46
src/components/modal/styles.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
@use "src/styles/vars";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: vars.$zindex00;
|
||||
padding-left: vars.$px02;
|
||||
padding-right: vars.$px02;
|
||||
margin: 0 !important;
|
||||
|
||||
&__stuff {
|
||||
> * + * {
|
||||
margin-top: vars.$px02;
|
||||
}
|
||||
}
|
||||
|
||||
&__box {
|
||||
padding: vars.$px03;
|
||||
width: 100%;
|
||||
max-width: vars.$widthinput;
|
||||
border-radius: vars.$px02;
|
||||
box-shadow: 0px vars.$px00 vars.$px00 rgba(0, 0, 0, 0.25);
|
||||
background-color: ui-vars.$white;
|
||||
|
||||
> * + * {
|
||||
margin-top: vars.$px03;
|
||||
}
|
||||
}
|
||||
|
||||
&__btns {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
* + * {
|
||||
margin-left: vars.$px02;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/components/obscure/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
import { getObscured } from "src/utils";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type ObscureProps = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const Obscure = ({ text }: ObscureProps) => {
|
||||
const [reveal, setReveal] = useState(false);
|
||||
const obscured = getObscured(text);
|
||||
|
||||
return (
|
||||
<div className="obscure">
|
||||
<span>{reveal ? text : obscured}</span>
|
||||
<button
|
||||
className="btn--type"
|
||||
type="button"
|
||||
onClick={() => setReveal(!reveal)}
|
||||
>
|
||||
{reveal ? <Icons.EyeOff /> : <Icons.Eye />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
29
src/components/obscure/styles.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
@use "src/styles/vars";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.obscure {
|
||||
@include ui-mixins.ms();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: ui-vars.$font-mono;
|
||||
|
||||
> span {
|
||||
/** CSS magic number goes with api keys grid... */
|
||||
@media (max-width: 940px) {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
> button[type="button"] {
|
||||
cursor: pointer;
|
||||
margin-left: vars.$px02;
|
||||
|
||||
svg {
|
||||
stroke: ui-vars.$jambonz;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import SetupTemplate from '../templates/SetupTemplate';
|
||||
import InternalTemplate from '../templates/InternalTemplate';
|
||||
import Link from '../elements/Link';
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 4rem;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const InvalidRoute = () => {
|
||||
const [ isLoggedIn, setIsLoggedIn ] = useState(false);
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem('token')) {
|
||||
setIsLoggedIn(true);
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
return (
|
||||
isLoggedIn ? (
|
||||
<InternalTemplate title="Invalid Route">
|
||||
<Container>
|
||||
That page doesn't exist.
|
||||
</Container>
|
||||
</InternalTemplate>
|
||||
) : (
|
||||
<SetupTemplate title="Invalid Route">
|
||||
<Container>
|
||||
<p>That page doesn't exist.</p>
|
||||
<p><Link to="/">Log In</Link></p>
|
||||
</Container>
|
||||
</SetupTemplate>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default InvalidRoute;
|
||||
@@ -1,228 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import SetupTemplate from '../templates/SetupTemplate';
|
||||
import Form from '../elements/Form';
|
||||
import Button from '../elements/Button';
|
||||
import Input from '../elements/Input';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import FormError from '../blocks/FormError';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const Login = props => {
|
||||
let history = useHistory();
|
||||
useEffect(() => {
|
||||
document.title = `Login | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
// Refs
|
||||
const refUsername = useRef(null);
|
||||
const refPassword = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ username, setUsername ] = useState('');
|
||||
const [ password, setPassword ] = useState('');
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidUsername, setInvalidUsername ] = useState(false);
|
||||
const [ invalidPassword, setInvalidPassword ] = useState(false);
|
||||
|
||||
const handleSubmit = async e => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidUsername(false);
|
||||
setInvalidPassword(false);
|
||||
|
||||
if (!username && !password) {
|
||||
setErrorMessage('Username and password are required');
|
||||
setInvalidUsername(true);
|
||||
setInvalidPassword(true);
|
||||
refUsername.current.focus();
|
||||
return;
|
||||
}
|
||||
if (!username) {
|
||||
setErrorMessage('Username is required');
|
||||
setInvalidUsername(true);
|
||||
refUsername.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
setErrorMessage('Password is required');
|
||||
setInvalidPassword(true);
|
||||
refPassword.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Log in
|
||||
const response = await axios({
|
||||
method: 'post',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/login',
|
||||
data: { username, password },
|
||||
});
|
||||
|
||||
// New account, password change required
|
||||
if (response.data.force_change) {
|
||||
// `user_sid` and `old_password` are needed for the new password form.
|
||||
// They're saved to sessionStorage so that the data does not persist.
|
||||
sessionStorage.setItem('user_sid', response.data.user_sid);
|
||||
sessionStorage.setItem('old_password', password);
|
||||
localStorage.setItem('token', response.data.token);
|
||||
history.push('/create-password');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save API key
|
||||
if (response.data.token) {
|
||||
localStorage.setItem('token', response.data.token);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Get account data
|
||||
//-----------------------------------------------------------------------------
|
||||
const serviceProvidersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/serviceProviders',
|
||||
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 voipCarriersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/voipCarriers',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const promiseAllValues = await Promise.all([
|
||||
serviceProvidersPromise,
|
||||
accountsPromise,
|
||||
applicationsPromise,
|
||||
voipCarriersPromise,
|
||||
]);
|
||||
|
||||
const serviceProviders = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[1].data;
|
||||
const applications = promiseAllValues[2].data;
|
||||
const voipCarriers = promiseAllValues[3].data;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Determine where to route user
|
||||
//-----------------------------------------------------------------------------
|
||||
if (
|
||||
(serviceProviders.length > 1) ||
|
||||
(accounts.length > 1) ||
|
||||
(accounts.length < 1) ||
|
||||
(applications.length > 1) ||
|
||||
(voipCarriers.length > 0)
|
||||
) {
|
||||
history.push('/internal/accounts');
|
||||
return;
|
||||
}
|
||||
|
||||
// const { sip_realm, registration_hook } = accounts[0];
|
||||
|
||||
// if (
|
||||
// (!sip_realm || !registration_hook) &&
|
||||
// !applications.length
|
||||
// ) {
|
||||
// history.push('/configure-account');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!applications.length) {
|
||||
// history.push('/create-application');
|
||||
// return;
|
||||
// }
|
||||
|
||||
history.push('/internal/accounts');
|
||||
|
||||
} catch (err) {
|
||||
// 400 --> one or both fields are empty (prevented by above error checking)
|
||||
// 403 --> username or password are incorrect
|
||||
if (
|
||||
err.response
|
||||
&& err.response.status
|
||||
&& err.response.status > 399
|
||||
&& err.response.status < 500
|
||||
) {
|
||||
setErrorMessage('Login credentials are incorrect');
|
||||
} else {
|
||||
setErrorMessage(
|
||||
(err.response && err.response.data && err.response.data.msg) ||
|
||||
'Something went wrong, please try again.'
|
||||
);
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SetupTemplate title="Log In">
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
large
|
||||
fullWidth
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
ref={refUsername}
|
||||
invalid={invalidUsername}
|
||||
autoFocus
|
||||
/>
|
||||
<PasswordInput
|
||||
large
|
||||
allowShowPassword
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="Password"
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
setErrorMessage={setErrorMessage}
|
||||
ref={refPassword}
|
||||
invalid={invalidPassword}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<FormError message={errorMessage} />
|
||||
)}
|
||||
<Button
|
||||
large
|
||||
fullWidth
|
||||
>
|
||||
Log In
|
||||
</Button>
|
||||
</Form>
|
||||
</SetupTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||