mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-02-08 04:59:37 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5141989bb5 | ||
|
|
35889ba122 | ||
|
|
09b0bc8dde | ||
|
|
4f1b928f8c | ||
|
|
94c0fc88c1 | ||
|
|
b5a559bd08 | ||
|
|
24cb269379 | ||
|
|
c73ca9f46a | ||
|
|
25a93edeac | ||
|
|
7e2488a9c3 | ||
|
|
42872e9878 | ||
|
|
809e1ae30f | ||
|
|
04c8d05266 | ||
|
|
e3d384158f | ||
|
|
01c0aa321e | ||
|
|
30bcf9414f | ||
|
|
fb2880b465 | ||
|
|
f5f92e58e1 | ||
|
|
33734c91f6 | ||
|
|
478949ef73 | ||
|
|
3ede7d5077 | ||
|
|
d36a291543 | ||
|
|
630b555fe7 | ||
|
|
f16063ba44 | ||
|
|
b4de0ba13f | ||
|
|
60e9792726 | ||
|
|
6cda579198 |
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:alpine as builder
|
||||
RUN apk update && apk add --no-cache python3 make g++
|
||||
WORKDIR /opt/app/
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
RUN npm prune
|
||||
|
||||
FROM node:alpine as webapp
|
||||
RUN apk add curl
|
||||
WORKDIR /opt/app
|
||||
COPY . /opt/app
|
||||
COPY --from=builder /opt/app/node_modules ./node_modules
|
||||
COPY ./entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
8
entrypoint.sh
Normal file
8
entrypoint.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
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
|
||||
934
package-lock.json
generated
934
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,10 @@
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"axios": "^0.19.2",
|
||||
"antd": "^4.15.4",
|
||||
"axios": "^0.21.1",
|
||||
"moment": "^2.29.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
@@ -15,11 +18,11 @@
|
||||
"styled-components": "^5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "PORT=3001 react-scripts start",
|
||||
"start": "PORT=${HTTP_PORT:-3001} react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"serve": "serve -s build -l 3001",
|
||||
"serve": "serve -s build -l ${HTTP_PORT:-3001}",
|
||||
"pm2": "pm2 start npm --name \"jambonz-webapp\" -- run serve",
|
||||
"deploy": "npm i && npm run build && npm run pm2"
|
||||
},
|
||||
|
||||
27
src/App.js
27
src/App.js
@@ -10,16 +10,20 @@ import ConfigureSipTrunk from './components/pages/setup/ConfigureSipTrunk';
|
||||
import SetupComplete from './components/pages/setup/SetupComplete';
|
||||
import AccountsList from './components/pages/internal/AccountsList';
|
||||
import ApplicationsList from './components/pages/internal/ApplicationsList';
|
||||
import SipTrunksList from './components/pages/internal/SipTrunksList';
|
||||
import CarriersList from './components/pages/internal/CarriersList';
|
||||
import PhoneNumbersList from './components/pages/internal/PhoneNumbersList';
|
||||
import MsTeamsTenantsList from './components/pages/internal/MsTeamsTenantsList';
|
||||
import AccountsAddEdit from './components/pages/internal/AccountsAddEdit';
|
||||
import ApplicationsAddEdit from './components/pages/internal/ApplicationsAddEdit';
|
||||
import SipTrunksAddEdit from './components/pages/internal/SipTrunksAddEdit';
|
||||
import CarriersAddEdit from './components/pages/internal/CarriersAddEdit';
|
||||
import PhoneNumbersAddEdit from './components/pages/internal/PhoneNumbersAddEdit';
|
||||
import MsTeamsTenantsAddEdit from './components/pages/internal/MsTeamsTenantsAddEdit';
|
||||
import Settings from './components/pages/internal/Settings';
|
||||
import RecentCallsList from './components/pages/internal/RecentCallsList';
|
||||
import AlertsList from './components/pages/internal/AlertsList';
|
||||
import InvalidRoute from './components/pages/InvalidRoute';
|
||||
import SpeechServicesList from './components/pages/internal/SpeechServicesList';
|
||||
import SpeechServicesAddEdit from './components/pages/internal/SpeechServicesAddEdit';
|
||||
|
||||
import Notification from './components/blocks/Notification';
|
||||
import Nav from './components/blocks/Nav';
|
||||
@@ -44,7 +48,8 @@ function App() {
|
||||
<SideMenu />
|
||||
<Route exact path="/internal/accounts"><AccountsList /></Route>
|
||||
<Route exact path="/internal/applications"><ApplicationsList /></Route>
|
||||
<Route exact path="/internal/sip-trunks"><SipTrunksList /></Route>
|
||||
<Route exact path="/internal/carriers"><CarriersList /></Route>
|
||||
<Route exact path="/internal/speech-services"><SpeechServicesList /></Route>
|
||||
<Route exact path="/internal/phone-numbers"><PhoneNumbersList /></Route>
|
||||
<Route exact path="/internal/ms-teams-tenants"><MsTeamsTenantsList /></Route>
|
||||
|
||||
@@ -63,10 +68,17 @@ function App() {
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/sip-trunks/add",
|
||||
"/internal/sip-trunks/:voip_carrier_sid/edit"
|
||||
"/internal/carriers/add",
|
||||
"/internal/carriers/:voip_carrier_sid/edit"
|
||||
]}>
|
||||
<SipTrunksAddEdit />
|
||||
<CarriersAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/speech-services/add",
|
||||
"/internal/speech-services/:speech_service_sid/edit"
|
||||
]}>
|
||||
<SpeechServicesAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
@@ -84,6 +96,9 @@ function App() {
|
||||
</Route>
|
||||
|
||||
<Route exact path="/internal/settings"><Settings /></Route>
|
||||
|
||||
<Route exact path="/internal/recent-calls"><RecentCallsList /></Route>
|
||||
<Route exact path="/internal/alerts"><AlertsList /></Route>
|
||||
</div>
|
||||
</Route>
|
||||
|
||||
|
||||
89
src/components/blocks/AntdTable.js
Normal file
89
src/components/blocks/AntdTable.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components/macro";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import Loader from "../../components/blocks/Loader";
|
||||
|
||||
import Table from "antd/lib/table";
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
width: 100%;
|
||||
margin-top: 1rem !important;
|
||||
|
||||
table {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
|
||||
tr,
|
||||
th,
|
||||
td {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
height: 32px;
|
||||
|
||||
.ant-pagination-simple-pager {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-item {
|
||||
border: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLoader = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const AntdTable = ({ dataSource, columns, loading, ...rest }) => {
|
||||
let props = {
|
||||
...rest,
|
||||
dataSource,
|
||||
columns,
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
props = {
|
||||
...props,
|
||||
loading: {
|
||||
spinning: true,
|
||||
indicator: (
|
||||
<StyledLoader>
|
||||
<Loader />
|
||||
</StyledLoader>
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return <StyledTable {...props} />;
|
||||
};
|
||||
|
||||
AntdTable.propTypes = {
|
||||
dataSource: PropTypes.array,
|
||||
loading: PropTypes.bool,
|
||||
columns: PropTypes.array,
|
||||
};
|
||||
|
||||
AntdTable.defaultProps = {
|
||||
dataSource: [],
|
||||
loading: false,
|
||||
columns: [],
|
||||
};
|
||||
|
||||
export default AntdTable;
|
||||
@@ -25,6 +25,8 @@ const ModalContainer = styled.div`
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
background: #FFF;
|
||||
text-align: left;
|
||||
|
||||
& h1 {
|
||||
margin-top: 0;
|
||||
font-size: 1.25rem;
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import React, { useContext } from 'react';
|
||||
/* 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;
|
||||
@@ -14,13 +27,6 @@ const StyledNav = styled.nav`
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.12);
|
||||
`;
|
||||
|
||||
const NavH1 = styled.h1`
|
||||
margin: 1.25rem 0 1.25rem 2rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: normal;
|
||||
line-height: 1em;
|
||||
`;
|
||||
|
||||
const LogOutContainer = styled.div`
|
||||
margin-right: 3rem;
|
||||
@media (max-width: 34rem) {
|
||||
@@ -28,10 +34,53 @@ const LogOutContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled(ReactRouterLink)`
|
||||
text-decoration: none;
|
||||
margin: 0 0 0 2rem;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledForm = styled(Form)`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
`;
|
||||
|
||||
const StyledLabel = styled(Label)`
|
||||
margin-right: 1rem;
|
||||
`;
|
||||
|
||||
const ModalContainer = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
width: 500;
|
||||
`;
|
||||
|
||||
const StyledFormError = styled(FormError)`
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
const Nav = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const setCurrentServiceProvider = useContext(ServiceProviderMethodContext);
|
||||
const [serviceProviders, setServiceProviders] = useState([]);
|
||||
const [showServiceProviderModal, setShowServiceProviderModal] = useState(false);
|
||||
const [showModalLoader, setShowModalLoader] = useState(false);
|
||||
const [serviceProviderName, setServiceProviderName] = useState("");
|
||||
const [serviceProviderInvalid, setServiceProviderInvalid] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const refServiceProvider = useRef(null);
|
||||
|
||||
const logOut = () => {
|
||||
localStorage.removeItem('token');
|
||||
@@ -44,9 +93,112 @@ const Nav = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeServiceProvider = (sp) => {
|
||||
if (sp === "add") {
|
||||
setShowServiceProviderModal(true);
|
||||
} else {
|
||||
setCurrentServiceProvider(sp);
|
||||
}
|
||||
};
|
||||
|
||||
const getServiceProviders = async () => {
|
||||
const jwt = localStorage.getItem('token');
|
||||
if (history.location.pathname !== '' && jwt) {
|
||||
const serviceProvidersResponse = await axios({
|
||||
method: 'get',
|
||||
baseURL: 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>
|
||||
<NavH1>Jambonz</NavH1>
|
||||
<StyledLink to="/internal/accounts">
|
||||
<img src={LogoJambong} alt="link-img" />
|
||||
</StyledLink>
|
||||
{location.pathname !== '/' && (
|
||||
<StyledForm>
|
||||
<StyledLabel htmlFor="serviceProvider">Service Provider:</StyledLabel>
|
||||
<Select
|
||||
name="serviceProvider"
|
||||
id="serviceProvider"
|
||||
value={currentServiceProvider}
|
||||
onChange={e => onChangeServiceProvider(e.target.value)}
|
||||
>
|
||||
{serviceProviders.map(a => (
|
||||
<option
|
||||
key={a.service_provider_sid}
|
||||
value={a.service_provider_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<AddModalButton
|
||||
addButtonText="Add Service Provider"
|
||||
onClick={()=>setShowServiceProviderModal(true)}
|
||||
/>
|
||||
</StyledForm>
|
||||
|
||||
)}
|
||||
{location.pathname !== '/' && (
|
||||
<LogOutContainer>
|
||||
<Button
|
||||
@@ -59,6 +211,37 @@ const Nav = () => {
|
||||
</Button>
|
||||
</LogOutContainer>
|
||||
)}
|
||||
{showServiceProviderModal && (
|
||||
<Modal
|
||||
title="Add New Service Provider"
|
||||
loader={showModalLoader}
|
||||
closeText="Close"
|
||||
actionText="Add"
|
||||
handleCancel={() => {
|
||||
setServiceProviderName("");
|
||||
setShowServiceProviderModal(false);
|
||||
}}
|
||||
handleSubmit={handleAddServiceProvider}
|
||||
content={
|
||||
<ModalContainer>
|
||||
<StyledLabel htmlFor="name">Name:</StyledLabel>
|
||||
<Input
|
||||
name="name"
|
||||
id="name"
|
||||
value={serviceProviderName}
|
||||
onChange={e => setServiceProviderName(e.target.value)}
|
||||
placeholder="Service provider name"
|
||||
invalid={serviceProviderInvalid}
|
||||
autoFocus
|
||||
ref={refServiceProvider}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<StyledFormError grid message={errorMessage} />
|
||||
)}
|
||||
</ModalContainer>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StyledNav>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
@@ -20,6 +21,7 @@ const Container = styled.div`
|
||||
const Sbcs = props => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const [ sbcs, setSbcs ] = useState('');
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
@@ -36,7 +38,7 @@ const Sbcs = props => {
|
||||
const sbcResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Sbcs',
|
||||
url: `/Sbcs?service_provider_sid=${currentServiceProvider}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
|
||||
@@ -5,10 +5,13 @@ import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
import { ShowMsTeamsStateContext, ShowMsTeamsDispatchContext } from '../../contexts/ShowMsTeamsContext';
|
||||
import { ReactComponent as AccountsIcon } from '../../images/AccountsIcon.svg';
|
||||
import { ReactComponent as ApplicationsIcon } from '../../images/ApplicationsIcon.svg';
|
||||
import { ReactComponent as SipTrunksIcon } from '../../images/SipTrunksIcon.svg';
|
||||
import { ReactComponent as CarriersIcon } from '../../images/CarriersIcon.svg';
|
||||
import { ReactComponent as PhoneNumbersIcon } from '../../images/PhoneNumbersIcon.svg';
|
||||
import { ReactComponent as MsTeamsIcon } from '../../images/MsTeamsIcon.svg';
|
||||
import { ReactComponent as SettingsIcon } from '../../images/SettingsIcon.svg';
|
||||
import { ReactComponent as RecentCallsIcon } from '../../images/RecentCallsIcon.svg';
|
||||
import { ReactComponent as AlertsIcon } from '../../images/AlertsIcon.svg';
|
||||
import { ReactComponent as SpeechIcon } from '../../images/SpeechIcon.svg';
|
||||
|
||||
const StyledSideMenu = styled.div`
|
||||
width: 15rem;
|
||||
@@ -32,14 +35,12 @@ const StyledNavLink = styled(NavLink).attrs({ activeClassName })`
|
||||
fill: #565656;
|
||||
|
||||
&.${activeClassName} {
|
||||
box-shadow: inset 3px 0 0 0 #D91C5C;
|
||||
color: #D91C5C;
|
||||
fill: #D91C5C;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
box-shadow: inset 0 0 0 3px #D91C5C;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -68,6 +69,13 @@ const MenuText = styled.span`
|
||||
outline: 0;
|
||||
`;
|
||||
|
||||
const StyledH2 = styled.h2`
|
||||
margin: 3rem 0 1rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #757575;
|
||||
`;
|
||||
|
||||
const MenuLink = props => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
return (
|
||||
@@ -95,14 +103,18 @@ const SideMenu = () => {
|
||||
}, []);
|
||||
return (
|
||||
<StyledSideMenu>
|
||||
<MenuLink to="/internal/settings" name="Settings" icon={<SettingsIcon />} />
|
||||
<MenuLink to="/internal/accounts" name="Accounts" icon={<AccountsIcon />} />
|
||||
<MenuLink to="/internal/applications" name="Applications" icon={<ApplicationsIcon />} />
|
||||
<MenuLink to="/internal/sip-trunks" name="SIP Trunks" icon={<SipTrunksIcon />} />
|
||||
<MenuLink to="/internal/recent-calls" name="Recent Calls" icon={<RecentCallsIcon />} />
|
||||
<MenuLink to="/internal/alerts" name="Alerts" icon={<AlertsIcon />} />
|
||||
<StyledH2>Bring Your Own Services</StyledH2>
|
||||
<MenuLink to="/internal/carriers" name="Carriers" icon={<CarriersIcon />} />
|
||||
<MenuLink to="/internal/speech-services" name="Speech" icon={<SpeechIcon />} />
|
||||
<MenuLink to="/internal/phone-numbers" name="Phone Numbers" icon={<PhoneNumbersIcon />} />
|
||||
{showMsTeams && (
|
||||
<MenuLink to="/internal/ms-teams-tenants" name="MS Teams Tenants" icon={<MsTeamsIcon />} />
|
||||
)}
|
||||
<MenuLink to="/internal/settings" name="Settings" icon={<SettingsIcon />} />
|
||||
</StyledSideMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,8 @@ import Modal from '../blocks/Modal.js';
|
||||
import FormError from '../blocks/FormError.js';
|
||||
import CopyableText from '../elements/CopyableText';
|
||||
import ToggleText from '../blocks/ToggleText.js';
|
||||
import { ReactComponent as CheckGreen } from '../../images/CheckGreen.svg';
|
||||
import { ReactComponent as ErrorIcon } from '../../images/ErrorIcon.svg';
|
||||
|
||||
const Td = styled.td`
|
||||
padding: 0.5rem 0;
|
||||
@@ -91,7 +93,7 @@ const TableContent = props => {
|
||||
};
|
||||
getNewContent();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
}, [props.getContent]);
|
||||
|
||||
//=============================================================================
|
||||
// Handle checkboxes
|
||||
@@ -389,6 +391,11 @@ const TableContent = props => {
|
||||
columnTitle = columnContent;
|
||||
} else if (a[c.key].type === 'masked') {
|
||||
columnContent = <ToggleText masked={a[c.key].masked} revealed={a[c.key].revealed} />;
|
||||
} else if (a[c.key].type === 'status') {
|
||||
columnContent = a[c.key].content === 'ok' ? <CheckGreen />
|
||||
: a[c.key].content === 'fail' ? <ErrorIcon />
|
||||
: a[c.key].content;
|
||||
columnTitle = a[c.key].title;
|
||||
}
|
||||
} else {
|
||||
columnContent = a[c.key];
|
||||
|
||||
@@ -70,6 +70,7 @@ const TableMenu = props => (
|
||||
selected={props.open}
|
||||
disabled={props.disabled}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
props.handleMenuOpen(props.sid);
|
||||
}}
|
||||
|
||||
@@ -51,6 +51,7 @@ const StyledLink = styled(FilteredLink)`
|
||||
|
||||
const Tooltip = styled.span`
|
||||
display: none;
|
||||
color: #767676;
|
||||
a:focus > &,
|
||||
a:hover > & {
|
||||
display: inline;
|
||||
|
||||
84
src/components/elements/AddModalButton.js
Normal file
84
src/components/elements/AddModalButton.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
|
||||
const FilteredLink = ({ addButtonText, ...props }) => (
|
||||
<Link {...props}>{props.children}</Link>
|
||||
);
|
||||
|
||||
const StyledLink = styled(FilteredLink)`
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
grid-column: 2;
|
||||
border-radius: 50%;
|
||||
text-decoration: none;
|
||||
color: #565656;
|
||||
margin-left: 1rem;
|
||||
position: relative;
|
||||
& > span:first-child {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
border-radius: 50%;
|
||||
outline: 0;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
font-size: 2.5rem;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
&:focus > span:first-child {
|
||||
border: 0.25rem solid #890934;
|
||||
}
|
||||
|
||||
&:hover > span:first-child {
|
||||
}
|
||||
|
||||
&:active > span:first-child {
|
||||
}
|
||||
`;
|
||||
|
||||
const Tooltip = styled.span`
|
||||
display: none;
|
||||
color: #767676;
|
||||
a:focus > &,
|
||||
a:hover > & {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
left: calc(100% + 0.75rem);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #FFF;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
z-index: 60;
|
||||
}
|
||||
`;
|
||||
|
||||
const AddModalButton = props => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
const history = useHistory();
|
||||
return (
|
||||
<StyledLink
|
||||
{...props}
|
||||
to={history.location.pathname}
|
||||
tabIndex={modalOpen ? '-1' : ''}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
+
|
||||
</span>
|
||||
<Tooltip>{props.addButtonText}</Tooltip>
|
||||
</StyledLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddModalButton;
|
||||
14
src/components/elements/Code.js
Normal file
14
src/components/elements/Code.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Code = styled.code`
|
||||
white-space: pre-wrap;
|
||||
text-align: left;
|
||||
background: #fbfbfb;
|
||||
padding: 0.5rem;
|
||||
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #B6B6B6;
|
||||
max-height: 120px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export default Code;
|
||||
65
src/components/elements/FileUpload.js
Normal file
65
src/components/elements/FileUpload.js
Normal file
@@ -0,0 +1,65 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
const FileUpload = (props, ref) => {
|
||||
return (
|
||||
<Container>
|
||||
<StyledInput
|
||||
id={props.id}
|
||||
name={props.id}
|
||||
type="file"
|
||||
onChange={props.onChange}
|
||||
validFile={props.validFile}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(FileUpload);
|
||||
@@ -24,6 +24,7 @@ const StyledReactRouterLink = styled(FilteredLink)`
|
||||
align-items: center;
|
||||
position: relative;
|
||||
outline: 0;
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
@@ -31,14 +32,18 @@ const StyledReactRouterLink = styled(FilteredLink)`
|
||||
margin: -0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0 0 0.125rem #D91C5C;
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
box-shadow: 0 0.125rem 0 #D91C5C;
|
||||
border-radius: 0;
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
&:active > span {}
|
||||
&:active > span {
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
${props => props.formLink && `
|
||||
grid-column: 2;
|
||||
|
||||
119
src/components/elements/Radio.js
Normal file
119
src/components/elements/Radio.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import Label from './Label';
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
const RadioContainer = styled.div`
|
||||
margin-left: ${props => props.noLeftMargin
|
||||
? '-0.5rem'
|
||||
: '0.5rem'
|
||||
};
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.25rem;
|
||||
padding: 0 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.125rem;
|
||||
${props => props.invalid && `
|
||||
border-color: #D91C5C;
|
||||
background: RGBA(217,28,92,0.2);
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledRadio = styled.input`
|
||||
outline: none;
|
||||
margin: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled(Label)`
|
||||
padding-left: 0.5rem;
|
||||
cursor: ${props => props.disabled
|
||||
? 'not-allowed'
|
||||
: 'pointer'
|
||||
};
|
||||
${props => props.tooltip && `
|
||||
& > span {
|
||||
border-bottom: 1px dotted;
|
||||
border-left: 1px solid transparent;
|
||||
cursor: help;
|
||||
}
|
||||
`}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.375rem;
|
||||
left: 0.5rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 1px solid #A5A5A5;
|
||||
border-radius: 50%;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
input:focus + &::before {
|
||||
border-color: #565656;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
input:checked + &::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0.75rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 50%;
|
||||
background: ${props => props.disabled
|
||||
? '#959595'
|
||||
: '#707070'
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
const Radio = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<RadioContainer
|
||||
invalid={props.invalid}
|
||||
noLeftMargin={props.noLeftMargin}
|
||||
>
|
||||
<StyledRadio
|
||||
name={props.name}
|
||||
id={props.id}
|
||||
value={props.id}
|
||||
type="radio"
|
||||
checked={props.checked}
|
||||
onChange={props.onChange}
|
||||
ref={inputRef}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
<StyledLabel
|
||||
htmlFor={props.id}
|
||||
tooltip={props.tooltip}
|
||||
invalid={props.invalid}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
<span>
|
||||
{props.label}
|
||||
{
|
||||
props.tooltip &&
|
||||
<Tooltip>
|
||||
{props.tooltip}
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
</StyledLabel>
|
||||
</RadioContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Radio);
|
||||
@@ -9,15 +9,46 @@ 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");
|
||||
|
||||
// Refs
|
||||
const refName = useRef(null);
|
||||
@@ -34,6 +65,7 @@ const AccountForm = props => {
|
||||
const [ method, setMethod ] = useState('POST');
|
||||
const [ user, setUser ] = useState('' || '');
|
||||
const [ password, setPassword ] = useState('' || '');
|
||||
const [ webhookSecret, setWebhookSecret ] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidName, setInvalidName ] = useState(false);
|
||||
@@ -53,10 +85,76 @@ const AccountForm = props => {
|
||||
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
|
||||
const [ accountApplications, setAccountApplications ] = useState([]);
|
||||
|
||||
const [ menuOpen, setMenuOpen ] = useState(null);
|
||||
const [showConfirmSecret, setShowConfirmSecret] = useState(false);
|
||||
const [generatingSecret, setGeneratingSecret] = useState(false);
|
||||
|
||||
const handleMenuOpen = sid => {
|
||||
if (menuOpen === sid) {
|
||||
setMenuOpen(null);
|
||||
} else {
|
||||
setMenuOpen(sid);
|
||||
}
|
||||
};
|
||||
|
||||
const 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 (!localStorage.getItem('token')) {
|
||||
if (!jwt) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
@@ -72,7 +170,7 @@ const AccountForm = props => {
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
promiseList.push(accountsPromise);
|
||||
@@ -83,7 +181,7 @@ const AccountForm = props => {
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
promiseList.push(applicationsPromise);
|
||||
@@ -95,7 +193,7 @@ const AccountForm = props => {
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/ServiceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
promiseList.push(serviceProvidersPromise);
|
||||
@@ -155,6 +253,7 @@ const AccountForm = props => {
|
||||
setMethod((acc.registration_hook && acc.registration_hook.method ) || 'post');
|
||||
setUser((acc.registration_hook && acc.registration_hook.username) || '');
|
||||
setPassword((acc.registration_hook && acc.registration_hook.password) || '');
|
||||
setWebhookSecret(acc.webhook_secret || '');
|
||||
|
||||
if (
|
||||
(acc.registration_hook && acc.registration_hook.username) ||
|
||||
@@ -274,6 +373,7 @@ const AccountForm = props => {
|
||||
username: user.trim() || null,
|
||||
password: password || null,
|
||||
},
|
||||
webhook_secret: webhookSecret || null,
|
||||
};
|
||||
|
||||
if (props.type === 'add') {
|
||||
@@ -293,7 +393,7 @@ const AccountForm = props => {
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
data: axiosData,
|
||||
});
|
||||
@@ -337,6 +437,19 @@ const AccountForm = props => {
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
type: 'button',
|
||||
name: 'Copy',
|
||||
action: copyWebhookSecret,
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'Generate new secret',
|
||||
action: generateWebhookSecret,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
showLoader
|
||||
? <Loader
|
||||
@@ -390,6 +503,16 @@ const AccountForm = props => {
|
||||
autoFocus={props.type === 'setup'}
|
||||
ref={refSipRealm}
|
||||
/>
|
||||
<Label htmlFor="webhookSecret">Webhook Secret</Label>
|
||||
<StyledInputGroup>
|
||||
<Label>{webhookSecret || "None"}</Label>
|
||||
<TableMenu
|
||||
sid="webhook"
|
||||
open={menuOpen === "webhook"}
|
||||
handleMenuOpen={handleMenuOpen}
|
||||
menuItems={webhookSecret ? menuItems: menuItems.slice(1)}
|
||||
/>
|
||||
</StyledInputGroup>
|
||||
|
||||
{props.type === 'edit' && (
|
||||
<React.Fragment>
|
||||
@@ -536,6 +659,23 @@ const AccountForm = props => {
|
||||
Skip for now — I'll complete later
|
||||
</Link>
|
||||
)}
|
||||
{showConfirmSecret && (
|
||||
<Modal
|
||||
title={generatingSecret ? "" : "Generate new secret"}
|
||||
loader={generatingSecret}
|
||||
hideButtons={generatingSecret}
|
||||
maskClosable={!generatingSecret}
|
||||
actionText="OK"
|
||||
content={
|
||||
<ModalContainer>
|
||||
<P>Press OK to generate a new webhook signing secret.</P>
|
||||
<P>Note: this will immediately invalidate the old webhook signing secret.</P>
|
||||
</ModalContainer>
|
||||
}
|
||||
handleCancel={() => setShowConfirmSecret(false)}
|
||||
handleSubmit={updateWebhookSecret}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ 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';
|
||||
|
||||
@@ -29,6 +30,8 @@ const ApplicationForm = props => {
|
||||
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('');
|
||||
@@ -41,6 +44,10 @@ const ApplicationForm = props => {
|
||||
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');
|
||||
@@ -48,14 +55,16 @@ const ApplicationForm = props => {
|
||||
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 [ 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('');
|
||||
@@ -66,6 +75,9 @@ const ApplicationForm = props => {
|
||||
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([]);
|
||||
@@ -167,6 +179,10 @@ const ApplicationForm = props => {
|
||||
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 || '');
|
||||
@@ -187,6 +203,13 @@ const ApplicationForm = props => {
|
||||
) {
|
||||
setShowStatusAuth(true);
|
||||
}
|
||||
|
||||
if (
|
||||
(app.messaging_hook && app.messaging_hook.username) ||
|
||||
(app.messaging_hook && app.messaging_hook.password)
|
||||
) {
|
||||
setShowMessagingAuth(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
setShowLoader(false);
|
||||
@@ -230,6 +253,8 @@ const ApplicationForm = props => {
|
||||
setInvalidStatusWebhook(false);
|
||||
setInvalidStatusWebhookUser(false);
|
||||
setInvalidStatusWebhookPass(false);
|
||||
setInvalidMessagingWebhookUser(false);
|
||||
setInvalidMessagingWebhookPass(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
@@ -299,6 +324,20 @@ const ApplicationForm = props => {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -335,6 +374,12 @@ const ApplicationForm = props => {
|
||||
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,
|
||||
@@ -604,6 +649,75 @@ const ApplicationForm = props => {
|
||||
|
||||
<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
|
||||
@@ -727,9 +841,25 @@ const ApplicationForm = props => {
|
||||
name="speechRecognizerVendor"
|
||||
id="speechRecognizerVendor"
|
||||
value={speechRecognizerVendor}
|
||||
onChange={e => setSpeechRecognizerVendor(e.target.value)}
|
||||
onChange={e => {
|
||||
setSpeechRecognizerVendor(e.target.value);
|
||||
|
||||
// Google and AWS have different language lists. If the newly chosen
|
||||
// vendor doesn't have the same language that was already in use,
|
||||
// select US English
|
||||
if ((
|
||||
e.target.value === 'google' &&
|
||||
!SpeechRecognizerLanguageGoogle.some(l => l.code === speechRecognizerLanguage)
|
||||
) || (
|
||||
e.target.value === 'aws' &&
|
||||
!SpeechRecognizerLanguageAws.some(l => l.code === speechRecognizerLanguage)
|
||||
)) {
|
||||
setSpeechRecognizerLanguage('en-US');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="google">Google</option>
|
||||
<option value="aws">AWS</option>
|
||||
</Select>
|
||||
<Label middle htmlFor="speechRecognizerLanguage">Language</Label>
|
||||
<Select
|
||||
@@ -739,9 +869,15 @@ const ApplicationForm = props => {
|
||||
value={speechRecognizerLanguage}
|
||||
onChange={e => setSpeechRecognizerLanguage(e.target.value)}
|
||||
>
|
||||
{SpeechRecognizerLanguageGoogle.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))}
|
||||
{speechRecognizerVendor === 'google' ? (
|
||||
SpeechRecognizerLanguageGoogle.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
) : (
|
||||
SpeechRecognizerLanguageAws.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
|
||||
1152
src/components/forms/CarrierForm.js
Normal file
1152
src/components/forms/CarrierForm.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -128,7 +128,7 @@ const PhoneNumberForm = props => {
|
||||
history.push('/internal/accounts');
|
||||
return;
|
||||
} else if (!sipTrunks.length) {
|
||||
history.push('/internal/sip-trunks');
|
||||
history.push('/internal/carriers');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,35 +6,29 @@ import { ShowMsTeamsDispatchContext } from '../../contexts/ShowMsTeamsContext';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
import Select from '../elements/Select';
|
||||
import Checkbox from '../elements/Checkbox';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Button from '../elements/Button';
|
||||
import Loader from '../blocks/Loader';
|
||||
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
import handleErrors from "../../helpers/handleErrors";
|
||||
|
||||
const SettingsForm = () => {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const refreshMsTeamsData = useContext(ShowMsTeamsDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Refs
|
||||
const refEnableMsTeams = useRef(null);
|
||||
const refSbcDomainName = useRef(null);
|
||||
const refSipRealm = useRef(null);
|
||||
const refRegWebhook = useRef(null);
|
||||
const refUser = useRef(null);
|
||||
const refPassword = useRef(null);
|
||||
const refServiceProviderName = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ enableMsTeams, setEnableMsTeams ] = useState(false);
|
||||
const [ sbcDomainName, setSbcDomainName ] = useState('');
|
||||
const [ sipRealm, setSipRealm ] = useState('');
|
||||
const [ regWebhook, setRegWebhook ] = useState('');
|
||||
const [ method, setMethod ] = useState('POST');
|
||||
const [ user, setUser ] = useState('');
|
||||
const [ password, setPassword ] = useState('');
|
||||
const [serviceProviderName, setServiceProviderName] = useState('');
|
||||
|
||||
// For when user has data in sbcDomainName and then taps the checkbox to disable MsTeams
|
||||
const [ savedSbcDomainName, setSavedSbcDomainName ] = useState('');
|
||||
@@ -42,17 +36,11 @@ const SettingsForm = () => {
|
||||
// Invalid form inputs
|
||||
const [ invalidEnableMsTeams, setInvalidEnableMsTeams ] = useState(false);
|
||||
const [ invalidSbcDomainName, setInvalidSbcDomainName ] = useState(false);
|
||||
const [ invalidSipRealm, setInvalidSipRealm ] = useState(false);
|
||||
const [ invalidRegWebhook, setInvalidRegWebhook ] = useState(false);
|
||||
const [ invalidUser, setInvalidUser ] = useState(false);
|
||||
const [ invalidPassword, setInvalidPassword ] = useState(false);
|
||||
const [invalidServiceProviderName, setInvalidServiceProviderName] = useState(false);
|
||||
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
const [ showAuth, setShowAuth ] = useState(false);
|
||||
const toggleAuth = () => setShowAuth(!showAuth);
|
||||
|
||||
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -71,56 +59,30 @@ const SettingsForm = () => {
|
||||
const serviceProvidersResponse = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/ServiceProviders',
|
||||
url: `/ServiceProviders/${currentServiceProvider}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const sp = serviceProvidersResponse.data[0];
|
||||
const sp = serviceProvidersResponse.data;
|
||||
|
||||
setServiceProviderName(sp.name || '');
|
||||
setServiceProviderSid(sp.service_provider_sid || '');
|
||||
setEnableMsTeams(sp.ms_teams_fqdn ? true : false);
|
||||
setSbcDomainName(sp.ms_teams_fqdn || '');
|
||||
setSipRealm(sp.root_domain || '');
|
||||
setRegWebhook((sp.registration_hook && sp.registration_hook.url) || '');
|
||||
setMethod((sp.registration_hook && sp.registration_hook.method) || 'post');
|
||||
setUser((sp.registration_hook && sp.registration_hook.username) || '');
|
||||
setPassword((sp.registration_hook && sp.registration_hook.password) || '');
|
||||
|
||||
if (
|
||||
(sp.registration_hook && sp.registration_hook.username) ||
|
||||
(sp.registration_hook && sp.registration_hook.password)
|
||||
) {
|
||||
setShowAuth(true);
|
||||
}
|
||||
|
||||
setShowLoader(false);
|
||||
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
getSettingsData();
|
||||
|
||||
if (currentServiceProvider) {
|
||||
getSettingsData();
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
const toggleMsTeams = (e) => {
|
||||
if (!e.target.checked && sbcDomainName) {
|
||||
@@ -145,16 +107,23 @@ const SettingsForm = () => {
|
||||
setErrorMessage('');
|
||||
setInvalidEnableMsTeams(false);
|
||||
setInvalidSbcDomainName(false);
|
||||
setInvalidSipRealm(false);
|
||||
setInvalidRegWebhook(false);
|
||||
setInvalidUser(false);
|
||||
setInvalidPassword(false);
|
||||
setInvalidServiceProviderName(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
//=============================================================================
|
||||
// data checks
|
||||
//=============================================================================
|
||||
if (!serviceProviderName.trim()) {
|
||||
errorMessages.push(
|
||||
'Please enter a Service Provider Name.'
|
||||
);
|
||||
setInvalidServiceProviderName(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refServiceProviderName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
if (enableMsTeams && !sbcDomainName) {
|
||||
errorMessages.push(
|
||||
'You must provide an SBC Domain Name in order to enable Microsoft Teams Direct Routing'
|
||||
@@ -177,42 +146,6 @@ const SettingsForm = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!sipRealm && (regWebhook || user || password)) {
|
||||
errorMessages.push(
|
||||
'You must provide a SIP Realm in order to provide a Registration Webhook'
|
||||
);
|
||||
setInvalidSipRealm(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refSipRealm.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (sipRealm && !regWebhook) {
|
||||
errorMessages.push(
|
||||
'You must provide a Registration Webhook when providing a SIP Realm'
|
||||
);
|
||||
setInvalidRegWebhook(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refRegWebhook.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((user && !password) || (!user && password)) {
|
||||
errorMessages.push('Username and password must be either both filled out or both empty.');
|
||||
setInvalidUser(true);
|
||||
setInvalidPassword(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!user) {
|
||||
refUser.current.focus();
|
||||
} else {
|
||||
refPassword.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
@@ -226,18 +159,9 @@ const SettingsForm = () => {
|
||||
//=============================================================================
|
||||
const data = {
|
||||
ms_teams_fqdn: sbcDomainName.trim() || null,
|
||||
root_domain: sipRealm.trim() || null,
|
||||
name: serviceProviderName.trim(),
|
||||
};
|
||||
|
||||
if (regWebhook) {
|
||||
data.registration_hook = {
|
||||
url: regWebhook.trim() || null,
|
||||
method,
|
||||
username: user.trim() || null,
|
||||
password: password || null,
|
||||
};
|
||||
}
|
||||
|
||||
await axios({
|
||||
method: 'put',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
@@ -291,6 +215,15 @@ const SettingsForm = () => {
|
||||
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
|
||||
@@ -316,117 +249,6 @@ const SettingsForm = () => {
|
||||
title={(!enableMsTeams && "You must enable Microsoft Teams Direct Routing in order to provide an SBC Domain Name") || ""}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="sipRealm">Fallback SIP Realm</Label>
|
||||
<Input
|
||||
name="sipRealm"
|
||||
id="sipRealm"
|
||||
value={sipRealm}
|
||||
onChange={e => setSipRealm(e.target.value)}
|
||||
placeholder="Domain name that accounts will use as a fallback"
|
||||
invalid={invalidSipRealm}
|
||||
autoFocus={!enableMsTeams}
|
||||
ref={refSipRealm}
|
||||
/>
|
||||
|
||||
<Label htmlFor="regWebhook">Registration Webhook</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
name="regWebhook"
|
||||
id="regWebhook"
|
||||
value={regWebhook}
|
||||
onChange={e => setRegWebhook(e.target.value)}
|
||||
placeholder="URL for your web application that handles registrations"
|
||||
invalid={invalidRegWebhook}
|
||||
ref={refRegWebhook}
|
||||
disabled={!sipRealm && !regWebhook && !user && !password}
|
||||
title={(
|
||||
!sipRealm &&
|
||||
!regWebhook &&
|
||||
!user &&
|
||||
!password &&
|
||||
"You must provide a SIP Realm in order to enter a registration webhook"
|
||||
) || ""}
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="method"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
name="method"
|
||||
id="method"
|
||||
value={method}
|
||||
onChange={e => setMethod(e.target.value)}
|
||||
disabled={!sipRealm && !regWebhook && !user && !password}
|
||||
title={(
|
||||
!sipRealm &&
|
||||
!regWebhook &&
|
||||
!user &&
|
||||
!password &&
|
||||
"You must provide a SIP Realm in order to enter a registration webhook"
|
||||
) || ""}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="user">User</Label>
|
||||
<Input
|
||||
name="user"
|
||||
id="user"
|
||||
value={user || ''}
|
||||
onChange={e => setUser(e.target.value)}
|
||||
placeholder="Optional"
|
||||
invalid={invalidUser}
|
||||
ref={refUser}
|
||||
disabled={!sipRealm && !regWebhook && !user && !password}
|
||||
title={(
|
||||
!sipRealm &&
|
||||
!regWebhook &&
|
||||
!user &&
|
||||
!password &&
|
||||
"You must provide a SIP Realm in order to enter a registration webhook"
|
||||
) || ""}
|
||||
/>
|
||||
<Label htmlFor="password" middle>Password</Label>
|
||||
<PasswordInput
|
||||
allowShowPassword
|
||||
name="password"
|
||||
id="password"
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
setErrorMessage={setErrorMessage}
|
||||
placeholder="Optional"
|
||||
invalid={invalidPassword}
|
||||
ref={refPassword}
|
||||
disabled={!sipRealm && !regWebhook && !user && !password}
|
||||
title={(
|
||||
!sipRealm &&
|
||||
!regWebhook &&
|
||||
!user &&
|
||||
!password &&
|
||||
"You must provide a SIP Realm in order to enter a registration webhook"
|
||||
) || ""}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
@@ -1,703 +0,0 @@
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
import Checkbox from '../elements/Checkbox';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Button from '../elements/Button';
|
||||
import TrashButton from '../elements/TrashButton';
|
||||
import Loader from '../blocks/Loader';
|
||||
import sortSipGateways from '../../helpers/sortSipGateways';
|
||||
import Link from '../elements/Link';
|
||||
|
||||
const SipTrunkForm = props => {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
// Refs
|
||||
const refName = useRef(null);
|
||||
const refIp = useRef([]);
|
||||
const refPort = useRef([]);
|
||||
const refInbound = useRef([]);
|
||||
const refOutbound = useRef([]);
|
||||
const refTrash = useRef([]);
|
||||
const refAdd = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ name, setName ] = useState('');
|
||||
const [ nameInvalid, setNameInvalid ] = useState(false);
|
||||
const [ description, setDescription ] = useState('');
|
||||
const [ e164, setE164 ] = useState(false);
|
||||
const [ sipGateways, setSipGateways ] = useState([
|
||||
{
|
||||
sip_gateway_sid: '',
|
||||
ip: '',
|
||||
port: 5060,
|
||||
inbound: true,
|
||||
outbound: true,
|
||||
invalidIp: false,
|
||||
invalidPort: false,
|
||||
invalidInbound: false,
|
||||
invalidOutbound: false,
|
||||
}
|
||||
]);
|
||||
|
||||
const [ sipTrunks, setSipTrunks ] = useState([]);
|
||||
const [ sipTrunkSid, setSipTrunkSid ] = useState('');
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const sipTrunksPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/VoipCarriers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const sipGatewaysPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/SipGateways`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const promiseAllValues = await Promise.all([
|
||||
sipTrunksPromise,
|
||||
sipGatewaysPromise,
|
||||
]);
|
||||
|
||||
const allSipTrunks = promiseAllValues[0].data;
|
||||
const allSipGateways = promiseAllValues[1].data;
|
||||
|
||||
setSipTrunks(allSipTrunks);
|
||||
|
||||
if (props.type === 'setup' && allSipTrunks.length > 1) {
|
||||
history.push('/internal/sip-trunks');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'That page is only accessible during setup.',
|
||||
});
|
||||
}
|
||||
|
||||
if (props.type === 'edit' || props.type === 'setup') {
|
||||
const currentSipTrunk = props.type === 'edit'
|
||||
? allSipTrunks.filter(s => s.voip_carrier_sid === props.voip_carrier_sid)
|
||||
: allSipTrunks;
|
||||
|
||||
if (props.type === 'edit' && !currentSipTrunk.length) {
|
||||
history.push('/internal/sip-trunks');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'That SIP trunk does not exist.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSipGateways = allSipGateways.filter(s => {
|
||||
return s.voip_carrier_sid === currentSipTrunk[0].voip_carrier_sid;
|
||||
});
|
||||
sortSipGateways(currentSipGateways);
|
||||
|
||||
if (currentSipTrunk.length) {
|
||||
setName(currentSipTrunk[0].name);
|
||||
setDescription(currentSipTrunk[0].description);
|
||||
setE164(currentSipTrunk[0].e164_leading_plus === 1);
|
||||
setSipGateways(currentSipGateways.map(s => ({
|
||||
sip_gateway_sid: s.sip_gateway_sid,
|
||||
ip: s.ipv4,
|
||||
port: s.port,
|
||||
inbound: s.inbound === 1,
|
||||
outbound: s.outbound === 1,
|
||||
invalidIp: false,
|
||||
invalidPort: false,
|
||||
invalidInbound: false,
|
||||
invalidOutbound: false,
|
||||
})));
|
||||
setSipTrunkSid(currentSipTrunk[0].voip_carrier_sid);
|
||||
}
|
||||
}
|
||||
setShowLoader(false);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
setErrorMessage('Something went wrong, please try again.');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get accounts',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const addSipGateway = () => {
|
||||
const newSipGateways = [
|
||||
...sipGateways,
|
||||
{
|
||||
sip_gateway_sid: '',
|
||||
ip: '',
|
||||
port: 5060,
|
||||
inbound: true,
|
||||
outbound: true,
|
||||
invalidIp: false,
|
||||
invalidPort: false,
|
||||
invalidInbound: false,
|
||||
invalidOutbound: false,
|
||||
}
|
||||
];
|
||||
setSipGateways(newSipGateways);
|
||||
};
|
||||
|
||||
const removeSipGateway = index => {
|
||||
const newSipGateways = sipGateways.filter((s,i) => i !== index);
|
||||
setSipGateways(newSipGateways);
|
||||
setErrorMessage('');
|
||||
};
|
||||
|
||||
const updateSipGateways = (e, i, key) => {
|
||||
const newSipGateways = [...sipGateways];
|
||||
const newValue =
|
||||
key === 'invalidIp' ||
|
||||
key === 'invalidPort' ||
|
||||
key === 'invalidInbound' ||
|
||||
key === 'invalidOutbound'
|
||||
? true
|
||||
: (key === 'inbound') || (key === 'outbound')
|
||||
? e.target.checked
|
||||
: e.target.value;
|
||||
newSipGateways[i][key] = newValue;
|
||||
setSipGateways(newSipGateways);
|
||||
};
|
||||
|
||||
const resetInvalidFields = () => {
|
||||
setNameInvalid(false);
|
||||
const newSipGateways = [...sipGateways];
|
||||
newSipGateways.forEach((s, i) => {
|
||||
newSipGateways[i].invalidIp = false;
|
||||
newSipGateways[i].invalidPort = false;
|
||||
newSipGateways[i].invalidInbound = false;
|
||||
newSipGateways[i].invalidOutbound = false;
|
||||
});
|
||||
setSipGateways(newSipGateways);
|
||||
};
|
||||
|
||||
const handleSubmit = async e => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
resetInvalidFields();
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if (!name) {
|
||||
errorMessages.push('Please enter a name for this SIP Trunk.');
|
||||
setNameInvalid(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sipGateways.length) {
|
||||
errorMessages.push('You must provide at least one SIP Gateway.');
|
||||
if (!focusHasBeenSet) {
|
||||
refAdd.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
const regIp = /^((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])$/;
|
||||
const regFqdn = /^([a-zA-Z][^.]*)(\.[^.]+){2,}$/;
|
||||
const regFqdnTopLevel = /^([a-zA-Z][^.]*)(\.[^.]+)$/;
|
||||
const regPort = /^[0-9]+$/;
|
||||
|
||||
sipGateways.forEach(async (gateway, i) => {
|
||||
//-----------------------------------------------------------------------------
|
||||
// IP validation
|
||||
//-----------------------------------------------------------------------------
|
||||
const type = regIp.test(gateway.ip.trim())
|
||||
? 'ip'
|
||||
: regFqdn.test(gateway.ip.trim())
|
||||
? 'fqdn'
|
||||
: regFqdnTopLevel.test(gateway.ip.trim())
|
||||
? 'fqdn-top-level'
|
||||
: 'invalid';
|
||||
|
||||
if (!gateway.ip) {
|
||||
errorMessages.push('The IP Address cannot be blank. Please provide an IP address or delete the row.');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
if (!focusHasBeenSet) {
|
||||
refIp.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
else if (type === 'fqdn-top-level') {
|
||||
errorMessages.push('When using an FQDN, you must use a subdomain (e.g. sip.example.com).');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
if (!focusHasBeenSet) {
|
||||
refIp.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
else if (type === 'invalid') {
|
||||
errorMessages.push('Please provide a valid IP address or fully qualified domain name.');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
if (!focusHasBeenSet) {
|
||||
refIp.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Port validation
|
||||
//-----------------------------------------------------------------------------
|
||||
if (
|
||||
gateway.port && (
|
||||
!(regPort.test(gateway.port.toString().trim()))
|
||||
|| (parseInt(gateway.port.toString().trim()) < 0)
|
||||
|| (parseInt(gateway.port.toString().trim()) > 65535)
|
||||
)
|
||||
) {
|
||||
errorMessages.push('Please provide a valid port number between 0 and 65535');
|
||||
updateSipGateways(null, i, 'invalidPort');
|
||||
if (!focusHasBeenSet) {
|
||||
refPort.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// inbound/outbound validation
|
||||
//-----------------------------------------------------------------------------
|
||||
if (type === 'fqdn' && (!gateway.outbound || gateway.inbound)) {
|
||||
errorMessages.push('A fully qualified domain name may only be used for outbound calls.');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
if (gateway.inbound) updateSipGateways(null, i, 'invalidInbound');
|
||||
if (!gateway.outbound) updateSipGateways(null, i, 'invalidOutbound');
|
||||
if (!focusHasBeenSet) {
|
||||
if (gateway.inbound) {
|
||||
refInbound.current[i].focus();
|
||||
} else {
|
||||
refOutbound.current[i].focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
else if (!gateway.inbound && !gateway.outbound) {
|
||||
errorMessages.push('Each SIP Gateway must accept inbound calls, outbound calls, or both.');
|
||||
updateSipGateways(null, i, 'invalidInbound');
|
||||
updateSipGateways(null, i, 'invalidOutbound');
|
||||
if (!focusHasBeenSet) {
|
||||
refInbound.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// duplicates validation
|
||||
//-----------------------------------------------------------------------------
|
||||
sipGateways.forEach((otherGateway, j) => {
|
||||
if (i >= j) return;
|
||||
if (!gateway.ip) return;
|
||||
if (type === 'invalid') return;
|
||||
if (gateway.ip === otherGateway.ip && gateway.port === otherGateway.port) {
|
||||
errorMessages.push('Each SIP gateway must have a unique IP address.');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
updateSipGateways(null, i, 'invalidPort');
|
||||
updateSipGateways(null, j, 'invalidIp');
|
||||
updateSipGateways(null, j, 'invalidPort');
|
||||
if (!focusHasBeenSet) {
|
||||
refTrash.current[j].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// remove duplicate error messages
|
||||
for (let i = 0; i < errorMessages.length; i++) {
|
||||
for (let j = 0; j < errorMessages.length; j++) {
|
||||
if (i >= j) continue;
|
||||
if (errorMessages[i] === errorMessages[j]) {
|
||||
errorMessages.splice(j, 1);
|
||||
j = j - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Submit
|
||||
//=============================================================================
|
||||
const creatingNewTrunk = props.type === 'add' || (props.type === 'setup' && !sipTrunks.length);
|
||||
|
||||
const method = creatingNewTrunk
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
|
||||
const url = creatingNewTrunk
|
||||
? '/VoipCarriers'
|
||||
: `/VoipCarriers/${sipTrunkSid}`;
|
||||
|
||||
// Create or update SIP Trunk / VoIP Carrier
|
||||
const voipCarrier = await axios({
|
||||
method,
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data: {
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
e164_leading_plus: e164 ? 1 : 0
|
||||
},
|
||||
});
|
||||
const voip_carrier_sid = voipCarrier.data.sid;
|
||||
|
||||
// get updated gateway info from API in order to delete ones that user has removed from UI
|
||||
let sipGatewaysFromAPI;
|
||||
if (!creatingNewTrunk) {
|
||||
const results = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/SipGateways',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
sipGatewaysFromAPI = results.data.filter(s => s.voip_carrier_sid === sipTrunkSid);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Create or Update SIP Gateways
|
||||
//-----------------------------------------------------------------------------
|
||||
// Keeping track of created SIP gateways in case one throws an error, then all
|
||||
// of the ones created before that (as well as the sip trunk) have to be deleted.
|
||||
let completedSipGateways = [];
|
||||
try {
|
||||
for (const s of sipGateways) {
|
||||
const creatingNewGateway = creatingNewTrunk || s.sip_gateway_sid === '';
|
||||
|
||||
const method = creatingNewGateway
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
const url = creatingNewGateway
|
||||
? '/SipGateways'
|
||||
: `/SipGateways/${s.sip_gateway_sid}`;
|
||||
|
||||
const data = {
|
||||
ipv4: s.ip.trim(),
|
||||
port: s.port.toString().trim(),
|
||||
inbound: s.inbound,
|
||||
outbound: s.outbound,
|
||||
};
|
||||
|
||||
if (creatingNewGateway) {
|
||||
data.voip_carrier_sid = voip_carrier_sid || sipTrunkSid;
|
||||
}
|
||||
|
||||
const result = await axios({
|
||||
method,
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data,
|
||||
});
|
||||
if (creatingNewGateway) {
|
||||
completedSipGateways.push(result.data.sid);
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
if (completedSipGateways.length) {
|
||||
for (const sid of completedSipGateways) {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/SipGateways/${sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (voip_carrier_sid) {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/VoipCarriers/${voip_carrier_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
// delete removed gateways (after add/update in case add/update caused errors)
|
||||
if (!creatingNewTrunk) {
|
||||
for (const remote of sipGatewaysFromAPI) {
|
||||
const match = sipGateways.filter(local => local.sip_gateway_sid === remote.sip_gateway_sid);
|
||||
if (!match.length) {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/SipGateways/${remote.sip_gateway_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (props.type === 'setup') {
|
||||
isMounted = false;
|
||||
history.push('/setup-complete');
|
||||
} else {
|
||||
isMounted = false;
|
||||
history.push('/internal/sip-trunks');
|
||||
const dispatchMessage = props.type === 'add'
|
||||
? 'SIP trunk created successfully'
|
||||
: 'SIP trunk updated successfully';
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: dispatchMessage
|
||||
});
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
isMounted = false;
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
setErrorMessage((err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.');
|
||||
console.log(err.response || err);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
showLoader
|
||||
? <Loader height={props.type === 'setup' ? '424px' : '376px'}/>
|
||||
: <Form
|
||||
large
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="name"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="SIP trunk provider name"
|
||||
invalid={nameInvalid}
|
||||
autoFocus
|
||||
ref={refName}
|
||||
/>
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="description"
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="Optional"
|
||||
/>
|
||||
<Label htmlFor="e164">E.164 Syntax</Label>
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
large={props.type === 'setup'}
|
||||
name="e164"
|
||||
id="e164"
|
||||
label="prepend a leading + on origination attempts"
|
||||
checked={e164}
|
||||
onChange={e => setE164(e.target.checked)}
|
||||
/>
|
||||
|
||||
<hr style={{ margin: '0.5rem -2rem' }} />
|
||||
<div
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>SIP Gateways</div>
|
||||
{
|
||||
sipGateways.length
|
||||
? <div>{/* for CSS grid layout */}</div>
|
||||
: null
|
||||
}
|
||||
{sipGateways.map((g, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<Label htmlFor={`sipGatewaysIp[${i}]`}>IP Address</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name={`sipGatewaysIp[${i}]`}
|
||||
id={`sipGatewaysIp[${i}]`}
|
||||
value={sipGateways[i].ip}
|
||||
onChange={e => updateSipGateways(e, i, 'ip')}
|
||||
placeholder={'1.2.3.4'}
|
||||
invalid={sipGateways[i].invalidIp}
|
||||
ref={ref => refIp.current[i] = ref}
|
||||
/>
|
||||
<Label
|
||||
middle
|
||||
htmlFor={`sipGatewaysPort[${i}]`}
|
||||
>
|
||||
Port
|
||||
</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
width="5rem"
|
||||
name={`sipGatewaysPort[${i}]`}
|
||||
id={`sipGatewaysPort[${i}]`}
|
||||
value={sipGateways[i].port}
|
||||
onChange={e => updateSipGateways(e, i, 'port')}
|
||||
placeholder="5060"
|
||||
invalid={sipGateways[i].invalidPort}
|
||||
ref={ref => refPort.current[i] = ref}
|
||||
/>
|
||||
<Checkbox
|
||||
large={props.type === 'setup'}
|
||||
id={`inbound[${i}]`}
|
||||
label="Inbound"
|
||||
tooltip="Sends us calls"
|
||||
checked={sipGateways[i].inbound}
|
||||
onChange={e => updateSipGateways(e, i, 'inbound')}
|
||||
invalid={sipGateways[i].invalidInbound}
|
||||
ref={ref => refInbound.current[i] = ref}
|
||||
/>
|
||||
<Checkbox
|
||||
large={props.type === 'setup'}
|
||||
id={`outbound[${i}]`}
|
||||
label="Outbound"
|
||||
tooltip="Accepts calls from us"
|
||||
checked={sipGateways[i].outbound}
|
||||
onChange={e => updateSipGateways(e, i, 'outbound')}
|
||||
invalid={sipGateways[i].invalidOutbound}
|
||||
ref={ref => refOutbound.current[i] = ref}
|
||||
/>
|
||||
<TrashButton
|
||||
onClick={() => removeSipGateway(i)}
|
||||
ref={ref => refTrash.current[i] = ref}
|
||||
/>
|
||||
</InputGroup>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<Button
|
||||
square
|
||||
type="button"
|
||||
onClick={addSipGateway}
|
||||
ref={refAdd}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
{props.type === 'edit' && (
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/sip-trunks');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
large={props.type === 'setup'}
|
||||
grid
|
||||
fullWidth={props.type === 'setup' || props.type === 'add'}
|
||||
>
|
||||
{props.type === 'setup'
|
||||
? 'Save and Continue'
|
||||
: props.type === 'add'
|
||||
? 'Add SIP Trunk'
|
||||
: 'Save'
|
||||
}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
|
||||
{props.type === 'setup' && (
|
||||
<Link
|
||||
formLink
|
||||
right
|
||||
to="/setup-complete"
|
||||
>
|
||||
Skip for now — I'll complete later
|
||||
</Link>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SipTrunkForm;
|
||||
567
src/components/forms/SpeechForm.js
Normal file
567
src/components/forms/SpeechForm.js
Normal file
@@ -0,0 +1,567 @@
|
||||
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 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);
|
||||
|
||||
// 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 {
|
||||
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) {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Check if user already has a speech service with the selected vendor
|
||||
const speechServices = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (type === 'add' && speechServices.data.some(speech => speech.vendor === vendor)) {
|
||||
setErrorMessage('You can only have one speech credential per vendor.');
|
||||
setShowLoader(false);
|
||||
if (vendor === 'google') {
|
||||
setInvalidVendorGoogle(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refVendorGoogle.current.focus();
|
||||
}
|
||||
} else if (vendor === 'aws') {
|
||||
setInvalidVendorAws(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refVendorAws.current.focus();
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
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>
|
||||
|
||||
{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;
|
||||
@@ -69,6 +69,7 @@ const Login = props => {
|
||||
// They're saved to sessionStorage so that the data does not persist.
|
||||
sessionStorage.setItem('user_sid', response.data.user_sid);
|
||||
sessionStorage.setItem('old_password', password);
|
||||
localStorage.setItem('token', response.data.token);
|
||||
history.push('/create-password');
|
||||
return;
|
||||
}
|
||||
@@ -143,20 +144,20 @@ const Login = props => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sip_realm, registration_hook } = accounts[0];
|
||||
// const { sip_realm, registration_hook } = accounts[0];
|
||||
|
||||
if (
|
||||
(!sip_realm || !registration_hook) &&
|
||||
!applications.length
|
||||
) {
|
||||
history.push('/configure-account');
|
||||
return;
|
||||
}
|
||||
// if (
|
||||
// (!sip_realm || !registration_hook) &&
|
||||
// !applications.length
|
||||
// ) {
|
||||
// history.push('/configure-account');
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!applications.length) {
|
||||
history.push('/create-application');
|
||||
return;
|
||||
}
|
||||
// if (!applications.length) {
|
||||
// history.push('/create-application');
|
||||
// return;
|
||||
// }
|
||||
|
||||
history.push('/internal/accounts');
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
|
||||
const AccountsList = () => {
|
||||
let history = useHistory();
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
useEffect(() => {
|
||||
document.title = `Accounts | Jambonz | Open Source CPAAS`;
|
||||
@@ -26,10 +28,11 @@ const AccountsList = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(!currentServiceProvider) return [];
|
||||
const results = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
|
||||
257
src/components/pages/internal/AlertsList.js
Normal file
257
src/components/pages/internal/AlertsList.js
Normal file
@@ -0,0 +1,257 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
import styled from "styled-components/macro";
|
||||
import { NotificationDispatchContext } from "../../../contexts/NotificationContext";
|
||||
import InternalTemplate from "../../templates/InternalTemplate";
|
||||
import Button from "../../../components/elements/Button";
|
||||
import InputGroup from "../../../components/elements/InputGroup";
|
||||
import Label from "../../../components/elements/Label";
|
||||
import Select from "../../../components/elements/Select";
|
||||
import AntdTable from "../../../components/blocks/AntdTable";
|
||||
import handleErrors from "../../../helpers/handleErrors";
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
& > span {
|
||||
height: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const AlertsIndex = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const jwt = localStorage.getItem("token");
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Table props
|
||||
const [alertsData, setAlertsData] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowCount, setRowCount] = useState(25);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Filter values
|
||||
const [account, setAccount] = useState("");
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
const [attemptedAt, setAttemptedAt] = useState("today");
|
||||
|
||||
//=============================================================================
|
||||
// Define Table props
|
||||
//=============================================================================
|
||||
const Columns = [
|
||||
{
|
||||
title: "Date",
|
||||
dataIndex: "time",
|
||||
key: "time",
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
title: "Message",
|
||||
dataIndex: "message",
|
||||
key: "message",
|
||||
width: 250,
|
||||
},
|
||||
];
|
||||
const { height } = window.screen;
|
||||
|
||||
const renderPagination = (page, type, originElement) => {
|
||||
let node = originElement;
|
||||
|
||||
switch (type) {
|
||||
case "page":
|
||||
node = <StyledButton gray={currentPage !== page}>{page}</StyledButton>;
|
||||
break;
|
||||
case "prev":
|
||||
node = <StyledButton>{`<`}</StyledButton>;
|
||||
break;
|
||||
case "next":
|
||||
node = <StyledButton>{`>`}</StyledButton>;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
//=============================================================================
|
||||
// Get alerts
|
||||
//=============================================================================
|
||||
const getAlerts = async () => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
let filter = {
|
||||
page: currentPage,
|
||||
count: rowCount,
|
||||
};
|
||||
|
||||
if (!account) {
|
||||
setAlertsData([]);
|
||||
setTotalCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
switch (attemptedAt) {
|
||||
case "today":
|
||||
filter.start = moment().startOf("date").toISOString();
|
||||
break;
|
||||
case "7d":
|
||||
filter.days = 7;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
const alerts = await axios({
|
||||
method: "get",
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/Accounts/${account}/Alerts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
params: {
|
||||
...filter,
|
||||
},
|
||||
});
|
||||
|
||||
if (isMounted) {
|
||||
const { total, data } = alerts.data;
|
||||
const simplififedAlerts = data.map((alert, index) => ({
|
||||
...alert,
|
||||
id: index,
|
||||
time: alert.time
|
||||
? moment(alert.time).format("YYYY MM.DD hh:mm a")
|
||||
: "",
|
||||
message: alert.message,
|
||||
}));
|
||||
|
||||
setAlertsData(simplififedAlerts);
|
||||
setTotalCount(total);
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage === 1) {
|
||||
getAlerts();
|
||||
} else {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [account, attemptedAt]);
|
||||
|
||||
useEffect(() => {
|
||||
getAlerts();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPage, rowCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
let isMounted = true;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
if (accountResponse.data.length > 0) {
|
||||
setAccount(accountResponse.data[0].account_sid);
|
||||
} else {
|
||||
setAccount("");
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
return (
|
||||
<InternalTemplate title="Alerts">
|
||||
<StyledInputGroup flexEnd space>
|
||||
<Label indented htmlFor="account">
|
||||
Account
|
||||
</Label>
|
||||
<AccountSelect
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={(e) => setAccount(e.target.value)}
|
||||
>
|
||||
{accountList.map((acc) => (
|
||||
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
|
||||
))}
|
||||
</AccountSelect>
|
||||
<Label middle htmlFor="daterange">
|
||||
Date
|
||||
</Label>
|
||||
<Select
|
||||
name="daterange"
|
||||
id="daterange"
|
||||
value={attemptedAt}
|
||||
onChange={(e) => setAttemptedAt(e.target.value)}
|
||||
>
|
||||
<option value="today">today</option>
|
||||
<option value="7d">last 7d</option>
|
||||
</Select>
|
||||
</StyledInputGroup>
|
||||
<AntdTable
|
||||
dataSource={alertsData}
|
||||
columns={Columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: ["bottomCenter"],
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(page);
|
||||
setRowCount(size);
|
||||
},
|
||||
showTotal: (total) => `Total: ${total} records`,
|
||||
current: currentPage,
|
||||
total: totalCount,
|
||||
pageSize: rowCount,
|
||||
pageSizeOptions: [25, 50, 100],
|
||||
showSizeChanger: true,
|
||||
itemRender: renderPagination,
|
||||
showLessItems: true,
|
||||
}}
|
||||
scroll={{ y: Math.max(height - 660, 200) }}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertsIndex;
|
||||
@@ -1,21 +1,90 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useEffect, useContext, useState, useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import styled from "styled-components/macro";
|
||||
import Select from "../../../components/elements/Select";
|
||||
import InputGroup from "../../../components/elements/InputGroup";
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import handleErrors from "../../../helpers/handleErrors";
|
||||
|
||||
const FilterLabel = styled.span`
|
||||
color: #231f20;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
margin-left: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const ApplicationsList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const jwt = localStorage.getItem('token');
|
||||
const [account, setAccount] = useState("");
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Applications | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
try {
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
if (accountResponse.data.length > 0) {
|
||||
setAccount(accountResponse.data[0].account_sid);
|
||||
} else {
|
||||
setAccount("");
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
//=============================================================================
|
||||
// Get applications
|
||||
//=============================================================================
|
||||
const getApplications = async () => {
|
||||
const getApplications = useCallback(async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
@@ -26,18 +95,13 @@ const ApplicationsList = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!account) {
|
||||
return [];
|
||||
}
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
url: `/Accounts/${account}/Applications`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
@@ -45,21 +109,19 @@ const ApplicationsList = () => {
|
||||
|
||||
const promiseAllValues = await Promise.all([
|
||||
applicationsPromise,
|
||||
accountsPromise,
|
||||
]);
|
||||
|
||||
const applications = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[1].data;
|
||||
|
||||
const simplifiedApplications = applications.map(app => {
|
||||
const account = accounts.filter(acc => acc.account_sid === app.account_sid);
|
||||
return {
|
||||
sid: app.application_sid,
|
||||
name: app.name,
|
||||
account_sid: app.account_sid,
|
||||
call_hook_url: app.call_hook && app.call_hook.url,
|
||||
status_hook_url: app.call_status_hook && app.call_status_hook.url,
|
||||
account: account[0].name,
|
||||
sid: app.application_sid,
|
||||
name: app.name,
|
||||
account_sid: app.account_sid,
|
||||
call_hook_url: app.call_hook && app.call_hook.url,
|
||||
status_hook_url: app.call_status_hook && app.call_status_hook.url,
|
||||
messaging_hook_url: app.messaging_hook && app.messaging_hook.url,
|
||||
account: app.account
|
||||
};
|
||||
});
|
||||
return(simplifiedApplications);
|
||||
@@ -82,17 +144,18 @@ const ApplicationsList = () => {
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [account]);
|
||||
|
||||
//=============================================================================
|
||||
// Delete application
|
||||
//=============================================================================
|
||||
const formatApplicationToDelete = app => {
|
||||
return [
|
||||
{ name: 'Name:', content: app.name || '[none]' },
|
||||
{ name: 'Account:', content: app.account || '[none]' },
|
||||
{ name: 'Calling Webhook:', content: app.call_hook_url || '[none]' },
|
||||
{ name: 'Call Status Webhook:', content: app.status_hook_url || '[none]' },
|
||||
{ name: 'Name:', content: app.name || '[none]' },
|
||||
{ name: 'Account:', content: app.account || '[none]' },
|
||||
{ name: 'Calling Webhook:', content: app.call_hook_url || '[none]' },
|
||||
{ name: 'Call Status Webhook:', content: app.status_hook_url || '[none]' },
|
||||
{ name: 'Messaging Webhook:', content: app.messaging_hook_url || '[none]' },
|
||||
];
|
||||
};
|
||||
const deleteApplication = async applicationToDelete => {
|
||||
@@ -196,6 +259,19 @@ const ApplicationsList = () => {
|
||||
addButtonText="Add an Application"
|
||||
addButtonLink="/internal/applications/add"
|
||||
>
|
||||
<StyledInputGroup flexEnd space>
|
||||
<FilterLabel htmlFor="account">Account:</FilterLabel>
|
||||
<AccountSelect
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={(e) => setAccount(e.target.value)}
|
||||
>
|
||||
{accountList.map((acc) => (
|
||||
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
|
||||
))}
|
||||
</AccountSelect>
|
||||
</StyledInputGroup>
|
||||
<TableContent
|
||||
name="application"
|
||||
urlParam="applications"
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import SipTrunkForm from '../../forms/SipTrunkForm';
|
||||
import CarrierForm from '../../forms/CarrierForm';
|
||||
import Sbcs from '../../blocks/Sbcs';
|
||||
|
||||
const SipTrunksAddEdit = () => {
|
||||
const CarriersAddEdit = () => {
|
||||
let { voip_carrier_sid } = useParams();
|
||||
const pageTitle = voip_carrier_sid ? 'Edit SIP Trunk' : 'Add SIP Trunk';
|
||||
const pageTitle = voip_carrier_sid ? 'Edit Carrier' : 'Add Carrier';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
subtitle={<Sbcs />}
|
||||
breadcrumbs={[
|
||||
{ name: 'SIP Trunks', url: '/internal/sip-trunks' },
|
||||
{ name: 'Carriers', url: '/internal/carriers' },
|
||||
{ name: pageTitle },
|
||||
]}
|
||||
>
|
||||
<SipTrunkForm
|
||||
<CarrierForm
|
||||
type={voip_carrier_sid ? 'edit' : 'add'}
|
||||
voip_carrier_sid={voip_carrier_sid}
|
||||
/>
|
||||
@@ -28,4 +29,4 @@ const SipTrunksAddEdit = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SipTrunksAddEdit;
|
||||
export default CarriersAddEdit;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useEffect, useContext, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
@@ -6,18 +7,21 @@ import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import Sbcs from '../../blocks/Sbcs';
|
||||
import sortSipGateways from '../../../helpers/sortSipGateways';
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
|
||||
const SipTrunksList = () => {
|
||||
const CarriersList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `SIP Trunks | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
document.title = `Carriers | Jambonz | Open Source CPAAS`;
|
||||
}, []);
|
||||
|
||||
//=============================================================================
|
||||
// Get sip trunks
|
||||
//=============================================================================
|
||||
const getSipTrunks = async () => {
|
||||
const getCarriers = useCallback(async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
@@ -28,30 +32,33 @@ const SipTrunksList = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!currentServiceProvider) return [];
|
||||
// Get all SIP trunks
|
||||
const trunkResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/VoipCarriers',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Get all SIP gateways
|
||||
const gatewayResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/SipGateways',
|
||||
url: `/ServiceProviders/${currentServiceProvider}/VoipCarriers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Add appropriate gateways to each trunk
|
||||
const trunkMap = {};
|
||||
for (const t of trunkResults.data) {
|
||||
const gws = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/SipGateways?voip_carrier_sid=${t.voip_carrier_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
trunkMap[t.voip_carrier_sid] = gws.data;
|
||||
}
|
||||
|
||||
const trunksWithGateways = trunkResults.data.map(t => {
|
||||
const gateways = gatewayResults.data.filter(g => t.voip_carrier_sid === g.voip_carrier_sid);
|
||||
const gateways = trunkMap[t.voip_carrier_sid] || [];
|
||||
sortSipGateways(gateways);
|
||||
return {
|
||||
...t,
|
||||
@@ -59,16 +66,19 @@ const SipTrunksList = () => {
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
const simplifiedSipTrunks = trunksWithGateways.map(t => ({
|
||||
const simplifiedCarriers = trunksWithGateways.map(t => ({
|
||||
sid: t.voip_carrier_sid,
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
gatewaysConcat: t.gateways.map(g => `${g.ipv4}:${g.port}`).join(', '),
|
||||
status: t.is_active === 1 ? "active" : "inactive",
|
||||
gatewaysConcat: `${
|
||||
t.gateways.filter((item) => item.inbound === 1).length
|
||||
} inbound, ${
|
||||
t.gateways.filter((item) => item.outbound === 1).length
|
||||
} outbound`,
|
||||
gatewaysList: t.gateways.map(g => `${g.ipv4}:${g.port}`),
|
||||
gatewaysSid: t.gateways.map(g => g.sip_gateway_sid),
|
||||
}));
|
||||
return(simplifiedSipTrunks);
|
||||
return(simplifiedCarriers);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
@@ -88,25 +98,23 @@ const SipTrunksList = () => {
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [currentServiceProvider, history, dispatch]);
|
||||
|
||||
//=============================================================================
|
||||
// Delete sip trunk
|
||||
//=============================================================================
|
||||
const formatSipTrunkToDelete = trunk => {
|
||||
const formatCarrierToDelete = trunk => {
|
||||
const gatewayName = trunk.gatewaysList.length > 1
|
||||
? 'SIP Gateways:'
|
||||
: 'SIP Gateway:';
|
||||
const gatewayContent = trunk.gatewaysList.length > 1
|
||||
? trunk.gatewaysList
|
||||
: trunk.gatewaysList[0];
|
||||
return [
|
||||
|
||||
return [
|
||||
{ name: 'Name:', content: trunk.name || '[none]' },
|
||||
{ name: 'Description:', content: trunk.description || '[none]' },
|
||||
{ name: gatewayName, content: gatewayContent || '[none]' },
|
||||
{ name: 'Status:', content: trunk.status || '[none]' },
|
||||
{ name: gatewayName, content: trunk.gatewaysConcat || '[none]' },
|
||||
];
|
||||
};
|
||||
const deleteSipTrunk = async sipTrunkToDelete => {
|
||||
const deleteCarrier = async carrierToDelete => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
@@ -118,7 +126,7 @@ const SipTrunksList = () => {
|
||||
return;
|
||||
}
|
||||
// delete associated gateways
|
||||
for (const sid of sipTrunkToDelete.gatewaysSid) {
|
||||
for (const sid of carrierToDelete.gatewaysSid) {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
@@ -132,7 +140,7 @@ const SipTrunksList = () => {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/VoipCarriers/${sipTrunkToDelete.sid}`,
|
||||
url: `/VoipCarriers/${carrierToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
@@ -160,25 +168,25 @@ const SipTrunksList = () => {
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate
|
||||
title="SIP Trunks"
|
||||
addButtonText="Add a SIP Trunk"
|
||||
addButtonLink="/internal/sip-trunks/add"
|
||||
title="Carriers"
|
||||
addButtonText="Add a Carrier"
|
||||
addButtonLink="/internal/carriers/add"
|
||||
subtitle={<Sbcs />}
|
||||
>
|
||||
<TableContent
|
||||
name="SIP trunk"
|
||||
urlParam="sip-trunks"
|
||||
getContent={getSipTrunks}
|
||||
name="Carrier"
|
||||
urlParam="carriers"
|
||||
getContent={getCarriers}
|
||||
columns={[
|
||||
{ header: 'Name', key: 'name' },
|
||||
{ header: 'Description', key: 'description' },
|
||||
{ header: 'SIP Gateways', key: 'gatewaysConcat' },
|
||||
{ header: 'Status', key: 'status' },
|
||||
{ header: 'Gateways', key: 'gatewaysConcat' },
|
||||
]}
|
||||
formatContentToDelete={formatSipTrunkToDelete}
|
||||
deleteContent={deleteSipTrunk}
|
||||
formatContentToDelete={formatCarrierToDelete}
|
||||
deleteContent={deleteCarrier}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default SipTrunksList;
|
||||
export default CarriersList;
|
||||
@@ -1,22 +1,25 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import React, { useState, useEffect, useContext, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import phoneNumberFormat from '../../../helpers/phoneNumberFormat';
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
|
||||
const PhoneNumbersList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Phone Number Routing | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
}, []);
|
||||
|
||||
//=============================================================================
|
||||
// Get phone numbers
|
||||
//=============================================================================
|
||||
const getPhoneNumbers = async () => {
|
||||
const getPhoneNumbers = useCallback(async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
@@ -27,10 +30,11 @@ const PhoneNumbersList = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(!currentServiceProvider) return [];
|
||||
const phoneNumbersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/PhoneNumbers',
|
||||
url: `/ServiceProviders/${currentServiceProvider}/PhoneNumbers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
@@ -38,7 +42,7 @@ const PhoneNumbersList = () => {
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
@@ -46,7 +50,7 @@ const PhoneNumbersList = () => {
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Applications`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
@@ -54,7 +58,7 @@ const PhoneNumbersList = () => {
|
||||
const sipTrunksPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/VoipCarriers',
|
||||
url: `/ServiceProviders/${currentServiceProvider}/VoipCarriers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
@@ -120,7 +124,7 @@ const PhoneNumbersList = () => {
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [currentServiceProvider, dispatch, history]);
|
||||
|
||||
//=============================================================================
|
||||
// Delete phone number
|
||||
|
||||
423
src/components/pages/internal/RecentCallsList.js
Normal file
423
src/components/pages/internal/RecentCallsList.js
Normal file
@@ -0,0 +1,423 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
import styled from "styled-components/macro";
|
||||
import { NotificationDispatchContext } from "../../../contexts/NotificationContext";
|
||||
import InternalTemplate from "../../templates/InternalTemplate";
|
||||
import AntdTable from "../../../components/blocks/AntdTable";
|
||||
import phoneNumberFormat from "../../../helpers/phoneNumberFormat";
|
||||
import timeFormat from "../../../helpers/timeFormat";
|
||||
import Label from "../../../components/elements/Label";
|
||||
import Button from "../../../components/elements/Button";
|
||||
import InputGroup from "../../../components/elements/InputGroup";
|
||||
import Select from "../../../components/elements/Select";
|
||||
import handleErrors from "../../../helpers/handleErrors";
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
|
||||
const FilterLabel = styled.span`
|
||||
color: #231f20;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
margin-left: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const ExpandedSection = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
& > span {
|
||||
height: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const RecentCallsIndex = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const jwt = localStorage.getItem("token");
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Table props
|
||||
const [recentCallsData, setRecentCallsData] = useState([]);
|
||||
const [rawData, setRawData] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowCount, setRowCount] = useState(25);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
|
||||
|
||||
// Filter values
|
||||
const [account, setAccount] = useState("");
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
const [attemptedAt, setAttemptedAt] = useState("today");
|
||||
const [dirFilter, setDirFilter] = useState("io");
|
||||
const [answered, setAnswered] = useState("all");
|
||||
// width
|
||||
const { height } = window.screen;
|
||||
|
||||
//=============================================================================
|
||||
// Define Table props
|
||||
//=============================================================================
|
||||
const Columns = [
|
||||
{
|
||||
title: "Date",
|
||||
dataIndex: "attempted_at",
|
||||
key: "attempted_at",
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
title: "Direction",
|
||||
dataIndex: "direction",
|
||||
key: "direction",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "From",
|
||||
dataIndex: "from",
|
||||
key: "from",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "To",
|
||||
dataIndex: "to",
|
||||
key: "to",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "Trunk",
|
||||
dataIndex: "trunk",
|
||||
key: "trunk",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "Duration",
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
width: 150,
|
||||
},
|
||||
];
|
||||
|
||||
//=============================================================================
|
||||
// Get recent calls
|
||||
//=============================================================================
|
||||
const handleFilterChange = () => {
|
||||
let filter = {
|
||||
page: currentPage,
|
||||
count: rowCount,
|
||||
};
|
||||
if (attemptedAt) {
|
||||
switch (attemptedAt) {
|
||||
case "today":
|
||||
filter.start = moment().startOf("date").toISOString();
|
||||
break;
|
||||
case "7d":
|
||||
filter.days = 7;
|
||||
break;
|
||||
case "14d":
|
||||
filter.days = 14;
|
||||
break;
|
||||
case "30d":
|
||||
filter.days = 30;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if (dirFilter === "inbound") {
|
||||
filter.direction = "inbound";
|
||||
} else if (dirFilter === "outbound") {
|
||||
filter.direction = "outbound";
|
||||
}
|
||||
|
||||
if (answered && answered !== "all") {
|
||||
filter.answered = answered === "answered" ? "true" : "false";
|
||||
}
|
||||
|
||||
getRecentCallsData(filter);
|
||||
};
|
||||
|
||||
const getRecentCallsData = async (filter = {}) => {
|
||||
let isMounted = true;
|
||||
|
||||
if (!account) {
|
||||
setRecentCallsData([]);
|
||||
setTotalCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await axios({
|
||||
method: "get",
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/Accounts/${account}/RecentCalls`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
params: {
|
||||
...filter,
|
||||
},
|
||||
});
|
||||
|
||||
if (isMounted) {
|
||||
const { total, data } = result.data;
|
||||
|
||||
setRawData([...data]);
|
||||
|
||||
const recentCalls = data.map((item, index) => ({
|
||||
key: index,
|
||||
...item,
|
||||
attempted_at: item.attempted_at
|
||||
? moment(item.attempted_at).format("YYYY MM.DD hh:mm a")
|
||||
: "",
|
||||
from: phoneNumberFormat(item.from),
|
||||
to: phoneNumberFormat(item.to),
|
||||
status: item.answered ? "answered" : item.termination_reason,
|
||||
duration: timeFormat(item.duration),
|
||||
}));
|
||||
|
||||
setRecentCallsData(recentCalls);
|
||||
setTotalCount(total);
|
||||
setExpandedRowKeys([]);
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderExpandedRow = (data) => {
|
||||
const fields = [
|
||||
"direction",
|
||||
"attempted_at",
|
||||
"answered_at",
|
||||
"terminated_at",
|
||||
"duration",
|
||||
"answered",
|
||||
"from",
|
||||
"to",
|
||||
"termination_reason",
|
||||
"call_sid",
|
||||
"sip_callid",
|
||||
"host",
|
||||
"remote_host",
|
||||
"sip_status",
|
||||
"trunk",
|
||||
];
|
||||
|
||||
return (
|
||||
<ExpandedSection>
|
||||
{fields.map((field, index) => {
|
||||
if (!rawData || !rawData[data.key]) {
|
||||
return null;
|
||||
}
|
||||
let label = rawData[data.key][field];
|
||||
|
||||
if (typeof label === "boolean") {
|
||||
label = label ? "true" : "false";
|
||||
}
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<Label>{`${field}:`}</Label>
|
||||
<Label>{label}</Label>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</ExpandedSection>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPagination = (page, type, originElement) => {
|
||||
let node = originElement;
|
||||
|
||||
switch (type) {
|
||||
case "page":
|
||||
node = <StyledButton gray={currentPage !== page}>{page}</StyledButton>;
|
||||
break;
|
||||
case "prev":
|
||||
node = <StyledButton>{`<`}</StyledButton>;
|
||||
break;
|
||||
case "next":
|
||||
node = <StyledButton>{`>`}</StyledButton>;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
const handleExpandChange = (expanded, record) => {
|
||||
if (expanded) {
|
||||
setExpandedRowKeys((prev) => [...prev, record.key]);
|
||||
} else {
|
||||
setExpandedRowKeys((prev) => [
|
||||
...prev.filter((item) => item !== record.key),
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage === 1) {
|
||||
handleFilterChange();
|
||||
} else {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [account, attemptedAt, dirFilter, answered]);
|
||||
|
||||
useEffect(() => {
|
||||
handleFilterChange();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPage, rowCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
let isMounted = true;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
if (accountResponse.data.length > 0) {
|
||||
setAccount(accountResponse.data[0].account_sid);
|
||||
} else {
|
||||
setAccount("");
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate title="Recent Calls">
|
||||
<StyledInputGroup flexEnd space>
|
||||
<FilterLabel htmlFor="account">Account:</FilterLabel>
|
||||
<AccountSelect
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={(e) => setAccount(e.target.value)}
|
||||
>
|
||||
{accountList.map((acc) => (
|
||||
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
|
||||
))}
|
||||
</AccountSelect>
|
||||
<FilterLabel htmlFor="daterange">Date:</FilterLabel>
|
||||
<Select
|
||||
name="daterange"
|
||||
id="daterange"
|
||||
value={attemptedAt}
|
||||
onChange={(e) => setAttemptedAt(e.target.value)}
|
||||
>
|
||||
<option value="today">today</option>
|
||||
<option value="7d">last 7d</option>
|
||||
<option value="14d">last 14d</option>
|
||||
<option value="30d">last 30d</option>
|
||||
</Select>
|
||||
<FilterLabel htmlFor="direction">Direction:</FilterLabel>
|
||||
<Select
|
||||
name="direction"
|
||||
id="direction"
|
||||
value={dirFilter}
|
||||
onChange={(e) => setDirFilter(e.target.value)}
|
||||
>
|
||||
<option value="io">either</option>
|
||||
<option value="inbound">inbound only</option>
|
||||
<option value="outbound">outbound only</option>
|
||||
</Select>
|
||||
<FilterLabel htmlFor="status">Status:</FilterLabel>
|
||||
<Select
|
||||
name="status"
|
||||
id="status"
|
||||
value={answered}
|
||||
onChange={(e) => setAnswered(e.target.value)}
|
||||
>
|
||||
<option value="all">all</option>
|
||||
<option value="answered">answered</option>
|
||||
<option value="not-answered">not answered</option>
|
||||
</Select>
|
||||
</StyledInputGroup>
|
||||
<AntdTable
|
||||
dataSource={recentCallsData}
|
||||
columns={Columns}
|
||||
rowKey="key"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: ["bottomCenter"],
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(page);
|
||||
setRowCount(size);
|
||||
},
|
||||
showTotal: (total) => `Total: ${total} records`,
|
||||
current: currentPage,
|
||||
total: totalCount,
|
||||
pageSize: rowCount,
|
||||
pageSizeOptions: [25, 50, 100],
|
||||
showSizeChanger: true,
|
||||
itemRender: renderPagination,
|
||||
showLessItems: true,
|
||||
}}
|
||||
scroll={{ y: Math.max(height - 580, 200) }}
|
||||
expandable={{
|
||||
expandedRowRender: renderExpandedRow,
|
||||
}}
|
||||
expandedRowKeys={expandedRowKeys}
|
||||
onExpand={handleExpandChange}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecentCallsIndex;
|
||||
31
src/components/pages/internal/SpeechServicesAddEdit.js
Normal file
31
src/components/pages/internal/SpeechServicesAddEdit.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import SpeechForm from '../../forms/SpeechForm';
|
||||
import Sbcs from '../../blocks/Sbcs';
|
||||
|
||||
const SpeechServicesAddEdit = () => {
|
||||
let { speech_service_sid } = useParams();
|
||||
const pageTitle = speech_service_sid ? 'Edit Speech Service' : 'Add Speech Service';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
subtitle={<Sbcs />}
|
||||
breadcrumbs={[
|
||||
{ name: 'Speech Services', url: '/internal/speech-services' },
|
||||
{ name: pageTitle },
|
||||
]}
|
||||
>
|
||||
<SpeechForm
|
||||
type={speech_service_sid ? 'edit' : 'add'}
|
||||
speech_service_sid={speech_service_sid}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeechServicesAddEdit;
|
||||
221
src/components/pages/internal/SpeechServicesList.js
Normal file
221
src/components/pages/internal/SpeechServicesList.js
Normal file
@@ -0,0 +1,221 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useContext, useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import handleErrors from '../../../helpers/handleErrors';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../../components/blocks/TableContent';
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import Sbcs from '../../blocks/Sbcs';
|
||||
|
||||
const SpeechServicesList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
//=============================================================================
|
||||
// Get speech services
|
||||
//=============================================================================
|
||||
const getSpeechServices = useCallback(async () => {
|
||||
const jwt = localStorage.getItem('token');
|
||||
try {
|
||||
if (!jwt) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!currentServiceProvider) return [];
|
||||
const speechServices = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
const credentialTestPromises = speechServices.data.map(s => {
|
||||
if (s.use_for_stt || s.use_for_tts) {
|
||||
return axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${s.speech_credential_sid}/test`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const testResposes = await Promise.all(credentialTestPromises);
|
||||
|
||||
const cleanedUpSpeechServices = speechServices.data.map((s, i) => {
|
||||
const testResults = testResposes[i] && testResposes[i].data;
|
||||
|
||||
let content = null;
|
||||
let title = null;
|
||||
|
||||
if (s.use_for_tts && s.use_for_stt) {
|
||||
|
||||
if (testResults.tts.status === 'ok' && testResults.stt.status === 'ok') {
|
||||
content = 'ok';
|
||||
title = 'Connection test successful';
|
||||
} else {
|
||||
content = 'fail';
|
||||
|
||||
if (testResults.tts.reason && testResults.stt.reason) {
|
||||
|
||||
if (testResults.tts.reason === testResults.stt.reason) {
|
||||
title = testResults.tts.reason;
|
||||
} else {
|
||||
title = `TTS: ${testResults.tts.reason}. STT: ${testResults.stt.reason}`;
|
||||
}
|
||||
|
||||
} else if (testResults.tts.reason) {
|
||||
title = `TTS: ${testResults.tts.reason}`;
|
||||
|
||||
} else if (testResults.stt.reason) {
|
||||
title = `STT: ${testResults.stt.reason}`;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (s.use_for_tts) {
|
||||
|
||||
content = testResults.tts.status;
|
||||
title = testResults.tts.status === 'ok'
|
||||
? 'Connection test successful'
|
||||
: testResults.tts.reason;
|
||||
|
||||
} else if (s.use_for_stt) {
|
||||
|
||||
content = testResults.stt.status;
|
||||
title = testResults.stt.status === 'ok'
|
||||
? 'Connection test successful'
|
||||
: testResults.stt.reason;
|
||||
|
||||
}
|
||||
|
||||
const { last_used } = s;
|
||||
let lastUsedString = 'Never used';
|
||||
if (last_used) {
|
||||
const currentDate = new Date();
|
||||
const lastUsedDate = new Date(last_used);
|
||||
currentDate.setHours(0,0,0,0);
|
||||
lastUsedDate.setHours(0,0,0,0);
|
||||
const daysDifference = Math.round((currentDate - lastUsedDate) / 1000 / 60 / 60 / 24);
|
||||
lastUsedString = daysDifference > 1
|
||||
? `${daysDifference} days ago`
|
||||
: daysDifference === 1
|
||||
? 'Yesterday'
|
||||
: daysDifference === 0
|
||||
? 'Today'
|
||||
: 'Never used';
|
||||
}
|
||||
return {
|
||||
sid: s.speech_credential_sid,
|
||||
vendor: s.vendor,
|
||||
usage: (s.use_for_tts && s.use_for_stt) ? 'TTS/STT'
|
||||
: s.use_for_tts ? 'TTS'
|
||||
: s.use_for_stt ? 'STT'
|
||||
: 'Not in use',
|
||||
last_used: lastUsedString,
|
||||
status: {
|
||||
type: 'status',
|
||||
content,
|
||||
title,
|
||||
},
|
||||
};
|
||||
});
|
||||
return(cleanedUpSpeechServices);
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch, fallbackMessage: 'Unable to get speech services' });
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
//=============================================================================
|
||||
// Delete speech service
|
||||
//=============================================================================
|
||||
const formatSpeechServiceToDelete = s => {
|
||||
return [
|
||||
{ name: 'Vendor', content: s.vendor || '[none]' },
|
||||
{ name: 'Usage', content: s.usage || '[none]' },
|
||||
{ name: 'Last Used', content: s.last_used || 'Never' },
|
||||
];
|
||||
};
|
||||
const deleteSpeechService = useCallback(async speechServiceToDelete => {
|
||||
const jwt = localStorage.getItem('token');
|
||||
try {
|
||||
if (!jwt) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete speech service
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speechServiceToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.error(err.response || err);
|
||||
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete speech service');
|
||||
}
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="normalTable"
|
||||
title="Speech Services"
|
||||
subtitle={<Sbcs />}
|
||||
addButtonText="Add Speech Service"
|
||||
addButtonLink="/internal/speech-services/add"
|
||||
>
|
||||
<TableContent
|
||||
normalTable
|
||||
name="speech service"
|
||||
urlParam="speech-services"
|
||||
getContent={getSpeechServices}
|
||||
columns={[
|
||||
{ header: 'Vendor', key: 'vendor', bold: true },
|
||||
{ header: 'Usage', key: 'usage', },
|
||||
{ header: 'Last Used', key: 'last_used', },
|
||||
{ header: 'Status', key: 'status', textAlign: 'center' },
|
||||
]}
|
||||
formatContentToDelete={formatSpeechServiceToDelete}
|
||||
deleteContent={deleteSpeechService}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeechServicesList;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import SetupTemplate from '../../templates/SetupTemplate';
|
||||
import SipTrunkForm from '../../forms/SipTrunkForm';
|
||||
import CarrierForm from '../../forms/CarrierForm';
|
||||
import Sbcs from '../../blocks/Sbcs';
|
||||
|
||||
const ConfigureSipTrunk = () => {
|
||||
@@ -14,7 +14,7 @@ const ConfigureSipTrunk = () => {
|
||||
subtitle={<Sbcs centered />}
|
||||
progress={3}
|
||||
>
|
||||
<SipTrunkForm
|
||||
<CarrierForm
|
||||
type="setup"
|
||||
/>
|
||||
</SetupTemplate>
|
||||
|
||||
@@ -123,25 +123,25 @@ const CreatePassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sip_realm, registration_hook } = accounts[0];
|
||||
// const { sip_realm, registration_hook } = accounts[0];
|
||||
|
||||
if (
|
||||
(!sip_realm || !registration_hook) &&
|
||||
!applications.length
|
||||
) {
|
||||
history.push('/configure-account');
|
||||
return;
|
||||
}
|
||||
// if (
|
||||
// (!sip_realm || !registration_hook) &&
|
||||
// !applications.length
|
||||
// ) {
|
||||
// history.push('/configure-account');
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!applications.length) {
|
||||
history.push('/create-application');
|
||||
return;
|
||||
}
|
||||
// if (!applications.length) {
|
||||
// history.push('/create-application');
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!voipCarriers.length) {
|
||||
history.push('/configure-sip-trunk');
|
||||
return;
|
||||
}
|
||||
// if (!voipCarriers.length) {
|
||||
// history.push('/configure-sip-trunk');
|
||||
// return;
|
||||
// }
|
||||
|
||||
history.push('/internal/accounts');
|
||||
}
|
||||
@@ -241,6 +241,9 @@ const CreatePassword = () => {
|
||||
old_password,
|
||||
new_password: password,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
}
|
||||
});
|
||||
|
||||
sessionStorage.removeItem('user_sid');
|
||||
@@ -250,7 +253,8 @@ const CreatePassword = () => {
|
||||
localStorage.setItem('token', response.data.token);
|
||||
}
|
||||
|
||||
history.push('/configure-account');
|
||||
// history.push('/configure-account');
|
||||
history.push('/internal/accounts');
|
||||
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
|
||||
16
src/contexts/ServiceProviderContext.js
Normal file
16
src/contexts/ServiceProviderContext.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React, { createContext, useState } from "react";
|
||||
|
||||
export const ServiceProviderValueContext = createContext();
|
||||
export const ServiceProviderMethodContext = createContext();
|
||||
|
||||
export function ServiceProvider(props) {
|
||||
const [currentServiceProvider, setCurrentServiceProvider] = useState("");
|
||||
|
||||
return (
|
||||
<ServiceProviderValueContext.Provider value={currentServiceProvider}>
|
||||
<ServiceProviderMethodContext.Provider value={setCurrentServiceProvider}>
|
||||
{props.children}
|
||||
</ServiceProviderMethodContext.Provider>
|
||||
</ServiceProviderValueContext.Provider>
|
||||
);
|
||||
}
|
||||
10
src/data/SpeechRecognizerLanguageAws.js
Normal file
10
src/data/SpeechRecognizerLanguageAws.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export default [
|
||||
{ name: 'Australian English', 'code': 'en-AU' },
|
||||
{ name: 'British English', 'code': 'en-GB' },
|
||||
{ name: 'US English', 'code': 'en-US' },
|
||||
{ name: 'French', 'code': 'fr-FR' },
|
||||
{ name: 'Canadian French', 'code': 'fr-CA' },
|
||||
{ name: 'German', 'code': 'de-DE' },
|
||||
{ name: 'Italian', 'code': 'it-IT' },
|
||||
{ name: 'US Spanish', 'code': 'es-US' },
|
||||
];
|
||||
41
src/helpers/handleErrors.js
Normal file
41
src/helpers/handleErrors.js
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
const handleErrors = ({ err, history, dispatch, redirect, setErrorMessage, fallbackMessage, preferFallback }) => {
|
||||
|
||||
const errorMessage = (err.response && err.response.data && err.response.data.msg)
|
||||
|| (preferFallback && fallbackMessage)
|
||||
|| err.message
|
||||
|| fallbackMessage
|
||||
|| 'Something went wrong, please try again.';
|
||||
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please sign in and try again.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (setErrorMessage) {
|
||||
setErrorMessage(errorMessage);
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error(err.response || err);
|
||||
}
|
||||
|
||||
if (redirect) {
|
||||
history.push(redirect);
|
||||
}
|
||||
};
|
||||
|
||||
export default handleErrors;
|
||||
12
src/helpers/timeFormat.js
Normal file
12
src/helpers/timeFormat.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
const timeFormat = seconds => {
|
||||
if (seconds < 60) {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
};
|
||||
|
||||
export default timeFormat;
|
||||
3
src/images/AlertsIcon.svg
Normal file
3
src/images/AlertsIcon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="21" height="19" viewBox="0 0 21 19" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.749 0.737865C11.1939 -0.245954 9.80608 -0.245955 9.25095 0.737863L0.19538 16.7864C-0.359751 17.7702 0.334161 19 1.44442 19H19.5556C20.6658 19 21.3598 17.7702 20.8046 16.7864L11.749 0.737865ZM11.5307 12.8922L12 10.367V7H9V10.367L9.5 12.8922H11.5307ZM11.5307 14.1799C11.2736 13.9958 10.9225 13.9037 10.4774 13.9037C10.0324 13.9037 9.68129 13.9958 9.42415 14.1799C9.17691 14.364 9.05328 14.6234 9.05328 14.9581C9.05328 15.2845 9.17691 15.5397 9.42415 15.7238C9.68129 15.9079 10.0324 16 10.4774 16C10.9225 16 11.2736 15.9079 11.5307 15.7238C11.7878 15.5397 11.9164 15.2845 11.9164 14.9581C11.9164 14.6234 11.7878 14.364 11.5307 14.1799Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 781 B |
|
Before Width: | Height: | Size: 827 B After Width: | Height: | Size: 827 B |
12
src/images/LogoJambong.svg
Normal file
12
src/images/LogoJambong.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="43" viewBox="0 0 128 43">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#231F20" fill-rule="nonzero" d="M3.285 3.867c-.254.454-.352.98-.278 1.496.06.42.23.817.495 1.148.264.332.612.587 1.007.738.485.185 1.017.205 1.514.056.498-.149.933-.457 1.238-.879.305-.42.465-.931.453-1.452-.012-.521-.194-1.024-.518-1.43-.324-.408-.772-.696-1.276-.822-.503-.126-1.034-.082-1.51.125-.477.207-.872.565-1.125 1.02zM3.177 23.187c0 1.132-.642 1.833-1.555 1.833H0v3.36c.338.238 1.25.442 2.163.442 2.873 0 5.306-2.24 5.306-5.126V9.676H3.177v13.511zM25.671 19.722v3.53c-.36.17-.751.262-1.149.271-.833.014-1.655-.193-2.384-.597-.73-.405-1.34-.995-1.773-1.71-.535.726-1.234 1.315-2.04 1.716-.807.402-1.697.604-2.597.591-3.756 0-6.692-3.123-6.692-7.06 0-3.938 2.936-7.06 6.692-7.06 1.422-.023 2.8.499 3.852 1.46V9.673h4.293v8.724c0 1.131.608 1.324 1.183 1.324h.615zm-5.88-3.259c-.003-.557-.148-1.104-.422-1.588-.273-.485-.666-.89-1.14-1.179-.475-.288-1.016-.449-1.57-.467-.554-.018-1.104.107-1.596.364-.353.171-.667.412-.925.708s-.455.642-.577 1.015c-.165.444-.23.918-.192 1.389.037.471.178.929.41 1.34.234.41.554.765.938 1.039.384.273.824.458 1.287.542.463.084.94.065 1.395-.055.455-.121.878-.341 1.24-.644.361-.303.652-.682.852-1.11.2-.429.303-.896.303-1.37l-.003.016zM27.611 23.252V9.674h4.326v1.528c.462-.56 1.04-1.011 1.694-1.322.654-.31 1.368-.474 2.091-.477 1.851 0 3.44 1.04 4.274 2.635.188-.275.416-.593.626-.836.466-.54 1.04-1.011 1.694-1.322.654-.31 1.368-.474 2.092-.477 2.737 0 4.9 2.274 4.9 5.257v8.588h-4.326v-7.871c.012-.278-.033-.556-.132-.817-.098-.26-.248-.497-.441-.697-.193-.2-.425-.359-.68-.466-.256-.108-.531-.162-.809-.159-.61.014-1.192.266-1.621.703-.421.43-.662 1.003-.676 1.604v7.703h-4.326v-7.871c.012-.278-.033-.556-.132-.817-.098-.26-.248-.497-.442-.697-.192-.2-.424-.359-.68-.466-.255-.108-.53-.162-.808-.159-.61.014-1.192.266-1.621.703-.43.438-.672 1.026-.677 1.64v7.67h-4.326z" transform="translate(-656 -16) translate(656 16)"/>
|
||||
<path fill="#231F20" d="M55.95 22.081v1.17h-4.326V0h4.326v10.86c1.052-.959 2.429-1.48 3.85-1.457 3.756 0 6.692 3.122 6.692 7.06 0 3.937-2.936 7.06-6.692 7.06-.9.013-1.79-.19-2.596-.59-.456-.228-.877-.514-1.254-.852zm.209-7.206c-.274.484-.419 1.031-.422 1.588l-.003-.015c0 .473.104.94.303 1.368.2.429.49.808.852 1.11.362.304.785.524 1.24.645.456.12.932.14 1.395.055.463-.084.903-.269 1.287-.542.384-.274.705-.628.937-1.04.234-.41.374-.868.412-1.339.037-.471-.028-.945-.193-1.389-.122-.373-.319-.719-.577-1.015-.258-.296-.572-.537-.925-.708-.492-.257-1.042-.382-1.596-.364-.554.018-1.095.18-1.57.467-.474.288-.867.694-1.14 1.179z" transform="translate(-656 -16) translate(656 16)"/>
|
||||
<path fill="#231F20" fill-rule="nonzero" d="M68.057 16.463c0-4.073 3.004-7.06 7.536-7.06 4.533 0 7.51 2.987 7.51 7.06 0 4.073-2.974 7.06-7.51 7.06s-7.536-2.987-7.536-7.06zm10.747 0c0-.638-.189-1.261-.541-1.792-.353-.53-.854-.943-1.441-1.187-.587-.244-1.232-.308-1.855-.184-.623.125-1.195.432-1.644.883-.45.45-.755 1.025-.879 1.65-.124.626-.06 1.275.183 1.864.243.59.654 1.093 1.182 1.447.528.355 1.15.544 1.784.544.423.004.842-.076 1.233-.237.39-.16.746-.398 1.044-.698.298-.3.533-.658.692-1.051.158-.394.237-.815.23-1.239h.012zM84.668 9.673v13.579h4.326V15.58c.005-.613.247-1.201.676-1.639.43-.437 1.011-.689 1.622-.703.277-.003.552.051.808.159.256.107.488.266.68.466.193.2.344.437.442.697.099.26.144.539.132.817v7.871h4.326V14.66c0-2.983-2.163-5.257-4.9-5.257-.724.003-1.438.166-2.092.477-.654.311-1.232.762-1.694 1.322v-1.53h-4.326z" transform="translate(-656 -16) translate(656 16)"/>
|
||||
<path fill="#DA1C5C" d="M108.45 13.255c-.344-.377-.977-.762-2.123-.722-2.3.081-3.477 2.276-3.208 3.556.214 1.019-.435 2.02-1.45 2.234-1.014.216-2.01-.436-2.225-1.455-.729-3.47 2.017-7.937 6.751-8.104 2.146-.076 3.872.682 5.022 1.94 1.11 1.214 1.596 2.805 1.465 4.255-.26 2.87-2.463 4.395-4.03 5.389l-.072.045c1.532.41 2.903 1.164 4.066 2.184 1.606 1.409 2.763 3.276 3.432 5.333 2.25.886 4.468 2.098 6.487 3.668.28-1.03.958-2.396 1.58-2.91 1.685 3.234 2.775 7.954 3.16 11.524-2.345-1.133-6.738-2.564-9.686-2.982.566-1.1 1.718-2.167 2.61-2.678-1.105-.855-2.285-1.588-3.501-2.204-.132 2.544-1.31 4.774-2.904 6.418-1.813 1.87-4.31 3.135-6.802 3.251-2.319.108-4.506-.512-6.137-1.86-1.663-1.376-2.615-3.408-2.556-5.78.057-2.289.995-4.49 2.728-6.064 1.748-1.587 4.19-2.432 7.064-2.222.983.071 2.002.206 3.041.408-.298-.392-.628-.748-.986-1.062-1.525-1.337-3.668-2.024-6.418-1.326-.064.016-.128.029-.192.038-.961.192-1.828.149-2.165-.381-.416-.654-.437-1.518.518-2.809.637-.913 1.447-1.609 2.22-2.168.6-.434 1.28-.853 1.876-1.22.226-.139.44-.27.633-.393 1.624-1.03 2.216-1.68 2.294-2.54.037-.405-.106-.942-.491-1.363zm4.451 17.537c-1.726-.527-3.445-.842-5.052-.96-1.963-.143-3.365.434-4.274 1.26-.925.839-1.459 2.041-1.492 3.36-.03 1.237.437 2.15 1.19 2.773.783.648 2.005 1.078 3.575 1.004 1.397-.064 3.03-.819 4.286-2.114 1.237-1.276 1.965-2.925 1.836-4.669-.016-.22-.04-.438-.069-.654z" transform="translate(-656 -16) translate(656 16)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
10
src/images/RecentCallsIcon.svg
Normal file
10
src/images/RecentCallsIcon.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="20" height="18" viewBox="0 0 20 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 0H0V3H5V0Z" />
|
||||
<path d="M5 10H0V13H5V10Z" />
|
||||
<path d="M0 5H5V8H0V5Z" />
|
||||
<path d="M5 15H0V18H5V15Z" />
|
||||
<path d="M7 0H20V3H7V0Z" />
|
||||
<path d="M20 10H7V13H20V10Z" />
|
||||
<path d="M7 5H20V8H7V5Z" />
|
||||
<path d="M20 15H7V18H20V15Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
6
src/images/SpeechIcon.svg
Normal file
6
src/images/SpeechIcon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C11.1712 18 12.281 17.749 13.2809 17.2987C13.5009 17.1997 13.7493 17.1835 13.9802 17.2531C15.0582 17.5782 16.1613 17.7632 17.0663 17.8677C16.7653 17.2512 16.4715 16.4797 16.2688 15.5535C16.2071 15.272 16.2699 14.9776 16.4411 14.7457C17.4211 13.4182 18 11.7781 18 10C18 5.58172 14.4183 2 10 2ZM19 19C19 20 18.9997 20 18.9997 20L18.9957 20L18.9875 20L18.9597 19.9998C18.9361 19.9995 18.9025 19.9991 18.8597 19.9983C18.7741 19.9966 18.6512 19.9933 18.4967 19.9868C18.1878 19.9738 17.7507 19.9479 17.2293 19.8965C16.2798 19.8028 15.0295 19.6221 13.7566 19.2701C12.5956 19.741 11.3269 20 10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10C20 12.0489 19.3828 13.9562 18.3244 15.5429C18.5644 16.4315 18.897 17.105 19.1771 17.5645C19.3383 17.8291 19.482 18.0224 19.5801 18.1443C19.6291 18.2052 19.6665 18.248 19.6889 18.2729C19.7001 18.2853 19.7076 18.2932 19.7107 18.2965L19.709 18.2948L19.7081 18.2939C19.7096 18.2954 19.7105 18.2963 19.7119 18.2977C19.9941 18.5837 20.0778 19.0111 19.9239 19.3827C19.7691 19.7564 19.4041 20 18.9997 20L19 19ZM19.7119 18.2977V18.2977Z" />
|
||||
<path d="M6 5H14V7H6V5Z" />
|
||||
<path d="M4 9H16V11H4V9Z" />
|
||||
<path d="M6 13H14V15H6V13Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -38,6 +38,7 @@ body {
|
||||
font-family: WorkSans, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import { NotificationProvider } from './contexts/NotificationContext';
|
||||
import { ModalProvider } from './contexts/ModalContext';
|
||||
import { ShowMsTeamsProvider } from './contexts/ShowMsTeamsContext';
|
||||
import { ServiceProvider } from './contexts/ServiceProviderContext';
|
||||
import App from './App';
|
||||
|
||||
import "antd/dist/antd.css";
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.render(
|
||||
<NotificationProvider>
|
||||
<ModalProvider>
|
||||
<ShowMsTeamsProvider>
|
||||
<App />
|
||||
<ServiceProvider>
|
||||
<App />
|
||||
</ServiceProvider>
|
||||
</ShowMsTeamsProvider>
|
||||
</ModalProvider>
|
||||
</NotificationProvider>,
|
||||
|
||||
Reference in New Issue
Block a user