Compare commits

...

222 Commits

Author SHA1 Message Date
Quan HL
a6216d1db1 wip 2024-01-27 11:41:21 +07:00
Quan HL
6dd445fdb4 wip 2024-01-27 11:22:46 +07:00
Quan HL
440cf25935 wip 2024-01-27 09:46:43 +07:00
Quan HL
97736654ef wip 2024-01-27 09:24:01 +07:00
Quan HL
2a3fc07306 fix speech selection show app languge and voice 2024-01-27 09:01:27 +07:00
Dave Horton
6e14207327 sync with change of property name to time_to_first_byte_ms (#394) 2024-01-25 13:16:42 -05:00
Hoan Luu Huu
8d8d46e76e draw tts region label to lower possition (#393) 2024-01-24 19:48:56 -05:00
Hoan Luu Huu
7f72d739cd Fix/tts region use tts_time_to_first_byte streaming api (#391)
* tts region tts_time_to_first_byte_ms

* wip
2024-01-24 10:19:46 -05:00
Hoan Luu Huu
c804d60664 wavesurfer region use s instead of sec (#390) 2024-01-24 08:44:31 -05:00
Hoan Luu Huu
df3fc8f2b7 playback latency region on wavesurfer (#387)
* playback latency region on wavesurfer

* remove start-audio region
2024-01-22 20:31:02 -05:00
Hoan Luu Huu
65e5b511c3 gather speech verb hook latency (#383)
* gather speech verb hook latency

* wip
2024-01-17 12:37:36 -05:00
Hoan Luu Huu
dc519bdef9 fix remove speech label, cause application save wrong label (#384)
* fix remove speech label, cause application save wrong label

* fix microsoft custom_tts input
2024-01-17 09:17:25 -05:00
Hoan Luu Huu
af1ba3a15c fix app cannot set elevenlab tts (#386) 2024-01-17 07:30:36 -05:00
Hoan Luu Huu
67b7792d04 tts options should change if language is changed (#381) 2024-01-14 21:48:43 -05:00
Hoan Luu Huu
c5e7eb0d23 fix stt/tts regoin on wrong position (#380)
* fix stt/tts regoin on wrong position

* wip

* fix speech credentials does not choose default model

* fix speech credentials does not choose default model
2024-01-13 11:08:07 -05:00
Hoan Luu Huu
6ddcb82adc update jambonz cloud free plan subscription (#379) 2024-01-12 09:24:10 -05:00
Hoan Luu Huu
8b9c7ca9c0 Feat/tts stt voices languages from api server (#376)
* support fetching tts/stt language and voice from api server

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
2024-01-12 07:34:22 -05:00
Hoan Luu Huu
353c7cfff8 update google tts language (#375) 2024-01-05 07:18:39 -05:00
Hoan Luu Huu
7828dc3827 fix default value for elevenlab optimize_streaming_latency (#374) 2024-01-03 07:21:29 -05:00
Hoan Luu Huu
213267f682 fix, change synthLange does not update synthVoice (#372) 2023-12-27 09:37:19 -05:00
Hoan Luu Huu
cf056ae6f1 fix issue that when speech has label, save application should use the first label in the list (#371) 2023-12-27 09:33:44 -05:00
Hoan Luu Huu
1c16d707ca fix deepgram tts default model and update stt languages (#370) 2023-12-26 18:48:59 -05:00
Hoan Luu Huu
2f2e58e180 support deepgram (#369)
* support deepgram

* wip
2023-12-26 07:53:13 -05:00
Hoan Luu Huu
eae674b992 fix application is blank page if choosing custom speech vendor (#361) 2023-12-06 09:06:57 -05:00
Hoan Luu Huu
aa7889a0d8 tts latency feature (#359)
* tts latency feature

* fix review comment
2023-12-05 20:33:53 -05:00
Hoan Luu Huu
a892550b06 elevenlabs new model and options (#357)
* elevenlabs new model and options

* beautify

* fix review comments
2023-11-30 10:20:35 -05:00
Hoan Luu Huu
053f8e509f update azure voices (#356) 2023-11-24 09:18:06 -05:00
Hoan Luu Huu
fc40695828 fix carrier registration status (#354) 2023-11-23 22:01:37 -05:00
Hoan Luu Huu
42af4f6243 fix elevenlabs set voice (#353) 2023-11-21 08:27:47 -06:00
Hoan Luu Huu
7ec8065977 fix lcr create update generate too much request (#351)
* fix lcr create update generate too much request

* wip
2023-11-14 08:07:29 -05:00
Dave Horton
d8f05da6fd fix regression bug: not sending aws_region (#348) 2023-11-09 16:02:42 -05:00
Dave Horton
15c2b955ca 0.8.5 2023-11-09 12:37:59 -05:00
Hoan Luu Huu
87b3ca7e94 Support whisper TTS (#346)
* support tts whisper

* support tts whisper

* wip

* wip

* fix wrong language and voice
2023-11-09 09:50:38 -05:00
Hoan Luu Huu
adafff7ec3 disable update client username/password (#345)
* disable update client username/password

* fix application stt vendor for elevenlabs
2023-11-08 12:13:08 -05:00
Dave Horton
bc9a2464fd Fine tune speech latency (#344)
* divide speech into 10ms segments when evaluating peaks for speech latency

* minor

* minor

* minor change for clarity
2023-11-07 14:40:44 -05:00
Dave Horton
2a6f8c272c assemblyai is stt only (#343) 2023-11-06 12:34:26 -05:00
Dave Horton
f031c47228 changes to enable/disable direct calling from chrome extension (#342)
* changes to enable/disable direct calling from chrome extension

* wip

* wip

* wip

---------

Co-authored-by: Quan HL <quan.luuhoang8@gmail.com>
2023-11-06 09:34:33 -05:00
Hoan Luu Huu
2e9b86c0c4 feat calculate speech recognition latency (#341)
* feat calculate speech recognition latency

* fix review comments

* wip

* wip

* wip

* wip

* wip
2023-11-06 09:31:24 -05:00
Hoan Luu Huu
dd93bedd0e Feat/assemblyai (#340)
* feat assembly ai

* feat assembly ai
2023-11-01 08:03:24 -04:00
Hoan Luu Huu
e2157ce50e feat google custom voice (#338)
* feat google custom voice

* google custom voice

* wip

* wip

* wip
2023-10-30 20:28:34 -04:00
Dave Horton
a382f21f86 #335 - allow top level fqdn for outbound gateway (#336) 2023-10-21 11:22:18 +02:00
Hoan Luu Huu
a20e1513bc remove warning message if there is no device call application (#334) 2023-10-20 13:18:01 +02:00
Hoan Luu Huu
af8c09587c fix wrong css for filter on each component (#333) 2023-10-20 08:16:00 +02:00
Hoan Luu Huu
3a19ff6840 upgrade wavesuffer to 7.3.4 (#329)
* upgrade wavesuffer to 7.3.4

* fix typo issue for wavesurfer

* fix

* fix
2023-10-18 19:54:59 +02:00
Hoan Luu Huu
729cefb06c fix css for recent calls in small screen (#331) 2023-10-16 12:51:49 +02:00
Hoan Luu Huu
26e3856603 add elevenlabs (#330)
* add elevenlabs

* wip

* wip

* fix review comments

* fetch voice and language for eleven labs

* fix revciew comment

* fix revciew comment

* fix revciew comment

* fixed review comment
2023-10-16 10:21:19 +02:00
Hoan Luu Huu
f5302583b5 fix cobalt default language (#326) 2023-09-27 07:50:11 -04:00
Hoan Luu Huu
b5c27bb096 add cobalt stt (#324)
* add cobalt stt

* update languages for cobalt

* update languages for cobalt
2023-09-26 08:42:34 -04:00
Hoan Luu Huu
4a2c36ebba allow sip port is null (#323)
* allow sip port is null

* fix port placeholder when protocol is tls or tls/srtp
2023-09-18 20:15:53 -04:00
Hoan Luu Huu
62234f9f64 pad crypto sip gateway (#322) 2023-09-18 07:59:07 -04:00
Anton Voylenko
9ddafee2cc feat: support s3 compatible storage (#318)
* feat: support s3 compatible storage

* reorder vendor list
2023-09-12 12:28:03 -04:00
Hoan Luu Huu
24fc9d1bff feat: custom Microsoft need to have input text for tts voice (#316)
* feat: custom Microsoft need to have input text for tts voice

* fix
2023-09-08 08:15:44 -04:00
Hoan Luu Huu
08ab494cef feat azure fromhost (#302)
* feat azure fromhost

* wip

* wip

* wip

* wip

* fix review comment

* fix review comment

* wip

* wip
2023-08-30 21:07:03 -04:00
Hoan Luu Huu
75e7785061 fix choose speech dedential by label (#315) 2023-08-30 09:22:39 -04:00
Hoan Luu Huu
72de9178a2 support delete record file (#313)
* support delete record file

* fix
2023-08-23 19:58:59 -04:00
Hoan Luu Huu
9741e5601f feat fallback speech vendor (#310)
* feat fallback speech vendor

* fix

* fix

* fix

* wip

* wip
2023-08-22 08:08:22 -04:00
Hoan Luu Huu
346ac66440 support azure storage (#312) 2023-08-22 07:50:38 -04:00
Hoan Luu Huu
843d1eda1e feat support multiple speech credential with different labels but same vendor (#305)
* feat support multiple speech credential with different labels but same vendor

* fix

* fix review comment

* fix review comment

* fix label tooltip

* fixed
2023-08-15 09:00:00 -04:00
Hoan Luu Huu
27f02c2bb3 update preview description (#301) 2023-08-04 09:51:01 -04:00
Hoan Luu Huu
bb18556a6c allow only card type in stripe PaymentElement (#299) 2023-08-04 06:57:10 -04:00
Hoan Luu Huu
393dd7374f fix clients make user confuse (#298)
* fix clients make user confuse

* fix
2023-08-03 21:13:04 -04:00
Hoan Luu Huu
4ad2154337 fix choose/edit sub domain (#295)
* fix choose/edit sub domain

* add carrier instructions to send call to sip-realm
2023-08-02 07:42:33 -04:00
Hoan Luu Huu
08d1293e34 feat: add login with google and github (#278)
* feat: add login with google and github
* feat: hosted version
* add register pages
* feat: add verify email code page
* register by email
* fix login
* fix logout
* add all stripe pages
* subscription delete account
* fix edit account and edit sip realm
* when user account login, remove cancel in edit page
* remove name
* update .env configuraiton vars
2023-07-30 22:33:58 -04:00
Hoan Luu Huu
4eb2281b9a add polyglot for google (#288)
* add polyglot for google

* add news voice
2023-07-29 08:00:44 -04:00
Hoan Luu Huu
61bd1f9bab feat: google storage for record all call (#292)
* feat: google storage for record all call

* fix

* wip

* wip

* wip

* wip

* fix

* fix

* fix

* fix

* fix
2023-07-28 12:03:54 -04:00
Hoan Luu Huu
16629ba508 fix clients (#289) 2023-07-25 09:00:46 -04:00
Hoan Luu Huu
63f8a82443 only download jaeger trace when open detail for recent call (#287) 2023-07-17 19:14:55 -04:00
Hoan Luu Huu
9ce1d83c8f fix: update name for carrier register authentication fields (#283) 2023-07-03 15:25:22 +01:00
Hoan Luu Huu
961b7ecccb recent call filter (#282)
* recent call filter

* fix review comment

* fix eslint
2023-07-03 15:06:09 +01:00
Dave Horton
3fb63c82ac update version 2023-06-28 09:28:46 +01:00
Hoan Luu Huu
cb2d5926b2 Fix/alerts (#276)
* improve alert view

* improve alert view
2023-06-18 11:05:26 +01:00
Hoan Luu Huu
595e900468 fix showing client password (#274)
* fix showing client password

* fix client password validation
2023-06-15 20:53:13 -04:00
Hoan Luu Huu
0dd9548600 feat: clients (#272)
* feat: clients

* fix typo

* fix reivew comments

* add error message if account miss realm or device calling app

* fix: remove Showed By
2023-06-15 07:37:10 -04:00
Hoan Luu Huu
96ffce8cd1 feat: provision record all call (#254) 2023-06-13 09:03:44 -04:00
Hoan Luu Huu
b1fe033c12 feat/ update google tts voice for neutral and studio (#271) 2023-06-13 07:42:19 -04:00
Hoan Luu Huu
a8d12546d9 filter by from and to (#269) 2023-06-09 13:43:13 -04:00
EgleH
724d86821d hint passwordSettings to user when creating password (#267)
Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-06-08 07:24:29 -04:00
Hoan Luu Huu
f91bbe9245 clean tts cache for account (#264) 2023-06-02 07:39:48 -04:00
Hoan Luu Huu
91625612d5 feat: add seconds to recent call sumary (#261) 2023-06-01 08:23:00 -04:00
Hoan Luu Huu
fbe71925b4 feat: admin clear cache (#263)
* feat: admin clear cache

* feat: admin clear cache
2023-06-01 07:49:26 -04:00
Dave Horton
377fd40e2c fix docker publish 2023-05-27 10:52:04 -04:00
Hoan Luu Huu
af37066201 fix: download pcap (#258) 2023-05-24 07:56:15 -04:00
Dave Horton
fffd86619d disabling lcr, jaeger, and custom speech for k8s (#256)
* disabling lcr, jaeger, and custom speech for k8s

* fix syntax error in entrypoint.sh
2023-05-22 15:03:43 -04:00
Dave Horton
77c270e078 fix docker build 2023-05-15 14:24:20 -04:00
Dave Horton
54ff53817f fix error when register status call-id is empty (#253) 2023-05-12 10:55:00 -04:00
Dave Horton
986b9a5eeb 0.8.3 2023-05-11 09:24:18 -04:00
Hoan Luu Huu
683693ec0e fix: tts.cached boolValue does not showup on spain detail (#250)
* fix: tts.cached boolValue does not showup on spain detail

* wip

* wip

* wip

* add env to disable jaeger tracing view

* fix: tts.cached boolValue does not showup on spain detail

* wip

* wip

* wip

* add env to disable jaeger tracing view

* fix: review coments

* fix review comment

* fix review comment
2023-05-11 08:34:16 -04:00
Hoan Luu Huu
6cb1c50cf0 feat: forgot password (#218)
* feat: forgot password

* feat: forgot password

* fix: enable flag

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
2023-05-10 21:32:14 -04:00
Hoan Luu Huu
3d9a39ac3b fix: add/edit lcr should have secondary text to explain the purpose of lcr (#247) 2023-05-10 16:05:57 -04:00
Hoan Luu Huu
4d7e84fa43 feat: add sip gateway protocol for outbound traffic (#249)
* feat: add sip gateway protocol for outbound traffic

* fix: add tls/srtp
2023-05-10 16:02:39 -04:00
Hoan Luu Huu
41423a443a add secondary text to lcr list (#245) 2023-05-08 09:23:32 -04:00
Hoan Luu Huu
bfbd66ef5c Feat/jaeger (#243)
* added dummy jaeger json file

* added jaeger types file

* added dev jaeger endpoint

* added jaeger modal with trace visual / information

* refactored jaeger logic
fixed offsets on short duration spans

* refactored into smaller components & added basic scroll bar

* removed buttons, added scroll-x, fixed details height and scroll-y

* shrunk bar graph to fit view port

* slight adjustments

* removed ref and now calculate width based on window innerwidth

* @media for phone layouts

* -fixed details width and padding.
-removed scroll.tsx as not needed now
-using SpanKind to find parent for now

* -reduced truncate size for smaller screens

* -root span is now determined from parentSpanId not being found

* removed un-needed calls to /getRecentCalls as this was causing a race condition when pcap & jaeger fetching at same time
- removed console.log's

* wip: add tabs for recent callt tracing

* wip: add tabs for recent callt tracing

* wip: add tabs for recent callt tracing

* fix: review comments

* fix: review comments

---------

Co-authored-by: ajukes <ajukes@vibecoms.co.uk>
2023-05-05 20:12:46 -04:00
Hoan Luu Huu
414bca4fdc fix: unchecked checkzone still render input with required (#242) 2023-05-05 07:34:37 -04:00
Hoan Luu Huu
a6c8257b60 feat: Lcr (#237)
* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix: final review version

* fix: review comments

* fix: review comments

* fix: review comments

* fix: review comments

* wip: implement drag and drop

* add box shadow for lcr route

* fix review comment
2023-05-04 19:54:46 -04:00
Hoan Luu Huu
dbb39db54e #239 (#240)
* system Information

* system Information
2023-05-04 19:41:25 -04:00
EgleH
6712d8944b add a fallback accountSid for account users (#238)
Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-04-24 07:47:42 -04:00
Hoan Luu Huu
7051985aad fix: update active sip_gateways (#235) 2023-04-13 10:22:08 -04:00
EgleH
88dae20666 Update base image to node:18.15-alpine3.16 (#234) 2023-04-12 13:15:39 -04:00
Dave Horton
977a08eaf7 push to docker 2023-04-10 09:41:37 -04:00
EgleH
1bfc722960 fix the webapp blocking account name if exists in anohter SP (#229)
* fix the webapp blocking account name if exists in anohter SP

* hard reload when changing SP

* password settings enhance special chars

---------

Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-04-06 07:43:10 -04:00
Hoan Luu Huu
49c18ebd26 feat: carrier register status (#226)
* feat: carrier register status

* fix: typo issue

* update PcapButton

* is active for voice gateway to true

* fix: download pcap

* fix: download pcap
2023-04-03 13:21:26 -04:00
EgleH
aba8b2be3a logout with one click (#223)
Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-03-30 07:44:19 -04:00
EgleH
f4d7880ab7 Add Logout call to signout (#221)
* Add Logout call to signout

* clean local storage even on error

---------

Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-03-29 08:54:17 -04:00
Dave Horton
7f93489580 bump version 2023-03-28 14:15:26 -04:00
Dave Horton
19fafdc908 minor label change 2023-03-25 12:13:13 -04:00
Hoan Luu Huu
a165bfc4d6 feat: onpremise nuance (#220)
Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
2023-03-25 11:21:14 -04:00
Hoan Luu Huu
e26d9b95cb fix: edit application does not clear webhook user/pass when checkbox is uncheck (#217)
Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
2023-03-21 20:14:16 -04:00
Hoan Luu Huu
e425d825bc feat: custom Vendor (#215)
* feat: custom Vendor

* feat: custom Vendor

* fix: application with custom tts/stt vendor

* fix custom speech name when editing

* fix: all comments

* fix: remove custom in the list and show extra (custom)

* fix: prettier and application sythesizer selector

* fix: addd VITE_DISABLE_CUSTOM_SPEECH

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
2023-03-14 08:45:06 -04:00
EgleH
a8d28da221 add soniox as speech provider (#211)
Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-03-03 18:53:46 -05:00
EgleH
e3855e83f7 conditional required causing issue with focusable fields (#210)
Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-02-23 12:18:01 -05:00
Dave Horton
446b6e76e2 update dockerfile 2023-02-23 08:21:09 -05:00
EgleH
0b55cdcf85 add env VITE_APP_DISABLE_DEFAULT_TRUNK_ROUTING (#209)
Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-02-22 14:05:56 -05:00
Snyk bot
f1743a9129 fix: Dockerfile to reduce vulnerabilities (#208)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314624
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314641
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314641
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314643
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314643
2023-02-22 07:32:42 -05:00
EgleH
ec46121696 upgrade node image (#206)
Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-02-20 10:31:07 -05:00
Hoan Luu Huu
b0808187bc feat: add app_json to application (#188)
* feat: add app_json to application

* fix: review comment

* fix: review comment

* fix: review comment

* textarea for initial json in applications form

* check/uncheck overide app_json will not erase app_json, update faile still have app_json

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-02-15 10:35:45 -05:00
Snyk bot
4a320b3c8c fix: Dockerfile to reduce vulnerabilities (#204)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-UPSTREAM-NODE-3035792
- https://snyk.io/vuln/SNYK-UPSTREAM-NODE-3035795
- https://snyk.io/vuln/SNYK-UPSTREAM-NODE-3092932
- https://snyk.io/vuln/SNYK-UPSTREAM-NODE-3092933
- https://snyk.io/vuln/SNYK-UPSTREAM-NODE-3105822
2023-02-15 08:11:36 -05:00
EgleH
c09ce5947e Bug/speech backwards compatibility (#203)
* add /Accounts api call back

* add /accounts call back

* include sp user into check

---------

Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-02-14 15:34:11 -05:00
Hoan Luu Huu
7890b7031f feat: update @jambonz/ui-kit:0.0.21 (#200)
* feat: update @jambonz/ui-kit:0.0.21

* fix security vulnerabilities

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-02-13 08:47:56 -05:00
Hoan Luu Huu
1be5dcc06b feat: nvidia tts/stt speech (#201)
* feat: nvidia tts/stt speech

* fix: rename riva server uri

* fix: review comments

* fix: review comments

* sort vendors in alphabetic order

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-02-13 08:37:32 -05:00
EgleH
6df9f0d6da Bug/sp user check before sp delete (#198)
* fix bug where you could not delete carriers and speech

* remove activeSP from localstore after delete

---------

Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-02-08 07:29:04 -05:00
Brandon Lee Kitajchuk
8f849ef290 Update CODEOWNERS 2023-02-05 08:22:23 -08:00
EgleH
ba7a4a706d Bug/fix recent calls refresh account assign (#194)
* fix applications add button

* fix persist implementation bugs

---------

Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-02-04 20:33:55 -05:00
EgleH
6a59b9d8d2 fix applications add button (#193)
Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-02-04 18:27:34 -05:00
EgleH
af3e724240 Feature/persist state (#191)
* persist SP implementation

* add account filter check after refresh

* store recent calls and alerts filter

---------

Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-02-02 09:53:04 -05:00
EgleH
51c2285bfb Fix fuzzy search to match against toLowerCase strings (#190)
* fix fuzzy search to match against toLowerCase strings

* add toLowercase to both sides

---------

Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-01-31 09:56:12 -05:00
EgleH
40c61eebca Combine account user lists for carriers and speech (#186)
* combine account user lists for carriers and speech

* edit application capitalization

---------

Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-01-30 10:36:54 -05:00
EgleH
4498f30beb Spinner spinning forever issue (#185)
* Spinner spinning forever issue

* fix mistake

Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-01-26 09:28:53 -05:00
EgleH
2a27730883 Improving naming consistency (#187)
* Improving naming consistency

* direct routing is a voice term

* fix more inconsistencies

* explicitly disable autocomplete

Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-01-26 09:01:18 -05:00
EgleH
2ae541c9b1 Fix issue where is_active user & force_change cannot be deactivated (#178)
* fix issue where is_active user & force_change cannot be deactivated

* fix API keys not visible for SP users

Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-01-23 08:07:45 -05:00
EgleH
4149930945 Always attach service_provider_sid to a carrier (#179)
* always attach service_provider_id to a carrier

* cleanup

Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-01-23 07:56:41 -05:00
EgleH
fd29f986e4 Date fix for alerts and recent calls (#176)
* date fix for alerts and recent calls

* add a check for value to exist

* getRecentCalls with sip_callid

Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-01-23 07:40:23 -05:00
EgleH
def7743b9e Scope optional limits (#169)
* scope optional limits

* hide and disable limits, initiate delete when unit changed

* add key to ScopedAccess component

Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-01-17 09:28:23 -05:00
EgleH
55c1b26ddc add instance ID field (#175)
Co-authored-by: EgleHelms <e.helms@cognigy.com>
2023-01-11 14:48:25 -05:00
Brandon Lee Kitajchuk
ef17ea9676 Fix logout with hard redirect -- dump state (#174) 2023-01-10 12:20:28 -05:00
kitajchuk
08dac043eb No more fixed position here 2023-01-10 08:42:59 -08:00
Brandon Lee Kitajchuk
ce48bdc8a4 Fix bug with bad user reference in internal accounts page (#172) 2023-01-07 22:56:43 -05:00
EgleH
93d313b21c feature/scope limits (#159)
* limitations to carriers and speech

* post speech to a different path if scoped user

* add filtering to speech creds

* check reroute when logging in

* faulty checks

* fix access conditionals

* add restrictions to applications and accounts

* apply 1st review comments

* fix routing

* apply review comments

* discussed changes

* useScopedRedirect

* Refactor how we manage scope for navi (#163)

* fix user name

* Fix useScopedRedirect hook (#164)

* Fix useScopedRedirect hook

* Refactor user UI

* Move user-me to own directory

* add scope limits to routes

* scope optional limits

* cleanup

* apply review comments

* Refactor conditional apiUrl logic -- add apiPath to deps in useApiData hook (#168)

* apply review comments - accountFilter remove from AccountScoped Views

* remove account filtering as it is done on back-end now

* Cleanup some things

* Clean up some scope things

* Implement account user PUT method for carriers

* filterScopeOptions accorfing to user scope

Co-authored-by: eglehelms <e.helms@cognigy.com>
Co-authored-by: Brandon Lee Kitajchuk <bk@kitajchuk.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-01-02 17:37:17 -05:00
EgleH
5af46101e1 fix state for tts_api_key and voice typo (#167)
Co-authored-by: eglehelms <e.helms@cognigy.com>
2022-12-14 19:15:21 -05:00
EgleH
86703dabfb Optional account call limits (#162)
* add optional account limits with env var

* add env sample and comment

* fix typo in selecti selection of unit

* apply review comments

Co-authored-by: eglehelms <e.helms@cognigy.com>
2022-12-10 12:00:02 -05:00
EgleH
6a82102b43 Add IBM as speech provider (#160)
* add ibm speech

* remove multimedia from recognizer languages

Co-authored-by: eglehelms <e.helms@cognigy.com>
2022-12-04 19:59:45 -05:00
kitajchuk
ce4c5bfb68 Changes for package-lock 2022-11-30 09:01:07 -08:00
kitajchuk
e0b5abc60a Remove unused acl stuff from navi 2022-11-30 08:58:33 -08:00
Brandon Lee Kitajchuk
bb4a4af2d5 Update contributors.md 2022-11-29 20:52:02 -08:00
EgleH
667e4a8883 ScopeAccess component implementation (#154)
* component based on enumScope

* apply review comments

* add store props

* Revert "add store props"

This reverts commit 0e0978c5f3.

* Tests for ScopedAccess (#156)

* Tests for ScopedAccess

* Create cypress mountTestProvider

Co-authored-by: eglehelms <e.helms@cognigy.com>
Co-authored-by: Brandon Lee Kitajchuk <bk@kitajchuk.com>
2022-11-29 10:19:27 -05:00
EgleH
5af7471886 Feature/users dashboard (#150)
* Add user list into nav, add user form with basic functionality

* add password check for temp password, add search filter

* apply review comments

* fix filters, apply review comments

* fix button placement

* handle SelfDelete

* apply review comments

* add conditional class

* creating scoped users

* apply review comments

* apply review comments

* apply review comments

Co-authored-by: eglehelms <e.helms@cognigy.com>
2022-11-23 13:12:19 -05:00
EgleH
a9edbdbd26 Add deepgram as STT (#151)
* add deepgram as stt

* add all languages

* remove unused types

* temporary remove Flemish

* apply review comments

Co-authored-by: eglehelms <e.helms@cognigy.com>
2022-11-16 10:18:28 -05:00
EgleH
29cd5a5fa7 Prefill predefined carrier gateways in a form (#147)
* add gateways api call to hook

* add route to carrier template

* apply review comments

* omit request.body if no payload is provided

* remove ternary where not needed

* fix typing of payload

* fix payload type to allow undefined

Co-authored-by: eglehelms <e.helms@cognigy.com>
2022-11-11 07:56:07 -05:00
EgleH
781ccb95f2 Add nuance speech provider (#144)
* add nuance as Synthesis recognizer

* fix getStatus for cred test check

* add all languages

* fixup nuance voice names

* fix label focus issue

Co-authored-by: eglehelms <e.helms@cognigy.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2022-11-06 11:01:55 -05:00
Brandon Lee Kitajchuk
3093f40e00 Feature: Multi-user: Changes to initial admin login processing / implement JWT (#140)
* Add simple JWT parsing function

* Implement user action for global store
2022-10-26 09:21:25 -07:00
Brandon Lee Kitajchuk
50d93b0089 Feature: Multi-user (#134)
* Feat: password settings (#130)

* wip: password settings

* wip: password settings

* wip: password settings

* wip: password settings

* fix: review comments

* fix: review comments

* fix: review comments

* fix: review comments

* Remove unnecessary selector

* Prefer self-closing syntax

* Add inputs to Carrier form to support outbound registration (#138)

* add fromUser fromDomain and usePublicIpInContact

* change label placement for usePublicIpInContact checkbox

* apply 1st review comments

Co-authored-by: eglehelms <e.helms@cognigy.com>

Co-authored-by: xquanluu <110280845+xquanluu@users.noreply.github.com>
Co-authored-by: EgleH <egle.helms@gmail.com>
Co-authored-by: eglehelms <e.helms@cognigy.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2022-10-25 09:04:59 -04:00
EgleH
da9ac66236 Add inputs to Carrier form to support outbound registration (#138)
* add fromUser fromDomain and usePublicIpInContact

* change label placement for usePublicIpInContact checkbox

* apply 1st review comments

Co-authored-by: eglehelms <e.helms@cognigy.com>
2022-10-23 11:35:05 -04:00
Brandon Lee Kitajchuk
3d14b4f411 Prefer self-closing syntax 2022-10-19 20:32:38 -07:00
Brandon Lee Kitajchuk
c8f1517421 Remove unnecessary selector 2022-10-19 20:10:23 -07:00
Brandon Lee Kitajchuk
2a6181615e Login layout container cypress test (#136) 2022-10-18 08:45:44 -07:00
xquanluu
874002386c fix: change custom speech label content (#133) 2022-10-04 20:13:00 -07:00
kitajchuk
18f531c478 Pagination optimizations 2022-10-04 07:28:53 -07:00
kitajchuk
584386c3ca Clear the temp state when credential is updated 2022-10-04 06:47:36 -07:00
xquanluu
1ece8bfbe4 Microsoft custom TTS and STT endpoints (#121)
* feat: custom voice and speech for microsoft

* remove checkzone

* fix: enable update region speech credential

* use tmp state to store custom tts/stt endpoint when enable/disable it

* feat: custom voice and speech for microsoft

* remove checkzone

* fix: enable update region speech credential

* use tmp state to store custom tts/stt endpoint when enable/disable it
2022-10-04 05:32:12 -07:00
Brandon Lee Kitajchuk
4a8eafe0e6 Add modal and toast portals to cypress index.html 2022-10-03 20:00:02 -07:00
kitajchuk
882a1bcf52 Change className for require-auth test div 2022-10-03 08:30:30 -07:00
kitajchuk
875cfb6b25 Provide component testing utils 2022-10-03 08:21:34 -07:00
kitajchuk
052ea4d6cb Reorganize some styles 2022-10-01 16:21:31 -07:00
kitajchuk
831a593e99 Better UI styling for mobile-level gateways trash button 2022-09-29 08:00:25 -07:00
kitajchuk
67193e168e Better UI styling for global error message 2022-09-29 07:54:42 -07:00
Brandon Lee Kitajchuk
76857c0a4b Implement initial Cypress testing configuration (#115)
* Implement initial Cypress testing configuration

* Add docs on cypress for testing

* Cypress cache for GitHub actions
2022-09-26 10:14:41 -07:00
kitajchuk
a6aee3802c Fix unnecessary extra conditions for predefinedCarriers effect 2022-09-26 07:30:32 -07:00
kitajchuk
a9b120c9fb Make useRedirect custom hook generic 2022-09-26 07:27:55 -07:00
kitajchuk
05985f2370 Fix PredefinedCarriers fetch -- find all for the interface accidentally changed this 2022-09-26 07:24:38 -07:00
kitajchuk
15d70362f9 Concise slim logic, better list view render conditions 2022-09-26 07:21:24 -07:00
Brandon Lee Kitajchuk
dc873c6e00 Fix MS Teams view bug for #113 (#114) 2022-09-26 07:10:17 -07:00
kitajchuk
6bf8d6959b Update some API type interface names 2022-09-24 15:43:11 -07:00
Brandon Lee Kitajchuk
c4ebe6d429 Better local limits ref 2022-09-24 10:29:19 -07:00
kitajchuk
6ac0ce824a Bump package version to v1.0.0 2022-09-24 08:09:34 -07:00
Brandon Lee Kitajchuk
f381eba694 jambonz webapp refresh (#64)
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

some house cleaning

file upload component

update pr-checks wildcard

wrappers for fetch transport -- any method (#78)

Refresh tweaks (#80)

* add alerts to mock api dev server

add webhook methods types

fix focus for file upload

update contrib readme

blob fetching

rest props spread for file-upload

* multi element fieldset structure, unique basic auth field names for accounts form

* Fix and simplify webhook state setting

* some ad-hoc cleanup for temp work

Adding generic account filter component (#82)

adding focused styles for account-filter

more robust account-filter props

updates to contrib readme

fix add service provider form a la new styles

required form field UI and labeling

Application page for refresh  (#79)

* Adding barely working Application page (#70)

* resolve conflict and update, still barely working

* perfectly working application page

* Fix the duplicated name logic

* strip some comments

* changes to sync

* delete more condition

* some more changes for parity

* revert changes

* applying b1a9a77

* changes requested

* changes suggested

* changes suggested

* sync changes

organize some styles a bit more

refactor generic small selector styles

use portals for modals and toasts

add new classNames to applications form

handle applications view without accounts condition

sweep through with some cleanup

type-safety for :POST and :PUT api methods

Speech service page for refresh (#84)

* initial commit

* more update, probably one more

* properly rebase

* check box works okay

* properly rebase**2

* initial cleanup and ux-flow evaluation

* obscure secrets on frontend for local state

* refine ui for credential status checks

* ignore error set on unmount for CredentialStatus

* fix obscure field type crash bug

* Update utils.ts

* Update utils.ts

* Update constants.ts

* wrap up the speech credentials flow

Co-authored-by: kitajchuk <bk@kitajchuk.com>

tweaks to ui elements etc

tweak some typings and minor ui styles

better not tested messaging for TTS/STT

better placeholder feature flag for dev

Adding some conditional utilities (#87)

Microsoft Tenant page for refresh (#86)

* initial commit

* fix backend error with adding

* changes suggested

* changes requested

* use all accounts for ms teams tenants

* ui tweaks, add last ditch redirect back to form

Co-authored-by: kitajchuk <bk@kitajchuk.com>

Phone number page for refresh (#85)

* initial commit so I can hop back

* working properly

* carrier related change and mass edit

* mvoing around

* UI for mass edit

* unset selected for mass edit

* some minor ui cleanup

* fix empty/bad classNames on edit action icon

Co-authored-by: kitajchuk <bk@kitajchuk.com>

Update .dockerignore

cleanup and port some helpers

fix applications index useEffect

group synthesis fields and recognizer fields for application form

tweak contrib readme

just run tsc in pre-commit

update contrib readme

Carrier page for refresh  (#89)

* initial commit

* working form but no put/post for gateways yet

* put/post for sip/smpp

* crud app done

* all the functionalities are here, unless it isnt

* changes suggested and delete sip/smpp when delete carrier

* Some initial UI cleanup etc...

* More UI cleanup and what not...

* No need for the 'Status' text here

* Remove the Grid component -- not reused

* Remove as much explicit null type as possible

* Use webhook methods constant in account form

* Some API constants and fix inbound/outbound smpp gateways delete with filter logic

* Tab handling logic for carrier form (#91)

* similar validation logic of sip for smpp

* Tech prefix tab validation

* revert to working sip gateway validation

* More validation cleanup

* Update index.ts

* Update index.ts

* More cleanup and form clarity for if/when required fields

* Fix some logic and reset gateways to delete when deleted

* use api data hooks for index partials

* smpp gateway validation and fqdn validations

* default application selector

* Fix up the SMPP dilemma...

* Typo and remove console log

* Tab switch for all validations

* Move empty SIP check to validation getter

* Render gateway validation messages near the invalid fields

* Explicit return on first active tab condition for browser constraints

* Use IP pattern for outbound smpp gateway since fqdn is disallowed here

* Add fqdn example to ip placeholders

* sticky tabs

* Tweak info text

* Gateway refetch code change

* delete gateways on demand

* move shared api fetching down into forms -- seems better actually

* Fix re-render glitch for gateways UI

Co-authored-by: kitajchuk <bk@kitajchuk.com>

Tweaks and minor cleanups

Few more minor tweaks

prettier package.json -- duh

Functional recent-calls dev server api

switch from moment to dayjs

Functional alerts dev server api

Dev server notes in readme

Fix applications bug which fixes current SP switch

Create hooks for vendor async data

No lazy load for routes (#97)

better speech hook

match dev mock paged response to api server paged response

Generic AccountSelect component

Generic useRedirect hook

Recent call page for refresh (#93)

* initial commit

* changes requested but yet to pcap

* pcap?

* Initial cleanup on RecentCalls

* Normalize set page number and fix status for mock dev server

* Listt item styles and details/pcap fix

* Refactor recent calls subcomponents

* Tighter section padding and smaller page titles

* Update _lists.scss

* Recent calls cleanup and some other tweaks

Co-authored-by: kitajchuk <bk@kitajchuk.com>

Implement proper Checkzone initial checked for carrier form

Add handleSelect prop to SelectFilter component and fix perPageFilter changes for recent calls

Alert view page for refresh  (#99)

* initial commit

* changes suggested

* changes requested

* Style alerts UI

Co-authored-by: kitajchuk <bk@kitajchuk.com>

Refresh enhancement pagination logic (#101)

* initial commit

* Sort of secret props...

Co-authored-by: kitajchuk <bk@kitajchuk.com>

Quick small screen mobile sweep -- add logout button to mobile navi

Update index.tsx

Tweak some styles and restyle navi SP selector

Add key prop to ApiKeys on Settings so SP switch refetches data

fix checkbox margin now that grid-gap is used

Style tweaks

Update types.ts

Update delete.tsx

Update delete.tsx

Move some variable declarations around

Generic application select for forms

Fix issue #105 for carrier form applications

Use memo for filtered carriers on list view

Refactor generic application filter component

Update application-filter.tsx

Update types.ts

Update index.tsx

Update index.tsx

Update index.tsx

Update index.tsx

Cleanup some stuff -- add locked prop for Passwd

Fix unauthorized logout scenarios -- no react state errors :)

Normalize React types usage

Fuzzy search filter for collection lists (#106)

* Fuzzy search filter for collection lists

* Tweak some things for responsiveness

* Carrier preset label and fix All accounts filter for carriers list

Set text overflow on search filter

PR checklist items

Match 'No ...' text for speech services

Return rawCollection if hasLength check is false

Responsive styling for list item--action rows

Fix defaultOption for AccountSelector

Refresh: Add API limits for issue #109 (#111)

* Add API limits for issue #109

* Tighten up initial field renders

Cleanup for issue #104 (#108)

Co-authored-by: kitajchuk <bk@kitajchuk.com>

Tweak local limits

Generic local limits component

No default local limits -- move to forms components

Safe set values for limits -- maintain controlled inputs

Ref support for local limits form component

Handle empty data for local limits effect

Implement DELETE for limits

Singular nomenclature for post limit(s)
2022-09-22 08:38:21 -07:00
kitajchuk
1276687cc0 Add package-lock changes for moment lib update 2022-09-22 08:26:41 -07:00
Paulo Telles
b97d5e538f update moment lib (#95)
Co-authored-by: p.souza <p.souza@cognigy.com>
2022-09-07 15:06:02 +02:00
Paulo Telles
2704e97a96 update node image (#94)
Co-authored-by: p.souza <p.souza@cognigy.com>
2022-09-07 13:47:36 +02:00
Dave Horton
4cfdfc3b49 sync package-lock.json 2022-08-28 22:18:17 +02:00
Dave Horton
47d73a7edd bump version to 0.7.6 2022-08-26 20:07:36 +02:00
Lê Hàn Minh Khang
c14fa5db34 remove dummy text from siprec tooltip (#69) 2022-08-06 08:46:28 +01:00
Lê Hàn Minh Khang
668d7f05f6 for #66 (#67) 2022-08-03 17:25:35 +01:00
Dave Horton
5ae88ff13e Dockerfile: update base image 2022-07-28 12:59:18 +01:00
Joan
06c21f2545 Add z-index property to Add button (#63)
* extended AWS region list

* added z-index to add button

Co-authored-by: Joan Salvatella <joan@bookline.io>
2022-07-11 10:33:11 +02:00
Joan
663aabc80c extended AWS region list (#62)
Co-authored-by: Joan Salvatella <joan@bookline.io>
2022-07-01 11:54:32 -04:00
Dave Horton
a7de0a494e update deps 2022-06-11 12:30:33 -04:00
Lê Hàn Minh Khang
b742e67715 Fix speech form dropdown menu reference (#61) 2022-05-26 08:51:49 -04:00
Lê Hàn Minh Khang
013681e7eb Update Azure speech list (#60)
* update azure region list

* format

* more meaningful name

* sorting and prettifying

* making sense of africa server
2022-05-25 11:02:07 -04:00
Lê Hàn Minh Khang
4912758120 Fix issue #57 (#58)
* fix issue #57

* fix further

* fix to phone page
2022-05-24 08:38:13 -04:00
Dave Horton
bb335d0838 Sp apikey UI fix (#56)
* service provider add/view/delete features added

* reverting .env back to port 3002

* fix ui

Co-authored-by: Conner Luzier <connerluzier@protonmail.com>
2022-05-16 11:20:34 -04:00
Dave Horton
38d26dddc8 new Microsoft languages added in 1.21.0 2022-05-12 19:34:49 -04:00
Conner Luzier
c0d531c63f removed radio buttons, replaced with dropdown (#53)
* removed radio buttons, replaced with dropdown

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

* added aws_region to post request

* no default setting on aws region

* aws_region required before saving

* no longer showing default on dropdown

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

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

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

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

* moved enviroment variable to gloabl

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

* added new line

* updated constants for window.jambonz

* removed NODE_ENV in favor of just window.JAMBONZ

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

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

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

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

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

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

* enable the env flag and move content to right place

* changes to support subspace (thanks to nimbleape)

* fix column names

* Implement SIP realm selection for Subspace API calls

* Hook up Subspace disable method

* Finish up Subspace API handling

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

Add Microsoft vendor to Application Form

Clean up UI for Speech and Application forms

* Remove Sbcs from SpeechServiceAddEdit Form
2021-11-17 20:49:31 -05:00
Dave Horton
ff4d6b6e11 version bump 2021-11-03 13:53:11 -04:00
Dave Horton
70387ff4f1 bump version 2021-10-21 13:09:07 -04:00
Dave Horton
7a4c583345 bump version 2021-10-21 13:01:30 -04:00
Brandon Lee Kitajchuk
d54fbc4782 Update IP whitelist tooltips for carrier form (#29) 2021-10-21 11:41:19 -04:00
Brandon Lee Kitajchuk
14dd1319d9 SMS for Carrier Form (#28) 2021-10-17 12:29:26 -04:00
332 changed files with 40389 additions and 26453 deletions

17
.dockerignore Normal file
View File

@@ -0,0 +1,17 @@
# Node / env / dist
node_modules
dist
dist-ssr
# Jambonz
.dockerignore
.git*
.husky
.vscode
docs
/public/fonts
server
CODEOWNERS
Dockerfile
LICENSE
README.md

29
.env
View File

@@ -1 +1,28 @@
REACT_APP_API_BASE_URL=http://[ip]:[port]/v1
VITE_API_BASE_URL=http://127.0.0.1:3000/v1
VITE_DEV_BASE_URL=http://127.0.0.1:3000/v1
## enables choosing units and lisenced account call limits
# VITE_APP_ENABLE_ACCOUNT_LIMITS_ALL=true
# disables controls for default application routing to carrier for SP and account level users
#VITE_APP_DISABLE_DEFAULT_TRUNK_ROUTING=true
## disables Least cost routing feature
#VITE_APP_LCR_DISABLED=true
## disables Jaeger Tracing feature
#VITE_APP_JAEGER_TRACING_DISABLED=true
## enable record All Calls feature
#VITE_APP_DISABLE_CALL_RECORDING=true
## enable Forgot password
#VITE_APP_ENABLE_FORGOT_PASSWORD=true
## enable hosted system
#VITE_APP_ENABLE_HOSTED_SYSTEM=true
## Google Client ID
#VITE_APP_GOOGLE_CLIENT_ID=
## Github Client ID
#VITE_APP_GITHUB_CLIENT_ID=
## Default jambonz service provider SID
#VITE_APP_DEFAULT_SERVICE_PROVIDER_SID=
## Base url for jambomz webapp
#VITE_APP_BASE_URL="http://jambonz.one"
## Strip publishable key
#VITE_APP_STRIPE_PUBLISHABLE_KEY="pk_test_EChRaX9Tjk8csZZVSeoGqNvu00lsJzjaU1"

View File

@@ -1,16 +0,0 @@
{
"extends": "react-app",
"rules": {
"linebreak-style": [
"error",
"unix"
],
"semi": [
"error",
"always"
],
"no-trailing-spaces": [
"error"
]
}
}

51
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: Docker
on:
push:
tags:
- "*"
jobs:
push:
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: prepare tag
id: prepare_tag
run: |
IMAGE_ID=jambonz/webapp
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
build-args: |
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
GITHUB_REF=$GITHUB_REF

44
.github/workflows/pr-checks.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: PR Checks
on:
push:
branches:
- main
pull_request:
branches:
- "**"
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: Cache cypress binary
id: cypress-cache
uses: actions/cache@v2
with:
path: /home/runner/.cache/Cypress
key: cypress-${{ hashFiles('package-lock.json') }}
- name: Install node_modules
if: steps.node-cache.outputs.cache-hit != 'true'
run: npm install
- name: Install cypress
if: steps.cypress-cache.outputs.cache-hit != 'true'
run: npx cypress install
- name: Run checks
run: |
npm run format
npm run lint
npm run test
npm run build

45
.gitignore vendored
View File

@@ -1,21 +1,30 @@
# 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 / env / dist
node_modules
dist
dist-ssr
*.local
yarn.lock
# Editor directories and files
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Jambonz
/public/fonts
# Cypress
cypress/videos

14
.husky/pre-commit Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# lint, prettier etc...
npx lint-staged
# run tests
# npm run test
# run build -- tsc
# npm run build
# run tsc
npx tsc

27
.prettierignore Normal file
View File

@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Node / env / dist
node_modules
dist
dist-ssr
*.local
yarn.lock
# Editor directories and files
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Jambonz
/public/fonts

1
.prettierrc.json Normal file
View File

@@ -0,0 +1 @@
{}

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-vscode.vscode-typescript-next"
]
}

9
.vscode/settings.json vendored Normal file
View 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
View File

@@ -0,0 +1,4 @@
# see https://bit.ly/3t2LLMR for CODEOWNERS syntax
# all files owned by:
* @davehorton

View File

@@ -1,15 +1,17 @@
FROM node:alpine as builder
FROM node:18.15-alpine3.16 as builder
RUN apk update && apk add --no-cache python3 make g++
COPY . /opt/app
WORKDIR /opt/app/
COPY package.json ./
RUN npm install
RUN npm run build
RUN npm prune
FROM node:alpine as webapp
FROM node:18.14.1-alpine as webapp
RUN apk add curl
WORKDIR /opt/app
COPY . /opt/app
COPY --from=builder /opt/app/node_modules ./node_modules
COPY --from=builder /opt/app/dist ./dist
COPY ./entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
ENTRYPOINT ["/entrypoint.sh"]

View File

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

View File

@@ -1,33 +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
Like production, you must specify the IP:port of the Jambonz API you will be hitting.
You can pull the public docker image for the web app:
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
```sh
docker pull ghcr.io/jambonz/webapp:latest
```
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
```

11
cypress.config.ts Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from "cypress";
export default defineConfig({
video: false,
component: {
devServer: {
framework: "react",
bundler: "vite",
},
},
});

View File

@@ -0,0 +1,10 @@
[
{
"name": "default account",
"account_sid": "9351f46a-678c-43f5-b8a6-d4eb58d131af"
},
{
"name": "custom account",
"account_sid": "04e36b64-a4e5-4221-be7e-db80da39234d"
}
]

View File

@@ -0,0 +1,10 @@
[
{
"name": "dial time",
"application_sid": "4ca2fb6a-8636-4f2e-96ff-8966c5e26f8e"
},
{
"name": "hello world",
"application_sid": "7087fe50-8acb-4f3b-b820-97b573723aab"
}
]

View File

@@ -0,0 +1,7 @@
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3NpZCI6IjFjNTc4MWQyLTY5MGItNDIwYy1iZDUzLTVkN2Y1NjMwMDVjOCIsInNjb3BlIjoiYWRtaW4iLCJmb3JjZV9jaGFuZ2UiOnRydWUsInBlcm1pc3Npb25zIjpbIlBST1ZJU0lPTl9VU0VSUyIsIlBST1ZJU0lPTl9TRVJWSUNFUyIsIlZJRVdfT05MWSJdLCJpYXQiOjE2NjY3OTgzMTEsImV4cCI6MTY2NjgwMTkxMX0.ZV3KnRit8WGpipfiiMAZ2AVLQ25csWje1-K6hdqxktE",
"user_sid": "78131ad5-f041-4d5d-821c-47b2d8c6d015",
"force_change": false,
"scope": "admin",
"permissions": ["VIEW_ONLY", "PROVISION_SERVICES", "PROVISION_USERS"]
}

View File

@@ -0,0 +1,39 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
export {};

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
<div id="modal"></div>
<div id="toast"></div>
</body>
</html>

View File

@@ -0,0 +1,65 @@
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import jambonz global styles
import "src/styles/index.scss";
// Import commands.js using ES2015 syntax:
import "./commands";
import React from "react";
import { mount } from "cypress/react18";
import { TestProvider } from "src/test";
import type { TestProviderProps } from "src/test";
import type { MountOptions, MountReturn } from "cypress/react";
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
declare global {
// Disabling, but: https://typescript-eslint.io/rules/no-namespace/...
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
mount: typeof mount;
/**
* Mounts a React node
* @param component React Node to mount
* @param options Additional options to pass into mount
*/
mountTestProvider(
component: React.ReactNode,
options?: MountOptions & { authProps?: TestProviderProps["authProps"] }
): Cypress.Chainable<MountReturn>;
}
}
}
Cypress.Commands.add("mount", mount);
// This gives us access to dispatch inside of our test wrappers...
Cypress.Commands.add("mountTestProvider", (component, options = {}) => {
const { authProps, ...mountOptions } = options;
const wrapper = (
<TestProvider authProps={authProps}>{component}</TestProvider>
);
return mount(wrapper, mountOptions);
});
// Example use:
// cy.mount(<MyComponent />)

197
docs/contributors.md Normal file
View File

@@ -0,0 +1,197 @@
<p align="center">
<a href="https://jambonz.org">
<img src="../public/icon192.png" height="128">
<h1 align="center">jambonz</h1>
</a>
</p>
<p align="center">
<a aria-label="GitHub CI" href="https://github.com/jambonz/jambonz-webapp/actions/workflows/main.yml">
<img alt="" src="https://github.com/jambonz/jambonz-webapp/actions/workflows/main.yml/badge.svg">
</a>
</p>
> Contributing to the web app source code
## :rocket: Getting started
In order to run the web app you'll need your local environment setup which you can do
following instructions in our [environment readme](./environment.md).
Once your environment is setup you can fork or clone this repo. To start the web app
just run `npm install` and then `npm start`.
## :pancakes: Dev stack
We're using [vite](https://vitejs.dev/) for development and
the main application is [react](https://reactjs.org/docs/getting-started.html)
with [typescript](https://www.typescriptlang.org/),
[prettier](https://prettier.io/), [eslint](https://eslint.org/),
[husky](https://typicode.github.io/husky/#/)
and [lint-staged](https://www.npmjs.com/package/lint-staged).
For testing we're using [cypress](https://docs.cypress.io/guides/component-testing/writing-your-first-component-test).
## :lock: Auth middleware
We have auth middleware that was initially based on this [useAuth](https://usehooks.com/useAuth/)
example but has been typed and modified to include a `RequireAuth` component for wrapping internal Routes.
The main hook you'll use here is `useAuth`. This hook provides the following:
- `token`
- `signin(user, pass)`
- `signout()`
- `authorized`
### A note on our ACL implementation
We have some simple ACL utilities for managing access to UI/routes based on conditions.
There is a basic `AccessControl` component and a handy `withAccessControl`
HOC for route containers with redirect. There is also a `useAccessControl` hook for
use at the component-level.
## :joystick: Application state
The state for the application has two parts: the local state and the remote server state.
The server state is the source of truth. We keep only the minimal amount of local state
necessary: the current logged in user, the list of service providers and the actively selected
current service provider. We also use local state for a basic permissions matrix and for the
toast notifications.
- `useSelectState(key)`: returns just the piece of state desired
- `useDispatch()`: returns global dispatch method
- `useAccessControl(acl)`: returns true/false for select ACL permissions
- `useFeatureFlag(flag)`: returns true/false for available feature flags
- `withSelectState([...keys])(Component)`: like redux connect it maps state to props
- `toastError(msg)`: helper for dispatching error toasts
- `toastSuccess(msg)`: helper for dispatching success toasts
## :wales: API implementation
We have a centralized API implementation that uses our normalized `fetchTransport` method
under the hood. We have `use` hooks for general `GET` fetching that return the `data` fetched,
a `refetcher` function that, when called, will update the data in the hook and therefore your
component will render the new data, and a possible `error` if the fetch failed. The general
consensus on when to use the hooks vs using a `getFetch` directly are dictated by whether the
API response data needs to be refetched locally based on some user action, such as deleting
an item from a list. In that case use the hooks, otherwise a `getFetch` pattern should work.
The hooks are:
- `useApiData(path)`: returns `[data, refetcher, error]`
- `useServiceProviderData(path)`: returns `[data, refetcher, error]`
All API requests are piped through the `fetchTransport` method which receives a generic type
and returns it as the type of response data resolved. Any `POST`, `PUT` or `DELETE` calls should
have a wrapper method that calls our more generic methods under the hood, which are:
- `getFetch(url)`
- `postFetch(url, payload)`
- `putFetch(url, payload)`
- `deleteFetch(url)`
- `getBlob(url)`
Example of wrapper API methods to `:POST` and `:PUT` for the `Account` type:
```ts
export const postAccount = (payload: Partial<Account>) => {
return postFetch<SidResponse, Partial<Account>>(API_ACCOUNTS, payload);
};
export const putAccount = (sid: string, payload: Partial<Account>) => {
return putFetch<EmptyResponse, Partial<Account>>(
`${API_ACCOUNTS}/${sid}`,
payload
);
};
```
### Local dev mock API server
There are two views that rely on call detail records (CDRs) that don't exist in the local
developer db when running the docker stack. For these views we have a local node express
server that replicates functional parity of the backend APIs in question so you can work
on the UI. The views are `Recent Calls` and `Alerts`. They are simple table views with
filter and pagination functionalities. You can view the implementation at `server/dev.server.ts`.
The approach is to replicate the pattern of how test data is seeded for the API server and
add the filtering on top of it with simple JavaScript functions. To run the dev server:
```shell
npm run dev:server
```
## :file_folder: Vendor data modules
Large data modules are used for menu options on the Applications and Speech Services
forms. These modules are loaded lazily and set to local state in the context in which
they are used. You can find the data modules and their type definitions in the `src/vendor`
directory.
## :sunrise: Component composition
All components that are used as Route elements are considered `containers`.
Containers are organized by `login` and `internal`, the latter of which requires
the user to be authorized via our auth middleware layer. Reusable components are
small with specific pieces of functionality and their own local state. We have
plenty of examples including `toast`, `modal` and so forth. You should review some
of each category of component to get an idea of how the patterns are put into practice.
## :art: UI and styling
We have a UI design system called [@jambonz/ui-kit](https://github.com/jambonz/jambonz-ui).
It's public on `npm` and is being used for this project. It's still small and simple
but provides the foundational package content for building jambonz UIs. You can view
the storybook for it [here](https://jambonz-ui.vercel.app/) as well as view the docs
for it [here](https://www.jambonz.org/docs/@jambonz/ui-kit/).
### A note on styles
While we use [sass](https://sass-lang.com/) with `scss` syntax it should be stated that the
primary objective is to simply write generally pure `css`. We take advantage of a few nice
features of `sass` like nesting for [BEM](http://getbem.com/naming/) module style etc. We
also take advantage of loading the source `sass` from the UI library. Here's an example of
the `BEM` style we use:
```scss
.example {
// This is the block
&--modifier {
// This is a modifier of the block
}
&__item {
// This is an element
&--modifer {
// This a modifer of the element
}
}
}
```
## :robot: Testing
We're using [cypress](https://docs.cypress.io/guides/component-testing/writing-your-first-component-test)
for component testing. Cypress is already configured and we're actively working on backfilling complete
test coverage of the application. There are some issues open for this so you may refer to those to check
out the progress.
All new components should have tests written alongside them. For example, if you component is called
`my-component.tsx` then you would also have a `my-component.cy.tsx` test file to go with it. You can
refer to existing tests for some common patterns we use to write tests.
## :heart: Contributing
If you would like to contribute to this project please follow these simple guidelines:
- Be excellent to each other!
- Follow the best practices and coding standards outlined here.
- Clone or fork this repo, write code and open a PR :+1:
- All code must pass the `pr-checks` and be reviewed by a code owner.
That's it!
## :beetle: Bugs?
If you find a bug please file an issue on this repository with as much information as
possible regarding replication etc :pray:.

95
docs/environment.md Normal file
View File

@@ -0,0 +1,95 @@
<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.
```bash
cd jambonz-api-server
npm run integration-test
```
This will take a few minutes to start, but eventually a successfull startup will eventually
look something like this:
```bash
$ npm run integration-test
> jambonz-api-server@v0.7.5 integration-test
> NODE_ENV=test JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js
starting dockerized mysql and redis..
mysql is running
creating database..
creating schema..
seeding database..
creating admin user..
reset_admin_password, initial admin password is admin
sipp exited with non-zero code 1 signal null
1
ready for testing!
{"level":30, "time": "2022-04-14T18:07:49.318Z","pid":5292,"hostname":"MacBook-Pro-2.local","msg":"listening for HTTP traffic on port 3000","v":1}
{"level":20, "time": "2022-04-14T18:07:49.325Z","pid":5292,"hostname":"MacBook-Pro-2.local","args":[],"msg":"redis event connect","v":1}
{"level":20, "time": "2022-04-14T18:07:49.345Z","pid":5292,"hostname":"MacBook-Pro-2.local","args":[],"msg":"redis event ready","v":1}
```
This starts the a docker-compose network running the following containers:
- mysql
- redis
- influxdb
- heplify-server
- drachtio
- homer-webapp
Leaving the jambonz-api-server process running, open another terminal window, cd into the
folder where you have checked out this project, and start it as shown below:
```
cd jambonz-webapp
npm i
npm start
```
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

View File

@@ -1,8 +1,25 @@
#!/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}"
echo "REACT_APP_API_BASE_URL=${REACT_APP_API_BASE_URL:-http://$PUBLIC_IPV4:$API_PORT/$API_VERSION}" > /opt/app/.env
cd /opt/app/
npm run build
npm run serve
API_BASE_URL=${API_BASE_URL:-http://$PUBLIC_IPV4:$API_PORT/$API_VERSION}
# Default to "false" if not set
DISABLE_LCR=${DISABLE_LCR:-false}
DISABLE_JAEGER_TRACING=${DISABLE_JAEGER_TRACING:-false}
DISABLE_CUSTOM_SPEECH=${DISABLE_CUSTOM_SPEECH:-false}
ENABLE_FORGOT_PASSWORD=${ENABLE_FORGOT_PASSWORD:-false}
DISABLE_CALL_RECORDING=${DISABLE_CALL_RECORDING:-false}
# Serialize window global to provide the API URL to static frontend dist
# This is declared and utilized in the web app: src/api/constants.ts
SCRIPT_TAG="<script>window.JAMBONZ = {API_BASE_URL: \"${API_BASE_URL}\",DISABLE_LCR: \"${DISABLE_LCR}\",DISABLE_JAEGER_TRACING: \"${DISABLE_JAEGER_TRACING}\",DISABLE_CUSTOM_SPEECH: \"${DISABLE_CUSTOM_SPEECH}\",ENABLE_FORGOT_PASSWORD: \"${ENABLE_FORGOT_PASSWORD}\",DISABLE_CALL_RECORDING: \"${DISABLE_CALL_RECORDING}\"};</script>"
sed -i -e "\@</head>@i\ $SCRIPT_TAG" ./dist/index.html
# Start the frontend web app static server
npm run serve

57
index.html Normal file
View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Build innovative voice and collaboration services with jambonz, the open-source communication platform for conversational AI providers and CSPs."
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link
rel="preload"
href="/fonts/objectivity-medium-webfont.woff2"
crossorigin="anonymous"
as="font"
type="font/woff"
/>
<link
rel="preload"
href="/fonts/objectivity-bold-webfont.woff2"
crossorigin="anonymous"
as="font"
type="font/woff"
/>
<link
rel="preload"
href="/fonts/objectivity-regular-webfont.woff2"
crossorigin="anonymous"
as="font"
type="font/woff"
/>
<link
rel="preload"
href="/fonts/objectivity-boldslanted-webfont.woff2"
crossorigin="anonymous"
as="font"
type="font/woff"
/>
<link
rel="preload"
href="/fonts/objectivity-regularslanted-webfont.woff2"
crossorigin="anonymous"
as="font"
type="font/woff"
/>
<title>Jambonz Portal | Jambonz CPaaS</title>
</head>
<body>
<div id="root"></div>
<div id="modal"></div>
<div id="toast"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

23860
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +1,128 @@
{
"name": "jambonz-cpaas-ui",
"version": "1.0.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"antd": "^4.15.4",
"axios": "^0.21.1",
"moment": "^2.29.1",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.1",
"serve": "^11.3.0",
"styled-components": "^5.0.1"
"name": "jambonz-webapp",
"description": "A simple provisioning web app for jambonz",
"version": "0.8.5",
"license": "MIT",
"type": "module",
"engines": {
"node": ">=14.18"
},
"contributors": [
{
"name": "Brandon Lee Kitajchuk",
"email": "bk@kitajchuk.com",
"url": "https://www.kitajchuk.com"
},
{
"name": "Lê Hàn Minh Khang",
"email": "mkhangle20@gmail.com"
},
{
"name": "Dave Horton",
"email": "daveh@drachtio.org",
"url": "https://drachtio.org"
}
],
"scripts": {
"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-kit/public/fonts public/fonts",
"start": "npm run dev",
"dev": "vite --port 3001",
"dev:server": "ts-node --esm server/dev.server.ts",
"prebuild": "rm -rf ./dist",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --max-warnings=0",
"format": "prettier --check .",
"test": "cypress run --component --headless",
"test:open": "cypress open --component",
"serve": "serve -s dist -l ${HTTP_PORT:-3001}",
"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-kit": "^0.0.21",
"@stripe/react-stripe-js": "^2.1.1",
"@stripe/stripe-js": "^1.54.1",
"dayjs": "^1.11.5",
"immutability-helper": "^3.1.1",
"react": "^18.0.0",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "^18.0.0",
"react-feather": "^2.0.10",
"react-router-dom": "^6.3.0",
"wavesurfer.js": "^7.3.4"
},
"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-dom": "^18.0.0",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"@vitejs/plugin-react": "^1.3.0",
"cors": "^2.8.5",
"cypress": "^10.8.0",
"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",
"nanoid": "^4.0.0",
"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"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

14
public/favicon.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/icon1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/icon192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
public/icon384.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/icon512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

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

View File

@@ -1,2 +1,2 @@
User-agent: *
Disallow:
Disallow:

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

267
server/dev.server.ts Normal file
View File

@@ -0,0 +1,267 @@
import fs from "fs";
import path from "path";
import cors from "cors";
import express from "express";
import { nanoid } from "nanoid";
import type { Request, Response } from "express";
import type {
Alert,
RecentCall,
PageQuery,
CallQuery,
PagedResponse,
} from "../src/api/types";
const app = express();
const port = 3002;
app.use(cors());
/** RecentCalls mock API responses for local dev */
app.get(
"/api/Accounts/:account_sid/RecentCalls",
(req: Request, res: Response) => {
const data: RecentCall[] = [];
const points = 500;
const start = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const now = new Date();
const increment = (now.getTime() - start.getTime()) / points;
for (let i = 0; i < 500; i++) {
const attempted_at = new Date(start.getTime() + i * increment);
const failed = 0 === i % 5;
const call_sid = nanoid();
const call: RecentCall = {
account_sid: req.params.account_sid,
call_sid,
from: "15083084809",
to: "18882349999",
answered: !failed,
sip_callid: `${nanoid()}@192.168.1.100`,
sip_status: 200,
duration: failed ? 0 : 45,
attempted_at: attempted_at.getTime(),
answered_at: attempted_at.getTime() + 3000,
terminated_at: attempted_at.getTime() + 45000,
termination_reason: "caller hungup",
host: "192.168.1.100",
remote_host: "3.55.24.34",
direction: 0 === i % 2 ? "inbound" : "outbound",
trunk: 0 === i % 2 ? "twilio" : "user",
trace_id: nanoid(),
recording_url: `http://127.0.0.1:3002/api/Accounts/${req.params.account_sid}/RecentCalls/${call_sid}/record`,
};
data.push(call);
}
const query: CallQuery = {
...req.query,
page: Number(req.query.page),
count: Number(req.query.count),
};
let filtered = data;
if (query.start) {
filtered = filtered.filter((call) => {
return call.attempted_at >= new Date(query.start!).getTime();
});
}
console.log("RecentCalls: filtered", filtered.length);
if (query.days) {
filtered = filtered.filter((call) => {
return (
call.attempted_at >=
new Date(Date.now() - query.days! * 24 * 60 * 60 * 1000).getTime()
);
});
}
console.log("RecentCalls: filtered", filtered.length);
if (query.direction) {
filtered = filtered.filter((call) => {
return call.direction === query.direction;
});
}
console.log("RecentCalls: filtered", filtered.length);
if (query.answered) {
filtered = filtered.filter((call) => {
return call.answered.toString() === query.answered;
});
}
console.log("RecentCalls: filtered", filtered.length);
const begin = (query.page - 1) * query.count;
const end = begin + query.count;
const paged = filtered.slice(begin, end);
console.log("RecentCalls: paged", paged.length);
console.log("---");
res.status(200).json(<PagedResponse<RecentCall>>{
page_size: query.count,
total: filtered.length,
page: query.page,
data: paged,
});
}
);
app.get(
"/api/Accounts/:account_sid/RecentCalls/:call_sid",
(req: Request, res: Response) => {
res.status(200).json({ total: Math.random() > 0.5 ? 1 : 0 });
}
);
app.get(
"/api/Accounts/:account_sid/RecentCalls/:call_sid/pcap",
(req: Request, res: Response) => {
/** Sample pcap file from: https://wiki.wireshark.org/SampleCaptures#sip-and-rtp */
const pcap: Buffer = fs.readFileSync(
path.resolve(process.cwd(), "server", "sample-sip-rtp-traffic.pcap")
);
res
.status(200)
.set({
"Content-Type": "application/octet-stream",
"Content-Disposition": "attachment",
})
.send(pcap); // server: Buffer => client: Blob
}
);
app.get(
"/api/Accounts/:account_sid/RecentCalls/:call_sid/record",
(req: Request, res: Response) => {
/** Sample pcap file from: https://wiki.wireshark.org/SampleCaptures#sip-and-rtp */
const wav: Buffer = fs.readFileSync(
path.resolve(process.cwd(), "server", "example.mp3")
);
res
.status(200)
.set({
"Content-Type": "audio/wav",
"Content-Disposition": "attachment",
})
.send(wav); // server: Buffer => client: Blob
}
);
app.get(
"/api/Accounts/:account_sid/RecentCalls/trace/:trace_id",
(req: Request, res: Response) => {
const json = fs.readFileSync(
path.resolve(process.cwd(), "server", "sample-jaeger.json"),
{ encoding: "utf8" }
);
res.status(200).json(JSON.parse(json));
}
);
/** Alerts mock API responses for local dev */
app.get("/api/Accounts/:account_sid/Alerts", (req: Request, res: Response) => {
const data: Alert[] = [];
const points = 500;
const start = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const now = new Date();
const increment = (now.getTime() - start.getTime()) / points;
const url = "http://foo.bar";
const vendor = "google";
const count = 500;
for (let i = 0; i < 500; i++) {
const time = new Date(start.getTime() + i * increment);
const scenario = i % 5;
let alert_type = "";
let message = "";
switch (scenario) {
case 0:
alert_type = "webhook-failure";
message = `${url} returned 404`;
break;
case 1:
alert_type = "webhook-connection-failure";
message = `failed to connect to ${url}`;
break;
case 2:
alert_type = "no-tts";
message = `text to speech credentials for ${vendor} have not been provisioned`;
break;
case 3:
alert_type = "no-carrier";
message = "outbound call failure: no carriers have been provisioned";
break;
case 4:
alert_type = "call-limit";
message = `you have exceeded your provisioned call limit of ${count}; please consider upgrading your plan`;
break;
default:
break;
}
const alert: Alert = {
account_sid: req.params.account_sid,
time: time.getTime(),
alert_type,
message,
detail: "",
};
data.push(alert);
}
const query: PageQuery = {
...req.query,
page: Number(req.query.page),
count: Number(req.query.count),
};
let filtered = data;
if (query.start) {
filtered = filtered.filter((call) => {
return call.time >= new Date(query.start!).getTime();
});
}
console.log("Alerts: filtered", filtered.length);
if (query.days) {
filtered = filtered.filter((call) => {
return (
call.time >=
new Date(Date.now() - query.days! * 24 * 60 * 60 * 1000).getTime()
);
});
}
console.log("Alerts: filtered", filtered.length);
const begin = (query.page - 1) * query.count;
const end = begin + query.count;
const paged = filtered.slice(begin, end);
console.log("Alerts: paged", paged.length);
console.log("---");
res.status(200).json(<PagedResponse<Alert>>{
page_size: query.count,
total: filtered.length,
page: query.page,
data: paged,
});
});
app.listen(port, () => {
console.log(`express server listening on port ${port}`);
});

BIN
server/example.mp3 Normal file

Binary file not shown.

4588
server/sample-jaeger.json Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

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

370
src/api/constants.ts Normal file
View File

@@ -0,0 +1,370 @@
import type {
Currency,
ElevenLabsOptions,
LimitField,
LimitUnitOption,
PasswordSettings,
SelectorOptions,
SipGateway,
SmppGateway,
WebHook,
WebhookOption,
} 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;
DISABLE_LCR: string;
DISABLE_JAEGER_TRACING: string;
DISABLE_CUSTOM_SPEECH: string;
ENABLE_FORGOT_PASSWORD: string;
ENABLE_HOSTED_SYSTEM: string;
DISABLE_CALL_RECORDING: string;
GITHUB_CLIENT_ID: string;
GOOGLE_CLIENT_ID: string;
BASE_URL: string;
DEFAULT_SERVICE_PROVIDER_SID: string;
STRIPE_PUBLISHABLE_KEY: string;
}
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;
/** Disable custom speech vendor*/
export const DISABLE_CUSTOM_SPEECH: boolean =
window.JAMBONZ?.DISABLE_CUSTOM_SPEECH === "true" ||
JSON.parse(import.meta.env.VITE_DISABLE_CUSTOM_SPEECH || "false");
/** Enable Forgot Password */
export const ENABLE_FORGOT_PASSWORD: boolean =
window.JAMBONZ?.ENABLE_FORGOT_PASSWORD === "true" ||
JSON.parse(import.meta.env.VITE_APP_ENABLE_FORGOT_PASSWORD || "false");
/** Enable Cloud version */
export const ENABLE_HOSTED_SYSTEM: boolean =
window.JAMBONZ?.ENABLE_HOSTED_SYSTEM === "true" ||
JSON.parse(import.meta.env.VITE_APP_ENABLE_HOSTED_SYSTEM || "false");
/** Disable Lcr */
export const DISABLE_LCR: boolean =
window.JAMBONZ?.DISABLE_LCR === "true" ||
JSON.parse(import.meta.env.VITE_APP_LCR_DISABLED || "false");
/** Disable jaeger tracing */
export const DISABLE_JAEGER_TRACING: boolean =
window.JAMBONZ?.DISABLE_JAEGER_TRACING === "true" ||
JSON.parse(import.meta.env.VITE_APP_JAEGER_TRACING_DISABLED || "false");
/** Enable Record All Call Feature */
export const DISABLE_CALL_RECORDING: boolean =
window.JAMBONZ?.DISABLE_CALL_RECORDING === "true" ||
JSON.parse(import.meta.env.VITE_APP_DISABLE_CALL_RECORDING || "false");
export const DEFAULT_SERVICE_PROVIDER_SID: string =
window.JAMBONZ?.DEFAULT_SERVICE_PROVIDER_SID ||
import.meta.env.VITE_APP_DEFAULT_SERVICE_PROVIDER_SID;
export const GITHUB_CLIENT_ID: string =
window.JAMBONZ?.GITHUB_CLIENT_ID || import.meta.env.VITE_APP_GITHUB_CLIENT_ID;
export const BASE_URL: string =
window.JAMBONZ?.BASE_URL || import.meta.env.VITE_APP_BASE_URL;
export const GOOGLE_CLIENT_ID: string =
window.JAMBONZ?.GOOGLE_CLIENT_ID || import.meta.env.VITE_APP_GOOGLE_CLIENT_ID;
export const STRIPE_PUBLISHABLE_KEY: string =
window.JAMBONZ?.STRIPE_PUBLISHABLE_KEY ||
import.meta.env.VITE_APP_STRIPE_PUBLISHABLE_KEY;
/** TCP Max Port */
export const TCP_MAX_PORT = 65535;
/** Tech Prefix minlength */
export const TECH_PREFIX_MINLENGTH = 3;
/** IP Types for validations */
export const IP = "ip";
export const FQDN = "fqdn";
export const FQDN_TOP_LEVEL = "fqdn-top-level";
export const INVALID = "invalid";
/** Default API object models */
export const DEFAULT_WEBHOOK: WebHook = {
url: "",
method: "POST",
username: "",
password: "",
};
/** Default SIP/SMPP Gateways */
export const DEFAULT_SIP_GATEWAY: SipGateway = {
voip_carrier_sid: "",
ipv4: "",
port: 5060,
netmask: 32,
is_active: true,
inbound: 1,
outbound: 0,
};
export const DEFAULT_SMPP_GATEWAY: SmppGateway = {
voip_carrier_sid: "",
ipv4: "",
port: 2775,
is_primary: false,
use_tls: false,
netmask: 32,
inbound: 1,
outbound: 1,
};
/** Netmask Bits */
export const NETMASK_BITS = Array(32)
.fill(0)
.map((_, index) => index + 1)
.reverse();
export const NETMASK_OPTIONS = NETMASK_BITS.map((bit) => ({
name: bit.toString(),
value: bit.toString(),
}));
/** SIP Gateway Protocol */
export const SIP_GATEWAY_PROTOCOL_OPTIONS = [
{
name: "UDP",
value: "udp",
},
{
name: "TCP",
value: "tcp",
},
{
name: "TLS",
value: "tls",
},
{
name: "TLS/SRTP",
value: "tls/srtp",
},
];
/**
* Record bucket type
*/
export const BUCKET_VENDOR_AWS = "aws_s3";
export const BUCKET_VENDOR_S3_COMPATIBLE = "s3_compatible";
export const BUCKET_VENDOR_GOOGLE = "google";
export const BUCKET_VENDOR_AZURE = "azure";
export const BUCKET_VENDOR_OPTIONS = [
{
name: "NONE",
value: "",
},
{
name: "AWS S3",
value: BUCKET_VENDOR_AWS,
},
{
name: "AWS S3 Compatible",
value: BUCKET_VENDOR_S3_COMPATIBLE,
},
{
name: "Azure Cloud Storage",
value: BUCKET_VENDOR_AZURE,
},
{
name: "Google Cloud Storage",
value: BUCKET_VENDOR_GOOGLE,
},
];
export const AUDIO_FORMAT_OPTIONS = [
{
name: "mp3",
value: "mp3",
},
{
name: "wav",
value: "wav",
},
];
export const DEFAULT_ELEVENLABS_MODEL = "eleven_multilingual_v2";
export const DEFAULT_WHISPER_MODEL = "tts-1";
// Google Custom Voice reported usage options
export const DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE = "REALTIME";
export const GOOGLE_CUSTOM_VOICES_REPORTED_USAGE = [
{ name: "REPORTED_USAGE_UNSPECIFIED", value: "REPORTED_USAGE_UNSPECIFIED" },
{ name: "REALTIME", value: "REALTIME" },
{ name: "OFFLINE", value: "OFFLINE" },
];
// Eleven Labs options
export const DEFAULT_ELEVENLABS_OPTIONS: Partial<ElevenLabsOptions> = {
optimize_streaming_latency: 3,
voice_settings: {
stability: 0.5,
similarity_boost: 0.5,
use_speaker_boost: true,
},
};
/** Password Length options */
export const PASSWORD_MIN = 8;
export const PASSWORD_LENGTHS_OPTIONS = Array(13)
.fill(PASSWORD_MIN)
.map((i, j) => ({
name: (i + j).toString(),
value: (i + j).toString(),
}));
/** List view filters */
export const DATE_SELECTION = [
{ name: "today", value: "today" },
{ name: "last 7d", value: "7" },
{ name: "last 14d", value: "14" },
{ name: "last 30d", value: "30" },
];
export const PER_PAGE_SELECTION = [
{ name: "25 / page", value: "25" },
{ name: "50 / page", value: "50" },
{ name: "100 / page", value: "100" },
];
export const USER_SCOPE_SELECTION: SelectorOptions[] = [
{ name: "All scopes", value: "all" },
{ name: "Admin", value: "admin" },
{ name: "Service provider", value: "service_provider" },
{ name: "Account", value: "account" },
];
/** Available webhook methods */
export const WEBHOOK_METHODS: WebhookOption[] = [
{
name: "POST",
value: "POST",
},
{
name: "GET",
value: "GET",
},
];
/** Various system limits */
export const LIMITS: LimitField[] = [
{
label: "Max calls",
category: "voice_call_session",
},
// {
// label: "Max registered devices (0=unlimited)",
// category: "device",
// },
// {
// label: "Max api calls per minute (0=unlimited)",
// category: "api_rate",
// },
{
label: "Licensed calls",
category: "voice_call_session_license",
},
{
label: "Max minutes",
category: "voice_call_minutes",
},
{
label: "Licensed minutes",
category: "voice_call_minutes_license",
},
];
export const LIMIT_MIN = "minute";
export const LIMIT_SESS = "session";
export const LIMIT_UNITS: LimitUnitOption[] = [
{
name: "Session",
value: LIMIT_SESS,
},
{
name: "Minute",
value: LIMIT_MIN,
},
];
export const DEFAULT_PSWD_SETTINGS: PasswordSettings = {
min_password_length: 6,
require_digit: 0,
require_special_character: 0,
};
export const PlanType = {
PAID: "paid",
TRIAL: "trial",
FREE: "free",
};
export const CurrencySymbol: Currency = {
usd: "$",
};
/** User scope values values */
export const USER_ADMIN = "admin";
export const USER_SP = "service_provider";
export const USER_ACCOUNT = "account";
/** Speech credential test result status values */
export const CRED_OK = "ok";
export const CRED_FAIL = "fail";
export const CRED_NOT_TESTED = "not tested";
/** Voip Carrier Register result status values */
export const CARRIER_REG_OK = "ok";
export const CARRIER_REG_FAIL = "fail";
export const PRIVACY_POLICY = "https://jambonz.org/privacy";
export const TERMS_OF_SERVICE = "https://jambonz.org/terms";
/** API base paths */
export const API_LOGIN = `${API_BASE_URL}/login`;
export const API_LOGOUT = `${API_BASE_URL}/logout`;
export const API_SBCS = `${API_BASE_URL}/Sbcs`;
export const API_USERS = `${API_BASE_URL}/Users`;
export const API_API_KEYS = `${API_BASE_URL}/ApiKeys`;
export const API_ACCOUNTS = `${API_BASE_URL}/Accounts`;
export const API_APPLICATIONS = `${API_BASE_URL}/Applications`;
export const API_PHONE_NUMBERS = `${API_BASE_URL}/PhoneNumbers`;
export const API_MS_TEAMS_TENANTS = `${API_BASE_URL}/MicrosoftTeamsTenants`;
export const API_SERVICE_PROVIDERS = `${API_BASE_URL}/ServiceProviders`;
export const API_CARRIERS = `${API_BASE_URL}/VoipCarriers`;
export const API_SMPP_GATEWAY = `${API_BASE_URL}/SmppGateways`;
export const API_SIP_GATEWAY = `${API_BASE_URL}/SipGateways`;
export const API_PASSWORD_SETTINGS = `${API_BASE_URL}/PasswordSettings`;
export const API_FORGOT_PASSWORD = `${API_BASE_URL}/forgot-password`;
export const API_SYSTEM_INFORMATION = `${API_BASE_URL}/SystemInformation`;
export const API_LCRS = `${API_BASE_URL}/Lcrs`;
export const API_LCR_ROUTES = `${API_BASE_URL}/LcrRoutes`;
export const API_LCR_CARRIER_SET_ENTRIES = `${API_BASE_URL}/LcrCarrierSetEntries`;
export const API_TTS_CACHE = `${API_BASE_URL}/TtsCache`;
export const API_CLIENTS = `${API_BASE_URL}/Clients`;
export const API_REGISTER = `${API_BASE_URL}/register`;
export const API_ACTIVATION_CODE = `${API_BASE_URL}/ActivationCode`;
export const API_AVAILABILITY = `${API_BASE_URL}/Availability`;
export const API_PRICE = `${API_BASE_URL}/Prices`;
export const API_SUBSCRIPTIONS = `${API_BASE_URL}/Subscriptions`;
export const API_CHANGE_PASSWORD = `${API_BASE_URL}/change-password`;
export const API_SIGNIN = `${API_BASE_URL}/signin`;
export const API_GOOGLE_CUSTOM_VOICES = `${API_BASE_URL}/GoogleCustomVoices`;

969
src/api/index.ts Normal file
View File

@@ -0,0 +1,969 @@
import { useEffect, useState } from "react";
import { useSelectState } from "src/store";
import { getToken, parseJwt } from "src/router/auth";
import {
DEV_BASE_URL,
API_BASE_URL,
API_LOGIN,
API_USERS,
API_SERVICE_PROVIDERS,
API_API_KEYS,
API_ACCOUNTS,
API_APPLICATIONS,
API_MS_TEAMS_TENANTS,
API_PHONE_NUMBERS,
API_CARRIERS,
API_SMPP_GATEWAY,
API_SIP_GATEWAY,
API_PASSWORD_SETTINGS,
API_FORGOT_PASSWORD,
USER_ACCOUNT,
API_LOGOUT,
API_SYSTEM_INFORMATION,
API_LCR_ROUTES,
API_LCR_CARRIER_SET_ENTRIES,
API_LCRS,
API_TTS_CACHE,
API_CLIENTS,
API_REGISTER,
API_ACTIVATION_CODE,
API_AVAILABILITY,
API_PRICE,
API_SUBSCRIPTIONS,
API_CHANGE_PASSWORD,
API_SIGNIN,
API_GOOGLE_CUSTOM_VOICES,
} from "./constants";
import { ROUTE_LOGIN } from "src/router/routes";
import {
SESS_FLASH_MSG,
MSG_SESS_EXPIRED,
MSG_SERVER_DOWN,
MSG_SOMETHING_WRONG,
} from "src/constants";
import type {
FetchError,
FetchTransport,
User,
UserLogin,
ServiceProvider,
SidResponse,
TokenResponse,
EmptyResponse,
SecretResponse,
UseApiData,
Alert,
PagedResponse,
RecentCall,
UserLoginPayload,
UserUpdatePayload,
ApiKey,
Account,
Application,
SpeechCredential,
MSTeamsTenant,
PhoneNumber,
Carrier,
SmppGateway,
SipGateway,
TotalResponse,
CallQuery,
PageQuery,
Limit,
LimitCategories,
PasswordSettings,
ForgotPassword,
SystemInformation,
Lcr,
LcrRoute,
LcrCarrierSetEntry,
BucketCredential,
BucketCredentialTestResult,
Client,
RegisterRequest,
RegisterResponse,
ActivationCode,
CurrentUserData,
PriceInfo,
Subscription,
DeleteAccount,
ChangePassword,
SignIn,
GoogleCustomVoice,
GoogleCustomVoicesQuery,
SpeechSupportedLanguagesAndVoices,
} from "./types";
import { Availability, StatusCodes } from "./types";
import { JaegerRoot } from "./jaeger-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: FetchTransport<Type> = {
headers: response.headers,
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
) {
// Handle blobs -- e.g. pcap file API for RecentCalls
if (
options.headers!["Content-Type" as keyof HeadersInit] ===
"application/octet-stream"
) {
const blob: Blob = await response.blob();
transport.blob = blob;
} else {
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",
...(token && { Authorization: `Bearer ${token}` }),
};
};
const getQuery = <Type>(query: Type) => {
return decodeURIComponent(
new URLSearchParams(query as unknown as Record<string, string>).toString()
);
};
/** 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_FLASH_MSG, msg);
window.location.href = ROUTE_LOGIN;
}
};
const handleUnauthorized = () => {
handleBadRequest(MSG_SESS_EXPIRED);
};
const handleUnreachable = () => {
handleBadRequest(MSG_SERVER_DOWN);
};
/** Wrapper for fetching Blobs -- API use case is RecentCalls pcap files */
export const getBlob = (url: string) => {
return fetchTransport(url, {
headers: {
...getAuthHeaders(),
"Content-Type": "application/octet-stream",
},
});
};
/** Simple wrappers for fetchTransport calls to any API, :GET, :POST, :PUT, :DELETE */
export const getFetch = <Type>(url: string) => {
return fetchTransport<Type>(url, {
headers: getAuthHeaders(),
});
};
export const postFetch = <Type, Payload = undefined>(
url: string,
payload?: Payload
) => {
return fetchTransport<Type>(url, {
method: "POST",
...(payload && { body: JSON.stringify(payload) }),
headers: getAuthHeaders(),
});
};
export const putFetch = <Type, Payload>(url: string, payload: Payload) => {
return fetchTransport<Type>(url, {
method: "PUT",
body: JSON.stringify(payload),
headers: getAuthHeaders(),
});
};
export const deleteFetch = <Type>(url: string) => {
return fetchTransport<Type>(url, {
method: "DELETE",
headers: getAuthHeaders(),
});
};
export const deleteFetchWithPayload = <Type, Payload>(
url: string,
payload: Payload
) => {
return fetchTransport<Type>(url, {
method: "DELETE",
headers: getAuthHeaders(),
body: JSON.stringify(payload),
});
};
/** All APIs need a wrapper utility that uses the FetchTransport */
export const postLogin = (payload: UserLoginPayload) => {
return fetchTransport<UserLogin>(API_LOGIN, {
method: "POST",
body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json",
},
});
};
export const postLogout = () => {
return postFetch<undefined>(API_LOGOUT);
};
/** Named wrappers for `postFetch` */
export const postServiceProviders = (payload: Partial<ServiceProvider>) => {
return postFetch<SidResponse, Partial<ServiceProvider>>(
API_SERVICE_PROVIDERS,
payload
);
};
export const postApiKey = (payload: Partial<ApiKey>) => {
return postFetch<TokenResponse, Partial<ApiKey>>(API_API_KEYS, payload);
};
export const postAccount = (payload: Partial<Account>) => {
return postFetch<SidResponse, Partial<Account>>(API_ACCOUNTS, payload);
};
export const postAccountBucketCredentialTest = (
sid: string,
payload: Partial<BucketCredential>
) => {
return postFetch<BucketCredentialTestResult, Partial<BucketCredential>>(
`${API_ACCOUNTS}/${sid}/BucketCredentialTest`,
payload
);
};
export const postApplication = (payload: Partial<Application>) => {
return postFetch<SidResponse, Partial<Application>>(
API_APPLICATIONS,
payload
);
};
export const postSpeechService = (
sid: string,
payload: Partial<SpeechCredential>
) => {
const userData = parseJwt(getToken());
const apiUrl =
userData.scope === USER_ACCOUNT
? `${API_ACCOUNTS}/${userData.account_sid}/SpeechCredentials`
: `${API_SERVICE_PROVIDERS}/${sid}/SpeechCredentials`;
return postFetch<SidResponse, Partial<SpeechCredential>>(apiUrl, payload);
};
export const postMsTeamsTentant = (payload: Partial<MSTeamsTenant>) => {
return postFetch<SidResponse, Partial<MSTeamsTenant>>(
API_MS_TEAMS_TENANTS,
payload
);
};
export const postPhoneNumber = (payload: Partial<PhoneNumber>) => {
return postFetch<SidResponse, Partial<PhoneNumber>>(
API_PHONE_NUMBERS,
payload
);
};
export const postCarrier = (sid: string, payload: Partial<Carrier>) => {
const userData = parseJwt(getToken());
const apiUrl =
userData.scope === USER_ACCOUNT
? `${API_ACCOUNTS}/${userData.account_sid}/VoipCarriers/`
: `${API_SERVICE_PROVIDERS}/${sid}/VoipCarriers/`;
return postFetch<SidResponse, Partial<SpeechCredential>>(apiUrl, payload);
};
export const postPredefinedCarrierTemplate = (
currentServiceProviderSid: string,
predefinedCarrierSid: string
) => {
return postFetch<SidResponse>(
`${API_BASE_URL}/ServiceProviders/${currentServiceProviderSid}/PredefinedCarriers/${predefinedCarrierSid}`
);
};
export const postPredefinedCarrierTemplateAccount = (
accountSid: string,
predefinedCarrierSid: string
) => {
return postFetch<SidResponse>(
`${API_BASE_URL}/Accounts/${accountSid}/PredefinedCarriers/${predefinedCarrierSid}`
);
};
export const postSipGateway = (payload: Partial<SipGateway>) => {
return postFetch<SidResponse, Partial<SipGateway>>(API_SIP_GATEWAY, payload);
};
export const postSmppGateway = (payload: Partial<SmppGateway>) => {
return postFetch<SidResponse, Partial<SmppGateway>>(
API_SMPP_GATEWAY,
payload
);
};
export const postServiceProviderLimit = (
sid: string,
payload: Partial<Limit>
) => {
return postFetch<SidResponse, Partial<Limit>>(
`${API_SERVICE_PROVIDERS}/${sid}/Limits`,
payload
);
};
export const postAccountLimit = (sid: string, payload: Partial<Limit>) => {
return postFetch<SidResponse, Partial<Limit>>(
`${API_ACCOUNTS}/${sid}/Limits`,
payload
);
};
export const postPasswordSettings = (payload: Partial<PasswordSettings>) => {
return postFetch<EmptyResponse, Partial<PasswordSettings>>(
API_PASSWORD_SETTINGS,
payload
);
};
export const postForgotPassword = (payload: Partial<ForgotPassword>) => {
return postFetch<EmptyResponse, Partial<ForgotPassword>>(
API_FORGOT_PASSWORD,
payload
);
};
export const postSystemInformation = (payload: Partial<SystemInformation>) => {
return postFetch<SystemInformation, Partial<SystemInformation>>(
API_SYSTEM_INFORMATION,
payload
);
};
export const postLcr = (payload: Partial<Lcr>) => {
return postFetch<SidResponse, Partial<Lcr>>(API_LCRS, payload);
};
export const postLcrCreateRoutes = (
sid: string,
payload: Partial<LcrRoute[]>
) => {
return postFetch<EmptyResponse, Partial<LcrRoute[]>>(
`${API_LCRS}/${sid}/Routes`,
payload
);
};
export const postLcrRoute = (payload: Partial<LcrRoute>) => {
return postFetch<SidResponse, Partial<LcrRoute>>(API_LCR_ROUTES, payload);
};
export const postLcrCarrierSetEntry = (
payload: Partial<LcrCarrierSetEntry>
) => {
return postFetch<SidResponse, Partial<LcrCarrierSetEntry>>(
API_LCR_CARRIER_SET_ENTRIES,
payload
);
};
export const postClient = (payload: Partial<Client>) => {
return postFetch<SidResponse, Partial<Client>>(API_CLIENTS, payload);
};
export const postRegister = (payload: Partial<RegisterRequest>) => {
return postFetch<RegisterResponse, Partial<RegisterRequest>>(
API_REGISTER,
payload
);
};
export const postSipRealms = (accountSid: string, domain: string) => {
return postFetch<EmptyResponse>(
`${API_ACCOUNTS}/${accountSid}/SipRealms/${domain}`
);
};
export const postSubscriptions = (payload: Partial<Subscription>) => {
return postFetch<Subscription, Partial<Subscription>>(
API_SUBSCRIPTIONS,
payload
);
};
export const postChangepassword = (payload: Partial<ChangePassword>) => {
return postFetch<EmptyResponse, Partial<ChangePassword>>(
API_CHANGE_PASSWORD,
payload
);
};
export const postSignIn = (payload: Partial<SignIn>) => {
return postFetch<SignIn, Partial<SignIn>>(API_SIGNIN, payload);
};
export const postGoogleCustomVoice = (payload: Partial<GoogleCustomVoice>) => {
return postFetch<SidResponse, Partial<GoogleCustomVoice>>(
API_GOOGLE_CUSTOM_VOICES,
payload
);
};
/** Named wrappers for `putFetch` */
export const putUser = (sid: string, payload: Partial<UserUpdatePayload>) => {
return putFetch<EmptyResponse, Partial<UserUpdatePayload>>(
`${API_USERS}/${sid}`,
payload
);
};
export const putServiceProvider = (
sid: string,
payload: Partial<ServiceProvider>
) => {
return putFetch<EmptyResponse, Partial<ServiceProvider>>(
`${API_SERVICE_PROVIDERS}/${sid}`,
payload
);
};
export const putAccount = (sid: string, payload: Partial<Account>) => {
return putFetch<EmptyResponse, Partial<Account>>(
`${API_ACCOUNTS}/${sid}`,
payload
);
};
export const putApplication = (sid: string, payload: Partial<Application>) => {
return putFetch<EmptyResponse, Partial<Application>>(
`${API_APPLICATIONS}/${sid}`,
payload
);
};
export const putSpeechService = (
sid1: string,
sid2: string,
payload: Partial<SpeechCredential>
) => {
const userData = parseJwt(getToken());
const apiUrl =
userData.scope === USER_ACCOUNT
? `${API_ACCOUNTS}/${userData.account_sid}/SpeechCredentials/${sid2}`
: `${API_SERVICE_PROVIDERS}/${sid1}/SpeechCredentials/${sid2}`;
return putFetch<EmptyResponse, Partial<SpeechCredential>>(apiUrl, payload);
};
export const putMsTeamsTenant = (
sid: string,
payload: Partial<MSTeamsTenant>
) => {
return putFetch<EmptyResponse, Partial<MSTeamsTenant>>(
`${API_MS_TEAMS_TENANTS}/${sid}`,
payload
);
};
export const putPhoneNumber = (sid: string, payload: Partial<PhoneNumber>) => {
return putFetch<EmptyResponse, Partial<PhoneNumber>>(
`${API_PHONE_NUMBERS}/${sid}`,
payload
);
};
export const putCarrier = (
sid1: string,
sid2: string,
payload: Partial<Carrier>
) => {
const userData = parseJwt(getToken());
const apiUrl =
userData.scope === USER_ACCOUNT
? `${API_ACCOUNTS}/${userData.account_sid}/VoipCarriers/${sid2}`
: `${API_SERVICE_PROVIDERS}/${sid1}/VoipCarriers/${sid2}`;
return putFetch<EmptyResponse, Partial<Carrier>>(apiUrl, payload);
};
export const putSipGateway = (sid: string, payload: Partial<SipGateway>) => {
return putFetch<EmptyResponse, Partial<SipGateway>>(
`${API_SIP_GATEWAY}/${sid}`,
payload
);
};
export const putSmppGateway = (sid: string, payload: Partial<SmppGateway>) => {
return putFetch<EmptyResponse, Partial<SmppGateway>>(
`${API_SMPP_GATEWAY}/${sid}`,
payload
);
};
export const putLcr = (sid: string, payload: Partial<Lcr>) => {
return putFetch<EmptyResponse, Partial<Lcr>>(`${API_LCRS}/${sid}`, payload);
};
export const putLcrUpdateRoutes = (
sid: string,
payload: Partial<LcrRoute[]>
) => {
return putFetch<EmptyResponse, Partial<LcrRoute[]>>(
`${API_LCRS}/${sid}/Routes`,
payload
);
};
export const putLcrRoutes = (sid: string, payload: Partial<LcrRoute>) => {
return putFetch<EmptyResponse, Partial<LcrRoute>>(
`${API_LCR_ROUTES}/${sid}`,
payload
);
};
export const putLcrCarrierSetEntries = (
sid: string,
payload: Partial<LcrCarrierSetEntry>
) => {
return putFetch<EmptyResponse, Partial<LcrCarrierSetEntry>>(
`${API_LCR_CARRIER_SET_ENTRIES}/${sid}`,
payload
);
};
export const putClient = (sid: string, payload: Partial<Client>) => {
return putFetch<EmptyResponse, Partial<Client>>(
`${API_CLIENTS}/${sid}`,
payload
);
};
export const putActivationCode = (
code: string,
payload: Partial<ActivationCode>
) => {
return putFetch<EmptyResponse, Partial<ActivationCode>>(
`${API_ACTIVATION_CODE}/${code}`,
payload
);
};
export const putGoogleCustomVoice = (
sid: string,
payload: Partial<GoogleCustomVoice>
) => {
return putFetch<EmptyResponse, Partial<GoogleCustomVoice>>(
`${API_GOOGLE_CUSTOM_VOICES}/${sid}`,
payload
);
};
/** Named wrappers for `deleteFetch` */
export const deleteUser = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_USERS}/${sid}`);
};
export const deleteServiceProvider = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_SERVICE_PROVIDERS}/${sid}`);
};
export const deleteApiKey = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_API_KEYS}/${sid}`);
};
export const deleteAccount = (sid: string, payload: Partial<DeleteAccount>) => {
return deleteFetchWithPayload<EmptyResponse, Partial<DeleteAccount>>(
`${API_ACCOUNTS}/${sid}`,
payload
);
};
export const deleteApplication = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_APPLICATIONS}/${sid}`);
};
export const deleteSpeechService = (sid1: string, sid2: string) => {
return deleteFetch<EmptyResponse>(
`${API_SERVICE_PROVIDERS}/${sid1}/SpeechCredentials/${sid2}`
);
};
export const deleteMsTeamsTenant = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_MS_TEAMS_TENANTS}/${sid}`);
};
export const deletePhoneNumber = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_PHONE_NUMBERS}/${sid}`);
};
export const deleteCarrier = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_CARRIERS}/${sid}`);
};
export const deleteSipGateway = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_SIP_GATEWAY}/${sid}`);
};
export const deleteSmppGateway = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_SMPP_GATEWAY}/${sid}`);
};
export const deleteServiceProviderLimit = (
sid: string,
cat: LimitCategories
) => {
return deleteFetch<EmptyResponse>(
`${API_SERVICE_PROVIDERS}/${sid}/Limits?category=${cat}`
);
};
export const deleteAccountLimit = (sid: string, cat: LimitCategories) => {
return deleteFetch<EmptyResponse>(
`${API_ACCOUNTS}/${sid}/Limits?category=${cat}`
);
};
export const deleteLcr = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_LCRS}/${sid}`);
};
export const deleteLcrRoute = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_LCR_ROUTES}/${sid}`);
};
export const deleteTtsCache = () => {
return deleteFetch<EmptyResponse>(API_TTS_CACHE);
};
export const deleteAccountTtsCache = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_BASE_URL}/Accounts/${sid}/TtsCache`);
};
export const deleteClient = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_CLIENTS}/${sid}`);
};
export const deleteRecord = (url: string) => {
return deleteFetch<EmptyResponse>(url);
};
export const deleteGoogleCustomVoice = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_GOOGLE_CUSTOM_VOICES}/${sid}`);
};
/** Named wrappers for `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`
);
};
export const getLcrs = () => {
return getFetch<Lcr[]>(API_LCRS);
};
export const getLcr = (sid: string) => {
return getFetch<Lcr>(`${API_LCRS}/${sid}`);
};
export const getLcrRoutes = (sid: string) => {
return getFetch<LcrRoute[]>(`${API_LCR_ROUTES}?lcr_sid=${sid}`);
};
export const getLcrRoute = (sid: string) => {
return getFetch<LcrRoute>(`${API_LCR_ROUTES}/${sid}`);
};
export const getLcrCarrierSetEtries = (sid: string) => {
return getFetch<LcrCarrierSetEntry[]>(
`${API_LCR_CARRIER_SET_ENTRIES}?lcr_route_sid=${sid}`
);
};
export const getClients = () => {
return getFetch<Client[]>(API_CLIENTS);
};
export const getClient = (sid: string) => {
return getFetch<Client[]>(`${API_CLIENTS}/${sid}`);
};
export const getAvailability = (domain: string) => {
return getFetch<Availability>(
`${API_AVAILABILITY}?type=subdomain&value=${domain}`
);
};
export const getGoogleCustomVoices = (
query: Partial<GoogleCustomVoicesQuery>
) => {
const qryStr = getQuery<Partial<GoogleCustomVoicesQuery>>(query);
return getFetch<GoogleCustomVoice[]>(`${API_GOOGLE_CUSTOM_VOICES}?${qryStr}`);
};
/** Wrappers for APIs that can have a mock dev server response */
export const getMe = () => {
return getFetch<CurrentUserData>(`${API_USERS}/me`);
};
export const getRecentCalls = (sid: string, query: Partial<CallQuery>) => {
const qryStr = getQuery<Partial<CallQuery>>(query);
return getFetch<PagedResponse<RecentCall>>(
import.meta.env.DEV
? `${DEV_BASE_URL}/Accounts/${sid}/RecentCalls?${qryStr}`
: `${API_ACCOUNTS}/${sid}/RecentCalls?${qryStr}`
);
};
export const getRecentCall = (sid: string, sipCallId: string) => {
return getFetch<TotalResponse>(
import.meta.env.DEV
? `${DEV_BASE_URL}/Accounts/${sid}/RecentCalls/${sipCallId}`
: `${API_ACCOUNTS}/${sid}/RecentCalls/${sipCallId}`
);
};
export const getPcap = (sid: string, sipCallId: string, method: string) => {
return getBlob(
import.meta.env.DEV
? `${DEV_BASE_URL}/Accounts/${sid}/RecentCalls/${sipCallId}/${method}/pcap`
: `${API_ACCOUNTS}/${sid}/RecentCalls/${sipCallId}/${method}/pcap`
);
};
export const getJaegerTrace = (sid: string, traceId: string) => {
return getFetch<JaegerRoot>(
import.meta.env.DEV
? `${DEV_BASE_URL}/Accounts/${sid}/RecentCalls/trace/${traceId}`
: `${API_ACCOUNTS}/${sid}/RecentCalls/trace/${traceId}`
);
};
export const getServiceProviderRecentCall = (
sid: string,
sipCallId: string
) => {
return getFetch<TotalResponse>(
import.meta.env.DEV
? `${DEV_BASE_URL}/ServiceProviders/${sid}/RecentCalls/${sipCallId}`
: `${API_SERVICE_PROVIDERS}/${sid}/RecentCalls/${sipCallId}`
);
};
export const getServiceProviderPcap = (
sid: string,
sipCallId: string,
method: string
) => {
return getBlob(
import.meta.env.DEV
? `${DEV_BASE_URL}/ServiceProviders/${sid}/RecentCalls/${sipCallId}/${method}/pcap`
: `${API_SERVICE_PROVIDERS}/${sid}/RecentCalls/${sipCallId}/${method}/pcap`
);
};
export const getAlerts = (sid: string, query: Partial<PageQuery>) => {
const qryStr = getQuery<Partial<PageQuery>>(query);
return getFetch<PagedResponse<Alert>>(
import.meta.env.DEV
? `${DEV_BASE_URL}/Accounts/${sid}/Alerts?${qryStr}`
: `${API_ACCOUNTS}/${sid}/Alerts?${qryStr}`
);
};
export const getPrice = () => {
return getFetch<PriceInfo[]>(API_PRICE);
};
export const getSpeechSupportedLanguagesAndVoices = (
sid: string | undefined,
vendor: string,
label: string
) => {
const userData = parseJwt(getToken());
const apiUrl =
(userData.scope === USER_ACCOUNT
? `${API_ACCOUNTS}/${userData.account_sid}`
: `${API_SERVICE_PROVIDERS}/${sid}`) +
`/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=${vendor}${
label ? `&label=${label}` : ""
}`;
return getFetch<SpeechSupportedLanguagesAndVoices>(apiUrl);
};
/** 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>();
const [error, setError] = useState<FetchError>();
const [refetch, setRefetch] = useState(0);
const refetcher = () => {
setRefetch(refetch + 1);
};
useEffect(() => {
let ignore = false;
// Don't fetch if api url is empty string ""
if (apiPath) {
getFetch<Type>(`${API_BASE_URL}/${apiPath}`)
.then(({ json }) => {
if (!ignore) {
setResult(json!);
}
})
.catch((error) => {
if (!ignore) {
setError(error);
}
});
}
return function cleanup() {
ignore = true;
};
// Refetch data if refetcher() is called OR api url changes
}, [refetch, apiPath]);
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>();
const [error, setError] = useState<FetchError>();
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];
};

87
src/api/jaeger-types.ts Normal file
View File

@@ -0,0 +1,87 @@
export interface JaegerRoot {
resourceSpans: JaegerResourceSpan[];
}
export interface JaegerResourceSpan {
resource: JaegerResource;
instrumentationLibrarySpans: InstrumentationLibrarySpan[];
}
export interface JaegerResource {
attributes: JaegerAttribute[];
}
export interface InstrumentationLibrarySpan {
instrumentationLibrary: InstrumentationLibrary;
spans: JaegerSpan[];
}
export interface InstrumentationLibrary {
name: string;
version: string;
}
export interface JaegerSpan {
traceId: string;
spanId: string;
parentSpanId: string;
name: string;
kind: string;
startTimeUnixNano: number;
endTimeUnixNano: number;
attributes: JaegerAttribute[];
}
export interface JaegerAttribute {
key: string;
value: JaegerValue;
}
export interface WaveSurferSttResult {
vendor: string;
transcript: string;
confidence: number;
language_code: string;
latency?: number;
}
export interface WaveSurferTtsLatencyResult {
vendor: string;
latency: string;
isCached: string;
}
export interface WaveSurferGatherSpeechVerbHookLatencyResult {
statusCode: number;
latency: string;
}
export interface WaveSurferDtmfResult {
dtmf: string;
duration: string;
}
export interface JaegerValue {
stringValue: string;
doubleValue: string;
boolValue: string;
}
export interface JaegerGroup {
level: number;
startPx: number;
endPx: number;
durationPx: number;
startMs: number;
endMs: number;
durationMs: number;
traceId: string;
spanId: string;
parentSpanId: string;
name: string;
kind: string;
startTimeUnixNano: number;
endTimeUnixNano: number;
attributes: JaegerAttribute[];
children: JaegerGroup[];
}

720
src/api/types.ts Normal file
View File

@@ -0,0 +1,720 @@
import type { Language, Model, Vendor, VoiceLanguage } from "src/vendor/types";
/** Simple types */
export type WebhookMethod = "POST" | "GET";
export type CredentialStatus = "ok" | "fail" | "not tested";
export type IpType = "ip" | "fqdn" | "fqdn-top-level" | "invalid";
export type LimitCategories =
| "api_rate"
| "voice_call_session"
| "device"
| "voice_call_minutes_license"
| "voice_call_minutes"
| "voice_call_session_license";
export type LimitUnit = "Session" | "Minute";
export interface LimitUnitOption {
name: LimitUnit;
value: Lowercase<LimitUnit>;
}
/** User roles / permissions */
export type UserScopes = "admin" | "service_provider" | "account";
export type UserPermissions =
| "VIEW_ONLY"
| "PROVISION_SERVICES"
| "PROVISION_USERS";
/** Status codes */
export enum StatusCodes {
OK = 200,
CREATED = 201,
ACCEPTED = 202,
NO_CONTENT = 204,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
UNPROCESSABLE_ENTITY = 422,
INTERNAL_SERVER_ERROR = 500,
/** SMPP temporarily unavailable */
TEMPORARILY_UNAVAILABLE = 480,
}
/** Fetch transport interfaces */
export interface FetchTransport<Type> {
headers: Headers;
status: StatusCodes;
json: Type;
blob?: Blob;
}
export interface FetchError {
status: StatusCodes;
msg: string;
}
export interface UseApiData {
<Type>(apiPath: string): [
Type | undefined,
() => void,
FetchError | undefined
];
}
/** API related interfaces */
export interface UseApiDataMap<Type> {
data?: Type;
error?: FetchError;
refetch: () => void;
}
export interface WebhookOption {
name: WebhookMethod;
value: WebhookMethod;
}
export interface SelectorOptions {
name: string;
value: string;
}
export interface DownloadedBlob {
data_url: string;
file_name: string;
}
export interface CredentialTest {
status: CredentialStatus;
reason: string;
}
export interface CredentialTestResult {
stt: CredentialTest;
tts: CredentialTest;
}
export interface BucketCredentialTestResult {
status: CredentialStatus;
reason: string;
}
export interface LimitField {
label: string;
category: LimitCategories;
}
export interface PasswordSettings {
min_password_length: number;
require_digit: number;
require_special_character: number;
}
export interface ForgotPassword {
email: string;
}
export interface SystemInformation {
domain_name: string;
sip_domain_name: string;
monitoring_domain_name: string;
}
export interface TtsCache {
size: number;
}
/** API responses/payloads */
export interface User {
scope: UserScopes;
user_sid: string;
name: string;
email: string;
is_active: boolean;
force_change: boolean;
account_sid: string | null;
account_name?: string | null;
service_provider_sid: string | null;
service_provider_name?: string | null;
initial_password?: string;
permissions?: UserPermissions[];
provider?: null | string;
}
export interface UserLogin {
token: string;
force_change: boolean;
scope: UserScopes;
user_sid: string;
permissions: UserPermissions[];
}
export interface UserLoginPayload {
username: string;
password: string;
}
export interface UserUpdatePayload {
old_password?: string;
new_password?: string;
initial_password: string | null;
email: string;
name: string;
force_change: boolean;
is_active: boolean;
service_provider_sid: string | null;
account_sid: string | null;
}
export interface UserJWT {
scope: UserScopes;
user_sid: string;
account_sid?: string | null;
service_provider_sid?: string | null;
permissions: UserPermissions[];
name: string;
}
export interface CurrentUserData {
user: User;
account?: Account;
subscription?: null | Subscription;
}
export interface ServiceProvider {
name: string;
ms_teams_fqdn: null | string;
service_provider_sid: string;
lcr_sid: null | string;
}
export interface Limit {
category: LimitCategories;
/** Empty string signals :DELETE */
/** @see src/components/forms/local-limits */
/** @see src/containers/internal/views/accounts/form */
/** @see src/containers/internal/views/settings/index */
quantity: number | string;
account_sid?: string;
account_limits_sid?: string;
service_provider_sid?: string;
service_provider_limits_sid?: string;
}
export interface ApiKey {
token: string;
last_used: null | string;
expires_at: null | string;
created_at: string;
api_key_sid: string;
account_sid: null | string;
service_provider_sid: null | string;
}
export interface WebHook {
url: string;
method: WebhookMethod;
username: null | string;
password: null | string;
webhook_sid?: null | string;
}
export interface Sbc {
ipv4: string;
port: number | string;
sbc_address_sid: string;
service_provider_sid: null | string;
}
export interface Smpp {
ipv4: string;
port: number | string;
use_tls: boolean;
is_primary: boolean;
smpp_address_sid: string;
service_provider_sid: null | string;
}
export interface Account {
name: string;
sip_realm: null | string;
root_domain?: null | string;
account_sid: string;
webhook_secret: string;
siprec_hook_sid: null | string;
queue_event_hook: null | WebHook;
registration_hook: null | WebHook;
service_provider_sid: string;
device_calling_application_sid: null | string;
record_all_calls: number;
record_format?: null | string;
bucket_credential: null | BucketCredential;
plan_type?: string;
device_to_call_ratio?: number;
trial_end_date?: null | string;
is_active: boolean;
}
export interface Product {
price_id?: null | string;
product_sid?: null | string;
name?: string;
quantity?: number;
}
export interface Subscription {
action?: null | string;
payment_method_id?: null | string;
account_subscription_sid?: null | string;
stripe_customer_id?: null | string;
products?: null | Product[];
start_date?: string;
status?: string;
client_secret?: null | string;
last4?: null | string;
exp_month?: null | string;
exp_year?: null | string;
card_type?: null | string;
reason?: null | string;
dry_run?: boolean;
currency?: null | string;
prorated_cost?: number;
monthly_cost?: number;
next_invoice_date?: null | string;
}
export interface AwsTag {
Key: string;
Value: string;
}
export interface BucketCredential {
vendor: null | string;
region?: null | string;
name?: null | string;
access_key_id?: null | string;
secret_access_key?: null | string;
tags?: null | AwsTag[];
service_key?: null | string;
connection_string?: null | string;
endpoint?: null | string;
}
export interface Application {
name: string;
app_json: null | string;
call_hook: null | WebHook;
account_sid: null | string;
messaging_hook: null | WebHook;
application_sid: string;
call_status_hook: null | WebHook;
speech_synthesis_voice: null | string;
speech_synthesis_vendor: null | Lowercase<Vendor>;
speech_synthesis_language: null | string;
speech_synthesis_label: null | string;
speech_recognizer_vendor: null | Lowercase<Vendor>;
speech_recognizer_language: null | string;
speech_recognizer_label: null | string;
record_all_calls: number;
use_for_fallback_speech: number;
fallback_speech_synthesis_vendor: null | string;
fallback_speech_synthesis_language: null | string;
fallback_speech_synthesis_voice: null | string;
fallback_speech_synthesis_label: null | string;
fallback_speech_recognizer_vendor: null | string;
fallback_speech_recognizer_language: null | string;
fallback_speech_recognizer_label: null | string;
}
export interface PhoneNumber {
number: string;
account_sid: null | string;
application_sid: null | string;
phone_number_sid: string;
voip_carrier_sid: null | string;
}
export interface MSTeamsTenant {
tenant_fqdn: string;
ms_teams_tenant_sid: string;
account_sid: null | string;
application_sid: null | string;
service_provider_sid: string;
}
export interface RecentCall {
account_sid: string;
call_sid: string;
from: string;
to: string;
answered: boolean;
sip_callid: string;
sip_status: number;
duration: number;
attempted_at: number;
answered_at: number;
terminated_at: number;
termination_reason: string;
host: string;
remote_host: string;
direction: string;
trunk: string;
trace_id: string;
recording_url?: string;
}
export interface GoogleCustomVoice {
google_custom_voice_sid?: string;
speech_credential_sid?: string;
name: string;
reported_usage: string;
model: string;
}
export interface SpeechCredential {
speech_credential_sid: string;
service_provider_sid: null | string;
account_sid: null | string;
vendor: Lowercase<Vendor>;
use_for_tts: number;
use_for_stt: number;
last_used: null | string;
region: null | string;
aws_region: null | string;
api_key: null | string;
access_key_id: null | string;
secret_access_key: null | string;
service_key: null | string;
use_custom_tts: number;
custom_tts_endpoint_url: null | string;
custom_tts_endpoint: null | string;
use_custom_stt: number;
custom_stt_endpoint_url: null | string;
custom_stt_endpoint: null | string;
client_id: null | string;
secret: null | string;
nuance_tts_uri: null | string;
nuance_stt_uri: null | string;
tts_api_key: null | string;
tts_region: null | string;
stt_api_key: null | string;
stt_region: null | string;
instance_id: null | string;
riva_server_uri: null | string;
auth_token: null | string;
custom_stt_url: null | string;
custom_tts_url: null | string;
label: null | string;
cobalt_server_uri: null | string;
model_id: null | string;
model: null | string;
options: null | string;
}
export interface Alert {
time: number;
account_sid: string;
alert_type: string;
message: string;
detail: string;
}
export interface CarrierRegisterStatus {
status: null | string;
reason: null | string;
cseq: null | string;
callId: null | string;
}
export interface Carrier {
voip_carrier_sid: string;
name: string;
description: null | string;
is_active: boolean;
service_provider_sid: string;
account_sid: null | string;
application_sid: null | string;
e164_leading_plus: boolean;
requires_register: boolean;
register_username: null | string;
register_password: null | string;
register_sip_realm: null | string;
register_from_user: null | string;
register_from_domain: null | string;
register_public_ip_in_contact: boolean;
tech_prefix: null | string;
diversion: null | string;
inbound_auth_username: string;
inbound_auth_password: string;
smpp_system_id: null | string;
smpp_password: null | string;
smpp_inbound_system_id: null | string;
smpp_inbound_password: null | string;
smpp_enquire_link_interval: number;
register_status: CarrierRegisterStatus;
}
export interface PredefinedCarrier extends Carrier {
requires_static_ip: boolean;
predefined_carrier_sid: string;
}
export interface Gateway {
voip_carrier_sid: string;
ipv4: string;
netmask: number;
inbound: number;
outbound: number;
}
export interface SipGateway extends Gateway {
sip_gateway_sid?: null | string;
is_active: boolean;
protocol?: string;
port: number | null;
pad_crypto?: boolean;
}
export interface SmppGateway extends Gateway {
smpp_gateway_sid?: null | string;
is_primary: boolean;
use_tls: boolean;
port: number;
}
export interface Lcr {
lcr_sid?: null | string;
is_active: boolean;
name: null | string;
default_carrier_set_entry_sid?: null | string;
account_sid: null | string;
service_provider_sid: null | string;
number_routes?: number;
}
export interface LcrRoute {
lcr_route_sid?: null | string;
lcr_sid: null | string;
regex: null | string;
description?: null | string;
priority: number;
lcr_carrier_set_entries?: LcrCarrierSetEntry[];
}
export interface LcrCarrierSetEntry {
lcr_carrier_set_entry_sid?: null | string;
workload?: number;
lcr_route_sid: null | string;
voip_carrier_sid: null | string;
priority: number;
}
export interface Client {
client_sid?: null | string;
account_sid: null | string;
username: null | string;
password?: null | string;
is_active: boolean;
allow_direct_app_calling: boolean;
allow_direct_queue_calling: boolean;
allow_direct_user_calling: boolean;
}
export interface PageQuery {
page: number;
count: number;
start?: string;
days?: number;
}
export interface CallQuery extends PageQuery {
direction?: string;
answered?: string;
}
export interface GoogleCustomVoicesQuery {
speech_credential_sid?: string;
label?: string;
account_sid?: string;
service_provider_sid: string;
}
export interface PagedResponse<Type> {
page_size: number;
total: number;
page: number;
data: Type[];
}
export interface SidResponse {
sid: string;
}
export interface UserSidResponse {
user_sid: string;
}
export interface TokenResponse extends SidResponse {
token: string;
}
export interface SecretResponse {
webhook_secret: string;
}
export interface EmptyResponse {
[key: string]: unknown;
}
export interface TotalResponse {
total: number;
}
export interface RegisterRequest {
service_provider_sid: string;
provider: string;
oauth2_code?: string;
oauth2_state?: string;
oauth2_client_id?: string;
oauth2_redirect_uri?: string;
locationBeforeAuth?: string;
name?: string;
email?: string;
password?: string;
email_activation_code?: string;
inviteCode?: string;
}
export interface RegisterResponse {
jwt: string;
user_sid: string;
account_sid: string;
root_domain: string;
}
export interface ActivationCode {
user_sid: string;
type: string;
}
export interface Availability {
available: boolean;
}
export interface Invoice {
total: number;
currency: null | string;
next_payment_attempt: null | string;
}
export type Currency = {
[key: string]: null | string;
};
export interface Recurring {
aggregate_usage: null | string;
interval: null | string;
interval_count: number;
trial_period_days: null | string;
usage_type: string;
}
export interface Price {
billing_scheme: string;
currency: string;
recurring: Recurring;
stripe_price_id: null | string;
tiers_mode: null | string;
type: null | string;
unit_amount: number;
unit_amount_decimal: null | string;
}
export interface PriceInfo {
category: null | string;
description: null | string;
name: null | string;
prices: Price[];
product_sid: null | string;
stripe_product_id: null | string;
unit_label: null | string;
}
export interface StripeCustomerId {
stripe_customer_id: null | string;
}
export interface Tier {
up_to: number;
flat_amount: number;
unit_amount: number;
}
export interface ServiceData {
category: null | string;
name: null | string;
service: null | string;
fees: number;
feesLabel: null | string;
cost: number;
capacity: number;
invalid: boolean;
currency: null | string;
min: number;
max: number;
dirty: boolean;
visible: boolean;
required: boolean;
billing_scheme?: null | string;
stripe_price_id?: null | string;
unit_label?: null | string;
product_sid?: null | string;
stripe_product_id?: null | string;
tiers?: Tier[];
}
export interface DeleteAccount {
password: string;
}
export interface ChangePassword {
old_password: null | string;
new_password: null | string;
}
export interface SignIn {
link?: null | string;
jwt?: null | string;
account_sid?: null | string;
}
export interface GetLanguagesAndVoices {
vendor: string;
label: string;
}
export interface SpeechSupportedLanguagesAndVoices {
tts: VoiceLanguage[];
stt: Language[];
models: Model[];
}
export interface ElevenLabsOptions {
optimize_streaming_latency: number;
voice_settings: Partial<{
similarity_boost: number;
stability: number;
style: number;
use_speaker_boost: boolean;
}>;
}

View File

@@ -0,0 +1,37 @@
import React from "react";
import { H1 } from "@jambonz/ui-kit";
import { AccessControl } from "./access-control";
import type { ACLProps } from "./access-control";
/** Wrapper to pass different ACLs */
const AccessControlTestWrapper = (props: Partial<ACLProps>) => {
return (
<AccessControl acl={props.acl!}>
<div className="acl-div">
<H1>ACL: {props.acl}</H1>
</div>
</AccessControl>
);
};
describe("<AccessControl>", () => {
it("mounts", () => {
cy.mountTestProvider(<AccessControlTestWrapper acl="hasAdminAuth" />);
});
it("doesn't have teams fqdn ACL", () => {
cy.mountTestProvider(<AccessControlTestWrapper acl="hasMSTeamsFqdn" />);
/** Default ACL disables MS Teams FQDN */
cy.get(".acl-div").should("not.exist");
});
it("has admin ACL", () => {
cy.mountTestProvider(<AccessControlTestWrapper acl="hasAdminAuth" />);
/** Default ACL applies admin auth -- the singleton admin user */
cy.get(".acl-div").should("exist");
});
});

View File

@@ -0,0 +1,20 @@
import React from "react";
import { useSelectState } from "src/store";
import type { ACL } from "src/store/types";
export type ACLProps = {
acl: keyof ACL;
children: React.ReactNode;
};
export const AccessControl = ({ acl, children }: ACLProps) => {
const accessControl = useSelectState("accessControl");
if (accessControl[acl]) {
return <>{children}</>;
}
return null;
};

View File

@@ -0,0 +1,74 @@
import React, { useState } from "react";
import { sortLocaleName } from "src/utils";
import { AccountFilter } from "./account-filter";
import type { AccountFilterProps } from "./account-filter";
import type { Account } from "src/api/types";
/** Import fixture data directly so we don't use cy.fixture() ... */
import accounts from "../../cypress/fixtures/accounts.json";
/** Wrapper to perform React state setup */
const AccountFilterTestWrapper = (props: Partial<AccountFilterProps>) => {
const [account, setAccount] = useState("");
return (
<AccountFilter
label="Test"
accounts={accounts as Account[]}
account={[account, setAccount]}
defaultOption={props.defaultOption}
/>
);
};
describe("<AccountFilter>", () => {
/** The AccountFilter uses sort with `localeCompare` */
const accountsSorted = accounts.sort(sortLocaleName);
it("mounts", () => {
cy.mount(<AccountFilterTestWrapper />);
});
it("has label text", () => {
cy.mount(<AccountFilterTestWrapper />);
/** Label text is properly set */
cy.get("label").should("have.text", "Test:");
});
it("has default value", () => {
cy.mount(<AccountFilterTestWrapper />);
/** Default value is properly set to first option */
cy.get("select").should("have.value", accountsSorted[0].account_sid);
});
it("updates value onChange", () => {
cy.mount(<AccountFilterTestWrapper />);
/** Assert onChange value updates */
cy.get("select").select(accountsSorted[1].account_sid);
cy.get("select").should("have.value", accountsSorted[1].account_sid);
});
it("manages the focused state", () => {
cy.mount(<AccountFilterTestWrapper />);
/** Test the `focused` state className (applied onFocus) */
cy.get("select").select(accountsSorted[1].account_sid);
cy.get(".account-filter").should("have.class", "focused");
cy.get("select").blur();
cy.get(".account-filter").should("not.have.class", "focused");
});
it("renders with default option", () => {
/** Test with the `defaultOption` prop */
cy.mount(<AccountFilterTestWrapper defaultOption />);
/** No default value is set when this prop is present */
cy.get("select").should("have.value", "");
});
});

View File

@@ -0,0 +1,76 @@
import React, { useEffect, useState } from "react";
import { classNames } from "@jambonz/ui-kit";
import { Icons } from "src/components/icons";
import type { Account } from "src/api/types";
import { hasLength, sortLocaleName } from "src/utils";
import { setAccountFilter } from "src/store/localStore";
export type AccountFilterProps = {
label?: string;
account: [string, React.Dispatch<React.SetStateAction<string>>];
accounts?: Account[];
defaultOption?: boolean;
};
/** This will apply the selected account SID so you can filter local data */
/** Currently used by: Applications, Recent Calls, Alerts, Carriers and Speech index views */
export const AccountFilter = ({
label = "Account",
account: [accountSid, setAccountSid],
accounts,
defaultOption,
}: AccountFilterProps) => {
const [focus, setFocus] = useState(false);
const classes = {
smsel: true,
"smsel--filter": true,
"account-filter": true,
focused: focus,
};
useEffect(() => {
if (hasLength(accounts) && !defaultOption) {
setAccountSid(accounts[0].account_sid);
}
}, [accounts, defaultOption, setAccountSid]);
return (
<div className={classNames(classes)}>
{label && <label htmlFor="account_filter">{label}:</label>}
<div>
<select
id="account_filter"
name="account_filter"
value={accountSid}
onChange={(e) => {
setAccountSid(e.target.value);
setAccountFilter(e.target.value);
}}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
>
{defaultOption ? (
<option value="">All accounts</option>
) : (
accounts &&
!accounts.length && <option value="">No accounts</option>
)}
{hasLength(accounts) &&
accounts.sort(sortLocaleName).map((acct) => {
return (
<option key={acct.account_sid} value={acct.account_sid}>
{acct.name}
</option>
);
})}
</select>
<span>
<Icons.ChevronUp />
<Icons.ChevronDown />
</span>
</div>
</div>
);
};

View File

@@ -0,0 +1,87 @@
import React, { useState } from "react";
import { sortLocaleName } from "src/utils";
import { ApplicationFilter } from "./application-filter";
import type { ApplicationFilterProps } from "./application-filter";
import type { Application } from "src/api/types";
/** Import fixture data directly so we don't use cy.fixture() ... */
import applications from "../../cypress/fixtures/applications.json";
/** Wrapper to perform React state setup */
const ApplicationFilterTestWrapper = (
props: Partial<ApplicationFilterProps>
) => {
const [application, setApplication] = useState("");
return (
<ApplicationFilter
label="Test"
applications={applications as Application[]}
application={[application, setApplication]}
defaultOption={props.defaultOption}
/>
);
};
describe("<ApplicationFilter>", () => {
/** The AccountFilter uses sort with `localeCompare` */
const applicationsSorted = applications.sort(sortLocaleName);
it("mounts", () => {
cy.mount(<ApplicationFilterTestWrapper />);
});
it("has label text", () => {
cy.mount(<ApplicationFilterTestWrapper />);
/** Label text is properly set */
cy.get("label").should("have.text", "Test:");
});
it("has default value", () => {
cy.mount(<ApplicationFilterTestWrapper />);
/** Default value is properly set to first option */
cy.get("select").should(
"have.value",
applicationsSorted[0].application_sid
);
});
it("updates value onChange", () => {
cy.mount(<ApplicationFilterTestWrapper />);
/** Assert onChange value updates */
cy.get("select").select(applicationsSorted[1].application_sid);
cy.get("select").should(
"have.value",
applicationsSorted[1].application_sid
);
});
it("manages focused state", () => {
cy.mount(<ApplicationFilterTestWrapper />);
/** Test the `focused` state className (applied onFocus) */
cy.get("select").select(applicationsSorted[1].application_sid);
cy.get(".application-filter").should("have.class", "focused");
cy.get("select").blur();
cy.get(".application-filter").should("not.have.class", "focused");
});
it("renders default option", () => {
/** Test with the `defaultOption` prop */
cy.mount(
<ApplicationFilterTestWrapper defaultOption="Choose Application" />
);
/** No default value is set when this prop is present */
cy.get("select").should("have.value", "");
/** Validate that our prop renders correct default option text */
cy.get("option").first().should("have.text", "Choose Application");
});
});

View File

@@ -0,0 +1,59 @@
import React, { useState } from "react";
import { classNames } from "@jambonz/ui-kit";
import { Icons } from "src/components/icons";
import { sortLocaleName } from "src/utils";
import type { Application } from "src/api/types";
export type ApplicationFilterProps = JSX.IntrinsicElements["select"] & {
label?: string;
application: [string, React.Dispatch<React.SetStateAction<string>>];
applications?: Application[];
defaultOption?: string;
};
export const ApplicationFilter = ({
label = "Application",
application: [applicationSid, setApplicationSid],
applications,
defaultOption,
}: ApplicationFilterProps) => {
const [focus, setFocus] = useState(false);
const classes = {
smsel: true,
"smsel--filter": true,
"application-filter": true,
focused: focus,
};
return (
<div className={classNames(classes)}>
<label htmlFor="application_filter">{label}:</label>
<div>
<select
id="application_filter"
name="application_filter"
value={applicationSid}
onChange={(e) => setApplicationSid(e.target.value)}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
>
{defaultOption && <option value="">{defaultOption}</option>}
{applications &&
applications.sort(sortLocaleName).map((app) => {
return (
<option key={app.application_sid} value={app.application_sid}>
{app.name}
</option>
);
})}
</select>
<span>
<Icons.ChevronUp />
<Icons.ChevronDown />
</span>
</div>
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,249 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
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';
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: process.env.REACT_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: process.env.REACT_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;

View File

@@ -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">&times;</span>
</CloseButton>
</NotificationDiv>
))}
</NotificationContainer>
);
};
export default Notification;

View File

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

View File

@@ -1,92 +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';
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: process.env.REACT_APP_API_BASE_URL,
url: `/Sbcs?service_provider_sid=${currentServiceProvider}`,
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;

View File

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

View File

@@ -1,462 +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
) && (
<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>&#9652;</span>
: <span>&#9662;</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;

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
import React from "react";
import { Icons } from "src/components/icons";
import { toastError, toastSuccess } from "src/store";
type ClipBoardProps = {
id?: string;
name?: string;
text: string;
};
/** Clipboard support...? */
const hasClipboard = typeof navigator.clipboard !== "undefined";
export const ClipBoard = ({ text, id = "", name = "" }: ClipBoardProps) => {
const handleClick = () => {
navigator.clipboard
.writeText(text)
.then(() => {
toastSuccess(
<>
<strong>{text}</strong> copied to clipboard
</>
);
})
.catch(() => {
toastError(
<>
Unable to copy <strong>{text}</strong>, please select the text and
right click to copy
</>
);
});
};
return (
<div className="clipboard inpbtn">
<input id={id} name={name} type="text" readOnly value={text} />
{hasClipboard && (
<button
className="btnty"
type="button"
title="Copy to clipboard"
onClick={handleClick}
>
<Icons.Clipboard />
</button>
)}
</div>
);
};

View File

@@ -0,0 +1,48 @@
import React from "react";
import { Icons } from "../icons";
import "./styles.scss";
type DomainInputProbs = {
id?: string;
name?: string;
value: string;
setValue: React.Dispatch<React.SetStateAction<string>>;
root_domain: string;
placeholder?: string;
is_valid: boolean;
};
export const DomainInput = ({
id,
name,
value,
setValue,
root_domain,
is_valid,
placeholder,
}: DomainInputProbs) => {
return (
<>
<div className="clipboard clipboard-domain">
<div className="input-container">
<input
id={id}
name={name}
type="text"
value={value}
placeholder={placeholder}
onChange={(e) => setValue(e.target.value)}
/>
<div className={`input-icon txt--${is_valid ? "teal" : "red"}`}>
{is_valid ? <Icons.CheckCircle /> : <Icons.XCircle />}
</div>
</div>
<div className="root-domain">
<p>{root_domain}</p>
</div>
</div>
</>
);
};
export default DomainInput;

View File

@@ -0,0 +1,55 @@
@use "../../styles/vars";
@use "../../styles/mixins";
@use "@jambonz/ui-kit/src/styles/vars" as ui-vars;
@use "@jambonz/ui-kit/src/styles/mixins" as ui-mixins;
.input-container {
position: relative;
display: inline-block;
width: 100%;
}
.clipboard-domain {
display: flex;
align-items: center;
input[type="text"],
input[type="number"] {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
width: 100%;
height: vars.$clipheight;
&:focus-visible {
outline: 0;
}
}
.internal form & {
max-width: calc(#{vars.$widthinput} - #{vars.$clipheight});
}
.input-icon {
position: absolute;
right: 5%;
top: 50%;
transform: translateY(-50%);
border-left: 0;
}
.root-domain {
height: vars.$clipheight;
border-bottom-right-radius: ui-vars.$px01;
border-top-right-radius: ui-vars.$px01;
border: 2px solid ui-vars.$grey;
border-left: 0;
background-color: ui-vars.$pink;
padding: ui-vars.$px01;
display: flex;
align-items: center;
&[disabled] {
@include mixins.disabled();
}
}
}

View File

@@ -0,0 +1,39 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import { Icons } from "src/components/icons";
type EditBoardProps = {
id?: string;
name?: string;
text: string;
path: string;
title?: string;
};
export const EditBoard = ({
text,
id = "",
name = "",
path,
title,
}: EditBoardProps) => {
const navigate = useNavigate();
const handleClick = () => {
navigate(path);
};
return (
<div className="clipboard inpbtn">
<input id={id} name={name} type="text" readOnly value={text} />
<button
className="btnty"
type="button"
title={title ? title : "Edit"}
onClick={handleClick}
>
<Icons.Edit />
</button>
</div>
);
};

View File

@@ -1,86 +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;
& > 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;

View File

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

View File

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

View File

@@ -1,137 +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}
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);

View File

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

View File

@@ -1,73 +0,0 @@
import React, { useContext } from 'react';
import styled from 'styled-components/macro';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import Button from './Button';
const Span = styled.span`
text-align: left;
${props => props.hasBorder ? `
height: 2.25rem;
display: flex;
align-items: center;
width: 100%;
padding: 0 1rem;
border: 1px solid #B6B6B6;
border-radius: 0.125rem;
` : ''}
`;
const StyledButton = styled(Button)`
margin-left: 1rem;
`;
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,32 +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;
&: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;

View File

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

View File

@@ -1,28 +0,0 @@
import styled from 'styled-components/macro';
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;
${props => !props.large ? `
white-space: nowrap;
` : `
text-align: left;
width: 22rem;
bottom: calc(100% + 0.5rem);
`}
}
}
`;
export default Tooltip;

View File

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

View File

@@ -1,781 +0,0 @@
import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import { 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 TableMenu from '../blocks/TableMenu';
import Loader from '../blocks/Loader';
import Modal from '../blocks/Modal';
import Button from '../elements/Button';
import Link from '../elements/Link';
import Tooltip from '../elements/Tooltip';
import CopyableText from '../elements/CopyableText';
import handleErrors from "../../helpers/handleErrors";
import styled from 'styled-components/macro';
const StyledInputGroup = styled(InputGroup)`
position: relative;
display: grid;
grid-template-columns: 1fr auto;
& > label {
text-align: left;
}
& > div:last-child {
margin-top: -24px;
}
`;
const ModalContainer = styled.div`
margin-top: 2rem;
`;
const P = styled.p`
margin: 0 0 1.5rem;
font-size: 14px;
font-weight: 500;
font-weight: 500;
color: #231f20;
`;
const AccountForm = props => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const jwt = localStorage.getItem("token");
const currentServiceProvider = useContext(ServiceProviderValueContext);
// Refs
const refName = useRef(null);
const refSipRealm = useRef(null);
const refRegWebhook = useRef(null);
const refRegUser = useRef(null);
const refRegPassword = useRef(null);
const refQueueWebhook = useRef(null);
const refQueueUser = useRef(null);
const refQueuePassword = useRef(null);
// Form inputs
const [ name, setName ] = useState('');
const [ sipRealm, setSipRealm ] = useState('');
const [ deviceCallingApplication, setDeviceCallingApplication ] = useState('');
const [ regWebhook, setRegWebhook ] = useState('');
const [ regMethod, setRegMethod ] = useState('POST');
const [ regUser, setRegUser ] = useState('' || '');
const [ regPassword, setRegPassword ] = useState('' || '');
const [ webhookSecret, setWebhookSecret ] = useState('');
const [ queueWebhook, setQueueWebhook ] = useState('');
const [ queueMethod, setQueueMethod ] = useState('POST');
const [ queueUser, setQueueUser ] = useState('' || '');
const [ queuePassword, setQueuePassword ] = useState('' || '');
// Invalid form inputs
const [ invalidName, setInvalidName ] = useState(false);
const [ invalidSipRealm, setInvalidSipRealm ] = useState(false);
const [ invalidRegWebhook, setInvalidRegWebhook ] = useState(false);
const [ invalidRegUser, setInvalidRegUser ] = useState(false);
const [ invalidRegPassword, setInvalidRegPassword ] = useState(false);
const [ invalidQueueWebhook, setInvalidQueueWebhook ] = useState(false);
const [ invalidQueueUser, setInvalidQueueUser ] = useState(false);
const [ invalidQueuePassword, setInvalidQueuePassword ] = useState(false);
const [ showLoader, setShowLoader ] = useState(true);
const [ errorMessage, setErrorMessage ] = useState('');
const [ showRegAuth, setShowRegAuth ] = useState(false);
const [ showQueueAuth, setShowQueueAuth ] = useState(false);
const toggleRegAuth = () => setShowRegAuth(!showRegAuth);
const toggleQueueAuth = () => setShowQueueAuth(!showQueueAuth);
const [ accounts, setAccounts ] = useState([]);
const [ accountSid, setAccountSid ] = useState('');
const [ accountApplications, setAccountApplications ] = useState([]);
const [ menuOpen, setMenuOpen ] = useState(null);
const [showConfirmSecret, setShowConfirmSecret] = useState(false);
const [generatingSecret, setGeneratingSecret] = useState(false);
const handleMenuOpen = sid => {
if (menuOpen === sid) {
setMenuOpen(null);
} else {
setMenuOpen(sid);
}
};
const copyWebhookSecret = async e => {
e.preventDefault();
setMenuOpen(null);
try {
await navigator.clipboard.writeText(webhookSecret);
dispatch({
type: 'ADD',
level: 'success',
message: `Webhook Secret copied to clipboard`,
});
} catch (err) {
dispatch({
type: 'ADD',
level: 'error',
message: `Unable to copy Webhook Secret.`,
});
}
};
const generateWebhookSecret = async e => {
e.preventDefault();
setShowConfirmSecret(true);
setMenuOpen(null);
};
const updateWebhookSecret = async () => {
try {
setGeneratingSecret(true);
const apiKeyResponse = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/Accounts/${accountSid}/WebhookSecret?regenerate=true`,
headers: {
Authorization: `Bearer ${jwt}`,
},
});
if (apiKeyResponse.status === 200) {
setWebhookSecret(apiKeyResponse.data.webhook_secret);
dispatch({
type: 'ADD',
level: 'success',
message: 'Webhook signing secret was successfully generated.',
});
}
} catch (err) {
handleErrors({ err, history, dispatch });
} finally {
setGeneratingSecret(false);
setShowConfirmSecret(false);
}
};
useEffect(() => {
const getAccounts = async () => {
try {
if (!jwt) {
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'You must log in to view that page.',
});
return;
}
const promiseList = [];
const accountsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Accounts',
headers: {
Authorization: `Bearer ${jwt}`,
},
});
promiseList.push(accountsPromise);
if (props.type === 'edit') {
const applicationsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Applications',
headers: {
Authorization: `Bearer ${jwt}`,
},
});
promiseList.push(applicationsPromise);
}
const promiseAllValues = await Promise.all(promiseList);
const accountsData = (promiseAllValues[0] && promiseAllValues[0].data) || [];
setAccounts(accountsData);
if (props.type === 'edit') {
const allApplications = (promiseAllValues[1] && promiseAllValues[1].data) || [];
const accountApplicationsData = allApplications.filter(app => {
return app.account_sid === props.account_sid;
});
setAccountApplications(accountApplicationsData);
}
if (props.type === 'setup' && accountsData.length > 1) {
history.push('/internal/accounts');
dispatch({
type: 'ADD',
level: 'error',
message: 'That page is only accessible during setup.',
});
}
if (props.type === 'setup' || props.type === 'edit') {
const currentAccount = props.account_sid
? accountsData.filter(a => a.account_sid === props.account_sid)
: accountsData;
const noAccountMessage = props.type === 'setup'
? 'You do not have an account. Please add one through the accounts page.'
: 'That account does not exist.';
if (!currentAccount.length) {
history.push('/internal/accounts');
dispatch({
type: 'ADD',
level: 'error',
message: noAccountMessage,
});
return;
}
const acc = currentAccount[0];
setAccountSid(acc.account_sid || '');
setName(acc.name || '');
setSipRealm(acc.sip_realm || '');
setDeviceCallingApplication(acc.device_calling_application_sid || '');
setRegWebhook((acc.registration_hook && acc.registration_hook.url ) || '');
setRegMethod((acc.registration_hook && acc.registration_hook.method ) || 'post');
setRegUser((acc.registration_hook && acc.registration_hook.username) || '');
setRegPassword((acc.registration_hook && acc.registration_hook.password) || '');
setQueueWebhook((acc.queue_event_hook && acc.queue_event_hook.url ) || '');
setQueueMethod((acc.queue_event_hook && acc.queue_event_hook.method ) || 'post');
setQueueUser((acc.queue_event_hook && acc.queue_event_hook.username) || '');
setQueuePassword((acc.queue_event_hook && acc.queue_event_hook.password) || '');
setWebhookSecret(acc.webhook_secret || '');
if (
(acc.registration_hook && acc.registration_hook.username) ||
(acc.registration_hook && acc.registration_hook.password)
) {
setShowRegAuth(true);
}
if (
(acc.queue_event_hook && acc.queue_event_hook.username) ||
(acc.queue_event_hook && acc.queue_event_hook.password)
) {
setShowQueueAuth(true);
}
}
setShowLoader(false);
} 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);
}
};
getAccounts();
// eslint-disable-next-line
}, []);
const handleSubmit = async (e) => {
let isMounted = true;
try {
setShowLoader(true);
e.preventDefault();
setErrorMessage('');
setInvalidName(false);
setInvalidSipRealm(false);
setInvalidRegWebhook(false);
setInvalidRegUser(false);
setInvalidRegPassword(false);
setInvalidQueueWebhook(false);
setInvalidQueueUser(false);
setInvalidQueuePassword(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 or sip_realm are already in use
accounts.forEach(a => {
if (a.account_sid === accountSid) {
return;
}
if (a.name === name) {
errorMessages.push(
'The name you have entered is already in use on another one of your accounts.'
);
setInvalidName(true);
if (!focusHasBeenSet) {
refName.current.focus();
focusHasBeenSet = true;
}
}
if (sipRealm && a.sip_realm === sipRealm) {
errorMessages.push(
'The SIP Realm you have entered is already in use on another one of your accounts.'
);
setInvalidSipRealm(true);
if (!focusHasBeenSet) {
refSipRealm.current.focus();
focusHasBeenSet = true;
}
}
});
if ((regUser && !regPassword) || (!regUser && regPassword)) {
errorMessages.push('Registration webhook username and password must be either both filled out or both empty.');
setInvalidRegUser(true);
setInvalidRegPassword(true);
if (!focusHasBeenSet) {
if (!regUser) {
refRegUser.current.focus();
} else {
refRegPassword.current.focus();
}
focusHasBeenSet = true;
}
}
if ((queueUser && !queuePassword) || (!queueUser && queuePassword)) {
errorMessages.push('Queue event webhook username and password must be either both filled out or both empty.');
setInvalidQueueUser(true);
setInvalidQueuePassword(true);
if (!focusHasBeenSet) {
if (!queueUser) {
refQueueUser.current.focus();
} else {
refQueuePassword.current.focus();
}
focusHasBeenSet = true;
}
}
if (errorMessages.length > 1) {
setErrorMessage(errorMessages);
return;
} else if (errorMessages.length === 1) {
setErrorMessage(errorMessages[0]);
return;
}
const axiosData = {
name: name.trim(),
sip_realm: sipRealm.trim() || null,
registration_hook: {
url: regWebhook.trim(),
method: regMethod,
username: regUser.trim() || null,
password: regPassword || null,
},
queue_event_hook: {
url: queueWebhook.trim(),
method: queueMethod,
username: queueUser.trim() || null,
password: queuePassword || null,
},
webhook_secret: webhookSecret || null,
};
if (props.type === 'add') {
axiosData.service_provider_sid = currentServiceProvider;
}
if (props.type === 'edit') {
axiosData.device_calling_application_sid = deviceCallingApplication || null;
}
const url = props.type === 'add'
? `/Accounts`
: `/Accounts/${accountSid}`;
await axios({
method: props.type === 'add' ? 'post' : 'put',
baseURL: process.env.REACT_APP_API_BASE_URL,
url,
headers: {
Authorization: `Bearer ${jwt}`,
},
data: axiosData,
});
if (props.type === 'setup') {
isMounted = false;
history.push('/create-application');
} else {
isMounted = false;
history.push('/internal/accounts');
const dispatchMessage = props.type === 'add'
? 'Account created successfully'
: 'Account 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);
}
}
};
const menuItems = [
{
type: 'button',
name: 'Copy',
action: copyWebhookSecret,
},
{
type: 'button',
name: 'Generate new secret',
action: generateWebhookSecret,
},
];
return (
showLoader
? <Loader
height={
props.type === 'setup'
? '309px'
: props.type === 'edit'
? '381px'
: '292px'
}
/>
: <Form
large
wideLabel={props.type === 'edit'}
onSubmit={handleSubmit}
>
{props.type === 'edit' && (
<React.Fragment>
<Label>AccountSid</Label>
<CopyableText text={accountSid} textType="AccountSid" />
</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="Account name"
invalid={invalidName}
autoFocus
ref={refName}
/>
</React.Fragment>
)}
<Label htmlFor="sipRealm">SIP Realm</Label>
<Input
large={props.type === 'setup'}
name="sipRealm"
id="sipRealm"
value={sipRealm}
onChange={e => setSipRealm(e.target.value)}
placeholder="The domain name that SIP devices will register with"
invalid={invalidSipRealm}
autoFocus={props.type === 'setup'}
ref={refSipRealm}
/>
<Label htmlFor="webhookSecret">Webhook Secret</Label>
<StyledInputGroup>
<Label>{webhookSecret || "None"}</Label>
<TableMenu
sid="webhook"
open={menuOpen === "webhook"}
handleMenuOpen={handleMenuOpen}
menuItems={webhookSecret ? menuItems: menuItems.slice(1)}
/>
</StyledInputGroup>
{props.type === 'edit' && (
<React.Fragment>
<Label tooltip htmlFor="deviceCallingApplication">
<span style={{ position: 'relative' }}>
Application for SIP Device Calls
<Tooltip large>
This application is used to handle incoming calls from SIP users who have registered to the Accounts SIP Realm.
</Tooltip>
</span>
</Label>
<Select
large={props.type === 'setup'}
name="deviceCallingApplication"
id="deviceCallingApplication"
value={deviceCallingApplication}
onChange={e => setDeviceCallingApplication(e.target.value)}
>
<option value="">-- NONE --</option>
{accountApplications && accountApplications.map(app => (
<option
key={app.application_sid}
value={app.application_sid}
>
{app.name}
</option>
))}
</Select>
</React.Fragment>
)}
<Label htmlFor="regWebhook">Registration Webhook</Label>
<InputGroup>
<Input
large={props.type === 'setup'}
name="regWebhook"
id="regWebhook"
value={regWebhook}
onChange={e => setRegWebhook(e.target.value)}
placeholder="URL for your web application that handles registrations"
invalid={invalidRegWebhook}
ref={refRegWebhook}
/>
<Label
middle
htmlFor="method"
>
Method
</Label>
<Select
large={props.type === 'setup'}
name="method"
id="method"
value={regMethod}
onChange={e => setRegMethod(e.target.value)}
>
<option value="POST">POST</option>
<option value="GET">GET</option>
</Select>
</InputGroup>
{showRegAuth ? (
<InputGroup>
<Label indented htmlFor="user">User</Label>
<Input
large={props.type === 'setup'}
name="user"
id="user"
value={regUser || ''}
onChange={e => setRegUser(e.target.value)}
placeholder="Optional"
invalid={invalidRegUser}
ref={refRegUser}
/>
<Label htmlFor="password" middle>Password</Label>
<PasswordInput
large={props.type === 'setup'}
allowShowPassword
name="password"
id="password"
password={regPassword}
setPassword={setRegPassword}
setErrorMessage={setErrorMessage}
placeholder="Optional"
invalid={invalidRegPassword}
ref={refRegPassword}
/>
</InputGroup>
) : (
<Button
text
formLink
type="button"
onClick={toggleRegAuth}
>
Use HTTP Basic Authentication
</Button>
)}
<Label htmlFor="queueWebhook">Queue Event Webhook</Label>
<InputGroup>
<Input
large={props.type === 'setup'}
name="queueWebhook"
id="queueWebhook"
value={queueWebhook}
onChange={e => setQueueWebhook(e.target.value)}
placeholder="URL to notify when a member joins or leaves a queue"
invalid={invalidQueueWebhook}
ref={refQueueWebhook}
/>
<Label
middle
htmlFor="method"
>
Method
</Label>
<Select
large={props.type === 'setup'}
name="method"
id="method"
value={queueMethod}
onChange={e => setQueueMethod(e.target.value)}
>
<option value="POST">POST</option>
</Select>
</InputGroup>
{showQueueAuth ? (
<InputGroup>
<Label indented htmlFor="user">User</Label>
<Input
large={props.type === 'setup'}
name="user"
id="user"
value={queueUser || ''}
onChange={e => setQueueUser(e.target.value)}
placeholder="Optional"
invalid={invalidQueueUser}
ref={refQueueUser}
/>
<Label htmlFor="password" middle>Password</Label>
<PasswordInput
large={props.type === 'setup'}
allowShowPassword
name="password"
id="password"
password={queuePassword}
setPassword={setQueuePassword}
setErrorMessage={setErrorMessage}
placeholder="Optional"
invalid={invalidQueuePassword}
ref={refQueuePassword}
/>
</InputGroup>
) : (
<Button
text
formLink
type="button"
onClick={toggleQueueAuth}
>
Use HTTP Basic Authentication
</Button>
)}
{errorMessage && (
<FormError grid message={errorMessage} />
)}
<InputGroup flexEnd spaced>
{props.type === 'edit' && (
<Button
grid
gray
type="button"
onClick={() => {
history.push('/internal/accounts');
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 Account'
: 'Save'
}
</Button>
</InputGroup>
{props.type === 'setup' && (
<Link
formLink
right
to="/create-application"
>
Skip for now &mdash; I'll complete later
</Link>
)}
{showConfirmSecret && (
<Modal
title={generatingSecret ? "" : "Generate new secret"}
loader={generatingSecret}
hideButtons={generatingSecret}
maskClosable={!generatingSecret}
actionText="OK"
content={
<ModalContainer>
<P>Press OK to generate a new webhook signing secret.</P>
<P>Note: this will immediately invalidate the old webhook signing secret.</P>
</ModalContainer>
}
handleCancel={() => setShowConfirmSecret(false)}
handleSubmit={updateWebhookSecret}
/>
)}
</Form>
);
};
export default AccountForm;

View File

@@ -1,925 +0,0 @@
import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import { 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 Loader from '../blocks/Loader';
import CopyableText from '../elements/CopyableText';
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: process.env.REACT_APP_API_BASE_URL,
url: '/Accounts',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const applicationsPromise = axios({
method: 'get',
baseURL: process.env.REACT_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: process.env.REACT_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
))
: 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 = SpeechSynthesisLanguageAws.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>
</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
))
: 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>
))
) : (
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>
)))
) : (
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>
</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>
))
) : (
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;

File diff suppressed because it is too large Load Diff

View File

@@ -1,387 +0,0 @@
import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import Form from '../elements/Form';
import Input from '../elements/Input';
import Label from '../elements/Label';
import Select from '../elements/Select';
import InputGroup from '../elements/InputGroup';
import FormError from '../blocks/FormError';
import Loader from '../blocks/Loader';
import Button from '../elements/Button';
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: process.env.REACT_APP_API_BASE_URL,
url: '/MicrosoftTeamsTenants',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const accountsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Accounts',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const applicationsPromise = axios({
method: 'get',
baseURL: process.env.REACT_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: process.env.REACT_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: process.env.REACT_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;

View File

@@ -1,455 +0,0 @@
import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import { 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';
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: process.env.REACT_APP_API_BASE_URL,
url: '/VoipCarriers',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const accountsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Accounts',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const applicationsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Applications',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const phoneNumbersPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/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: process.env.REACT_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;

View File

@@ -1,368 +0,0 @@
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";
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: process.env.REACT_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: process.env.REACT_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: process.env.REACT_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;

View File

@@ -1,581 +0,0 @@
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 Radio from '../elements/Radio';
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'
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 refAccessKeyId = useRef(null);
const refSecretAccessKey = useRef(null);
const refUseForTts = useRef(null);
const refUseForStt = 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('');
// Invalid form inputs
const [ invalidVendorGoogle, setInvalidVendorGoogle ] = useState(false);
const [ invalidVendorAws, setInvalidVendorAws ] = useState(false);
const [ invalidAccessKeyId, setInvalidAccessKeyId ] = useState(false);
const [ invalidSecretAccessKey, setInvalidSecretAccessKey ] = useState(false);
const [ invalidUseForTts, setInvalidUseForTts ] = useState(false);
const [ invalidUseForStt, setInvalidUseForStt ] = 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: process.env.REACT_APP_API_BASE_URL,
url: '/Accounts',
headers: {
Authorization: `Bearer ${jwt}`,
},
});
setAccounts(accountsResponse.data);
if (type === 'edit') {
const speechCredential = await axios({
method: 'get',
baseURL: process.env.REACT_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 || '');
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);
setInvalidAccessKeyId(false);
setInvalidSecretAccessKey(false);
setInvalidUseForTts(false);
setInvalidUseForStt(false);
let errorMessages = [];
let focusHasBeenSet = false;
if (!vendor) {
errorMessages.push('Please select a vendor.');
setInvalidVendorGoogle(true);
setInvalidVendorAws(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 (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: process.env.REACT_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,
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: process.env.REACT_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: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}`,
headers: {
Authorization: `Bearer ${jwt}`,
},
});
}
if (type === 'edit') {
await axios({
method,
baseURL: process.env.REACT_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="name">Vendor</Label>
<InputGroup>
<Radio
noLeftMargin
name="vendor"
id="google"
label="Google"
checked={vendor === 'google'}
onChange={() => setVendor('google')}
invalid={invalidVendorGoogle}
ref={refVendorGoogle}
disabled={type === 'edit'}
/>
<Radio
name="vendor"
id="aws"
label="Amazon Web Services"
checked={vendor === 'aws'}
onChange={() => setVendor('aws')}
invalid={invalidVendorAws}
ref={refVendorAws}
disabled={type === 'edit'}
/>
</InputGroup>
<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>
{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'}
/>
</>
) : (
null
)}
{vendor === 'google' || vendor === 'aws' ? (
<>
<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"
checked={useForStt}
onChange={e => setUseForStt(e.target.checked)}
invalid={invalidUseForStt}
ref={refUseForStt}
/>
</>
) : (
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">
{type === 'add'
? 'Add Speech Service'
: 'Save'
}
</Button>
</StyledButtonGroup>
</Form>
)
);
};
export default SpeechServicesAddEdit;

View File

@@ -0,0 +1,69 @@
import React, { useEffect, forwardRef } from "react";
import { Selector } from "src/components/forms";
import type { Account } from "src/api/types";
import { hasLength } from "src/utils";
type AccountSelectProps = {
label?: string;
account: [string, React.Dispatch<React.SetStateAction<string>>];
accounts?: Account[];
defaultOption?: boolean;
/** Native select element attributes we support */
required?: boolean;
disabled?: boolean;
};
type SelectorRef = HTMLSelectElement;
export const AccountSelect = forwardRef<SelectorRef, AccountSelectProps>(
(
{
label = "Account",
account: [accountSid, setAccountSid],
accounts,
required = true,
defaultOption,
...restProps
}: AccountSelectProps,
ref
) => {
useEffect(() => {
if (hasLength(accounts) && !accountSid && !defaultOption) {
setAccountSid(accounts[0].account_sid);
}
}, [accounts, accountSid, defaultOption]);
return (
<>
<label htmlFor="account_sid">
{label} {required && <span>*</span>}
</label>
<Selector
ref={ref}
id="account_sid"
name="account_sid"
required={required}
value={accountSid}
options={(defaultOption
? [{ name: "All accounts", value: "" }]
: []
).concat(
hasLength(accounts)
? accounts.map((account) => ({
name: account.name,
value: account.account_sid,
}))
: []
)}
onChange={(e) => setAccountSid(e.target.value)}
{...restProps}
/>
</>
);
}
);
AccountSelect.displayName = "AccountSelect";

View File

@@ -0,0 +1,75 @@
import React, { useEffect, forwardRef } from "react";
import { Selector } from "src/components/forms";
import { hasLength } from "src/utils";
import type { Application } from "src/api/types";
import type { IMessage } from "src/store/types";
type ApplicationSelectProps = {
id?: string;
label?: IMessage;
application: [string, React.Dispatch<React.SetStateAction<string>>];
applications?: Application[];
defaultOption?: string;
/** Native select element attributes we support */
required?: boolean;
disabled?: boolean;
};
type SelectorRef = HTMLSelectElement;
export const ApplicationSelect = forwardRef<
SelectorRef,
ApplicationSelectProps
>(
(
{
id = "application_sid",
label = "Application",
application: [applicationSid, setApplicationSid],
applications,
required = false,
defaultOption,
...restProps
}: ApplicationSelectProps,
ref
) => {
useEffect(() => {
if (hasLength(applications) && !applicationSid && !defaultOption) {
setApplicationSid(applications[0].application_sid);
}
}, [applications, applicationSid, defaultOption]);
return (
<>
<label htmlFor={id}>
{label} {required && <span>*</span>}
</label>
<Selector
ref={ref}
id={id}
name={id}
required={required}
value={applicationSid}
options={(defaultOption
? [{ name: defaultOption, value: "" }]
: []
).concat(
hasLength(applications)
? applications.map((application) => ({
name: application.name,
value: application.application_sid,
}))
: []
)}
onChange={(e) => setApplicationSid(e.target.value)}
{...restProps}
/>
</>
);
}
);
ApplicationSelect.displayName = "ApplicationSelect";

Some files were not shown because too many files have changed in this diff Show More