mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-07-04 19:21:58 +00:00
jambonz webapp refresh (#64)
initial scaffold
switch to preact/compat
add feather icons dep
jambonz-ui, index.html
stub auth and store
readme tweaks
alias preact in vite config
more readme tweaks
Update README.md
lots of things
login flow...
add notes on apis by route
lots of work...
readmes
constants
Update login.tsx
Update index.ts
Update index.ts
Update create-password.tsx
Update actions.ts
Update index.tsx
Update index.tsx
Update actions.ts
Update index.ts
react version for eslint
some refactor and cleanup
Update api.ts
Update create-password.tsx
fetch transport wrapper
api util
toast time -- oops
msg constants
img path for docs/readmes
global dispatch, generic actions etc...
unreachable and stuff
properly wrap require-auth routes
support promise chain and async/await for api fetch transport
initial responsive navi menu
Update navi-data.ts
Update navi.tsx
Update styles.scss
Rename navi-data.ts to navi-items.ts
Update navi.tsx
Update index.ts
Update layout.tsx
Update index.ts
Update layout.tsx
Update index.tsx
Update index.tsx
Update actions.ts
Update index.tsx
Update index.tsx
Update create-password.tsx
Update login.tsx
Update create-password.tsx
move things around
access control interface
Update index.tsx
acl component etc
working on settings form..
more settings, forms, HOCs
service providers workflow
button up modals and toasts
mobile navi and toast timeout
Update index.tsx
Update index.ts
Update and rename index.ts to index.tsx
Update create-password.tsx
Update create-password.tsx
Update Dockerfile
Update entrypoint.sh
Update Dockerfile
Update navi.tsx
Update auth.tsx
Update auth.tsx
Update layout.tsx
Update layout.tsx
Update login.tsx
Update login.tsx
Update settings.tsx
Update index.tsx
Update index.ts
better lint-staged
fix sp undefined
toast dispatch helpers
sass vars -- no magic numbers
Update index.ts
Update create-password.tsx
Update login.tsx
Update index.ts
Update settings.tsx
Update accounts.tsx
working on settings...
Update index.ts
Update settings.tsx
Update index.tsx
more settings view...
get rid of most any usage
Update index.tsx
better api hook
get strong with types
obscured text component
HOC for dispatch type-safety
tweak api types
github icon on login layout
responsive grid -- api keys
better fetch transport with resolve/reject
fix generic action/dispatch typings
prefer interface for GlobalDispatch
Update index.ts
Update auth.tsx
Update auth.tsx
Update create-password.tsx
checkzones
wrap up checkzones
move styles around...
alias src
stub internal views
stub not found container
contrib readme and codeowners
Update README.md
Update and rename setup.md to environment.md
Update environment.md
Update environment.md
Update contrib.md
Update contrib.md
Update contrib.md
Update and rename contrib.md to contributors.md
Update contributors.md
Update index.ts
use api data hook
accounts stub, generic apikeys container
account edit form
Update edit.tsx
Update edit.tsx
add/edit for account form
lots of good refactors
check current sp on settings
grid stuff
Update index.scss
Update styles.scss
Update contributors.md
Update constants.ts
stubbing accounts as card view
Update types.ts
Update types.ts
Update auth.tsx
Update create-password.tsx
Update index.ts
Update index.tsx
fix enum status codes
component cleanup
delete account flow
Update types.ts
Update delete.tsx
Update use-mobile-media.ts
acl hoc
Update types.ts
Update index.ts
Update types.ts
fix generic useapidata
Update types.ts
Update types.ts
Update types.ts
Create index.tsx
Create types.ts
Update types.ts
Create types.ts
Update index.tsx
button up acl, feature flags and docs
subspace initial feature stub
fix some things
wrap up subspace feature
tooltip
Update subspace.tsx
Delete styles.scss
Update types.ts
Update auth.tsx
Update index.ts
some more type stuff
add react/jsx-key error for missing shorthand frag keys
basic spinner...
no accounts
data files for regions and speech
vendor selector logic
tighten up vendor stuff
bit more cleanup
Update types.ts
Update index.tsx
Update index.tsx
Update subspace.tsx
fix some type things
stub mock dev server implementation
add parity for account siprec_hook_sid
latest jambonz-ui update
cleanup package.json
fix docker stuff
docker notes in readme
adding github actions
package lock version
remove unused jest deps
update jambonz-ui
new new jambonz-ui
list view vs cards view
fix no accounts list view
fix prettier config
some house cleaning
file upload component
update pr-checks wildcard
wrappers for fetch transport -- any method (#78)
Refresh tweaks (#80)
* add alerts to mock api dev server
add webhook methods types
fix focus for file upload
update contrib readme
blob fetching
rest props spread for file-upload
* multi element fieldset structure, unique basic auth field names for accounts form
* Fix and simplify webhook state setting
* some ad-hoc cleanup for temp work
Adding generic account filter component (#82)
adding focused styles for account-filter
more robust account-filter props
updates to contrib readme
fix add service provider form a la new styles
required form field UI and labeling
Application page for refresh (#79)
* Adding barely working Application page (#70)
* resolve conflict and update, still barely working
* perfectly working application page
* Fix the duplicated name logic
* strip some comments
* changes to sync
* delete more condition
* some more changes for parity
* revert changes
* applying b1a9a77
* changes requested
* changes suggested
* changes suggested
* sync changes
organize some styles a bit more
refactor generic small selector styles
use portals for modals and toasts
add new classNames to applications form
handle applications view without accounts condition
sweep through with some cleanup
type-safety for :POST and :PUT api methods
Speech service page for refresh (#84)
* initial commit
* more update, probably one more
* properly rebase
* check box works okay
* properly rebase**2
* initial cleanup and ux-flow evaluation
* obscure secrets on frontend for local state
* refine ui for credential status checks
* ignore error set on unmount for CredentialStatus
* fix obscure field type crash bug
* Update utils.ts
* Update utils.ts
* Update constants.ts
* wrap up the speech credentials flow
Co-authored-by: kitajchuk <bk@kitajchuk.com>
tweaks to ui elements etc
tweak some typings and minor ui styles
better not tested messaging for TTS/STT
better placeholder feature flag for dev
Adding some conditional utilities (#87)
Microsoft Tenant page for refresh (#86)
* initial commit
* fix backend error with adding
* changes suggested
* changes requested
* use all accounts for ms teams tenants
* ui tweaks, add last ditch redirect back to form
Co-authored-by: kitajchuk <bk@kitajchuk.com>
Phone number page for refresh (#85)
* initial commit so I can hop back
* working properly
* carrier related change and mass edit
* mvoing around
* UI for mass edit
* unset selected for mass edit
* some minor ui cleanup
* fix empty/bad classNames on edit action icon
Co-authored-by: kitajchuk <bk@kitajchuk.com>
Update .dockerignore
cleanup and port some helpers
fix applications index useEffect
group synthesis fields and recognizer fields for application form
tweak contrib readme
just run tsc in pre-commit
update contrib readme
Carrier page for refresh (#89)
* initial commit
* working form but no put/post for gateways yet
* put/post for sip/smpp
* crud app done
* all the functionalities are here, unless it isnt
* changes suggested and delete sip/smpp when delete carrier
* Some initial UI cleanup etc...
* More UI cleanup and what not...
* No need for the 'Status' text here
* Remove the Grid component -- not reused
* Remove as much explicit null type as possible
* Use webhook methods constant in account form
* Some API constants and fix inbound/outbound smpp gateways delete with filter logic
* Tab handling logic for carrier form (#91)
* similar validation logic of sip for smpp
* Tech prefix tab validation
* revert to working sip gateway validation
* More validation cleanup
* Update index.ts
* Update index.ts
* More cleanup and form clarity for if/when required fields
* Fix some logic and reset gateways to delete when deleted
* use api data hooks for index partials
* smpp gateway validation and fqdn validations
* default application selector
* Fix up the SMPP dilemma...
* Typo and remove console log
* Tab switch for all validations
* Move empty SIP check to validation getter
* Render gateway validation messages near the invalid fields
* Explicit return on first active tab condition for browser constraints
* Use IP pattern for outbound smpp gateway since fqdn is disallowed here
* Add fqdn example to ip placeholders
* sticky tabs
* Tweak info text
* Gateway refetch code change
* delete gateways on demand
* move shared api fetching down into forms -- seems better actually
* Fix re-render glitch for gateways UI
Co-authored-by: kitajchuk <bk@kitajchuk.com>
Tweaks and minor cleanups
Few more minor tweaks
prettier package.json -- duh
Functional recent-calls dev server api
switch from moment to dayjs
Functional alerts dev server api
Dev server notes in readme
Fix applications bug which fixes current SP switch
Create hooks for vendor async data
No lazy load for routes (#97)
better speech hook
match dev mock paged response to api server paged response
Generic AccountSelect component
Generic useRedirect hook
Recent call page for refresh (#93)
* initial commit
* changes requested but yet to pcap
* pcap?
* Initial cleanup on RecentCalls
* Normalize set page number and fix status for mock dev server
* Listt item styles and details/pcap fix
* Refactor recent calls subcomponents
* Tighter section padding and smaller page titles
* Update _lists.scss
* Recent calls cleanup and some other tweaks
Co-authored-by: kitajchuk <bk@kitajchuk.com>
Implement proper Checkzone initial checked for carrier form
Add handleSelect prop to SelectFilter component and fix perPageFilter changes for recent calls
Alert view page for refresh (#99)
* initial commit
* changes suggested
* changes requested
* Style alerts UI
Co-authored-by: kitajchuk <bk@kitajchuk.com>
Refresh enhancement pagination logic (#101)
* initial commit
* Sort of secret props...
Co-authored-by: kitajchuk <bk@kitajchuk.com>
Quick small screen mobile sweep -- add logout button to mobile navi
Update index.tsx
Tweak some styles and restyle navi SP selector
Add key prop to ApiKeys on Settings so SP switch refetches data
fix checkbox margin now that grid-gap is used
Style tweaks
Update types.ts
Update delete.tsx
Update delete.tsx
Move some variable declarations around
Generic application select for forms
Fix issue #105 for carrier form applications
Use memo for filtered carriers on list view
Refactor generic application filter component
Update application-filter.tsx
Update types.ts
Update index.tsx
Update index.tsx
Update index.tsx
Update index.tsx
Cleanup some stuff -- add locked prop for Passwd
Fix unauthorized logout scenarios -- no react state errors :)
Normalize React types usage
Fuzzy search filter for collection lists (#106)
* Fuzzy search filter for collection lists
* Tweak some things for responsiveness
* Carrier preset label and fix All accounts filter for carriers list
Set text overflow on search filter
PR checklist items
Match 'No ...' text for speech services
Return rawCollection if hasLength check is false
Responsive styling for list item--action rows
Fix defaultOption for AccountSelector
Refresh: Add API limits for issue #109 (#111)
* Add API limits for issue #109
* Tighten up initial field renders
Cleanup for issue #104 (#108)
Co-authored-by: kitajchuk <bk@kitajchuk.com>
Tweak local limits
Generic local limits component
No default local limits -- move to forms components
Safe set values for limits -- maintain controlled inputs
Ref support for local limits form component
Handle empty data for local limits effect
Implement DELETE for limits
Singular nomenclature for post limit(s)
This commit is contained in:
committed by
GitHub
parent
1276687cc0
commit
f381eba694
-111
@@ -1,111 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
|
||||
import { NotificationStateContext } from './contexts/NotificationContext';
|
||||
|
||||
import Login from './components/pages/Login';
|
||||
import CreatePassword from './components/pages/setup/CreatePassword';
|
||||
import ConfigureAccount from './components/pages/setup/ConfigureAccount';
|
||||
import CreateApplication from './components/pages/setup/CreateApplication';
|
||||
import ConfigureSipTrunk from './components/pages/setup/ConfigureSipTrunk';
|
||||
import SetupComplete from './components/pages/setup/SetupComplete';
|
||||
import AccountsList from './components/pages/internal/AccountsList';
|
||||
import ApplicationsList from './components/pages/internal/ApplicationsList';
|
||||
import CarriersList from './components/pages/internal/CarriersList';
|
||||
import PhoneNumbersList from './components/pages/internal/PhoneNumbersList';
|
||||
import MsTeamsTenantsList from './components/pages/internal/MsTeamsTenantsList';
|
||||
import AccountsAddEdit from './components/pages/internal/AccountsAddEdit';
|
||||
import ApplicationsAddEdit from './components/pages/internal/ApplicationsAddEdit';
|
||||
import CarriersAddEdit from './components/pages/internal/CarriersAddEdit';
|
||||
import PhoneNumbersAddEdit from './components/pages/internal/PhoneNumbersAddEdit';
|
||||
import MsTeamsTenantsAddEdit from './components/pages/internal/MsTeamsTenantsAddEdit';
|
||||
import Settings from './components/pages/internal/Settings';
|
||||
import RecentCallsList from './components/pages/internal/RecentCallsList';
|
||||
import AlertsList from './components/pages/internal/AlertsList';
|
||||
import InvalidRoute from './components/pages/InvalidRoute';
|
||||
import SpeechServicesList from './components/pages/internal/SpeechServicesList';
|
||||
import SpeechServicesAddEdit from './components/pages/internal/SpeechServicesAddEdit';
|
||||
|
||||
import Notification from './components/blocks/Notification';
|
||||
import Nav from './components/blocks/Nav';
|
||||
import SideMenu from './components/blocks/SideMenu';
|
||||
|
||||
function App() {
|
||||
const notifications = useContext(NotificationStateContext);
|
||||
return (
|
||||
<Router>
|
||||
<Notification notifications={notifications} />
|
||||
<Nav />
|
||||
<Switch>
|
||||
<Route exact path="/"><Login /></Route>
|
||||
<Route exact path="/create-password"><CreatePassword /></Route>
|
||||
<Route exact path="/configure-account"><ConfigureAccount /></Route>
|
||||
<Route exact path="/create-application"><CreateApplication /></Route>
|
||||
<Route exact path="/configure-sip-trunk"><ConfigureSipTrunk /></Route>
|
||||
<Route exact path="/setup-complete"><SetupComplete /></Route>
|
||||
|
||||
<Route path="/internal">
|
||||
<div style={{ display: "flex" }}>
|
||||
<SideMenu />
|
||||
<Route exact path="/internal/accounts"><AccountsList /></Route>
|
||||
<Route exact path="/internal/applications"><ApplicationsList /></Route>
|
||||
<Route exact path="/internal/carriers"><CarriersList /></Route>
|
||||
<Route exact path="/internal/speech-services"><SpeechServicesList /></Route>
|
||||
<Route exact path="/internal/phone-numbers"><PhoneNumbersList /></Route>
|
||||
<Route exact path="/internal/ms-teams-tenants"><MsTeamsTenantsList /></Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/accounts/add",
|
||||
"/internal/accounts/:account_sid/edit"
|
||||
]}>
|
||||
<AccountsAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/applications/add",
|
||||
"/internal/applications/:application_sid/edit"
|
||||
]}>
|
||||
<ApplicationsAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/carriers/add",
|
||||
"/internal/carriers/:voip_carrier_sid/edit"
|
||||
]}>
|
||||
<CarriersAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/speech-services/add",
|
||||
"/internal/speech-services/:speech_service_sid/edit"
|
||||
]}>
|
||||
<SpeechServicesAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/phone-numbers/add",
|
||||
"/internal/phone-numbers/:phone_number_sid/edit"
|
||||
]}>
|
||||
<PhoneNumbersAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path={[
|
||||
"/internal/ms-teams-tenants/add",
|
||||
"/internal/ms-teams-tenants/:ms_teams_tenant_sid/edit"
|
||||
]}>
|
||||
<MsTeamsTenantsAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path="/internal/settings"><Settings /></Route>
|
||||
|
||||
<Route exact path="/internal/recent-calls"><RecentCallsList /></Route>
|
||||
<Route exact path="/internal/alerts"><AlertsList /></Route>
|
||||
</div>
|
||||
</Route>
|
||||
|
||||
<Route><InvalidRoute /></Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,141 @@
|
||||
import type {
|
||||
LimitField,
|
||||
SipGateway,
|
||||
SmppGateway,
|
||||
WebHook,
|
||||
WebhookOption,
|
||||
} from "./types";
|
||||
|
||||
/** This window object is serialized and injected at docker runtime */
|
||||
/** The API url is constructed with the docker containers `ip:port` */
|
||||
interface JambonzWindowObject {
|
||||
API_BASE_URL: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
JAMBONZ: JambonzWindowObject;
|
||||
}
|
||||
}
|
||||
|
||||
/** https://vitejs.dev/guide/env-and-mode.html#env-files */
|
||||
export const API_BASE_URL =
|
||||
window.JAMBONZ?.API_BASE_URL || import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
/** Serves mock API responses from a local dev API server */
|
||||
export const DEV_BASE_URL = import.meta.env.VITE_DEV_BASE_URL;
|
||||
|
||||
/** TCP Max Port */
|
||||
export const TCP_MAX_PORT = 65535;
|
||||
|
||||
/** Tech Prefix minlength */
|
||||
export const TECH_PREFIX_MINLENGTH = 3;
|
||||
|
||||
/** IP Types for validations */
|
||||
export const IP = "ip";
|
||||
export const FQDN = "fqdn";
|
||||
export const FQDN_TOP_LEVEL = "fqdn-top-level";
|
||||
export const INVALID = "invalid";
|
||||
|
||||
/** Default API object models */
|
||||
export const DEFAULT_WEBHOOK: WebHook = {
|
||||
url: "",
|
||||
method: "POST",
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
/** Default SIP/SMPP Gateways */
|
||||
export const DEFAULT_SIP_GATEWAY: SipGateway = {
|
||||
voip_carrier_sid: "",
|
||||
ipv4: "",
|
||||
port: 5060,
|
||||
netmask: 32,
|
||||
is_active: false,
|
||||
inbound: 1,
|
||||
outbound: 0,
|
||||
};
|
||||
|
||||
export const DEFAULT_SMPP_GATEWAY: SmppGateway = {
|
||||
voip_carrier_sid: "",
|
||||
ipv4: "",
|
||||
port: 2775,
|
||||
is_primary: false,
|
||||
use_tls: false,
|
||||
netmask: 32,
|
||||
inbound: 1,
|
||||
outbound: 1,
|
||||
};
|
||||
|
||||
/** Netmask Bits */
|
||||
export const NETMASK_BITS = Array(32)
|
||||
.fill(0)
|
||||
.map((_, index) => index + 1)
|
||||
.reverse();
|
||||
|
||||
export const NETMASK_OPTIONS = NETMASK_BITS.map((bit) => ({
|
||||
name: bit.toString(),
|
||||
value: bit.toString(),
|
||||
}));
|
||||
|
||||
/** List view filters */
|
||||
export const DATE_SELECTION = [
|
||||
{ name: "today", value: "today" },
|
||||
{ name: "last 7d", value: "7" },
|
||||
{ name: "last 14d", value: "14" },
|
||||
{ name: "last 30d", value: "30" },
|
||||
];
|
||||
|
||||
export const PER_PAGE_SELECTION = [
|
||||
{ name: "25 / page", value: "25" },
|
||||
{ name: "50 / page", value: "50" },
|
||||
{ name: "100 / page", value: "100" },
|
||||
];
|
||||
|
||||
/** Available webhook methods */
|
||||
export const WEBHOOK_METHODS: WebhookOption[] = [
|
||||
{
|
||||
name: "POST",
|
||||
value: "POST",
|
||||
},
|
||||
{
|
||||
name: "GET",
|
||||
value: "GET",
|
||||
},
|
||||
];
|
||||
|
||||
/** Various system limits */
|
||||
/** For now we are only supporting `voice_call_session` */
|
||||
export const LIMITS: LimitField[] = [
|
||||
{
|
||||
label: "Max concurrent calls (0=unlimited)",
|
||||
category: "voice_call_session",
|
||||
},
|
||||
// {
|
||||
// label: "Max registered devices (0=unlimited)",
|
||||
// category: "device",
|
||||
// },
|
||||
// {
|
||||
// label: "Max api calls per minute (0=unlimited)",
|
||||
// category: "api_rate",
|
||||
// },
|
||||
];
|
||||
|
||||
/** Speech credential test result status values */
|
||||
export const CRED_OK = "ok";
|
||||
export const CRED_FAIL = "fail";
|
||||
export const CRED_NOT_TESTED = "not tested";
|
||||
|
||||
/** API base paths */
|
||||
export const API_LOGIN = `${API_BASE_URL}/login`;
|
||||
export const API_SBCS = `${API_BASE_URL}/Sbcs`;
|
||||
export const API_USERS = `${API_BASE_URL}/Users`;
|
||||
export const API_API_KEYS = `${API_BASE_URL}/ApiKeys`;
|
||||
export const API_ACCOUNTS = `${API_BASE_URL}/Accounts`;
|
||||
export const API_APPLICATIONS = `${API_BASE_URL}/Applications`;
|
||||
export const API_PHONE_NUMBERS = `${API_BASE_URL}/PhoneNumbers`;
|
||||
export const API_MS_TEAMS_TENANTS = `${API_BASE_URL}/MicrosoftTeamsTenants`;
|
||||
export const API_SERVICE_PROVIDERS = `${API_BASE_URL}/ServiceProviders`;
|
||||
export const API_CARRIERS = `${API_BASE_URL}/VoipCarriers`;
|
||||
export const API_SMPP_GATEWAY = `${API_BASE_URL}/SmppGateways`;
|
||||
export const API_SIP_GATEWAY = `${API_BASE_URL}/SipGateways`;
|
||||
@@ -0,0 +1,587 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useSelectState } from "src/store";
|
||||
import { getToken } from "src/router/auth";
|
||||
import {
|
||||
DEV_BASE_URL,
|
||||
API_BASE_URL,
|
||||
API_LOGIN,
|
||||
API_USERS,
|
||||
API_SERVICE_PROVIDERS,
|
||||
API_API_KEYS,
|
||||
API_ACCOUNTS,
|
||||
API_APPLICATIONS,
|
||||
API_MS_TEAMS_TENANTS,
|
||||
API_PHONE_NUMBERS,
|
||||
API_CARRIERS,
|
||||
API_SMPP_GATEWAY,
|
||||
API_SIP_GATEWAY,
|
||||
} from "./constants";
|
||||
import { ROUTE_LOGIN } from "src/router/routes";
|
||||
import {
|
||||
SESS_UNAUTHORIZED,
|
||||
MSG_SESS_EXPIRED,
|
||||
MSG_SERVER_DOWN,
|
||||
MSG_SOMETHING_WRONG,
|
||||
} from "src/constants";
|
||||
|
||||
import type {
|
||||
FetchError,
|
||||
FetchTransport,
|
||||
User,
|
||||
UserLogin,
|
||||
ServiceProvider,
|
||||
SidResponse,
|
||||
TokenResponse,
|
||||
EmptyResponse,
|
||||
SecretResponse,
|
||||
UseApiData,
|
||||
Alert,
|
||||
PagedResponse,
|
||||
RecentCall,
|
||||
UserLoginPayload,
|
||||
UserUpdatePayload,
|
||||
ApiKey,
|
||||
Account,
|
||||
Application,
|
||||
SpeechCredential,
|
||||
MSTeamsTenant,
|
||||
PhoneNumber,
|
||||
Carrier,
|
||||
SmppGateway,
|
||||
SipGateway,
|
||||
TotalResponse,
|
||||
CallQuery,
|
||||
PageQuery,
|
||||
Limit,
|
||||
LimitCategories,
|
||||
} from "./types";
|
||||
import { StatusCodes } from "./types";
|
||||
|
||||
/** Wrap all requests to normalize response handling */
|
||||
const fetchTransport = <Type>(
|
||||
url: string,
|
||||
options: RequestInit
|
||||
): Promise<FetchTransport<Type>> => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const transport: FetchTransport<Type> = {
|
||||
status: response.status,
|
||||
json: <Type>{},
|
||||
};
|
||||
|
||||
// Redirect unauthorized
|
||||
if (response.status === StatusCodes.UNAUTHORIZED) {
|
||||
handleUnauthorized();
|
||||
reject();
|
||||
}
|
||||
|
||||
// API error handling returns { msg: string; }
|
||||
// See @type StatusJSON and StatusEmpty in ./types
|
||||
if (
|
||||
response.status >= StatusCodes.BAD_REQUEST &&
|
||||
response.status <= StatusCodes.INTERNAL_SERVER_ERROR
|
||||
) {
|
||||
try {
|
||||
const errJson = await response.json();
|
||||
reject(<FetchError>{
|
||||
status: response.status,
|
||||
...errJson,
|
||||
});
|
||||
} catch (error) {
|
||||
reject(<FetchError>{
|
||||
status: response.status,
|
||||
msg: MSG_SOMETHING_WRONG,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// API success handling returns a valid JSON response
|
||||
// This could either be a DTO object or a generic response
|
||||
// See types for various responses in ./types
|
||||
if (
|
||||
response.status === StatusCodes.OK ||
|
||||
response.status === StatusCodes.CREATED
|
||||
) {
|
||||
// Handle blobs -- e.g. pcap file API for RecentCalls
|
||||
if (
|
||||
options.headers!["Content-Type" as keyof HeadersInit] ===
|
||||
"application/octet-stream"
|
||||
) {
|
||||
const blob: Blob = await response.blob();
|
||||
|
||||
transport.blob = blob;
|
||||
} else {
|
||||
const json: Type = await response.json();
|
||||
|
||||
transport.json = json;
|
||||
}
|
||||
}
|
||||
|
||||
resolve(transport);
|
||||
// TypeError "Failed to fetch"
|
||||
// net::ERR_CONNECTION_REFUSED
|
||||
// This is the case if the server is unreachable...
|
||||
} catch (error: unknown) {
|
||||
// Caveat -- we don't kill the app if this is a bad request on local dev server
|
||||
if (!url.includes(DEV_BASE_URL)) {
|
||||
handleUnreachable();
|
||||
}
|
||||
|
||||
reject(<FetchError>{
|
||||
status: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
msg: (error as TypeError).message,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const token = getToken();
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
};
|
||||
|
||||
const getQuery = <Type>(query: Type) => {
|
||||
return decodeURIComponent(
|
||||
new URLSearchParams(query as unknown as Record<string, string>).toString()
|
||||
);
|
||||
};
|
||||
|
||||
/** Hard boot on 401 status code for unauthorized users */
|
||||
/** Since you're unauthorized there's no harm just reloading the app from "/" */
|
||||
/** We set a storage item for the dispatch message that is captured in the Login container */
|
||||
const handleBadRequest = (msg: string) => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
if (window.location.pathname !== ROUTE_LOGIN) {
|
||||
sessionStorage.setItem(SESS_UNAUTHORIZED, msg);
|
||||
window.location.href = ROUTE_LOGIN;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnauthorized = () => {
|
||||
handleBadRequest(MSG_SESS_EXPIRED);
|
||||
};
|
||||
|
||||
const handleUnreachable = () => {
|
||||
handleBadRequest(MSG_SERVER_DOWN);
|
||||
};
|
||||
|
||||
/** Wrapper for fetching Blobs -- API use case is RecentCalls pcap files */
|
||||
|
||||
export const getBlob = (url: string) => {
|
||||
return fetchTransport(url, {
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/** Simple wrappers for fetchTransport calls to any API, :GET, :POST, :PUT, :DELETE */
|
||||
|
||||
export const getFetch = <Type>(url: string) => {
|
||||
return fetchTransport<Type>(url, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const postFetch = <Type, Payload>(url: string, payload: Payload) => {
|
||||
return fetchTransport<Type>(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const putFetch = <Type, Payload>(url: string, payload: Payload) => {
|
||||
return fetchTransport<Type>(url, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload),
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteFetch = <Type>(url: string) => {
|
||||
return fetchTransport<Type>(url, {
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
/** All APIs need a wrapper utility that uses the FetchTransport */
|
||||
|
||||
export const postLogin = (payload: UserLoginPayload) => {
|
||||
return fetchTransport<UserLogin>(API_LOGIN, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/** Named wrappers for `postFetch` */
|
||||
|
||||
export const postServiceProviders = (payload: Partial<ServiceProvider>) => {
|
||||
return postFetch<SidResponse, Partial<ServiceProvider>>(
|
||||
API_SERVICE_PROVIDERS,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const postApiKey = (payload: Partial<ApiKey>) => {
|
||||
return postFetch<TokenResponse, Partial<ApiKey>>(API_API_KEYS, payload);
|
||||
};
|
||||
|
||||
export const postAccount = (payload: Partial<Account>) => {
|
||||
return postFetch<SidResponse, Partial<Account>>(API_ACCOUNTS, payload);
|
||||
};
|
||||
|
||||
export const postApplication = (payload: Partial<Application>) => {
|
||||
return postFetch<SidResponse, Partial<Application>>(
|
||||
API_APPLICATIONS,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const postSpeechService = (
|
||||
sid: string,
|
||||
payload: Partial<SpeechCredential>
|
||||
) => {
|
||||
return postFetch<SidResponse, Partial<SpeechCredential>>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid}/SpeechCredentials`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const postMsTeamsTentant = (payload: Partial<MSTeamsTenant>) => {
|
||||
return postFetch<SidResponse, Partial<MSTeamsTenant>>(
|
||||
API_MS_TEAMS_TENANTS,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const postPhoneNumber = (payload: Partial<PhoneNumber>) => {
|
||||
return postFetch<SidResponse, Partial<PhoneNumber>>(
|
||||
API_PHONE_NUMBERS,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const postCarrier = (sid: string, payload: Partial<Carrier>) => {
|
||||
return postFetch<SidResponse, Partial<Carrier>>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid}/VoipCarriers/`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const postSipGateway = (payload: Partial<SipGateway>) => {
|
||||
return postFetch<SidResponse, Partial<SipGateway>>(API_SIP_GATEWAY, payload);
|
||||
};
|
||||
|
||||
export const postSmppGateway = (payload: Partial<SmppGateway>) => {
|
||||
return postFetch<SidResponse, Partial<SmppGateway>>(
|
||||
API_SMPP_GATEWAY,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const postServiceProviderLimit = (
|
||||
sid: string,
|
||||
payload: Partial<Limit>
|
||||
) => {
|
||||
return postFetch<SidResponse, Partial<Limit>>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid}/Limits`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const postAccountLimit = (sid: string, payload: Partial<Limit>) => {
|
||||
return postFetch<SidResponse, Partial<Limit>>(
|
||||
`${API_ACCOUNTS}/${sid}/Limits`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
/** Named wrappers for `putFetch` */
|
||||
|
||||
export const putUser = (sid: string, payload: UserUpdatePayload) => {
|
||||
return putFetch<EmptyResponse, UserUpdatePayload>(
|
||||
`${API_USERS}/${sid}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const putServiceProvider = (
|
||||
sid: string,
|
||||
payload: Partial<ServiceProvider>
|
||||
) => {
|
||||
return putFetch<EmptyResponse, Partial<ServiceProvider>>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const putAccount = (sid: string, payload: Partial<Account>) => {
|
||||
return putFetch<EmptyResponse, Partial<Account>>(
|
||||
`${API_ACCOUNTS}/${sid}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const putApplication = (sid: string, payload: Partial<Application>) => {
|
||||
return putFetch<EmptyResponse, Partial<Application>>(
|
||||
`${API_APPLICATIONS}/${sid}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const putSpeechService = (
|
||||
sid1: string,
|
||||
sid2: string,
|
||||
payload: Partial<SpeechCredential>
|
||||
) => {
|
||||
return putFetch<EmptyResponse, Partial<SpeechCredential>>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid1}/SpeechCredentials/${sid2}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const putMsTeamsTenant = (
|
||||
sid: string,
|
||||
payload: Partial<MSTeamsTenant>
|
||||
) => {
|
||||
return putFetch<EmptyResponse, Partial<MSTeamsTenant>>(
|
||||
`${API_MS_TEAMS_TENANTS}/${sid}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const putPhoneNumber = (sid: string, payload: Partial<PhoneNumber>) => {
|
||||
return putFetch<EmptyResponse, Partial<PhoneNumber>>(
|
||||
`${API_PHONE_NUMBERS}/${sid}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const putCarrier = (
|
||||
sid1: string,
|
||||
sid2: string,
|
||||
payload: Partial<Carrier>
|
||||
) => {
|
||||
return putFetch<EmptyResponse, Partial<Carrier>>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid1}/VoipCarriers/${sid2}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const putSipGateway = (sid: string, payload: Partial<SipGateway>) => {
|
||||
return putFetch<EmptyResponse, Partial<SipGateway>>(
|
||||
`${API_SIP_GATEWAY}/${sid}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const putSmppGateway = (sid: string, payload: Partial<SmppGateway>) => {
|
||||
return putFetch<EmptyResponse, Partial<SmppGateway>>(
|
||||
`${API_SMPP_GATEWAY}/${sid}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
/** Named wrappers for `deleteFetch` */
|
||||
|
||||
export const deleteServiceProvider = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_SERVICE_PROVIDERS}/${sid}`);
|
||||
};
|
||||
|
||||
export const deleteApiKey = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_API_KEYS}/${sid}`);
|
||||
};
|
||||
|
||||
export const deleteAccount = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_ACCOUNTS}/${sid}`);
|
||||
};
|
||||
|
||||
export const deleteApplication = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_APPLICATIONS}/${sid}`);
|
||||
};
|
||||
|
||||
export const deleteSpeechService = (sid1: string, sid2: string) => {
|
||||
return deleteFetch<EmptyResponse>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid1}/SpeechCredentials/${sid2}`
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteMsTeamsTenant = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_MS_TEAMS_TENANTS}/${sid}`);
|
||||
};
|
||||
|
||||
export const deletePhoneNumber = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_PHONE_NUMBERS}/${sid}`);
|
||||
};
|
||||
|
||||
export const deleteCarrier = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_CARRIERS}/${sid}`);
|
||||
};
|
||||
|
||||
export const deleteSipGateway = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_SIP_GATEWAY}/${sid}`);
|
||||
};
|
||||
|
||||
export const deleteSmppGateway = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_SMPP_GATEWAY}/${sid}`);
|
||||
};
|
||||
|
||||
export const deleteServiceProviderLimit = (
|
||||
sid: string,
|
||||
cat: LimitCategories
|
||||
) => {
|
||||
return deleteFetch<EmptyResponse>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid}/Limits?category=${cat}`
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteAccountLimit = (sid: string, cat: LimitCategories) => {
|
||||
return deleteFetch<EmptyResponse>(
|
||||
`${API_ACCOUNTS}/${sid}/Limits?category=${cat}`
|
||||
);
|
||||
};
|
||||
|
||||
/** Named wrappers for `getFetch` */
|
||||
|
||||
/** This is not a functioning API at the moment... */
|
||||
export const getUser = (sid: string) => {
|
||||
return getFetch<User>(`${API_USERS}/${sid}`);
|
||||
};
|
||||
|
||||
export const getServiceProviders = () => {
|
||||
return getFetch<ServiceProvider[]>(API_SERVICE_PROVIDERS);
|
||||
};
|
||||
|
||||
export const getAccountWebhook = (sid: string) => {
|
||||
return getFetch<SecretResponse>(
|
||||
`${API_ACCOUNTS}/${sid}/WebhookSecret?regenerate=true`
|
||||
);
|
||||
};
|
||||
|
||||
/** Wrappers for APIs that can have a mock dev server response */
|
||||
|
||||
export const getRecentCalls = (sid: string, query: Partial<CallQuery>) => {
|
||||
const qryStr = getQuery<Partial<CallQuery>>(query);
|
||||
|
||||
return getFetch<PagedResponse<RecentCall>>(
|
||||
import.meta.env.DEV
|
||||
? `${DEV_BASE_URL}/Accounts/${sid}/RecentCalls?${qryStr}`
|
||||
: `${API_ACCOUNTS}/${sid}/RecentCalls?${qryStr}`
|
||||
);
|
||||
};
|
||||
|
||||
export const getRecentCall = (sid: string, callSid: string) => {
|
||||
return getFetch<TotalResponse>(
|
||||
import.meta.env.DEV
|
||||
? `${DEV_BASE_URL}/Accounts/${sid}/RecentCalls/${callSid}`
|
||||
: `${API_ACCOUNTS}/${sid}/RecentCalls/${callSid}`
|
||||
);
|
||||
};
|
||||
|
||||
export const getPcap = (sid: string, callSid: string) => {
|
||||
return getBlob(
|
||||
import.meta.env.DEV
|
||||
? `${DEV_BASE_URL}/Accounts/${sid}/RecentCalls/${callSid}/pcap`
|
||||
: `${API_ACCOUNTS}/${sid}/RecentCalls/${callSid}/pcap`
|
||||
);
|
||||
};
|
||||
|
||||
export const getAlerts = (sid: string, query: Partial<PageQuery>) => {
|
||||
const qryStr = getQuery<Partial<PageQuery>>(query);
|
||||
|
||||
return getFetch<PagedResponse<Alert>>(
|
||||
import.meta.env.DEV
|
||||
? `${DEV_BASE_URL}/Accounts/${sid}/Alerts?${qryStr}`
|
||||
: `${API_ACCOUNTS}/${sid}/Alerts?${qryStr}`
|
||||
);
|
||||
};
|
||||
|
||||
/** Hooks for components to fetch data with refetch method */
|
||||
|
||||
/** :GET /{apiPath} -- this is generic for any fetch of data collections */
|
||||
export const useApiData: UseApiData = <Type>(apiPath: string) => {
|
||||
const [result, setResult] = useState<Type>();
|
||||
const [error, setError] = useState<FetchError>();
|
||||
const [refetch, setRefetch] = useState(0);
|
||||
|
||||
const refetcher = () => {
|
||||
setRefetch(refetch + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
getFetch<Type>(`${API_BASE_URL}/${apiPath}`)
|
||||
.then(({ json }) => {
|
||||
if (!ignore) {
|
||||
setResult(json!);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!ignore) {
|
||||
setError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return function cleanup() {
|
||||
ignore = true;
|
||||
};
|
||||
}, [refetch]);
|
||||
|
||||
return [result, refetcher, error];
|
||||
};
|
||||
|
||||
/** Only for a couple routes but makes these fetches nice at the component level */
|
||||
/** Wrapping up the currentServiceProvider logic here also streamlines component use */
|
||||
/** :GET /ServiceProviders/:service_provider_sid/ApiKeys */
|
||||
/** :GET /ServiceProviders/:service_provider_sid/Accounts */
|
||||
export const useServiceProviderData: UseApiData = <Type>(apiPath: string) => {
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [result, setResult] = useState<Type>();
|
||||
const [error, setError] = useState<FetchError>();
|
||||
const [refetch, setRefetch] = useState(0);
|
||||
|
||||
const refetcher = () => {
|
||||
setRefetch(refetch + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
if (currentServiceProvider) {
|
||||
getFetch<Type>(
|
||||
`${API_SERVICE_PROVIDERS}/${currentServiceProvider.service_provider_sid}/${apiPath}`
|
||||
)
|
||||
.then(({ json }) => {
|
||||
if (!ignore) {
|
||||
setResult(json!);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!ignore) {
|
||||
setError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return function cleanup() {
|
||||
ignore = true;
|
||||
};
|
||||
}, [currentServiceProvider, refetch]);
|
||||
|
||||
return [result, refetcher, error];
|
||||
};
|
||||
@@ -0,0 +1,329 @@
|
||||
import type { Vendor } from "src/vendor/types";
|
||||
|
||||
/** Simple types */
|
||||
|
||||
export type WebhookMethod = "POST" | "GET";
|
||||
|
||||
export type CredentialStatus = "ok" | "fail" | "not tested";
|
||||
|
||||
export type IpType = "ip" | "fqdn" | "fqdn-top-level" | "invalid";
|
||||
|
||||
export type LimitCategories = "api_rate" | "voice_call_session" | "device";
|
||||
|
||||
/** Status codes */
|
||||
|
||||
export enum StatusCodes {
|
||||
OK = 200,
|
||||
CREATED = 201,
|
||||
ACCEPTED = 202,
|
||||
NO_CONTENT = 204,
|
||||
BAD_REQUEST = 400,
|
||||
UNAUTHORIZED = 401,
|
||||
FORBIDDEN = 403,
|
||||
NOT_FOUND = 404,
|
||||
UNPROCESSABLE_ENTITY = 422,
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
/** SMPP temporarily unavailable */
|
||||
TEMPORARILY_UNAVAILABLE = 480,
|
||||
}
|
||||
|
||||
/** Fetch transport interfaces */
|
||||
|
||||
export interface FetchTransport<Type> {
|
||||
status: StatusCodes;
|
||||
json: Type;
|
||||
blob?: Blob;
|
||||
}
|
||||
|
||||
export interface FetchError {
|
||||
status: StatusCodes;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export interface UseApiData {
|
||||
<Type>(apiPath: string): [
|
||||
Type | undefined,
|
||||
() => void,
|
||||
FetchError | undefined
|
||||
];
|
||||
}
|
||||
|
||||
/** API related interfaces */
|
||||
|
||||
export interface UseApiDataMap<Type> {
|
||||
data?: Type;
|
||||
error?: FetchError;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
export interface WebhookOption {
|
||||
name: WebhookMethod;
|
||||
value: WebhookMethod;
|
||||
}
|
||||
|
||||
export interface Pcap {
|
||||
data_url: string;
|
||||
file_name: string;
|
||||
}
|
||||
|
||||
export interface CredentialTest {
|
||||
status: CredentialStatus;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface CredentialTestResult {
|
||||
stt: CredentialTest;
|
||||
tts: CredentialTest;
|
||||
}
|
||||
|
||||
export interface LimitField {
|
||||
label: string;
|
||||
category: LimitCategories;
|
||||
}
|
||||
|
||||
/** API responses/payloads */
|
||||
|
||||
export interface User {
|
||||
user_sid: string;
|
||||
}
|
||||
|
||||
export interface UserLogin {
|
||||
token: string;
|
||||
user_sid: string;
|
||||
force_change: boolean;
|
||||
}
|
||||
|
||||
export interface UserLoginPayload {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UserUpdatePayload {
|
||||
old_password: string;
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
export interface ServiceProvider {
|
||||
name: string;
|
||||
ms_teams_fqdn: null | string;
|
||||
service_provider_sid: string;
|
||||
}
|
||||
|
||||
export interface Limit {
|
||||
category: LimitCategories;
|
||||
/** Empty string signals :DELETE */
|
||||
/** @see src/components/forms/local-limits */
|
||||
/** @see src/containers/internal/views/accounts/form */
|
||||
/** @see src/containers/internal/views/settings/index */
|
||||
quantity: number | string;
|
||||
account_sid?: string;
|
||||
account_limits_sid?: string;
|
||||
service_provider_sid?: string;
|
||||
service_provider_limits_sid?: string;
|
||||
}
|
||||
|
||||
export interface ApiKey {
|
||||
token: string;
|
||||
last_used: null | string;
|
||||
expires_at: null | string;
|
||||
created_at: string;
|
||||
api_key_sid: string;
|
||||
account_sid: null | string;
|
||||
service_provider_sid: null | string;
|
||||
}
|
||||
|
||||
export interface WebHook {
|
||||
url: string;
|
||||
method: WebhookMethod;
|
||||
username: null | string;
|
||||
password: null | string;
|
||||
webhook_sid?: null | string;
|
||||
}
|
||||
|
||||
export interface Sbc {
|
||||
ipv4: string;
|
||||
port: number | string;
|
||||
sbc_address_sid: string;
|
||||
service_provider_sid: null | string;
|
||||
}
|
||||
|
||||
export interface Smpp {
|
||||
ipv4: string;
|
||||
port: number | string;
|
||||
use_tls: boolean;
|
||||
is_primary: boolean;
|
||||
smpp_address_sid: string;
|
||||
service_provider_sid: null | string;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
name: string;
|
||||
sip_realm: null | string;
|
||||
account_sid: string;
|
||||
webhook_secret: string;
|
||||
siprec_hook_sid: null | string;
|
||||
queue_event_hook: null | WebHook;
|
||||
registration_hook: null | WebHook;
|
||||
service_provider_sid: string;
|
||||
device_calling_application_sid: null | string;
|
||||
}
|
||||
|
||||
export interface Application {
|
||||
name: string;
|
||||
call_hook: null | WebHook;
|
||||
account_sid: null | string;
|
||||
messaging_hook: null | WebHook;
|
||||
application_sid: string;
|
||||
call_status_hook: null | WebHook;
|
||||
speech_synthesis_voice: null | string;
|
||||
speech_synthesis_vendor: null | Lowercase<Vendor>;
|
||||
speech_synthesis_language: null | string;
|
||||
speech_recognizer_vendor: null | Lowercase<Vendor>;
|
||||
speech_recognizer_language: null | string;
|
||||
}
|
||||
|
||||
export interface PhoneNumber {
|
||||
number: string;
|
||||
account_sid: null | string;
|
||||
application_sid: null | string;
|
||||
phone_number_sid: string;
|
||||
voip_carrier_sid: null | string;
|
||||
}
|
||||
|
||||
export interface MSTeamsTenant {
|
||||
tenant_fqdn: string;
|
||||
ms_teams_tenant_sid: string;
|
||||
account_sid: null | string;
|
||||
application_sid: null | string;
|
||||
service_provider_sid: string;
|
||||
}
|
||||
|
||||
export interface RecentCall {
|
||||
account_sid: string;
|
||||
call_sid: string;
|
||||
from: string;
|
||||
to: string;
|
||||
answered: boolean;
|
||||
sip_callid: string;
|
||||
sip_status: number;
|
||||
duration: number;
|
||||
attempted_at: number;
|
||||
answered_at: number;
|
||||
terminated_at: number;
|
||||
termination_reason: string;
|
||||
host: string;
|
||||
remote_host: string;
|
||||
direction: string;
|
||||
trunk: string;
|
||||
}
|
||||
|
||||
export interface SpeechCredential {
|
||||
speech_credential_sid: string;
|
||||
service_provider_sid: null | string;
|
||||
account_sid: null | string;
|
||||
vendor: Lowercase<Vendor>;
|
||||
use_for_tts: number;
|
||||
use_for_stt: number;
|
||||
last_used: null | string;
|
||||
region: null | string;
|
||||
aws_region: null | string;
|
||||
api_key: null | string;
|
||||
access_key_id: null | string;
|
||||
secret_access_key: null | string;
|
||||
service_key: null | string;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
time: number;
|
||||
account_sid: string;
|
||||
alert_type: string;
|
||||
message: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface Carrier {
|
||||
voip_carrier_sid: string;
|
||||
name: string;
|
||||
description: null | string;
|
||||
is_active: boolean;
|
||||
service_provider_sid: string;
|
||||
account_sid: null | string;
|
||||
application_sid: null | string;
|
||||
e164_leading_plus: boolean;
|
||||
requires_register: boolean;
|
||||
register_username: null | string;
|
||||
register_sip_realm: null | string;
|
||||
register_password: null | string;
|
||||
tech_prefix: null | string;
|
||||
diversion: null | string;
|
||||
inbound_auth_username: string;
|
||||
inbound_auth_password: string;
|
||||
smpp_system_id: null | string;
|
||||
smpp_password: null | string;
|
||||
smpp_inbound_system_id: null | string;
|
||||
smpp_inbound_password: null | string;
|
||||
smpp_enquire_link_interval: number;
|
||||
}
|
||||
|
||||
export interface PredefinedCarriers extends Carrier {
|
||||
requires_static_ip: boolean;
|
||||
}
|
||||
|
||||
export interface ProtocolGateway {
|
||||
voip_carrier_sid: string;
|
||||
ipv4: string;
|
||||
port: number;
|
||||
netmask: number;
|
||||
inbound: number;
|
||||
outbound: number;
|
||||
}
|
||||
|
||||
export interface SipGateway extends ProtocolGateway {
|
||||
sip_gateway_sid?: null | string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface SmppGateway extends ProtocolGateway {
|
||||
smpp_gateway_sid?: null | string;
|
||||
is_primary: boolean;
|
||||
use_tls: boolean;
|
||||
}
|
||||
|
||||
export interface PageQuery {
|
||||
page: number;
|
||||
count: number;
|
||||
start?: string;
|
||||
days?: number;
|
||||
}
|
||||
|
||||
export interface CallQuery extends PageQuery {
|
||||
direction?: string;
|
||||
answered?: string;
|
||||
}
|
||||
|
||||
export interface PagedResponse<Type> {
|
||||
page_size: number;
|
||||
total: number;
|
||||
page: number;
|
||||
data: Type[];
|
||||
}
|
||||
|
||||
export interface SidResponse {
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface TokenResponse extends SidResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface SecretResponse {
|
||||
webhook_secret: string;
|
||||
}
|
||||
|
||||
export interface EmptyResponse {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface TotalResponse {
|
||||
total: number;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import { useSelectState } from "src/store";
|
||||
|
||||
import type { ACL } from "src/store/types";
|
||||
|
||||
type ACLProps = {
|
||||
acl: keyof ACL;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const AccessControl = ({ acl, children }: ACLProps) => {
|
||||
const accessControl = useSelectState("accessControl");
|
||||
|
||||
if (accessControl[acl]) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { classNames } from "jambonz-ui";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import type { Account } from "src/api/types";
|
||||
import { hasLength } from "src/utils";
|
||||
|
||||
type AccountFilterProps = {
|
||||
label?: string;
|
||||
account: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
accounts?: Account[];
|
||||
defaultOption?: boolean;
|
||||
};
|
||||
|
||||
/** This will apply the selected account SID so you can filter local data */
|
||||
/** Currently used by: Applications, Recent Calls, Alerts, Carriers and Speech index views */
|
||||
export const AccountFilter = ({
|
||||
label = "Account",
|
||||
account: [accountSid, setAccountSid],
|
||||
accounts,
|
||||
defaultOption,
|
||||
}: AccountFilterProps) => {
|
||||
const [focus, setFocus] = useState(false);
|
||||
const classes = {
|
||||
smsel: true,
|
||||
"smsel--filter": true,
|
||||
"account-filter": true,
|
||||
focused: focus,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasLength(accounts) && !defaultOption) {
|
||||
setAccountSid(accounts[0].account_sid);
|
||||
}
|
||||
}, [accounts, defaultOption, setAccountSid]);
|
||||
|
||||
return (
|
||||
<div className={classNames(classes)}>
|
||||
<label htmlFor="account_filter">{label}:</label>
|
||||
<div>
|
||||
<select
|
||||
id="account_filter"
|
||||
name="account_filter"
|
||||
value={accountSid}
|
||||
onChange={(e) => setAccountSid(e.target.value)}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
>
|
||||
{defaultOption ? (
|
||||
<option value="">All accounts</option>
|
||||
) : (
|
||||
accounts &&
|
||||
!accounts.length && <option value="">No accounts</option>
|
||||
)}
|
||||
{hasLength(accounts) &&
|
||||
accounts
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((acct) => {
|
||||
return (
|
||||
<option key={acct.account_sid} value={acct.account_sid}>
|
||||
{acct.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span>
|
||||
<Icons.ChevronUp />
|
||||
<Icons.ChevronDown />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useState } from "react";
|
||||
import { classNames } from "jambonz-ui";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import type { Application } from "src/api/types";
|
||||
|
||||
type ApplicationFilterProps = JSX.IntrinsicElements["select"] & {
|
||||
label?: string;
|
||||
application: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
applications?: Application[];
|
||||
defaultOption?: string;
|
||||
};
|
||||
|
||||
export const ApplicationFilter = ({
|
||||
label = "Application",
|
||||
application: [applicationSid, setApplicationSid],
|
||||
applications,
|
||||
defaultOption,
|
||||
}: ApplicationFilterProps) => {
|
||||
const [focus, setFocus] = useState(false);
|
||||
const classes = {
|
||||
smsel: true,
|
||||
"smsel--filter": true,
|
||||
"application-filter": true,
|
||||
focused: focus,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(classes)}>
|
||||
<label htmlFor="application_filter">{label}:</label>
|
||||
<div>
|
||||
<select
|
||||
id="application_filter"
|
||||
name="application_filter"
|
||||
value={applicationSid}
|
||||
onChange={(e) => setApplicationSid(e.target.value)}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
>
|
||||
{defaultOption && <option value="">{defaultOption}</option>}
|
||||
{applications &&
|
||||
applications
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((app) => {
|
||||
return (
|
||||
<option key={app.application_sid} value={app.application_sid}>
|
||||
{app.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span>
|
||||
<Icons.ChevronUp />
|
||||
<Icons.ChevronDown />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components/macro";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import Loader from "../../components/blocks/Loader";
|
||||
|
||||
import Table from "antd/lib/table";
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
width: 100%;
|
||||
margin-top: 1rem !important;
|
||||
|
||||
table {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
|
||||
tr,
|
||||
th,
|
||||
td {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
height: 32px;
|
||||
|
||||
.ant-pagination-simple-pager {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-item {
|
||||
border: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLoader = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const AntdTable = ({ dataSource, columns, loading, ...rest }) => {
|
||||
let props = {
|
||||
...rest,
|
||||
dataSource,
|
||||
columns,
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
props = {
|
||||
...props,
|
||||
loading: {
|
||||
spinning: true,
|
||||
indicator: (
|
||||
<StyledLoader>
|
||||
<Loader />
|
||||
</StyledLoader>
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return <StyledTable {...props} />;
|
||||
};
|
||||
|
||||
AntdTable.propTypes = {
|
||||
dataSource: PropTypes.array,
|
||||
loading: PropTypes.bool,
|
||||
columns: PropTypes.array,
|
||||
};
|
||||
|
||||
AntdTable.defaultProps = {
|
||||
dataSource: [],
|
||||
loading: false,
|
||||
columns: [],
|
||||
};
|
||||
|
||||
export default AntdTable;
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as Chevron } from '../../images/Chevron.svg';
|
||||
import Link from '../elements/Link';
|
||||
|
||||
const BreadcrumbsContainer = styled.div`
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Breadcrumbs = props => {
|
||||
return (
|
||||
<BreadcrumbsContainer>
|
||||
{props.breadcrumbs.map((b, i) => (
|
||||
b.url
|
||||
? <React.Fragment key={i}>
|
||||
<Link to={b.url}>{b.name}</Link>
|
||||
<Chevron style={{ margin: '0 0.75rem' }} />
|
||||
</React.Fragment>
|
||||
: <span key={i}>{b.name}</span>
|
||||
))}
|
||||
</BreadcrumbsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Breadcrumbs;
|
||||
@@ -1,45 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as ErrorIcon } from '../../images/ErrorIcon.svg';
|
||||
|
||||
const FormErrorContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
background: RGBA(217, 28, 92, 0.2);
|
||||
${props => !props.grid && `margin-bottom: 1rem;`}
|
||||
color: #76042A;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
${props => props.grid && `grid-column: 2;`}
|
||||
& > div {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
& ul {
|
||||
margin: 0.25rem 0 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
& li {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const FormError = props => (
|
||||
<FormErrorContainer {...props}>
|
||||
<ErrorIcon />
|
||||
<div>
|
||||
{typeof props.message === 'object' && props.message.length ? (
|
||||
<ul>
|
||||
{props.message.map((message, i) => (
|
||||
<li key={i}>{message}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
props.message
|
||||
)}
|
||||
</div>
|
||||
</FormErrorContainer>
|
||||
);
|
||||
|
||||
export default FormError;
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 2rem;
|
||||
${props => props.height && `
|
||||
height: ${props.height};
|
||||
`}
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Spinner = styled.div`
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
border: 4px solid #E3E3E3;
|
||||
border-top-color: #D91C5C;
|
||||
border-radius: 50%;
|
||||
animation: spin 1.25s linear infinite;
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(-45deg); }
|
||||
100% { transform: rotate(315deg); }
|
||||
}
|
||||
`;
|
||||
|
||||
const Loader = props => {
|
||||
return (
|
||||
<Container height={props.height}>
|
||||
<Spinner />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
@@ -1,129 +0,0 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { ModalDispatchContext } from '../../contexts/ModalContext';
|
||||
import styled from 'styled-components/macro';
|
||||
import Button from '../elements/Button';
|
||||
import Loader from '../blocks/Loader';
|
||||
|
||||
const Overlay = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 90;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ModalContainer = styled.div`
|
||||
max-height: calc(100% - 2rem);
|
||||
max-width: 700px;
|
||||
overflow: auto;
|
||||
margin: 1rem;
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
background: #FFF;
|
||||
text-align: left;
|
||||
|
||||
& h1 {
|
||||
margin-top: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const LoaderContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -1.5rem;
|
||||
height: 100%;
|
||||
width: calc(100% + 3rem);
|
||||
background: #FFF;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
${props => props.normalPadding ? `
|
||||
margin-top: 1rem;
|
||||
` : `
|
||||
margin: 1rem -0.5rem -0.5rem 0;
|
||||
`}
|
||||
& > * {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Modal = props => {
|
||||
|
||||
// Handle modal context, which tells other elements to be disabled while modal is open
|
||||
const setModalOpen = useContext(ModalDispatchContext);
|
||||
useEffect(() => {
|
||||
setModalOpen(true);
|
||||
return () => setModalOpen(false);
|
||||
});
|
||||
|
||||
// Lock scroll on desktop and Android
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => document.body.style.overflow = 'auto';
|
||||
});
|
||||
|
||||
// Lock scroll on iOS
|
||||
useEffect(() => {
|
||||
const stopTouchScroll = e => e.preventDefault();
|
||||
window.addEventListener('touchmove', stopTouchScroll);
|
||||
return () => window.removeEventListener('touchmove', stopTouchScroll);
|
||||
});
|
||||
|
||||
// Close modal on Escape
|
||||
useEffect(() => {
|
||||
const closeOnEsc = e => {
|
||||
if (e.key === 'Escape' || e.key === 'Esc') {
|
||||
props.handleCancel();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', closeOnEsc);
|
||||
return () => window.removeEventListener('keydown', closeOnEsc);
|
||||
});
|
||||
|
||||
return (
|
||||
<Overlay onClick={props.handleCancel}>
|
||||
<ModalContainer onClick={e => e.stopPropagation()}>
|
||||
<h1>{props.title}</h1>
|
||||
<ContentContainer>
|
||||
{props.content}
|
||||
<ButtonContainer normalPadding={props.normalButtonPadding}>
|
||||
<Button inModal gray onClick={props.handleCancel}>
|
||||
{props.closeText || "Cancel"}
|
||||
</Button>
|
||||
{props.actionText && (
|
||||
<Button
|
||||
inModal
|
||||
disabled={props.loader}
|
||||
onClick={props.handleSubmit}
|
||||
>
|
||||
{props.actionText}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonContainer>
|
||||
{props.loader && (
|
||||
<LoaderContainer>
|
||||
<Loader />
|
||||
</LoaderContainer>
|
||||
)}
|
||||
</ContentContainer>
|
||||
</ModalContainer>
|
||||
</Overlay>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
@@ -1,251 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useContext, useEffect, useState, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import Button from '../elements/Button';
|
||||
import Label from '../elements/Label';
|
||||
import Select from '../elements/Select';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Modal from '../blocks/Modal';
|
||||
import FormError from '../blocks/FormError';
|
||||
import handleErrors from "../../helpers/handleErrors";
|
||||
import { Link as ReactRouterLink } from 'react-router-dom';
|
||||
import { ServiceProviderValueContext, ServiceProviderMethodContext } from '../../contexts/ServiceProviderContext';
|
||||
import LogoJambong from "../../images/LogoJambong.svg";
|
||||
import AddModalButton from '../elements/AddModalButton';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const StyledNav = styled.nav`
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.12);
|
||||
`;
|
||||
|
||||
const LogOutContainer = styled.div`
|
||||
margin-right: 3rem;
|
||||
@media (max-width: 34rem) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled(ReactRouterLink)`
|
||||
text-decoration: none;
|
||||
margin: 0 0 0 2rem;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledForm = styled(Form)`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
`;
|
||||
|
||||
const StyledLabel = styled(Label)`
|
||||
margin-right: 1rem;
|
||||
`;
|
||||
|
||||
const ModalContainer = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
width: 500;
|
||||
`;
|
||||
|
||||
const StyledFormError = styled(FormError)`
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
const Nav = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const setCurrentServiceProvider = useContext(ServiceProviderMethodContext);
|
||||
const [serviceProviders, setServiceProviders] = useState([]);
|
||||
const [showServiceProviderModal, setShowServiceProviderModal] = useState(false);
|
||||
const [showModalLoader, setShowModalLoader] = useState(false);
|
||||
const [serviceProviderName, setServiceProviderName] = useState("");
|
||||
const [serviceProviderInvalid, setServiceProviderInvalid] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const refServiceProvider = useRef(null);
|
||||
|
||||
const logOut = () => {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: "You've successfully logged out",
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeServiceProvider = (sp) => {
|
||||
if (sp === "add") {
|
||||
setShowServiceProviderModal(true);
|
||||
} else {
|
||||
setCurrentServiceProvider(sp);
|
||||
}
|
||||
};
|
||||
|
||||
const getServiceProviders = async () => {
|
||||
const jwt = localStorage.getItem('token');
|
||||
if (history.location.pathname !== '' && jwt) {
|
||||
const serviceProvidersResponse = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/ServiceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setServiceProviders(
|
||||
(serviceProvidersResponse.data || []).sort(
|
||||
(a, b) => a.name.localeCompare(b.name)
|
||||
)
|
||||
);
|
||||
|
||||
const isExisted = serviceProvidersResponse.data.find(item => item.service_provider_sid === currentServiceProvider);
|
||||
if (!isExisted) {
|
||||
setCurrentServiceProvider(serviceProvidersResponse.data[0].service_provider_sid);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddServiceProvider = async () => {
|
||||
if (serviceProviderName) {
|
||||
setServiceProviderInvalid(false);
|
||||
setErrorMessage("");
|
||||
|
||||
try {
|
||||
setShowModalLoader(true);
|
||||
const jwt = localStorage.getItem('token');
|
||||
|
||||
const serviceProviderResponse = await axios({
|
||||
method: 'post',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
data: {
|
||||
name: serviceProviderName,
|
||||
},
|
||||
});
|
||||
|
||||
setCurrentServiceProvider(serviceProviderResponse.data.sid);
|
||||
|
||||
getServiceProviders();
|
||||
setShowServiceProviderModal(false);
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch, setErrorMessage });
|
||||
} finally {
|
||||
setShowModalLoader(false);
|
||||
}
|
||||
} else {
|
||||
setServiceProviderInvalid(true);
|
||||
setErrorMessage("Please enter a name for Service Provider");
|
||||
if (refServiceProvider && refServiceProvider.current) {
|
||||
refServiceProvider.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getServiceProviders();
|
||||
}, [history.location.pathname]);
|
||||
|
||||
return (
|
||||
<StyledNav>
|
||||
<StyledLink to="/internal/accounts">
|
||||
<img src={LogoJambong} alt="link-img" />
|
||||
</StyledLink>
|
||||
{location.pathname !== '/' && (
|
||||
<StyledForm>
|
||||
<StyledLabel htmlFor="serviceProvider">Service Provider:</StyledLabel>
|
||||
<Select
|
||||
name="serviceProvider"
|
||||
id="serviceProvider"
|
||||
value={currentServiceProvider}
|
||||
onChange={e => onChangeServiceProvider(e.target.value)}
|
||||
>
|
||||
{serviceProviders.map(a => (
|
||||
<option
|
||||
key={a.service_provider_sid}
|
||||
value={a.service_provider_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<AddModalButton
|
||||
addButtonText="Add Service Provider"
|
||||
onClick={()=>setShowServiceProviderModal(true)}
|
||||
/>
|
||||
</StyledForm>
|
||||
|
||||
)}
|
||||
{location.pathname !== '/' && (
|
||||
<LogOutContainer>
|
||||
<Button
|
||||
large
|
||||
gray
|
||||
text
|
||||
onClick={logOut}
|
||||
>
|
||||
Log Out
|
||||
</Button>
|
||||
</LogOutContainer>
|
||||
)}
|
||||
{showServiceProviderModal && (
|
||||
<Modal
|
||||
title="Add New Service Provider"
|
||||
loader={showModalLoader}
|
||||
closeText="Close"
|
||||
actionText="Add"
|
||||
handleCancel={() => {
|
||||
setServiceProviderName("");
|
||||
setShowServiceProviderModal(false);
|
||||
}}
|
||||
handleSubmit={handleAddServiceProvider}
|
||||
content={
|
||||
<ModalContainer>
|
||||
<StyledLabel htmlFor="name">Name:</StyledLabel>
|
||||
<Input
|
||||
name="name"
|
||||
id="name"
|
||||
value={serviceProviderName}
|
||||
onChange={e => setServiceProviderName(e.target.value)}
|
||||
placeholder="Service provider name"
|
||||
invalid={serviceProviderInvalid}
|
||||
autoFocus
|
||||
ref={refServiceProvider}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<StyledFormError grid message={errorMessage} />
|
||||
)}
|
||||
</ModalContainer>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StyledNav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Nav;
|
||||
@@ -1,128 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import { ReactComponent as CheckGreen } from '../../images/CheckGreen.svg';
|
||||
import { ReactComponent as ErrorIcon } from '../../images/ErrorIcon.svg';
|
||||
|
||||
const NotificationContainer = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
`;
|
||||
|
||||
const NotificationDiv = styled.div`
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 28rem;
|
||||
padding: 0.75rem;
|
||||
background: #fff;
|
||||
border: 1px solid ${props => (
|
||||
props.level === 'success'
|
||||
? '#61c43e'
|
||||
: props.level === 'error'
|
||||
? '#D91C5C'
|
||||
: '#949494'
|
||||
)};
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
pointer-events: auto;
|
||||
& svg {
|
||||
flex-shrink: 0;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 1.5rem;
|
||||
color: #767676;
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
padding: 0.25rem;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
border-color: #767676;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
color: #d91c5c;
|
||||
}
|
||||
`;
|
||||
|
||||
const InfoIcon = styled.span`
|
||||
flex-shrink: 0;
|
||||
margin-right: 0.75rem;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background: #949494;
|
||||
color: #FFF;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const Notification = props => {
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
return (
|
||||
<NotificationContainer>
|
||||
{props.notifications.map(n => (
|
||||
<NotificationDiv
|
||||
key={n.id}
|
||||
level={n.level}
|
||||
>
|
||||
{n.level === 'success'
|
||||
? <CheckGreen />
|
||||
: n.level === 'error'
|
||||
? <ErrorIcon />
|
||||
: <InfoIcon>i</InfoIcon>
|
||||
}
|
||||
<span>{n.message}</span>
|
||||
<CloseButton
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: 'REMOVE',
|
||||
id: n.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span tabIndex="-1">×</span>
|
||||
</CloseButton>
|
||||
</NotificationDiv>
|
||||
))}
|
||||
</NotificationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notification;
|
||||
@@ -1,113 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Container = styled.div`
|
||||
height: 8rem;
|
||||
width: 45rem;
|
||||
max-width: 100%;
|
||||
padding: 3.5rem;
|
||||
@media (max-width: 30rem) {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Step = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
color: #FFF;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
border-radius: 50%;
|
||||
${props => props.incomplete ? `
|
||||
border: 0.25rem solid #A6A6A6;
|
||||
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.08),
|
||||
0 0 3px rgba(0, 0, 0, 0.08),
|
||||
inset 0 3px 3px rgba(0, 0, 0, 0.08),
|
||||
inset 0 0 3px rgba(0, 0, 0, 0.08);
|
||||
` : `
|
||||
background: #D91C5C;
|
||||
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.08),
|
||||
0 0 3px rgba(0, 0, 0, 0.08);
|
||||
`
|
||||
}
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
const Line = styled.div`
|
||||
width: 50%;
|
||||
height: 0.25rem;
|
||||
margin: -2px;
|
||||
background: ${props => props.incomplete
|
||||
? '#A6A6A6'
|
||||
: '#D91C5C'
|
||||
};
|
||||
`;
|
||||
|
||||
const Checkmark = styled.div`
|
||||
height: 6px;
|
||||
width: 11px;
|
||||
border-left: 2px solid #FFF;
|
||||
border-bottom: 2px solid #FFF;
|
||||
transform: rotate(-45deg);
|
||||
`;
|
||||
|
||||
const Title = styled.span`
|
||||
position: absolute;
|
||||
top: 2.5rem;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
@media (max-width: 30rem) {
|
||||
white-space: normal;
|
||||
}
|
||||
color: ${props => props.active ? '#D91C5C' : '#767676'};
|
||||
font-weight: ${props => props.active ? 'bold' : 'normal'};
|
||||
`;
|
||||
|
||||
const ProgressVisualization = props => (
|
||||
!props.progress
|
||||
? <Container />
|
||||
: <Container>
|
||||
<Step>
|
||||
{props.progress === 1
|
||||
? '1'
|
||||
: <Checkmark />
|
||||
}
|
||||
<Title active={props.progress === 1}>
|
||||
Configure Account
|
||||
</Title>
|
||||
</Step>
|
||||
<Line incomplete={props.progress < 2} />
|
||||
<Step incomplete={props.progress < 2} >
|
||||
{props.progress < 2
|
||||
? null
|
||||
: props.progress === 2
|
||||
? '2'
|
||||
: <Checkmark />
|
||||
}
|
||||
<Title active={props.progress === 2}>
|
||||
Create Application
|
||||
</Title>
|
||||
</Step>
|
||||
<Line incomplete={props.progress < 3} />
|
||||
<Step incomplete={props.progress < 3} >
|
||||
{props.progress < 3
|
||||
? null
|
||||
: props.progress === 3
|
||||
? '3'
|
||||
: <Checkmark />
|
||||
}
|
||||
<Title active={props.progress === 3}>
|
||||
Configure SIP Trunk
|
||||
</Title>
|
||||
</Step>
|
||||
</Container>
|
||||
);
|
||||
|
||||
export default ProgressVisualization;
|
||||
@@ -1,94 +0,0 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import axios from 'axios';
|
||||
import styled from 'styled-components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
// import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const Container = styled.div`
|
||||
margin-top: 0.25rem;
|
||||
${props => props.centered && `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
& ul {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const Sbcs = props => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
// const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const [ sbcs, setSbcs ] = useState('');
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const sbcResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
// url: `/Sbcs?service_provider_sid=${currentServiceProvider}`,
|
||||
url: '/Sbcs',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
setSbcs(sbcResults.data);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const text = 'Have your SIP trunking provider(s) send calls to';
|
||||
return (
|
||||
sbcs.length > 1
|
||||
? <Container centered={props.centered}>
|
||||
{text}:
|
||||
<ul>
|
||||
{sbcs.map(sbc => (
|
||||
<li key={sbc.sbc_address_sid}>
|
||||
{`${sbc.ipv4}:${sbc.port}`}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Container>
|
||||
: sbcs.length === 1
|
||||
? <Container>
|
||||
{text} {sbcs[0].ipv4}:{sbcs[0].port}
|
||||
</Container>
|
||||
: null
|
||||
);
|
||||
};
|
||||
|
||||
export default Sbcs;
|
||||
@@ -1,122 +0,0 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
import { ShowMsTeamsStateContext, ShowMsTeamsDispatchContext } from '../../contexts/ShowMsTeamsContext';
|
||||
import { ReactComponent as AccountsIcon } from '../../images/AccountsIcon.svg';
|
||||
import { ReactComponent as ApplicationsIcon } from '../../images/ApplicationsIcon.svg';
|
||||
import { ReactComponent as CarriersIcon } from '../../images/CarriersIcon.svg';
|
||||
import { ReactComponent as PhoneNumbersIcon } from '../../images/PhoneNumbersIcon.svg';
|
||||
import { ReactComponent as MsTeamsIcon } from '../../images/MsTeamsIcon.svg';
|
||||
import { ReactComponent as SettingsIcon } from '../../images/SettingsIcon.svg';
|
||||
import { ReactComponent as RecentCallsIcon } from '../../images/RecentCallsIcon.svg';
|
||||
import { ReactComponent as AlertsIcon } from '../../images/AlertsIcon.svg';
|
||||
import { ReactComponent as SpeechIcon } from '../../images/SpeechIcon.svg';
|
||||
|
||||
const StyledSideMenu = styled.div`
|
||||
width: 15rem;
|
||||
flex-shrink: 0;
|
||||
height: calc(100vh - 4rem);
|
||||
overflow: auto;
|
||||
background: #FFF;
|
||||
padding-top: 3.25rem;
|
||||
`;
|
||||
|
||||
const activeClassName = 'nav-item-active';
|
||||
|
||||
const StyledNavLink = styled(NavLink).attrs({ activeClassName })`
|
||||
height: 2.75rem;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
color: #565656;
|
||||
fill: #565656;
|
||||
|
||||
&.${activeClassName} {
|
||||
color: #D91C5C;
|
||||
fill: #D91C5C;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: RGBA(217, 28, 92, 0.1);
|
||||
color: #C0134D;
|
||||
fill: #C0134D;
|
||||
}
|
||||
&.${activeClassName}:hover {
|
||||
color: #D91C5C;
|
||||
fill: #D91C5C;
|
||||
}
|
||||
`;
|
||||
|
||||
const IconContainer = styled.span`
|
||||
width: 3rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: 0;
|
||||
`;
|
||||
|
||||
const MenuText = styled.span`
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
outline: 0;
|
||||
`;
|
||||
|
||||
const StyledH2 = styled.h2`
|
||||
margin: 3rem 0 1rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #757575;
|
||||
`;
|
||||
|
||||
const MenuLink = props => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
return (
|
||||
<StyledNavLink
|
||||
to={props.to}
|
||||
activeClassName={activeClassName}
|
||||
tabIndex={modalOpen ? '-1' : ''}
|
||||
>
|
||||
<IconContainer tabIndex="-1">
|
||||
{props.icon}
|
||||
</IconContainer>
|
||||
<MenuText tabIndex="-1">
|
||||
{props.name}
|
||||
</MenuText>
|
||||
</StyledNavLink>
|
||||
);
|
||||
};
|
||||
|
||||
const SideMenu = () => {
|
||||
const showMsTeams = useContext(ShowMsTeamsStateContext);
|
||||
const getMsTeamsData = useContext(ShowMsTeamsDispatchContext);
|
||||
useEffect(() => {
|
||||
getMsTeamsData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
return (
|
||||
<StyledSideMenu>
|
||||
<MenuLink to="/internal/settings" name="Settings" icon={<SettingsIcon />} />
|
||||
<MenuLink to="/internal/accounts" name="Accounts" icon={<AccountsIcon />} />
|
||||
<MenuLink to="/internal/applications" name="Applications" icon={<ApplicationsIcon />} />
|
||||
<MenuLink to="/internal/recent-calls" name="Recent Calls" icon={<RecentCallsIcon />} />
|
||||
<MenuLink to="/internal/alerts" name="Alerts" icon={<AlertsIcon />} />
|
||||
<StyledH2>Bring Your Own Services</StyledH2>
|
||||
<MenuLink to="/internal/carriers" name="Carriers" icon={<CarriersIcon />} />
|
||||
<MenuLink to="/internal/speech-services" name="Speech" icon={<SpeechIcon />} />
|
||||
<MenuLink to="/internal/phone-numbers" name="Phone Numbers" icon={<PhoneNumbersIcon />} />
|
||||
{showMsTeams && (
|
||||
<MenuLink to="/internal/ms-teams-tenants" name="MS Teams Tenants" icon={<MsTeamsIcon />} />
|
||||
)}
|
||||
</StyledSideMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default SideMenu;
|
||||
@@ -1,463 +0,0 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import Table from '../elements/Table.js';
|
||||
import Button from '../elements/Button.js';
|
||||
import Checkbox from '../elements/Checkbox.js';
|
||||
import TableMenu from '../blocks/TableMenu.js';
|
||||
import Loader from '../blocks/Loader.js';
|
||||
import Modal from '../blocks/Modal.js';
|
||||
import FormError from '../blocks/FormError.js';
|
||||
import CopyableText from '../elements/CopyableText';
|
||||
import ToggleText from '../blocks/ToggleText.js';
|
||||
import { ReactComponent as CheckGreen } from '../../images/CheckGreen.svg';
|
||||
import { ReactComponent as ErrorIcon } from '../../images/ErrorIcon.svg';
|
||||
|
||||
const Td = styled.td`
|
||||
padding: 0.5rem 0;
|
||||
&:first-child {
|
||||
font-weight: 500;
|
||||
padding-right: 1.5rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
& ul {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const TableContent = props => {
|
||||
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
const [ showTableLoader, setShowTableLoader ] = useState(true);
|
||||
const [ showModalLoader, setShowModalLoader ] = useState(false);
|
||||
const [ content, setContent ] = useState([]);
|
||||
const [ contentToDelete, setContentToDelete ] = useState({});
|
||||
|
||||
//=============================================================================
|
||||
// Get and sort content
|
||||
//=============================================================================
|
||||
const [ sort, setSort ] = useState({
|
||||
column: props.columns[0].key,
|
||||
order: 'asc',
|
||||
});
|
||||
const sortTableContent = ({ newContent, column }) => {
|
||||
const newSortOrder = sort.column === column
|
||||
? sort.order === 'asc'
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
: 'asc';
|
||||
column = column || sort.column;
|
||||
newContent = newContent || content;
|
||||
const sortedContent = [...newContent];
|
||||
sortedContent.sort((a, b) => {
|
||||
let valA;
|
||||
let valB;
|
||||
if (!a[column]) {
|
||||
valA = '';
|
||||
valB = '';
|
||||
} else if (typeof a[column] === 'object') {
|
||||
if (a[column].type === 'masked') {
|
||||
valA = a[column].masked;
|
||||
valB = b[column].masked;
|
||||
}
|
||||
if (a[column].type === 'normal') {
|
||||
valA = a[column].content;
|
||||
valB = b[column].content;
|
||||
}
|
||||
} else {
|
||||
valA = (a[column] && a[column].toLowerCase()) || '';
|
||||
valB = (b[column] && b[column].toLowerCase()) || '';
|
||||
}
|
||||
if (newSortOrder === 'asc') {
|
||||
return valA > valB ? 1 : valA < valB ? -1 : 0;
|
||||
} else {
|
||||
return valA < valB ? 1 : valA > valB ? -1 : 0;
|
||||
}
|
||||
});
|
||||
setContent(sortedContent);
|
||||
setSort({
|
||||
column,
|
||||
order: newSortOrder
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
const getNewContent = async () => {
|
||||
const newContent = await props.getContent();
|
||||
sortTableContent({ newContent });
|
||||
setShowTableLoader(false);
|
||||
};
|
||||
getNewContent();
|
||||
// eslint-disable-next-line
|
||||
}, [props.getContent]);
|
||||
|
||||
//=============================================================================
|
||||
// Handle checkboxes
|
||||
//=============================================================================
|
||||
const [ selected, setSelected ] = useState([]);
|
||||
const checkboxesToggleAll = e => {
|
||||
if (content.length === selected.length) {
|
||||
setSelected([]);
|
||||
} else {
|
||||
setSelected(content.map(c => c.sid));
|
||||
}
|
||||
};
|
||||
const checkboxesToggleOne = e => {
|
||||
const sid = e.target.value;
|
||||
setSelected(prev => {
|
||||
if (prev.includes(sid)) {
|
||||
return prev.filter(p => p !== sid);
|
||||
} else {
|
||||
return [...prev, sid];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleBulkAction = async (selected, i) => {
|
||||
setShowTableLoader(true);
|
||||
const success = await props.bulkAction(selected, i);
|
||||
if (success) {
|
||||
const newContent = await props.getContent();
|
||||
sortTableContent({ newContent });
|
||||
setSelected([]);
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: 'Number routing updated',
|
||||
});
|
||||
}
|
||||
setShowTableLoader(false);
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Handle Open Menus (i.e. bulk action menu or 3 dots on right of each row)
|
||||
//=============================================================================
|
||||
const [ menuOpen, setMenuOpen ] = useState(null);
|
||||
useEffect(() => {
|
||||
const hideMenu = () => setMenuOpen(null);
|
||||
window.addEventListener('click', hideMenu);
|
||||
return () => window.removeEventListener('click', hideMenu);
|
||||
}, []);
|
||||
const handleMenuOpen = sid => {
|
||||
if (menuOpen === sid) {
|
||||
setMenuOpen(null);
|
||||
} else {
|
||||
setMenuOpen(sid);
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Handle Adding content
|
||||
//=============================================================================
|
||||
const [ showNewContentModal, setShowNewContentModal ] = useState(false);
|
||||
const [ showNewContentLoader, setShowNewContentLoader ] = useState(false);
|
||||
const [ newItem, setNewItem ] = useState('');
|
||||
const addContent = async () => {
|
||||
setShowNewContentModal(true);
|
||||
setShowNewContentLoader(true);
|
||||
const result = await props.addContent();
|
||||
if (result !== 'error') {
|
||||
const newContent = await props.getContent();
|
||||
sortTableContent({ newContent });
|
||||
setNewItem(result);
|
||||
} else {
|
||||
setShowNewContentModal(false);
|
||||
}
|
||||
setShowNewContentLoader(false);
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Handle Deleting content
|
||||
//=============================================================================
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
const deleteContent = async () => {
|
||||
setShowModalLoader(true);
|
||||
const result = await props.deleteContent(contentToDelete);
|
||||
if (result === 'success') {
|
||||
const newContent = await props.getContent();
|
||||
sortTableContent({ newContent });
|
||||
setContentToDelete({});
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: `${props.name.charAt(0).toUpperCase()}${props.name.slice(1)} deleted successfully`,
|
||||
});
|
||||
} else {
|
||||
setErrorMessage(result);
|
||||
}
|
||||
setSelected([]);
|
||||
setShowModalLoader(false);
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<React.Fragment>
|
||||
{showNewContentModal && (
|
||||
<Modal
|
||||
title={`Here is your new ${props.name}`}
|
||||
closeText="Close"
|
||||
loader={showNewContentLoader}
|
||||
content={
|
||||
<CopyableText
|
||||
text={newItem}
|
||||
textType={props.name}
|
||||
inModal
|
||||
hasBorder
|
||||
/>
|
||||
}
|
||||
handleCancel={() => setShowNewContentModal(false)}
|
||||
normalButtonPadding
|
||||
/>
|
||||
)}
|
||||
|
||||
{contentToDelete && (
|
||||
contentToDelete.name ||
|
||||
contentToDelete.number ||
|
||||
contentToDelete.tenant_fqdn ||
|
||||
contentToDelete.token ||
|
||||
contentToDelete.vendor
|
||||
) && (
|
||||
<Modal
|
||||
title={`Are you sure you want to delete the following ${props.name}?`}
|
||||
loader={showModalLoader}
|
||||
content={
|
||||
<div>
|
||||
<table>
|
||||
<tbody>
|
||||
{props.formatContentToDelete(contentToDelete).map((d, i) => (
|
||||
<tr key={i}>
|
||||
<Td>{d.name}</Td>
|
||||
<Td>
|
||||
{typeof d.content === 'string'
|
||||
? d.content
|
||||
: <ul>
|
||||
{d.content.map((c, i) => (
|
||||
<li key={i}>{c}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
</Td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{errorMessage && (
|
||||
<FormError message={errorMessage} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
handleCancel={() => {
|
||||
setContentToDelete({});
|
||||
setErrorMessage('');
|
||||
}}
|
||||
handleSubmit={deleteContent}
|
||||
actionText="Delete"
|
||||
/>
|
||||
)}
|
||||
<Table
|
||||
withCheckboxes={props.withCheckboxes}
|
||||
rowsHaveDeleteButtons={props.rowsHaveDeleteButtons}
|
||||
>
|
||||
{/* colgroup is used to set the width of the last column because the
|
||||
last two <th> are combined in a colSpan="2", preventing the columns from
|
||||
being given an expicit width (`table-layout: fixed;` requires setting
|
||||
column width in the first row) */}
|
||||
{!props.rowsHaveDeleteButtons && (
|
||||
<colgroup>
|
||||
<col
|
||||
span={
|
||||
props.withCheckboxes
|
||||
? props.columns.length + 1
|
||||
: props.columns.length
|
||||
}
|
||||
/>
|
||||
<col style={{ width: '4rem' }}></col>
|
||||
</colgroup>
|
||||
)}
|
||||
<thead>
|
||||
<tr>
|
||||
{props.withCheckboxes && (
|
||||
<th>
|
||||
<Button
|
||||
checkbox={
|
||||
!selected.length
|
||||
? 'none'
|
||||
: content.length === selected.length
|
||||
? 'all'
|
||||
: 'partial'
|
||||
}
|
||||
onClick={checkboxesToggleAll}
|
||||
/>
|
||||
</th>
|
||||
)}
|
||||
{props.columns.map((c, i) => (
|
||||
<th
|
||||
key={c.key}
|
||||
style={{ width: c.width }}
|
||||
colSpan={!props.addContent && (i === props.columns.length - 1) ? '2' : null}
|
||||
>
|
||||
{selected.length && i === props.columns.length - 1 ? (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
marginLeft: '-1rem',
|
||||
}}
|
||||
>
|
||||
<TableMenu
|
||||
bulkEditMenu
|
||||
buttonText="Choose Application"
|
||||
sid="bulk-menu"
|
||||
open={menuOpen === 'bulk-menu'}
|
||||
handleMenuOpen={handleMenuOpen}
|
||||
disabled={modalOpen}
|
||||
menuItems={
|
||||
props.bulkMenuItems.map(i => ({
|
||||
name: i.name,
|
||||
type: 'button',
|
||||
action: () => handleBulkAction(selected, i),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
gray
|
||||
tableHeaderLink
|
||||
onClick={() => sortTableContent({ column: c.key })}
|
||||
>
|
||||
{c.header}
|
||||
{sort.column === c.key
|
||||
? sort.order === 'asc'
|
||||
? <span>▴</span>
|
||||
: <span>▾</span>
|
||||
: null
|
||||
}
|
||||
</Button>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
{props.addContent && (
|
||||
<th>
|
||||
<Button onClick={addContent}>+</Button>
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{showTableLoader ? (
|
||||
<tr>
|
||||
<td colSpan={props.withCheckboxes ? props.columns.length + 1 : props.columns.length}>
|
||||
<Loader height={'71px'} />
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
!content || !content.length ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={props.withCheckboxes ? props.columns.length + 1 : props.columns.length}
|
||||
style={{ textAlign: 'center' }}
|
||||
>
|
||||
No {props.name}s
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
content.map(a => (
|
||||
<tr key={a.sid}>
|
||||
{props.withCheckboxes && (
|
||||
<td>
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
id={a.sid}
|
||||
value={a.sid}
|
||||
onChange={checkboxesToggleOne}
|
||||
checked={selected.includes(a.sid)}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{props.columns.map((c, i) => {
|
||||
let columnContent = '';
|
||||
let columnTitle = null;
|
||||
if (a[c.key]) {
|
||||
if (typeof a[c.key] === 'object') {
|
||||
if (a[c.key].type === 'normal') {
|
||||
columnContent = a[c.key].content;
|
||||
columnTitle = columnContent;
|
||||
} else if (a[c.key].type === 'masked') {
|
||||
columnContent = <ToggleText masked={a[c.key].masked} revealed={a[c.key].revealed} />;
|
||||
} else if (a[c.key].type === 'status') {
|
||||
columnContent = a[c.key].content === 'ok' ? <CheckGreen />
|
||||
: a[c.key].content === 'fail' ? <ErrorIcon />
|
||||
: a[c.key].content;
|
||||
columnTitle = a[c.key].title;
|
||||
}
|
||||
} else {
|
||||
columnContent = a[c.key];
|
||||
columnTitle = columnContent;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<td key={c.key} style={{ fontWeight: c.fontWeight }}>
|
||||
{i === 0 && props.urlParam
|
||||
? <span>
|
||||
<Link
|
||||
to={`/internal/${props.urlParam}/${a.sid}/edit`}
|
||||
tabIndex={modalOpen ? '-1' : ''}
|
||||
>
|
||||
<span tabIndex="-1" title={columnTitle}>
|
||||
{columnContent}
|
||||
</span>
|
||||
</Link>
|
||||
</span>
|
||||
: <span title={columnTitle}>{columnContent}</span>
|
||||
}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td>
|
||||
{props.rowsHaveDeleteButtons ? (
|
||||
<Button
|
||||
gray
|
||||
onClick={() => setContentToDelete(a)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
) : (
|
||||
<TableMenu
|
||||
sid={a.sid}
|
||||
open={menuOpen === a.sid}
|
||||
handleMenuOpen={handleMenuOpen}
|
||||
disabled={modalOpen}
|
||||
menuItems={[
|
||||
{
|
||||
name: 'Edit',
|
||||
type: 'link',
|
||||
url: `/internal/${props.urlParam}/${a.sid}/edit`,
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
type: 'button',
|
||||
action: () => setContentToDelete(a),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableContent;
|
||||
@@ -1,104 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
import { ReactComponent as MenuDots } from '../../images/MenuDots.svg';
|
||||
import Button from '../elements/Button';
|
||||
|
||||
const Container = styled.div`
|
||||
position: absolute;
|
||||
right: ${props => props.bulkEditMenu
|
||||
? '0'
|
||||
: '1.75rem'
|
||||
};
|
||||
top: ${props => props.bulkEditMenu
|
||||
? 'calc(100% + 0.25rem)'
|
||||
: '3rem'
|
||||
};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 0.5rem 0;
|
||||
border-radius: 0.25rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 0.5rem 0.5rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.5rem rgba(0, 0, 0, 0.12);
|
||||
z-index: 70;
|
||||
`;
|
||||
|
||||
const buttonLink = css`
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
color: #565656;
|
||||
outline: none;
|
||||
|
||||
& > span {
|
||||
outline: none;
|
||||
line-height: 1rem;
|
||||
padding: 1rem;
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
box-shadow: inset 0 0 0 0.125rem #D91C5C;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
background: #EEE;
|
||||
}
|
||||
`;
|
||||
|
||||
const MenuLink = styled(Link)`
|
||||
${buttonLink}
|
||||
`;
|
||||
|
||||
const MenuButton = styled.button`
|
||||
${buttonLink}
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const TableMenu = props => (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
bulkEditMenu={props.bulkEditMenu}
|
||||
tableMenu={!props.bulkEditMenu}
|
||||
selected={props.open}
|
||||
disabled={props.disabled}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
props.handleMenuOpen(props.sid);
|
||||
}}
|
||||
>
|
||||
{props.buttonText || <MenuDots />}
|
||||
</Button>
|
||||
{props.open && (
|
||||
<Container
|
||||
bulkEditMenu={props.bulkEditMenu}
|
||||
>
|
||||
{props.menuItems.map((m, i) => (
|
||||
m.type === 'link'
|
||||
? <MenuLink key={i} to={m.url}>
|
||||
<span tabIndex="-1">
|
||||
{m.name}
|
||||
</span>
|
||||
</MenuLink>
|
||||
: <MenuButton key={i}
|
||||
onClick={m.action}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
{m.name}
|
||||
</span>
|
||||
</MenuButton>
|
||||
))}
|
||||
</Container>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
export default TableMenu;
|
||||
@@ -1,60 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as ViewPassword } from '../../images/ViewPassword.svg';
|
||||
import { ReactComponent as HidePassword } from '../../images/HidePassword.svg';
|
||||
|
||||
const Container = styled.span`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const ToggleVisibilityButton = styled.button`
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
left: 22rem;
|
||||
width: 2.5rem;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
padding: 0;
|
||||
|
||||
& > span {
|
||||
height: 2.25rem;
|
||||
width: 2.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: 0;
|
||||
border-radius: 0.25rem;
|
||||
fill: #767676;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
fill: #565656;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
box-shadow: inset 0 0 0 0.125rem #767676;
|
||||
}
|
||||
`;
|
||||
|
||||
const ToggleText = props => {
|
||||
const [ mode, setMode ] = useState('masked');
|
||||
return (
|
||||
<Container>
|
||||
{mode === 'masked' ? props.masked : props.revealed}
|
||||
<ToggleVisibilityButton
|
||||
onClick={() => setMode(mode === 'masked' ? 'revealed' : 'masked')}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
{mode === 'masked'
|
||||
? <ViewPassword />
|
||||
: <HidePassword />
|
||||
}
|
||||
</span>
|
||||
</ToggleVisibilityButton>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleText;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
|
||||
type ClipBoardProps = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
/** Clipboard support...? */
|
||||
const hasClipboard = typeof navigator.clipboard !== "undefined";
|
||||
|
||||
export const ClipBoard = ({ text, id = "", name = "" }: ClipBoardProps) => {
|
||||
const handleClick = () => {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
toastSuccess(
|
||||
<>
|
||||
<strong>{text}</strong> copied to clipboard
|
||||
</>
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toastError(
|
||||
<>
|
||||
Unable to copy <strong>{text}</strong>, please select the text and
|
||||
right click to copy
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="clipboard inpbtn">
|
||||
<input id={id} name={name} type="text" readOnly value={text} />
|
||||
{hasClipboard && (
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
title="Copy to clipboard"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Icons.Clipboard />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
|
||||
const FilteredLink = ({ addButtonText, ...props }) => (
|
||||
<Link {...props}>{props.children}</Link>
|
||||
);
|
||||
|
||||
const StyledLink = styled(FilteredLink)`
|
||||
position: absolute;
|
||||
top: 7rem;
|
||||
right: 3rem;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
grid-column: 2;
|
||||
border-radius: 50%;
|
||||
text-decoration: none;
|
||||
color: #565656;
|
||||
z-index: 1;
|
||||
|
||||
& > span:first-child {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: 3.5rem;
|
||||
width: 3.5rem;
|
||||
border-radius: 50%;
|
||||
outline: 0;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
font-size: 2.5rem;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
&:focus > span:first-child {
|
||||
border: 0.25rem solid #890934;
|
||||
}
|
||||
|
||||
&:hover > span:first-child {
|
||||
}
|
||||
|
||||
&:active > span:first-child {
|
||||
}
|
||||
`;
|
||||
|
||||
const Tooltip = styled.span`
|
||||
display: none;
|
||||
color: #767676;
|
||||
a:focus > &,
|
||||
a:hover > & {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
right: calc(100% + 0.75rem);
|
||||
top: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #FFF;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
z-index: 60;
|
||||
}
|
||||
`;
|
||||
|
||||
const AddButton = props => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
return (
|
||||
<StyledLink
|
||||
{...props}
|
||||
tabIndex={modalOpen ? '-1' : ''}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
+
|
||||
</span>
|
||||
<Tooltip>{props.addButtonText}</Tooltip>
|
||||
</StyledLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButton;
|
||||
@@ -1,84 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
|
||||
const FilteredLink = ({ addButtonText, ...props }) => (
|
||||
<Link {...props}>{props.children}</Link>
|
||||
);
|
||||
|
||||
const StyledLink = styled(FilteredLink)`
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
grid-column: 2;
|
||||
border-radius: 50%;
|
||||
text-decoration: none;
|
||||
color: #565656;
|
||||
margin-left: 1rem;
|
||||
position: relative;
|
||||
& > span:first-child {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
border-radius: 50%;
|
||||
outline: 0;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
font-size: 2.5rem;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
&:focus > span:first-child {
|
||||
border: 0.25rem solid #890934;
|
||||
}
|
||||
|
||||
&:hover > span:first-child {
|
||||
}
|
||||
|
||||
&:active > span:first-child {
|
||||
}
|
||||
`;
|
||||
|
||||
const Tooltip = styled.span`
|
||||
display: none;
|
||||
color: #767676;
|
||||
a:focus > &,
|
||||
a:hover > & {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
left: calc(100% + 0.75rem);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #FFF;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
z-index: 60;
|
||||
}
|
||||
`;
|
||||
|
||||
const AddModalButton = props => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
const history = useHistory();
|
||||
return (
|
||||
<StyledLink
|
||||
{...props}
|
||||
to={history.location.pathname}
|
||||
tabIndex={modalOpen ? '-1' : ''}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
+
|
||||
</span>
|
||||
<Tooltip>{props.addButtonText}</Tooltip>
|
||||
</StyledLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddModalButton;
|
||||
@@ -1,272 +0,0 @@
|
||||
import React, { useContext, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const StyledButton = styled.button`
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
grid-column: 2;
|
||||
${props => props.fullWidth
|
||||
? `width: 100%;`
|
||||
: `justify-self: start;`
|
||||
}
|
||||
${props => props.bottomGap && `
|
||||
margin-bottom: 1rem;
|
||||
`}
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
outline: 0;
|
||||
height: ${
|
||||
props => props.large
|
||||
? '3rem'
|
||||
: '2.25rem'
|
||||
};
|
||||
${props => props.fullWidth && `
|
||||
width: 100%;
|
||||
`}
|
||||
${props => props.square
|
||||
? `width: 2.25rem;`
|
||||
: `padding: 0 1rem;`
|
||||
}
|
||||
border-radius: 0.25rem;
|
||||
background: ${
|
||||
props => props.text
|
||||
? 'none'
|
||||
: props.gray
|
||||
? '#E3E3E3'
|
||||
: '#D91C5C'
|
||||
};
|
||||
color: ${
|
||||
props => props.gray
|
||||
? '#565656'
|
||||
: props.text
|
||||
? '#D91C5C'
|
||||
: '#FFF'
|
||||
};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
box-shadow: ${props => props.text
|
||||
? ''
|
||||
: '0 0.125rem 0.25rem rgba(0,0,0,0.12),'
|
||||
}
|
||||
inset 0 0 0
|
||||
${props => props.text
|
||||
? '0.125rem'
|
||||
: '0.25rem'
|
||||
}
|
||||
${props => props.gray
|
||||
? '#767676'
|
||||
: '#890934'
|
||||
};
|
||||
}
|
||||
|
||||
&:hover:not([disabled]) > span {
|
||||
background: ${props => props.text
|
||||
? '#E3E3E3'
|
||||
: props.gray
|
||||
? '#C6C6C6'
|
||||
: '#BD164E'
|
||||
};
|
||||
}
|
||||
|
||||
&:active:not([disabled]) > span {
|
||||
background: ${props => props.text
|
||||
? '#D5D5D5'
|
||||
: props.gray
|
||||
? '#B6B6B6'
|
||||
: '#A40D40'
|
||||
};
|
||||
}
|
||||
|
||||
${props => props.formLink && `
|
||||
justify-self: start;
|
||||
|
||||
& > span {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
padding: 0.25rem;
|
||||
margin: -0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0 0 0.125rem #D91C5C;
|
||||
}
|
||||
|
||||
&:hover:not([disabled]) > span,
|
||||
&:active:not([disabled]) > span {
|
||||
background: none;
|
||||
box-shadow: 0 0.125rem 0 #D91C5C;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:active > span {
|
||||
background: none;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.tableHeaderLink && `
|
||||
& > span {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:focus > span,
|
||||
&:hover:not([disabled]) > span,
|
||||
&:active:not([disabled]) > span {
|
||||
padding: 0.625rem;
|
||||
margin: -0.625rem;
|
||||
}
|
||||
|
||||
& > span > *:last-child {
|
||||
color: #8F8F8F;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.right && `
|
||||
justify-self: end;
|
||||
`}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Table Menu (3 dots on right of each row)
|
||||
//=============================================================================
|
||||
${props => props.tableMenu && `
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
& > span {
|
||||
background: ${props.selected
|
||||
? '#E3E3E3'
|
||||
: 'none'
|
||||
};
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
border-radius: 50%;
|
||||
outline: 0;
|
||||
fill: #767676;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
border: 2px solid #D91C5C;
|
||||
background: ${props.selected
|
||||
? 'RGBA(217, 28, 92, 0.15)'
|
||||
: 'none'
|
||||
};
|
||||
fill: #D91C5C;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover:not([disabled]) > span,
|
||||
&:active:not([disabled]) > span {
|
||||
background: ${props.selected
|
||||
? 'RGBA(217, 28, 92, 0.15)'
|
||||
: 'none'
|
||||
};
|
||||
fill: #D91C5C;
|
||||
}
|
||||
`}
|
||||
|
||||
//=============================================================================
|
||||
// "Check All" button for bulk editing in table
|
||||
//=============================================================================
|
||||
${props => props.checkbox && `
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
& > span {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 1px solid #A5A5A5;
|
||||
border-radius: 0.125rem;
|
||||
background: #FFF;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
border-color: #565656;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
&:hover:not([disabled]) > span,
|
||||
&:active:not([disabled]) > span {
|
||||
background: none;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => (props.checkbox === 'all' || props.checkbox === 'partial') && `
|
||||
& > span,
|
||||
&:hover:not([disabled]) > span {
|
||||
background: #D91C5C;
|
||||
border-color: #D91C5C;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
border: 3px solid #890934;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.checkbox === 'all' && `
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.35rem;
|
||||
left: 0.25rem;
|
||||
height: 8px;
|
||||
width: 15px;
|
||||
border-left: 2px solid #FFF;
|
||||
border-bottom: 2px solid #FFF;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.checkbox === 'partial' && `
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.6875rem;
|
||||
left: 0.1875rem;
|
||||
height: 0.125rem;
|
||||
width: 1.125rem;
|
||||
background: #FFF;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const Button = (props, ref) => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
const buttonRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
buttonRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<StyledButton
|
||||
{...props}
|
||||
ref={buttonRef}
|
||||
disabled={(modalOpen && !props.inModal) || props.disabled}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
{props.children}
|
||||
</span>
|
||||
</StyledButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Button);
|
||||
@@ -1,138 +0,0 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import Label from './Label';
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
const CheckboxContainer = styled.div`
|
||||
margin-left: ${props => props.noLeftMargin
|
||||
? '0'
|
||||
: props.invalid
|
||||
? '0.5rem'
|
||||
: '1rem'
|
||||
};
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: ${props => props.large
|
||||
? '3rem'
|
||||
: '2.25rem'
|
||||
};
|
||||
${props => props.invalid && `
|
||||
margin-right: -0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
border: 1px solid #D91C5C;
|
||||
border-radius: 0.125rem;
|
||||
background: RGBA(217,28,92,0.2);
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledCheckbox = styled.input`
|
||||
margin: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled(Label)`
|
||||
margin-left: 0.5rem;
|
||||
cursor: pointer;
|
||||
${props => props.tooltip && `
|
||||
& > span {
|
||||
border-bottom: 1px dotted;
|
||||
border-left: 1px solid transparent;
|
||||
cursor: help;
|
||||
}
|
||||
`}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: ${props => props.large
|
||||
? '0.75rem'
|
||||
: '0.375rem'
|
||||
};
|
||||
left: ${props => props.invalid
|
||||
? '0.5rem'
|
||||
: '0'
|
||||
};
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 1px solid #A5A5A5;
|
||||
border-radius: 0.125rem;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
input:focus + &::before {
|
||||
border-color: #565656;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
input:checked + &::before {
|
||||
background: #D91C5C;
|
||||
border-color: #D91C5C;
|
||||
}
|
||||
|
||||
input:checked:focus + &::before {
|
||||
border: 3px solid #890934;
|
||||
}
|
||||
|
||||
input:checked + &::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: ${props => props.large
|
||||
? '1.1rem'
|
||||
: '0.725rem'
|
||||
};
|
||||
left: ${props => props.invalid
|
||||
? '0.75rem'
|
||||
: '0.25rem'
|
||||
};
|
||||
height: 8px;
|
||||
width: 15px;
|
||||
border-left: 2px solid #FFF;
|
||||
border-bottom: 2px solid #FFF;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const Checkbox = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<CheckboxContainer
|
||||
invalid={props.invalid}
|
||||
noLeftMargin={props.noLeftMargin}
|
||||
>
|
||||
<StyledCheckbox
|
||||
id={props.id}
|
||||
name={props.id}
|
||||
type="checkbox"
|
||||
checked={props.checked}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
value={props.value}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<StyledLabel
|
||||
htmlFor={props.id}
|
||||
tooltip={props.tooltip}
|
||||
invalid={props.invalid}
|
||||
>
|
||||
<span>
|
||||
{props.label}
|
||||
{
|
||||
props.tooltip &&
|
||||
<Tooltip>
|
||||
{props.tooltip}
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
</StyledLabel>
|
||||
</CheckboxContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Checkbox);
|
||||
@@ -1,14 +0,0 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Code = styled.code`
|
||||
white-space: pre-wrap;
|
||||
text-align: left;
|
||||
background: #fbfbfb;
|
||||
padding: 0.5rem;
|
||||
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #B6B6B6;
|
||||
max-height: 120px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export default Code;
|
||||
@@ -1,62 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import Button from './Button';
|
||||
import Span from './Span';
|
||||
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
margin-left: 1rem;
|
||||
`;
|
||||
|
||||
const CopyableText = props => {
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
const copyText = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(props.text);
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: `${props.textType} copied to clipboard`,
|
||||
});
|
||||
} catch (err) {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: `Unable to copy ${props.textType}, please select the text and right click to copy`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof navigator.clipboard === 'undefined') {
|
||||
return (
|
||||
<Span hasBorder={props.hasBorder}>
|
||||
{props.text}
|
||||
<StyledButton
|
||||
text
|
||||
formLink
|
||||
inModal={props.inModal}
|
||||
type="button"
|
||||
>
|
||||
</StyledButton>
|
||||
</Span>
|
||||
);
|
||||
}
|
||||
else return (
|
||||
<Span hasBorder={props.hasBorder}>
|
||||
{props.text}
|
||||
<StyledButton
|
||||
text
|
||||
formLink
|
||||
inModal={props.inModal}
|
||||
type="button"
|
||||
onClick={copyText}
|
||||
>
|
||||
copy
|
||||
</StyledButton>
|
||||
</Span>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyableText;
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
height: 2.25rem;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
border: 0;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
&:after {
|
||||
content: '${props => props.validFile ? 'Choose a Different File' : 'Choose File'}';
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2.25rem;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus:after {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12),
|
||||
inset 0 0 0 0.25rem #890934;
|
||||
}
|
||||
|
||||
&:hover:not([disabled]):after {
|
||||
background: #BD164E;
|
||||
}
|
||||
|
||||
&:active:not([disabled]):after {
|
||||
background: #A40D40;
|
||||
}
|
||||
|
||||
&::file-selector-button {
|
||||
content: '${props => props.validFile ? 'Choose a Different File' : 'Choose File'}';
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2.25rem;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&:focus::file-selector-button {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12),
|
||||
inset 0 0 0 0.25rem #890934;
|
||||
}
|
||||
|
||||
&:hover:not([disabled])::file-selector-button {
|
||||
background: #BD164E;
|
||||
}
|
||||
|
||||
&:active:not([disabled])::file-selector-button {
|
||||
background: #A40D40;
|
||||
}
|
||||
`;
|
||||
|
||||
const FileUpload = (props, ref) => {
|
||||
return (
|
||||
<Container>
|
||||
<StyledInput
|
||||
id={props.id}
|
||||
name={props.id}
|
||||
type="file"
|
||||
onChange={props.onChange}
|
||||
validFile={props.validFile}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(FileUpload);
|
||||
@@ -1,30 +0,0 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Form = styled.form`
|
||||
text-align: right;
|
||||
padding: 2rem;
|
||||
${props => !props.large && `
|
||||
& input {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`}
|
||||
& hr {
|
||||
margin: 0 -2rem;
|
||||
background: none;
|
||||
border: 0;
|
||||
border-top: 1px solid #C6C6C6;
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
${props => props.large && `
|
||||
display: grid;
|
||||
grid-template-columns: ${props.wideLabel
|
||||
? '1.75fr'
|
||||
: '1.3fr'
|
||||
} 10fr;
|
||||
grid-row-gap: 1rem;
|
||||
grid-column-gap: 0.75rem;
|
||||
align-items: center;
|
||||
`}
|
||||
`;
|
||||
|
||||
export default Form;
|
||||
@@ -1,9 +0,0 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const H1 = styled.h1`
|
||||
font-size: 3rem;
|
||||
margin: 1rem 0;
|
||||
font-weight: normal;
|
||||
`;
|
||||
|
||||
export default H1;
|
||||
@@ -1,112 +0,0 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as ViewPassword } from '../../images/ViewPassword.svg';
|
||||
import { ReactComponent as HidePassword } from '../../images/HidePassword.svg';
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
${props => props.width && `
|
||||
width: ${props.width};
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
height: ${props => props.large
|
||||
? '3rem'
|
||||
: '2.25rem'
|
||||
};
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
border: 1px solid #B6B6B6;
|
||||
${props => props.invalid && `
|
||||
background: RGBA(217,28,92,0.2);
|
||||
border-color: #D91C5C;
|
||||
`}
|
||||
border-radius: 0.125rem;
|
||||
color: inherit;
|
||||
&:focus {
|
||||
border-color: ${props => props.invalid
|
||||
? '#890934;'
|
||||
: '#565656;'
|
||||
}
|
||||
outline: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
&:disabled {
|
||||
background: #DDD;
|
||||
border: 1px solid #B6B6B6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
|
||||
const PasswordButton = styled.button`
|
||||
position: absolute;
|
||||
top: ${props => props.large ? '0.375rem' : '0.25rem'};
|
||||
right: 1rem;
|
||||
height: ${props => props.large ? '2.25rem' : '1.75rem'};
|
||||
width: 2.5rem;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
padding: 0;
|
||||
|
||||
& > span {
|
||||
height: ${props => props.large ? '2.25rem' : '1.75rem'};
|
||||
width: 2.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: 0;
|
||||
border-radius: 0.25rem;
|
||||
fill: #D91C5C;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
fill: #BD164E;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
box-shadow: inset 0 0 0 0.125rem #D91C5C;
|
||||
}
|
||||
`;
|
||||
|
||||
const Input = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<Container
|
||||
width={props.width}
|
||||
>
|
||||
<StyledInput
|
||||
{...props}
|
||||
ref={inputRef}
|
||||
/>
|
||||
{
|
||||
props.allowShowPassword &&
|
||||
<PasswordButton
|
||||
type="button"
|
||||
large={props.large}
|
||||
onClick={props.toggleShowPassword}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
{
|
||||
props.showPassword
|
||||
? <HidePassword />
|
||||
: <ViewPassword />
|
||||
}
|
||||
</span>
|
||||
</PasswordButton>
|
||||
}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Input);
|
||||
@@ -1,18 +0,0 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const InputGroup = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-column: 2;
|
||||
${props => props.flexEnd && 'justify-content: flex-end;'}
|
||||
${props => props.spaced && `
|
||||
& > * {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
& > *:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export default InputGroup;
|
||||
@@ -1,16 +0,0 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Label = styled.label`
|
||||
color: #767676;
|
||||
${props => props.indented && `margin-right: 0.5rem;`}
|
||||
${props => props.middle && `margin: 0 0.5rem 0 1rem;`}
|
||||
${props => props.tooltip && `
|
||||
& > span {
|
||||
border-bottom: 1px dotted;
|
||||
border-left: 1px solid transparent;
|
||||
cursor: help;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export default Label;
|
||||
@@ -1,72 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link as ReactRouterLink } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ModalStateContext } from '../../contexts/ModalContext';
|
||||
|
||||
const FilteredLink = ({ formLink, right, inModal, ...props }) => (
|
||||
<ReactRouterLink {...props}>{props.children}</ReactRouterLink>
|
||||
);
|
||||
|
||||
const StyledReactRouterLink = styled(FilteredLink)`
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #D91C5C;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
outline: 0;
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
padding: 0.25rem;
|
||||
margin: -0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0 0 0.125rem #D91C5C;
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
box-shadow: 0 0.125rem 0 #D91C5C;
|
||||
border-radius: 0;
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
&:active > span {
|
||||
color: #D91C5C;
|
||||
}
|
||||
|
||||
${props => props.formLink && `
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
`}
|
||||
|
||||
${props => props.right && `
|
||||
justify-self: end;
|
||||
`}
|
||||
`;
|
||||
|
||||
const Link = props => {
|
||||
const modalOpen = useContext(ModalStateContext);
|
||||
return (
|
||||
<StyledReactRouterLink
|
||||
{...props}
|
||||
tabIndex={modalOpen && !props.inModal ? '-1' : ''}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
{props.children}
|
||||
</span>
|
||||
</StyledReactRouterLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default Link;
|
||||
@@ -1,35 +0,0 @@
|
||||
import React, { useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import Input from '../elements/Input';
|
||||
|
||||
const PasswordInput = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
const [ showPassword, setShowPassword ] = useState(false);
|
||||
const toggleShowPassword = () => setShowPassword(!showPassword);
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
ref={inputRef}
|
||||
showPassword={showPassword}
|
||||
toggleShowPassword={toggleShowPassword}
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={props.password}
|
||||
onChange={e => props.setPassword(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (!showPassword && e.getModifierState('CapsLock')) {
|
||||
props.setErrorMessage('CAPSLOCK is enabled!');
|
||||
} else {
|
||||
props.setErrorMessage('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(PasswordInput);
|
||||
@@ -1,119 +0,0 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import Label from './Label';
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
const RadioContainer = styled.div`
|
||||
margin-left: ${props => props.noLeftMargin
|
||||
? '-0.5rem'
|
||||
: '0.5rem'
|
||||
};
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.25rem;
|
||||
padding: 0 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.125rem;
|
||||
${props => props.invalid && `
|
||||
border-color: #D91C5C;
|
||||
background: RGBA(217,28,92,0.2);
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledRadio = styled.input`
|
||||
outline: none;
|
||||
margin: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled(Label)`
|
||||
padding-left: 0.5rem;
|
||||
cursor: ${props => props.disabled
|
||||
? 'not-allowed'
|
||||
: 'pointer'
|
||||
};
|
||||
${props => props.tooltip && `
|
||||
& > span {
|
||||
border-bottom: 1px dotted;
|
||||
border-left: 1px solid transparent;
|
||||
cursor: help;
|
||||
}
|
||||
`}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.375rem;
|
||||
left: 0.5rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 1px solid #A5A5A5;
|
||||
border-radius: 50%;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
input:focus + &::before {
|
||||
border-color: #565656;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
input:checked + &::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0.75rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 50%;
|
||||
background: ${props => props.disabled
|
||||
? '#959595'
|
||||
: '#707070'
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
const Radio = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<RadioContainer
|
||||
invalid={props.invalid}
|
||||
noLeftMargin={props.noLeftMargin}
|
||||
>
|
||||
<StyledRadio
|
||||
name={props.name}
|
||||
id={props.id}
|
||||
value={props.id}
|
||||
type="radio"
|
||||
checked={props.checked}
|
||||
onChange={props.onChange}
|
||||
ref={inputRef}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
<StyledLabel
|
||||
htmlFor={props.id}
|
||||
tooltip={props.tooltip}
|
||||
invalid={props.invalid}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
<span>
|
||||
{props.label}
|
||||
{
|
||||
props.tooltip &&
|
||||
<Tooltip>
|
||||
{props.tooltip}
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
</StyledLabel>
|
||||
</RadioContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Radio);
|
||||
@@ -1,33 +0,0 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Select = styled.select`
|
||||
height: ${props => props.large
|
||||
? '3rem'
|
||||
: '2.25rem'
|
||||
};
|
||||
padding: 0 0.75rem;
|
||||
border: 1px solid #B6B6B6;
|
||||
border-radius: 0.125rem;
|
||||
background: #fff;
|
||||
color: inherit;
|
||||
max-width: 230px;
|
||||
&:focus {
|
||||
border-color: #565656;
|
||||
outline: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
${props => props.invalid && `
|
||||
background: RGBA(217,28,92,0.2);
|
||||
border-color: #D91C5C;
|
||||
&:focus {
|
||||
border-color: #890934;
|
||||
}
|
||||
`}
|
||||
|
||||
&:disabled {
|
||||
background: #DDD;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Select;
|
||||
@@ -1,16 +0,0 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Span = styled.span`
|
||||
text-align: left;
|
||||
${props => props.hasBorder ? `
|
||||
height: 2.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
border: 1px solid #B6B6B6;
|
||||
border-radius: 0.125rem;
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
export default Span;
|
||||
@@ -1,103 +0,0 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Table = styled.table`
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
min-width: 38rem;
|
||||
|
||||
& > thead {
|
||||
background: #F7F7F7;
|
||||
}
|
||||
|
||||
& tr {
|
||||
border-bottom: 1px solid #E0E0E0;
|
||||
}
|
||||
|
||||
& thead tr {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
& tbody tr {
|
||||
height: 5.5rem;
|
||||
}
|
||||
|
||||
& tbody tr:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
& th {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
color: #717171;
|
||||
}
|
||||
|
||||
& th,
|
||||
& td {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
& td > span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
& td > span > a {
|
||||
outline: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
& td > span > a > span {
|
||||
outline: 0;
|
||||
color: #565656;
|
||||
}
|
||||
|
||||
& td > span > a:hover > span {
|
||||
box-shadow: 0 0.125rem 0 #565656;
|
||||
}
|
||||
|
||||
& td > span > a:focus > span {
|
||||
padding: 0.625rem;
|
||||
margin: -0.625rem;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: inset 0 0 0 0.125rem #D91C5C;
|
||||
}
|
||||
|
||||
& td:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
& td:last-child {
|
||||
overflow: inherit;
|
||||
position: relative;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
${props => props.withCheckboxes && `
|
||||
& th:first-child,
|
||||
& td:first-child {
|
||||
width: 3rem;
|
||||
padding: 1.25rem 0 1.25rem 1.25rem;
|
||||
}
|
||||
& td:nth-child(2) {
|
||||
font-weight: bold;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.rowsHaveDeleteButtons && `
|
||||
& th:last-child {
|
||||
width: 9rem;
|
||||
text-align: right;
|
||||
}
|
||||
& td:last-child {
|
||||
padding: 0 1.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export default Table;
|
||||
@@ -1,122 +0,0 @@
|
||||
import styled from 'styled-components/macro';
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
|
||||
import Link from './Link';
|
||||
|
||||
const Tooltip = styled.span`
|
||||
display: none;
|
||||
|
||||
label > span:hover > & {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: calc(50% - 1rem);
|
||||
transform: translateX(50%);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #FFF;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
z-index: 80;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLinkWithTooltip = styled.span`
|
||||
position: relative;
|
||||
|
||||
> span {
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, calc(-100% - 5px), 0);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid #C6C6C6;
|
||||
background: #FFF;
|
||||
z-index: 80;
|
||||
white-space: pre;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-top: 8px solid #FFF;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid #C6C6C6;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const LinkWithTooltip = props => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const tooltipRef = useRef();
|
||||
const triggerRef = useRef();
|
||||
|
||||
const handleLinkClick = useCallback(() => {
|
||||
setIsActive((oldActive) => {
|
||||
const newActive = !oldActive;
|
||||
return newActive;
|
||||
});
|
||||
}, [setIsActive]);
|
||||
|
||||
const handleOuterClick = useCallback((e) => {
|
||||
if (!tooltipRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tooltipRef.current.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (triggerRef.current.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleLinkClick();
|
||||
}, [tooltipRef, triggerRef, handleLinkClick]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleOuterClick, false);
|
||||
|
||||
return () => document.removeEventListener('click', handleOuterClick, false);
|
||||
}, [handleOuterClick]);
|
||||
|
||||
return (
|
||||
<StyledLinkWithTooltip>
|
||||
<Link to="#" onClick={handleLinkClick}>
|
||||
<span ref={triggerRef}>{props.children}</span>
|
||||
</Link>
|
||||
{isActive ? (
|
||||
<span ref={tooltipRef}>
|
||||
{props.tipText}
|
||||
</span>
|
||||
) : null}
|
||||
</StyledLinkWithTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
LinkWithTooltip,
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
@@ -1,55 +0,0 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as TrashIcon } from '../../images/TrashIcon.svg';
|
||||
|
||||
const StyledButton = styled.button`
|
||||
display: flex;
|
||||
background: none;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: 0.5rem;
|
||||
fill: #949494;
|
||||
& > span {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 2.2rem;
|
||||
width: 1.9rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0.25rem;
|
||||
outline: 0;
|
||||
}
|
||||
&:focus > span {
|
||||
box-shadow: inset 0 0 0 0.125rem #D91C5C;
|
||||
fill: #D91C5C;
|
||||
}
|
||||
&:hover > span {
|
||||
fill: #D91C5C;
|
||||
}
|
||||
&:active > span {
|
||||
}
|
||||
`;
|
||||
|
||||
const TrashButton = (props, ref) => {
|
||||
const buttonRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
buttonRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<StyledButton
|
||||
type="button"
|
||||
{...props}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
<TrashIcon />
|
||||
</span>
|
||||
</StyledButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(TrashButton);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,977 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
import Select from '../elements/Select';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Button from '../elements/Button';
|
||||
import SpeechSynthesisLanguageGoogle from '../../data/SpeechSynthesisLanguageGoogle';
|
||||
import SpeechSynthesisLanguageAws from '../../data/SpeechSynthesisLanguageAws';
|
||||
import SpeechRecognizerLanguageGoogle from '../../data/SpeechRecognizerLanguageGoogle';
|
||||
import SpeechRecognizerLanguageAws from '../../data/SpeechRecognizerLanguageAws';
|
||||
import SpeechRecognizerLanguageMicrosoft from '../../data/SpeechRecognizerLanguageMicrosoft';
|
||||
import SpeechSynthesisLanguageMicrosoft from '../../data/SpeechSynthesisLanguageMicrosoft';
|
||||
import SpeechSynthesisLanguageWellSaid from '../../data/SpeechSynthesisLanguageWellSaid';
|
||||
import Loader from '../blocks/Loader';
|
||||
import CopyableText from '../elements/CopyableText';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const ApplicationForm = props => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Refs
|
||||
const refName = useRef(null);
|
||||
const refAccount = useRef(null);
|
||||
const refCallWebhook = useRef(null);
|
||||
const refCallWebhookUser = useRef(null);
|
||||
const refCallWebhookPass = useRef(null);
|
||||
const refStatusWebhook = useRef(null);
|
||||
const refStatusWebhookUser = useRef(null);
|
||||
const refStatusWebhookPass = useRef(null);
|
||||
const refMessagingWebhookUser = useRef(null);
|
||||
const refMessagingWebhookPass = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ name, setName ] = useState('');
|
||||
const [ accountSid, setAccountSid ] = useState('');
|
||||
const [ callWebhook, setCallWebhook ] = useState('');
|
||||
const [ callWebhookMethod, setCallWebhookMethod ] = useState('POST');
|
||||
const [ callWebhookUser, setCallWebhookUser ] = useState('');
|
||||
const [ callWebhookPass, setCallWebhookPass ] = useState('');
|
||||
const [ statusWebhook, setStatusWebhook ] = useState('');
|
||||
const [ statusWebhookMethod, setStatusWebhookMethod ] = useState('POST');
|
||||
const [ statusWebhookUser, setStatusWebhookUser ] = useState('');
|
||||
const [ statusWebhookPass, setStatusWebhookPass ] = useState('');
|
||||
const [ messagingWebhook, setMessagingWebhook ] = useState('');
|
||||
const [ messagingWebhookMethod, setMessagingWebhookMethod ] = useState('POST');
|
||||
const [ messagingWebhookUser, setMessagingWebhookUser ] = useState('');
|
||||
const [ messagingWebhookPass, setMessagingWebhookPass ] = useState('');
|
||||
const [ speechSynthesisVendor, setSpeechSynthesisVendor ] = useState('google');
|
||||
const [ speechSynthesisLanguage, setSpeechSynthesisLanguage ] = useState('en-US');
|
||||
const [ speechSynthesisVoice, setSpeechSynthesisVoice ] = useState('en-US-Standard-C');
|
||||
const [ speechRecognizerVendor, setSpeechRecognizerVendor ] = useState('google');
|
||||
const [ speechRecognizerLanguage, setSpeechRecognizerLanguage ] = useState('en-US');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidName, setInvalidName ] = useState(false);
|
||||
const [ invalidAccount, setInvalidAccount ] = useState(false);
|
||||
const [ invalidCallWebhook, setInvalidCallWebhook ] = useState(false);
|
||||
const [ invalidCallWebhookUser, setInvalidCallWebhookUser ] = useState(false);
|
||||
const [ invalidCallWebhookPass, setInvalidCallWebhookPass ] = useState(false);
|
||||
const [ invalidStatusWebhook, setInvalidStatusWebhook ] = useState(false);
|
||||
const [ invalidStatusWebhookUser, setInvalidStatusWebhookUser ] = useState(false);
|
||||
const [ invalidStatusWebhookPass, setInvalidStatusWebhookPass ] = useState(false);
|
||||
const [ invalidMessagingWebhookUser, setInvalidMessagingWebhookUser ] = useState(false);
|
||||
const [ invalidMessagingWebhookPass, setInvalidMessagingWebhookPass ] = useState(false);
|
||||
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
const [ showCallAuth, setShowCallAuth ] = useState(false);
|
||||
const toggleCallAuth = () => setShowCallAuth(!showCallAuth);
|
||||
|
||||
const [ showStatusAuth, setShowStatusAuth ] = useState(false);
|
||||
const toggleStatusAuth = () => setShowStatusAuth(!showStatusAuth);
|
||||
|
||||
const [ showMessagingAuth, setShowMessagingAuth ] = useState(false);
|
||||
const toggleMessagingAuth = () => setShowMessagingAuth(!showMessagingAuth);
|
||||
|
||||
const [ accounts, setAccounts ] = useState([]);
|
||||
const [ applications, setApplications ] = useState([]);
|
||||
const [ applicationSid, setApplicationSid ] = useState([]);
|
||||
|
||||
// See if user logged in
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const promiseAllValues = await Promise.all([
|
||||
accountsPromise,
|
||||
applicationsPromise,
|
||||
]);
|
||||
|
||||
const accounts = promiseAllValues[0].data.filter(a => a.service_provider_sid === currentServiceProvider);
|
||||
const applications = promiseAllValues[1].data;
|
||||
|
||||
setAccounts(accounts);
|
||||
setApplications(applications);
|
||||
|
||||
if (accounts.length === 0) {
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must create an account before you can create an application.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(props.type === 'setup' && accounts.length > 1) ||
|
||||
(props.type === 'setup' && applications.length > 1)
|
||||
) {
|
||||
history.push('/internal/applications');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'That page is only accessible during setup.',
|
||||
});
|
||||
}
|
||||
|
||||
if (props.type === 'add' && accounts.length === 1) {
|
||||
setAccountSid(accounts[0].account_sid);
|
||||
}
|
||||
|
||||
if (props.type === 'setup' || props.type === 'edit') {
|
||||
const currentApplication = props.type === 'edit'
|
||||
? applications.filter(a => a.application_sid === props.application_sid)
|
||||
: applications;
|
||||
|
||||
if (props.type === 'edit' && !currentApplication.length) {
|
||||
history.push('/internal/applications');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'That application does not exist.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentApplication.length) {
|
||||
setName('default application');
|
||||
setAccountSid(accounts[0].account_sid);
|
||||
} else {
|
||||
const app = currentApplication[0];
|
||||
setName( app.name || '');
|
||||
setCallWebhook( (app.call_hook && app.call_hook.url) || '');
|
||||
setCallWebhookMethod( (app.call_hook && app.call_hook.method) || 'post');
|
||||
setCallWebhookUser( (app.call_hook && app.call_hook.username) || '');
|
||||
setCallWebhookPass( (app.call_hook && app.call_hook.password) || '');
|
||||
setStatusWebhook( (app.call_status_hook && app.call_status_hook.url) || '');
|
||||
setStatusWebhookMethod( (app.call_status_hook && app.call_status_hook.method) || 'post');
|
||||
setStatusWebhookUser( (app.call_status_hook && app.call_status_hook.username) || '');
|
||||
setStatusWebhookPass( (app.call_status_hook && app.call_status_hook.password) || '');
|
||||
setMessagingWebhook( (app.messaging_hook && app.messaging_hook.url) || '');
|
||||
setMessagingWebhookMethod( (app.messaging_hook && app.messaging_hook.method) || 'post');
|
||||
setMessagingWebhookUser( (app.messaging_hook && app.messaging_hook.username) || '');
|
||||
setMessagingWebhookPass( (app.messaging_hook && app.messaging_hook.password) || '');
|
||||
setSpeechSynthesisVendor( app.speech_synthesis_vendor || '');
|
||||
setSpeechSynthesisLanguage( app.speech_synthesis_language || '');
|
||||
setSpeechSynthesisVoice( app.speech_synthesis_voice || '');
|
||||
setSpeechRecognizerVendor( app.speech_recognizer_vendor || '');
|
||||
setSpeechRecognizerLanguage( app.speech_recognizer_language || '');
|
||||
setAccountSid( app.account_sid || '');
|
||||
setApplicationSid( app.application_sid);
|
||||
if (
|
||||
(app.call_hook && app.call_hook.username) ||
|
||||
(app.call_hook && app.call_hook.password)
|
||||
) {
|
||||
setShowCallAuth(true);
|
||||
}
|
||||
|
||||
if (
|
||||
(app.call_status_hook && app.call_status_hook.username) ||
|
||||
(app.call_status_hook && app.call_status_hook.password)
|
||||
) {
|
||||
setShowStatusAuth(true);
|
||||
}
|
||||
|
||||
if (
|
||||
(app.messaging_hook && app.messaging_hook.username) ||
|
||||
(app.messaging_hook && app.messaging_hook.password)
|
||||
) {
|
||||
setShowMessagingAuth(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
setShowLoader(false);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get accounts',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidName(false);
|
||||
setInvalidAccount(false);
|
||||
setInvalidCallWebhook(false);
|
||||
setInvalidCallWebhookUser(false);
|
||||
setInvalidCallWebhookPass(false);
|
||||
setInvalidStatusWebhook(false);
|
||||
setInvalidStatusWebhookUser(false);
|
||||
setInvalidStatusWebhookPass(false);
|
||||
setInvalidMessagingWebhookUser(false);
|
||||
setInvalidMessagingWebhookPass(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if ((props.type === 'add' || props.type === 'edit') && !name) {
|
||||
errorMessages.push('Please provide a name.');
|
||||
setInvalidName(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
// check if name is already in use
|
||||
|
||||
if ((props.type === 'add' || props.type === 'edit') && !accountSid) {
|
||||
errorMessages.push('Please choose an account for this application to be associated with.');
|
||||
setInvalidAccount(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refAccount.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!callWebhook) {
|
||||
errorMessages.push('Please enter a Calling Webhook.');
|
||||
setInvalidCallWebhook(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refCallWebhook.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!statusWebhook) {
|
||||
errorMessages.push('Please enter a Call Status Webhook.');
|
||||
setInvalidStatusWebhook(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refStatusWebhook.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((callWebhookUser && !callWebhookPass) || (!callWebhookUser && callWebhookPass)) {
|
||||
errorMessages.push('Calling Webhook username and password must be either both filled out or both empty.');
|
||||
setInvalidCallWebhookUser(true);
|
||||
setInvalidCallWebhookPass(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!callWebhookUser) {
|
||||
refCallWebhookUser.current.focus();
|
||||
} else {
|
||||
refCallWebhookPass.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((statusWebhookUser && !statusWebhookPass) || (!statusWebhookUser && statusWebhookPass)) {
|
||||
errorMessages.push('Call Status Webhook username and password must be either both filled out or both empty.');
|
||||
setInvalidStatusWebhookUser(true);
|
||||
setInvalidStatusWebhookPass(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!statusWebhookUser) {
|
||||
refStatusWebhookUser.current.focus();
|
||||
} else {
|
||||
refStatusWebhookPass.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((messagingWebhookUser && !messagingWebhookPass) || (!messagingWebhookUser && messagingWebhookPass)) {
|
||||
errorMessages.push('Messaging Webhook username and password must be either both filled out or both empty.');
|
||||
setInvalidMessagingWebhookUser(true);
|
||||
setInvalidMessagingWebhookPass(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!messagingWebhookUser) {
|
||||
refMessagingWebhookUser.current.focus();
|
||||
} else {
|
||||
refMessagingWebhookPass.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Submit
|
||||
//=============================================================================
|
||||
const shouldCreateNew = props.type === 'add' || (props.type === 'setup' && !applications.length);
|
||||
|
||||
const method = shouldCreateNew
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
const url = shouldCreateNew
|
||||
? '/Applications'
|
||||
: `/Applications/${applicationSid}`;
|
||||
|
||||
const data = {
|
||||
account_sid: accountSid,
|
||||
name: name.trim(),
|
||||
call_hook: {
|
||||
url: callWebhook.trim(),
|
||||
method: callWebhookMethod,
|
||||
username: callWebhookUser.trim() || null,
|
||||
password: callWebhookPass || null,
|
||||
},
|
||||
call_status_hook: {
|
||||
url: statusWebhook.trim(),
|
||||
method: statusWebhookMethod,
|
||||
username: statusWebhookUser.trim() || null,
|
||||
password: statusWebhookPass || null,
|
||||
},
|
||||
messaging_hook: {
|
||||
url: messagingWebhook.trim(),
|
||||
method: messagingWebhookMethod,
|
||||
username: messagingWebhookUser.trim() || null,
|
||||
password: messagingWebhookPass || null,
|
||||
},
|
||||
speech_synthesis_vendor: speechSynthesisVendor,
|
||||
speech_synthesis_language: speechSynthesisLanguage,
|
||||
speech_synthesis_voice: speechSynthesisVoice,
|
||||
speech_recognizer_vendor: speechRecognizerVendor,
|
||||
speech_recognizer_language: speechRecognizerLanguage,
|
||||
};
|
||||
|
||||
await axios({
|
||||
method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
if (props.type === 'setup') {
|
||||
isMounted = false;
|
||||
history.push('/configure-sip-trunk');
|
||||
} else {
|
||||
isMounted = false;
|
||||
history.push('/internal/applications');
|
||||
const dispatchMessage = props.type === 'add'
|
||||
? 'Application created successfully'
|
||||
: 'Application updated successfully';
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: dispatchMessage
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
isMounted = false;
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
setErrorMessage((err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.');
|
||||
console.log(err.response || err);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
showLoader
|
||||
? <Loader
|
||||
height={
|
||||
props.type === 'setup'
|
||||
? '505px'
|
||||
: props.type === 'edit'
|
||||
? '646px'
|
||||
: '611px'
|
||||
}
|
||||
/>
|
||||
: <Form
|
||||
large
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
|
||||
{props.type === 'edit' && (
|
||||
<React.Fragment>
|
||||
<Label>ApplicationSid</Label>
|
||||
<CopyableText text={applicationSid} textType="ApplicationSid" />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{(props.type === 'add' || props.type === 'edit') && (
|
||||
<React.Fragment>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="name"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="Application name"
|
||||
invalid={invalidName}
|
||||
autoFocus
|
||||
ref={refName}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="account">Account</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="account"
|
||||
id="account"
|
||||
value={accountSid}
|
||||
onChange={e => setAccountSid(e.target.value)}
|
||||
invalid={invalidAccount}
|
||||
ref={refAccount}
|
||||
>
|
||||
{(
|
||||
(accounts.length > 1) ||
|
||||
(props.type === 'edit' && accounts[0] && accountSid !== accounts[0].account_sid)
|
||||
) && (
|
||||
<option value="">
|
||||
-- Choose the account this application will be associated with --
|
||||
</option>
|
||||
)}
|
||||
{accounts.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
|
||||
<option
|
||||
key={a.account_sid}
|
||||
value={a.account_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<hr />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
<Label htmlFor="callWebhook">Calling Webhook</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="callWebhook"
|
||||
id="callWebhook"
|
||||
value={callWebhook}
|
||||
onChange={e => setCallWebhook(e.target.value)}
|
||||
placeholder="URL for your web application that will handle calls"
|
||||
invalid={invalidCallWebhook}
|
||||
ref={refCallWebhook}
|
||||
autoFocus={props.type === 'setup'}
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="callWebhookMethod"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="callWebhookMethod"
|
||||
id="callWebhookMethod"
|
||||
value={callWebhookMethod}
|
||||
onChange={e => setCallWebhookMethod(e.target.value)}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showCallAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="callWebhookUser">User</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="callWebhookUser"
|
||||
id="callWebhookUser"
|
||||
value={callWebhookUser}
|
||||
onChange={e => setCallWebhookUser(e.target.value)}
|
||||
placeholder="Optional"
|
||||
invalid={invalidCallWebhookUser}
|
||||
ref={refCallWebhookUser}
|
||||
/>
|
||||
<Label htmlFor="callWebhookPass" middle>Password</Label>
|
||||
<PasswordInput
|
||||
large={props.type === 'setup'}
|
||||
allowShowPassword
|
||||
name="callWebhookPass"
|
||||
id="callWebhookPass"
|
||||
password={callWebhookPass}
|
||||
setPassword={setCallWebhookPass}
|
||||
setErrorMessage={setErrorMessage}
|
||||
placeholder="Optional"
|
||||
invalid={invalidCallWebhookPass}
|
||||
ref={refCallWebhookPass}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleCallAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="statusWebhook">Call Status Webhook</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="statusWebhook"
|
||||
id="statusWebhook"
|
||||
value={statusWebhook}
|
||||
onChange={e => setStatusWebhook(e.target.value)}
|
||||
placeholder="URL for your web application that will receive call status"
|
||||
invalid={invalidStatusWebhook}
|
||||
ref={refStatusWebhook}
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="statusWebhookMethod"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="statusWebhookMethod"
|
||||
id="statusWebhookMethod"
|
||||
value={statusWebhookMethod}
|
||||
onChange={e => setStatusWebhookMethod(e.target.value)}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showStatusAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="statusWebhookUser">User</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="statusWebhookUser"
|
||||
id="statusWebhookUser"
|
||||
value={statusWebhookUser}
|
||||
onChange={e => setStatusWebhookUser(e.target.value)}
|
||||
placeholder="Optional"
|
||||
invalid={invalidStatusWebhookUser}
|
||||
ref={refStatusWebhookUser}
|
||||
/>
|
||||
<Label htmlFor="statusWebhookPass" middle>Password</Label>
|
||||
<PasswordInput
|
||||
large={props.type === 'setup'}
|
||||
allowShowPassword
|
||||
name="statusWebhookPass"
|
||||
id="statusWebhookPass"
|
||||
password={statusWebhookPass}
|
||||
setPassword={setStatusWebhookPass}
|
||||
setErrorMessage={setErrorMessage}
|
||||
placeholder="Optional"
|
||||
invalid={invalidStatusWebhookPass}
|
||||
ref={refStatusWebhookPass}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleStatusAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="messagingWebhook">Messaging Webhook</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="messagingWebhook"
|
||||
id="messagingWebhook"
|
||||
value={messagingWebhook}
|
||||
onChange={e => setMessagingWebhook(e.target.value)}
|
||||
placeholder="URL for your web application that will receive SMS"
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="messagingWebhookMethod"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="messagingWebhookMethod"
|
||||
id="messagingWebhookMethod"
|
||||
value={messagingWebhookMethod}
|
||||
onChange={e => setMessagingWebhookMethod(e.target.value)}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showMessagingAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="messagingWebhookUser">User</Label>
|
||||
<Input
|
||||
large={props.type === 'setup'}
|
||||
name="messagingWebhookUser"
|
||||
id="messagingWebhookUser"
|
||||
value={messagingWebhookUser}
|
||||
onChange={e => setMessagingWebhookUser(e.target.value)}
|
||||
placeholder="Optional"
|
||||
invalid={invalidMessagingWebhookUser}
|
||||
ref={refMessagingWebhookUser}
|
||||
/>
|
||||
<Label htmlFor="messagingWebhookPass" middle>Password</Label>
|
||||
<PasswordInput
|
||||
large={props.type === 'setup'}
|
||||
allowShowPassword
|
||||
name="messagingWebhookPass"
|
||||
id="messagingWebhookPass"
|
||||
password={messagingWebhookPass}
|
||||
setPassword={setMessagingWebhookPass}
|
||||
setErrorMessage={setErrorMessage}
|
||||
placeholder="Optional"
|
||||
invalid={invalidMessagingWebhookPass}
|
||||
ref={refMessagingWebhookPass}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleMessagingAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="speechSynthesisVendor">Speech Synthesis Vendor</Label>
|
||||
<InputGroup>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="speechSynthesisVendor"
|
||||
id="speechSynthesisVendor"
|
||||
value={speechSynthesisVendor}
|
||||
onChange={e => {
|
||||
setSpeechSynthesisVendor(e.target.value);
|
||||
|
||||
// When using Google and en-US, ensure "Standard-C" is used as default
|
||||
if (
|
||||
e.target.value === 'google' &&
|
||||
speechSynthesisLanguage === 'en-US'
|
||||
) {
|
||||
setSpeechSynthesisVoice('en-US-Standard-C');
|
||||
return;
|
||||
}
|
||||
|
||||
// Google and AWS have different voice lists. See if the newly
|
||||
// chosen vendor has the same language as what was already in use.
|
||||
let newLang = e.target.value === 'google'
|
||||
? SpeechSynthesisLanguageGoogle.find(l => (
|
||||
l.code === speechSynthesisLanguage
|
||||
))
|
||||
: e.target.value === 'microsoft'
|
||||
? SpeechSynthesisLanguageMicrosoft.find(l => (
|
||||
l.code === speechSynthesisLanguage
|
||||
))
|
||||
: e.target.value === 'wellsaid'
|
||||
? SpeechSynthesisLanguageWellSaid.find(l => (
|
||||
l.code === speechSynthesisLanguage
|
||||
))
|
||||
: SpeechSynthesisLanguageAws.find(l => (
|
||||
l.code === speechSynthesisLanguage
|
||||
));
|
||||
|
||||
// if not, use en-US as fallback.
|
||||
if (!newLang) {
|
||||
setSpeechSynthesisLanguage('en-US');
|
||||
|
||||
if (e.target.value === 'google') {
|
||||
setSpeechSynthesisVoice('en-US-Standard-C');
|
||||
return;
|
||||
}
|
||||
|
||||
newLang = e.target.value === 'aws'
|
||||
? SpeechSynthesisLanguageAws.find(l => (
|
||||
l.code === 'en-US'
|
||||
))
|
||||
: SpeechSynthesisLanguageMicrosoft.find(l => (
|
||||
l.code === 'en-US'
|
||||
));
|
||||
}
|
||||
|
||||
// Update state to reflect first voice option for language
|
||||
setSpeechSynthesisVoice(newLang.voices[0].value);
|
||||
}}
|
||||
>
|
||||
<option value="google">Google</option>
|
||||
<option value="aws">AWS</option>
|
||||
<option value="microsoft">Microsoft</option>
|
||||
<option value="wellsaid">WellSaid</option>
|
||||
</Select>
|
||||
<Label middle htmlFor="speechSynthesisLanguage">Language</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="speechSynthesisLanguage"
|
||||
id="speechSynthesisLanguage"
|
||||
value={speechSynthesisLanguage}
|
||||
onChange={e => {
|
||||
setSpeechSynthesisLanguage(e.target.value);
|
||||
|
||||
// When using Google and en-US, ensure "Standard-C" is used as default
|
||||
if (
|
||||
(speechSynthesisVendor === 'google')
|
||||
&& (e.target.value === 'en-US')
|
||||
) {
|
||||
setSpeechSynthesisVoice('en-US-Standard-C');
|
||||
return;
|
||||
}
|
||||
|
||||
const newLang = speechSynthesisVendor === 'google'
|
||||
? SpeechSynthesisLanguageGoogle.find(l => (
|
||||
l.code === e.target.value
|
||||
))
|
||||
: speechSynthesisVendor === 'microsoft'
|
||||
? SpeechSynthesisLanguageMicrosoft.find(l => (
|
||||
l.code === e.target.value
|
||||
))
|
||||
: speechSynthesisVendor === 'wellsaid'
|
||||
? SpeechSynthesisLanguageWellSaid.find(l => (
|
||||
l.code === e.target.value
|
||||
))
|
||||
: SpeechSynthesisLanguageAws.find(l => (
|
||||
l.code === e.target.value
|
||||
));
|
||||
|
||||
setSpeechSynthesisVoice(newLang.voices[0].value);
|
||||
|
||||
}}
|
||||
>
|
||||
{speechSynthesisVendor === 'google' ? (
|
||||
SpeechSynthesisLanguageGoogle.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
) : speechSynthesisVendor === 'microsoft' ? (
|
||||
SpeechSynthesisLanguageMicrosoft.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
) : speechSynthesisVendor === 'wellsaid' ? (
|
||||
SpeechSynthesisLanguageWellSaid.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
) : (
|
||||
SpeechSynthesisLanguageAws.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
<Label middle htmlFor="speechSynthesisVoice">Voice</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="speechSynthesisVoice"
|
||||
id="speechSynthesisVoice"
|
||||
value={speechSynthesisVoice}
|
||||
onChange={e => setSpeechSynthesisVoice(e.target.value)}
|
||||
>
|
||||
{speechSynthesisVendor === 'google' ? (
|
||||
SpeechSynthesisLanguageGoogle
|
||||
.filter(l => l.code === speechSynthesisLanguage)
|
||||
.map(m => m.voices.map(v => (
|
||||
<option key={v.value} value={v.value}>{v.name}</option>
|
||||
)))
|
||||
) : speechSynthesisVendor === 'microsoft' ? (
|
||||
SpeechSynthesisLanguageMicrosoft
|
||||
.filter(l => l.code === speechSynthesisLanguage)
|
||||
.map(m => m.voices.map(v => (
|
||||
<option key={v.value} value={v.value}>{v.name}</option>
|
||||
)))
|
||||
) : speechSynthesisVendor === 'wellsaid' ? (
|
||||
SpeechSynthesisLanguageWellSaid
|
||||
.filter(l => l.code === speechSynthesisLanguage)
|
||||
.map(m => m.voices.map(v => (
|
||||
<option key={v.value} value={v.value}>{v.name}</option>
|
||||
)))
|
||||
) : (
|
||||
SpeechSynthesisLanguageAws
|
||||
.filter(l => l.code === speechSynthesisLanguage)
|
||||
.map(m => m.voices.map(v => (
|
||||
<option key={v.value} value={v.value}>{v.name}</option>
|
||||
)))
|
||||
)}
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="speechRecognizerVendor">Speech Recognizer Vendor</Label>
|
||||
<InputGroup>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="speechRecognizerVendor"
|
||||
id="speechRecognizerVendor"
|
||||
value={speechRecognizerVendor}
|
||||
onChange={e => {
|
||||
setSpeechRecognizerVendor(e.target.value);
|
||||
|
||||
// Google and AWS have different language lists. If the newly chosen
|
||||
// vendor doesn't have the same language that was already in use,
|
||||
// select US English
|
||||
if ((
|
||||
e.target.value === 'google' &&
|
||||
!SpeechRecognizerLanguageGoogle.some(l => l.code === speechRecognizerLanguage)
|
||||
) || (
|
||||
e.target.value === 'aws' &&
|
||||
!SpeechRecognizerLanguageAws.some(l => l.code === speechRecognizerLanguage)
|
||||
)) {
|
||||
setSpeechRecognizerLanguage('en-US');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="google">Google</option>
|
||||
<option value="aws">AWS</option>
|
||||
<option value="microsoft">Microsoft</option>
|
||||
</Select>
|
||||
<Label middle htmlFor="speechRecognizerLanguage">Language</Label>
|
||||
<Select
|
||||
large={props.type === 'setup'}
|
||||
name="speechRecognizerLanguage"
|
||||
id="speechRecognizerLanguage"
|
||||
value={speechRecognizerLanguage}
|
||||
onChange={e => setSpeechRecognizerLanguage(e.target.value)}
|
||||
>
|
||||
{speechRecognizerVendor === 'google' ? (
|
||||
SpeechRecognizerLanguageGoogle.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
) : speechRecognizerVendor === 'microsoft' ? (
|
||||
SpeechRecognizerLanguageMicrosoft.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
) : (
|
||||
SpeechRecognizerLanguageAws.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
{props.type === 'edit' && (
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/applications');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
large={props.type === 'setup'}
|
||||
grid
|
||||
fullWidth={props.type === 'setup' || props.type === 'add'}
|
||||
>
|
||||
{props.type === 'setup'
|
||||
? 'Save and Continue'
|
||||
: props.type === 'add'
|
||||
? 'Add Application'
|
||||
: 'Save'
|
||||
}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplicationForm;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,389 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
import Select from '../elements/Select';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Loader from '../blocks/Loader';
|
||||
import Button from '../elements/Button';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const MsTeamsTenantForm = props => {
|
||||
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
// Refs
|
||||
const refDomainName = useRef(null);
|
||||
const refAccount = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ domainName, setDomainName ] = useState('');
|
||||
const [ account, setAccount ] = useState('');
|
||||
const [ application, setApplication ] = useState('');
|
||||
|
||||
// Select list values
|
||||
const [ accountValues, setAccountValues ] = useState('');
|
||||
const [ applicationValues, setApplicationValues ] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidDomainName, setInvalidDomainName ] = useState(false);
|
||||
const [ invalidAccount, setInvalidAccount ] = useState(false);
|
||||
|
||||
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
|
||||
const [ tenants, setTenants ] = useState('');
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
// Check if user is logged in
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tenantsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/MicrosoftTeamsTenants',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const promises = [
|
||||
tenantsPromise,
|
||||
accountsPromise,
|
||||
applicationsPromise,
|
||||
];
|
||||
|
||||
if (props.type === 'add') {
|
||||
promises.push(axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/ServiceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
const promiseAllValues = await Promise.all(promises);
|
||||
|
||||
const tenants = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[1].data;
|
||||
const applications = promiseAllValues[2].data;
|
||||
|
||||
setTenants(tenants);
|
||||
setAccountValues(accounts);
|
||||
setApplicationValues(applications);
|
||||
|
||||
if (props.type === 'add') {
|
||||
const serviceProviders = promiseAllValues[3].data;
|
||||
setServiceProviderSid(serviceProviders[0].service_provider_sid);
|
||||
}
|
||||
|
||||
if (!accounts.length) {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must create an account before you can create a Microsoft Teams Tenant.',
|
||||
});
|
||||
history.push('/internal/accounts');
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.type === 'edit') {
|
||||
const tenantData = tenants.filter(tenant => {
|
||||
return tenant.ms_teams_tenant_sid === props.ms_teams_tenant_sid;
|
||||
});
|
||||
|
||||
if (!tenantData.length) {
|
||||
history.push('/internal/ms-teams-tenants');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'That tenant does not exist.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setDomainName (( tenantData[0] && tenantData[0].tenant_fqdn ) || '');
|
||||
setAccount (( tenantData[0] && tenantData[0].account_sid ) || '');
|
||||
setApplication(( tenantData[0] && tenantData[0].application_sid) || '');
|
||||
}
|
||||
|
||||
if (props.type === 'add' && accounts.length === 1) {
|
||||
setAccount(accounts[0].account_sid);
|
||||
}
|
||||
|
||||
setShowLoader(false);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async e => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidDomainName(false);
|
||||
setInvalidAccount(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if (!domainName) {
|
||||
errorMessages.push('Please provide a domain name');
|
||||
setInvalidDomainName(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refDomainName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
// check if domain name is already in use
|
||||
for (const tenant of tenants) {
|
||||
if (tenant.ms_teams_tenant_sid === props.ms_teams_tenant_sid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tenant.tenant_fqdn === domainName) {
|
||||
errorMessages.push(
|
||||
'The domain name you have entered is already in use.'
|
||||
);
|
||||
setInvalidDomainName(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refDomainName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!account) {
|
||||
errorMessages.push('Please select an account');
|
||||
setInvalidAccount(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refAccount.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Submit
|
||||
//=============================================================================
|
||||
const method = props.type === 'add'
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
const url = props.type === 'add'
|
||||
? `/MicrosoftTeamsTenants`
|
||||
: `/MicrosoftTeamsTenants/${props.ms_teams_tenant_sid}`;
|
||||
|
||||
const data = {
|
||||
tenant_fqdn: domainName.trim(),
|
||||
account_sid: account,
|
||||
application_sid: application || null,
|
||||
};
|
||||
|
||||
if (props.type === 'add') {
|
||||
data.service_provider_sid = serviceProviderSid;
|
||||
}
|
||||
|
||||
await axios({
|
||||
method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data
|
||||
});
|
||||
|
||||
const dispatchMessage = props.type === 'add'
|
||||
? 'Tenant created successfully'
|
||||
: 'Tenant updated successfully';
|
||||
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: dispatchMessage
|
||||
});
|
||||
|
||||
isMounted = false;
|
||||
history.push('/internal/ms-teams-tenants');
|
||||
} catch (err) {
|
||||
setErrorMessage(
|
||||
(err.response && err.response.data && err.response.data.msg) ||
|
||||
'Something went wrong, please try again.'
|
||||
);
|
||||
console.log(err.response || err);
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
showLoader
|
||||
? <Loader height={'258px'}/>
|
||||
: <Form
|
||||
large
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="domainName">Domain Name</Label>
|
||||
<Input
|
||||
name="domainName"
|
||||
id="domainName"
|
||||
value={domainName}
|
||||
onChange={e => setDomainName(e.target.value)}
|
||||
placeholder="Tenant's fully qualified domain name"
|
||||
invalid={invalidDomainName}
|
||||
autoFocus
|
||||
ref={refDomainName}
|
||||
/>
|
||||
|
||||
<Label htmlFor="account">Account</Label>
|
||||
<Select
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={e => setAccount(e.target.value)}
|
||||
invalid={invalidAccount}
|
||||
ref={refAccount}
|
||||
>
|
||||
{(
|
||||
(accountValues.length > 1) ||
|
||||
(props.type === 'edit' && account !== accountValues[0].account_sid)
|
||||
) && (
|
||||
<option value="">-- Choose the account that this tenant should be associated with --</option>
|
||||
)}
|
||||
{accountValues.map(a => (
|
||||
<option
|
||||
key={a.account_sid}
|
||||
value={a.account_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Label htmlFor="application">Application</Label>
|
||||
<Select
|
||||
name="application"
|
||||
id="application"
|
||||
value={application}
|
||||
onChange={e => setApplication(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
{props.type === 'add'
|
||||
? '-- OPTIONAL: Choose the application that this tenant should be associated with --'
|
||||
: '-- NONE --'
|
||||
}
|
||||
</option>
|
||||
{applicationValues.map(a => (
|
||||
<option
|
||||
key={a.application_sid}
|
||||
value={a.application_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
{props.type === 'edit' && (
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/ms-teams-tenants');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
grid
|
||||
fullWidth={props.type === 'add'}
|
||||
>
|
||||
{props.type === 'add'
|
||||
? 'Add Microsoft Teams Tenant'
|
||||
: 'Save'
|
||||
}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default MsTeamsTenantForm;
|
||||
@@ -1,457 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
import Select from '../elements/Select';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Loader from '../blocks/Loader';
|
||||
import Button from '../elements/Button';
|
||||
import phoneNumberFormat from '../../helpers/phoneNumberFormat';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const PhoneNumberForm = props => {
|
||||
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Refs
|
||||
const refPhoneNumber = useRef(null);
|
||||
const refSipTrunk = useRef(null);
|
||||
const refAccount = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ phoneNumber, setPhoneNumber ] = useState('');
|
||||
const [ sipTrunk, setSipTrunk ] = useState('');
|
||||
const [ account, setAccount ] = useState('');
|
||||
const [ application, setApplication ] = useState('');
|
||||
|
||||
// Select list values
|
||||
const [ sipTrunkValues, setSipTrunkValues ] = useState('');
|
||||
const [ accountValues, setAccountValues ] = useState('');
|
||||
const [ applicationValues, setApplicationValues ] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidPhoneNumber, setInvalidPhoneNumber ] = useState(false);
|
||||
const [ invalidSipTrunk, setInvalidSipTrunk ] = useState(false);
|
||||
const [ invalidAccount, setInvalidAccount ] = useState(false);
|
||||
|
||||
const [ phoneNumbers, setPhoneNumbers ] = useState('');
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
// Check if user is logged in
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sipTrunksPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `ServiceProviders/${currentServiceProvider}/VoipCarriers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `ServiceProviders/${currentServiceProvider}/Applications`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const phoneNumbersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `ServiceProviders/${currentServiceProvider}/PhoneNumbers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const promises = [
|
||||
sipTrunksPromise,
|
||||
accountsPromise,
|
||||
applicationsPromise,
|
||||
phoneNumbersPromise,
|
||||
];
|
||||
|
||||
const promiseAllValues = await Promise.all(promises);
|
||||
|
||||
const sipTrunks = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[1].data;
|
||||
const applications = promiseAllValues[2].data;
|
||||
const phoneNumbers = promiseAllValues[3].data;
|
||||
|
||||
setSipTrunkValues(sipTrunks);
|
||||
setAccountValues(accounts);
|
||||
setApplicationValues(applications);
|
||||
setPhoneNumbers(phoneNumbers);
|
||||
|
||||
if (!accounts.length) {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must create an account before you can create a phone number.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!sipTrunks.length) {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must create a SIP trunk before you can create a phone number.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!accounts.length) {
|
||||
history.push('/internal/accounts');
|
||||
return;
|
||||
} else if (!sipTrunks.length) {
|
||||
history.push('/internal/carriers');
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.type === 'edit') {
|
||||
const phoneNumberData = promiseAllValues[3] && promiseAllValues[3].data.filter(p => {
|
||||
return p.phone_number_sid === props.phone_number_sid;
|
||||
});
|
||||
|
||||
if (!phoneNumberData.length) {
|
||||
history.push('/internal/phone-numbers');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'That phone number does not exist.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setPhoneNumber (( phoneNumberData[0] && phoneNumberFormat(phoneNumberData[0].number)) || '');
|
||||
setSipTrunk (( phoneNumberData[0] && phoneNumberData[0].voip_carrier_sid ) || '');
|
||||
setAccount (( phoneNumberData[0] && phoneNumberData[0].account_sid ) || '');
|
||||
setApplication (( phoneNumberData[0] && phoneNumberData[0].application_sid ) || '');
|
||||
}
|
||||
|
||||
if (props.type === 'add') {
|
||||
if (sipTrunks.length === 1) { setSipTrunk(sipTrunks[0].voip_carrier_sid); }
|
||||
if ( accounts.length === 1) { setAccount( accounts[0].account_sid ); }
|
||||
}
|
||||
|
||||
setShowLoader(false);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async e => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidPhoneNumber(false);
|
||||
setInvalidSipTrunk(false);
|
||||
setInvalidAccount(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if (!phoneNumber) {
|
||||
errorMessages.push('Please provide a phone number');
|
||||
setInvalidPhoneNumber(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refPhoneNumber.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
// check if phone number is already in use
|
||||
for (const num of phoneNumbers) {
|
||||
if (num.phone_number_sid === props.phone_number_sid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (num.number === phoneNumber) {
|
||||
errorMessages.push(
|
||||
'The phone number you have entered is already in use.'
|
||||
);
|
||||
setInvalidPhoneNumber(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refPhoneNumber.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!sipTrunk) {
|
||||
errorMessages.push('Please select a SIP trunk');
|
||||
setInvalidSipTrunk(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refSipTrunk.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
errorMessages.push('Please select an account');
|
||||
setInvalidAccount(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refAccount.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Submit
|
||||
//=============================================================================
|
||||
const method = props.type === 'add'
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
const url = props.type === 'add'
|
||||
? `/PhoneNumbers`
|
||||
: `/PhoneNumbers/${props.phone_number_sid}`;
|
||||
|
||||
const data = {
|
||||
account_sid: account,
|
||||
application_sid: application || null,
|
||||
};
|
||||
|
||||
const cleanedUpNumber = phoneNumber.trim().replace(/[\s-()+]/g,'');
|
||||
|
||||
if (props.type === 'add') {
|
||||
data.number = cleanedUpNumber;
|
||||
data.voip_carrier_sid = sipTrunk;
|
||||
}
|
||||
|
||||
await axios({
|
||||
method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data
|
||||
});
|
||||
|
||||
const dispatchMessage = props.type === 'add'
|
||||
? 'Phone number created successfully'
|
||||
: 'Phone number updated successfully';
|
||||
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: dispatchMessage
|
||||
});
|
||||
|
||||
isMounted = false;
|
||||
history.push('/internal/phone-numbers');
|
||||
} catch (err) {
|
||||
setErrorMessage(
|
||||
(err.response && err.response.data && err.response.data.msg) ||
|
||||
'Something went wrong, please try again.'
|
||||
);
|
||||
console.log(err.response || err);
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
showLoader
|
||||
? <Loader height={'310px'}/>
|
||||
: <Form
|
||||
large
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="phoneNumber">Phone Number</Label>
|
||||
<Input
|
||||
name="phoneNumber"
|
||||
id="phoneNumber"
|
||||
value={phoneNumber}
|
||||
onChange={e => setPhoneNumber(e.target.value)}
|
||||
placeholder="Phone number that will be sending calls to this service"
|
||||
invalid={invalidPhoneNumber}
|
||||
autoFocus
|
||||
ref={refPhoneNumber}
|
||||
disabled={props.type === 'edit'}
|
||||
/>
|
||||
|
||||
<Label htmlFor="sipTrunk">SIP Trunk</Label>
|
||||
<Select
|
||||
name="sipTrunk"
|
||||
id="sipTrunk"
|
||||
value={sipTrunk}
|
||||
onChange={e => setSipTrunk(e.target.value)}
|
||||
invalid={invalidSipTrunk}
|
||||
ref={refSipTrunk}
|
||||
disabled={props.type === 'edit'}
|
||||
>
|
||||
{(
|
||||
(sipTrunkValues.length > 1) ||
|
||||
(props.type === 'edit' && sipTrunk !== sipTrunkValues[0].voip_carrier_sid)
|
||||
) && (
|
||||
<option value="">-- Choose the SIP trunk that this phone number belongs to --</option>
|
||||
)}
|
||||
{sipTrunkValues.map(s => (
|
||||
<option
|
||||
key={s.voip_carrier_sid}
|
||||
value={s.voip_carrier_sid}
|
||||
>
|
||||
{s.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Label htmlFor="account">Account</Label>
|
||||
<Select
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={(e) => {
|
||||
setAccount(e.target.value);
|
||||
setApplication('');
|
||||
}}
|
||||
invalid={invalidAccount}
|
||||
ref={refAccount}
|
||||
>
|
||||
{(
|
||||
(accountValues.length > 1) ||
|
||||
(props.type === 'edit' && account !== accountValues[0].account_sid)
|
||||
) && (
|
||||
<option value="">-- Choose the account that this phone number should be associated with --</option>
|
||||
)}
|
||||
{accountValues.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
|
||||
<option
|
||||
key={a.account_sid}
|
||||
value={a.account_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Label htmlFor="application">Application</Label>
|
||||
<Select
|
||||
name="application"
|
||||
id="application"
|
||||
value={application}
|
||||
onChange={e => setApplication(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
{props.type === 'add'
|
||||
? '-- OPTIONAL: Choose the application that will receive calls from this number --'
|
||||
: '-- NONE --'
|
||||
}
|
||||
</option>
|
||||
{applicationValues.filter((a) => {
|
||||
// Map an application to a service provider through it's account_sid
|
||||
const acct = accountValues.find(ac => a.account_sid === ac.account_sid);
|
||||
|
||||
if (account) {
|
||||
return a.account_sid === account;
|
||||
}
|
||||
|
||||
return acct.service_provider_sid === currentServiceProvider;
|
||||
}).map(a => (
|
||||
<option
|
||||
key={a.application_sid}
|
||||
value={a.application_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
{props.type === 'edit' && (
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/phone-numbers');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
grid
|
||||
fullWidth={props.type === 'add'}
|
||||
>
|
||||
{props.type === 'add'
|
||||
? 'Add Phone Number'
|
||||
: 'Save'
|
||||
}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhoneNumberForm;
|
||||
@@ -1,370 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import styled from 'styled-components';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import { ShowMsTeamsDispatchContext } from '../../contexts/ShowMsTeamsContext';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
import Checkbox from '../elements/Checkbox';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Button from '../elements/Button';
|
||||
import Loader from '../blocks/Loader';
|
||||
import Modal from '../blocks/Modal';
|
||||
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
import handleErrors from "../../helpers/handleErrors";
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const Td = styled.td`
|
||||
padding: 0.5rem 0;
|
||||
&:first-child {
|
||||
font-weight: 500;
|
||||
padding-right: 1.5rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
& ul {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const SettingsForm = () => {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const refreshMsTeamsData = useContext(ShowMsTeamsDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Refs
|
||||
const refEnableMsTeams = useRef(null);
|
||||
const refSbcDomainName = useRef(null);
|
||||
const refServiceProviderName = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [enableMsTeams, setEnableMsTeams] = useState(false);
|
||||
const [sbcDomainName, setSbcDomainName] = useState('');
|
||||
const [serviceProviderName, setServiceProviderName] = useState('');
|
||||
|
||||
// For when user has data in sbcDomainName and then taps the checkbox to disable MsTeams
|
||||
const [savedSbcDomainName, setSavedSbcDomainName] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [invalidEnableMsTeams, setInvalidEnableMsTeams] = useState(false);
|
||||
const [invalidSbcDomainName, setInvalidSbcDomainName] = useState(false);
|
||||
const [invalidServiceProviderName, setInvalidServiceProviderName] = useState(false);
|
||||
|
||||
const [showLoader, setShowLoader] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [serviceProviderSid, setServiceProviderSid] = useState('');
|
||||
const [serviceProviders, setServiceProviders] = useState([]);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const getSettingsData = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceProvidersResponse = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const sps = serviceProvidersResponse.data;
|
||||
const sp = sps.find(s => s.service_provider_sid === currentServiceProvider);
|
||||
|
||||
setServiceProviders(sps);
|
||||
setServiceProviderName(sp.name || '');
|
||||
setServiceProviderSid(sp.service_provider_sid || '');
|
||||
setEnableMsTeams(sp.ms_teams_fqdn ? true : false);
|
||||
setSbcDomainName(sp.ms_teams_fqdn || '');
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (currentServiceProvider) {
|
||||
getSettingsData();
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
const toggleMsTeams = (e) => {
|
||||
if (!e.target.checked && sbcDomainName) {
|
||||
setSavedSbcDomainName(sbcDomainName);
|
||||
setSbcDomainName('');
|
||||
}
|
||||
if (e.target.checked && savedSbcDomainName) {
|
||||
setSbcDomainName(savedSbcDomainName);
|
||||
setSavedSbcDomainName('');
|
||||
}
|
||||
setEnableMsTeams(e.target.checked);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setErrorMessage('');
|
||||
|
||||
axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${serviceProviderSid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
setConfirmDelete(false);
|
||||
setErrorMessage('');
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: 'Service Provider Deleted'
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.response.data.msg);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
//=============================================================================
|
||||
// reset
|
||||
//=============================================================================
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidEnableMsTeams(false);
|
||||
setInvalidSbcDomainName(false);
|
||||
setInvalidServiceProviderName(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
//=============================================================================
|
||||
// data checks
|
||||
//=============================================================================
|
||||
if (!serviceProviderName.trim()) {
|
||||
errorMessages.push(
|
||||
'Please enter a Service Provider Name.'
|
||||
);
|
||||
setInvalidServiceProviderName(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refServiceProviderName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
if (enableMsTeams && !sbcDomainName) {
|
||||
errorMessages.push(
|
||||
'You must provide an SBC Domain Name in order to enable Microsoft Teams Direct Routing'
|
||||
);
|
||||
setInvalidSbcDomainName(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refSbcDomainName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!enableMsTeams && sbcDomainName) {
|
||||
errorMessages.push(
|
||||
'You must check "Enable Microsoft Teams Direct Routing" to enable this feature, or remove the SBC Domain Name provided'
|
||||
);
|
||||
setInvalidEnableMsTeams(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refEnableMsTeams.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// submit data
|
||||
//=============================================================================
|
||||
const data = {
|
||||
ms_teams_fqdn: sbcDomainName.trim() || null,
|
||||
name: serviceProviderName.trim(),
|
||||
};
|
||||
|
||||
await axios({
|
||||
method: 'put',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${serviceProviderSid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
refreshMsTeamsData();
|
||||
|
||||
//=============================================================================
|
||||
// redirect
|
||||
//=============================================================================
|
||||
isMounted = false;
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: 'Settings updated'
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
isMounted = false;
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
setErrorMessage((err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.');
|
||||
console.log(err.response || err);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
showLoader
|
||||
? <Loader height="365px" />
|
||||
: (
|
||||
<>
|
||||
<Form
|
||||
large
|
||||
wideLabel
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="serviceProviderName">Service Provider Name</Label>
|
||||
<Input
|
||||
name="serviceProviderName"
|
||||
id="serviceProviderName"
|
||||
value={serviceProviderName}
|
||||
onChange={e => setServiceProviderName(e.target.value)}
|
||||
invalid={invalidServiceProviderName}
|
||||
ref={refServiceProviderName}
|
||||
/>
|
||||
<div>{/* needed for CSS grid layout */}</div>
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
id="enableMsTeams"
|
||||
label="Enable Microsoft Teams Direct Routing"
|
||||
checked={enableMsTeams}
|
||||
onChange={toggleMsTeams}
|
||||
invalid={invalidEnableMsTeams}
|
||||
ref={refEnableMsTeams}
|
||||
/>
|
||||
|
||||
<Label htmlFor="sbcDomainName">SBC Domain Name</Label>
|
||||
<Input
|
||||
name="sbcDomainName"
|
||||
id="sbcDomainName"
|
||||
value={sbcDomainName}
|
||||
onChange={e => setSbcDomainName(e.target.value)}
|
||||
placeholder="Fully qualified domain name used for Microsoft Teams"
|
||||
invalid={invalidSbcDomainName}
|
||||
autoFocus={enableMsTeams}
|
||||
ref={refSbcDomainName}
|
||||
disabled={!enableMsTeams}
|
||||
title={(!enableMsTeams && "You must enable Microsoft Teams Direct Routing in order to provide an SBC Domain Name") || ""}
|
||||
/>
|
||||
|
||||
{errorMessage && !confirmDelete && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<InputGroup flexEnd spaced>
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{serviceProviders.length > 1 && (
|
||||
<Button
|
||||
grid
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => setConfirmDelete(true)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<Button grid>Save</Button>
|
||||
</InputGroup>
|
||||
</Form>
|
||||
|
||||
{confirmDelete && serviceProviders.length > 1 && (
|
||||
<Modal
|
||||
title="Are you sure you want to delete the Service Provider?"
|
||||
loader={false}
|
||||
content={
|
||||
<div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<Td>Service Provider Name:</Td>
|
||||
<Td>{serviceProviderName}</Td>
|
||||
</tr>
|
||||
<tr>
|
||||
<Td>SBC Domain Name:</Td>
|
||||
<Td>{sbcDomainName || '[none]'}</Td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{errorMessage && (
|
||||
<FormError message={errorMessage} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
handleCancel={() => {
|
||||
setConfirmDelete(false);
|
||||
setErrorMessage('');
|
||||
}}
|
||||
handleSubmit={handleDelete}
|
||||
actionText="Delete"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsForm;
|
||||
@@ -1,710 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import styled from "styled-components/macro";
|
||||
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import handleErrors from '../../helpers/handleErrors';
|
||||
import Form from '../elements/Form';
|
||||
import Input from '../elements/Input';
|
||||
import Label from '../elements/Label';
|
||||
import Select from '../elements/Select';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import Checkbox from '../elements/Checkbox';
|
||||
import FileUpload from '../elements/FileUpload';
|
||||
import Code from '../elements/Code';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Button from '../elements/Button';
|
||||
import Loader from '../blocks/Loader';
|
||||
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
|
||||
|
||||
import AwsRegions from '../../data/AwsRegions';
|
||||
import MicrosoftAzureRegions from '../../data/MicrosoftAzureRegions';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const StyledButtonGroup = styled(InputGroup)`
|
||||
@media (max-width: 576.98px) {
|
||||
width: 100%;
|
||||
|
||||
& > *:first-child {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& > *:last-child {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
${props => props.type === 'add' ? `
|
||||
@media (max-width: 459.98px) {
|
||||
flex-direction: column;
|
||||
|
||||
& > *:first-child {
|
||||
width: 100%;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
}
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
const SpeechServicesAddEdit = (props) => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const jwt = localStorage.getItem('token');
|
||||
|
||||
let { speech_service_sid } = useParams();
|
||||
const type = speech_service_sid ? 'edit' : 'add';
|
||||
|
||||
// Refs
|
||||
const refVendorGoogle = useRef(null);
|
||||
const refVendorAws = useRef(null);
|
||||
const refVendorMs = useRef(null);
|
||||
const refVendorWellSaid = useRef(null);
|
||||
const refAccessKeyId = useRef(null);
|
||||
const refSecretAccessKey = useRef(null);
|
||||
const refUseForTts = useRef(null);
|
||||
const refUseForStt = useRef(null);
|
||||
const refApiKey = useRef(null);
|
||||
const refRegion = useRef(null);
|
||||
const refAwsRegion = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [vendor, setVendor] = useState('');
|
||||
const [serviceKey, setServiceKey] = useState('');
|
||||
const [displayedServiceKey, setDisplayedServiceKey] = useState('');
|
||||
const [accessKeyId, setAccessKeyId] = useState('');
|
||||
const [secretAccessKey, setSecretAccessKey] = useState('');
|
||||
const [useForTts, setUseForTts] = useState(false);
|
||||
const [useForStt, setUseForStt] = useState(false);
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
const [accountSid, setAccountSid] = useState('');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [region, setRegion] = useState('');
|
||||
const [awsregion, setAwsRegion] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [invalidVendorGoogle, setInvalidVendorGoogle] = useState(false);
|
||||
const [invalidVendorAws, setInvalidVendorAws] = useState(false);
|
||||
const [invalidVendorMs, setInvalidVendorMs] = useState(false);
|
||||
const [invalidVendorWellSaid, setInvalidVendorWellSaid] = useState(false);
|
||||
const [invalidAccessKeyId, setInvalidAccessKeyId] = useState(false);
|
||||
const [invalidSecretAccessKey, setInvalidSecretAccessKey] = useState(false);
|
||||
const [invalidUseForTts, setInvalidUseForTts] = useState(false);
|
||||
const [invalidUseForStt, setInvalidUseForStt] = useState(false);
|
||||
const [invalidApiKey, setInvalidApiKey] = useState(false);
|
||||
const [invalidRegion, setInvalidRegion] = useState(false);
|
||||
const [invalidAwsRegion, setInvalidAwsRegion] = useState(false);
|
||||
|
||||
const [originalTtsValue, setOriginalTtsValue] = useState(null);
|
||||
const [originalSttValue, setOriginalSttValue] = useState(null);
|
||||
|
||||
const [validServiceKey, setValidServiceKey] = useState(false);
|
||||
|
||||
const [showLoader, setShowLoader] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
const accountsResponse = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccounts(accountsResponse.data);
|
||||
|
||||
if (type === 'edit') {
|
||||
const speechCredential = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
let serviceKeyJson = '';
|
||||
let displayedServiceKeyJson = '';
|
||||
|
||||
try {
|
||||
serviceKeyJson = JSON.parse(speechCredential.data.service_key);
|
||||
displayedServiceKeyJson = JSON.stringify(serviceKeyJson, null, 2);
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
setAccountSid(speechCredential.data.account_sid || '');
|
||||
setVendor(speechCredential.data.vendor || undefined);
|
||||
setServiceKey(serviceKeyJson || '');
|
||||
setDisplayedServiceKey(displayedServiceKeyJson || '');
|
||||
setAccessKeyId(speechCredential.data.access_key_id || '');
|
||||
setSecretAccessKey(speechCredential.data.secret_access_key || '');
|
||||
setApiKey(speechCredential.data.api_key || '');
|
||||
setRegion(speechCredential.data.region || '');
|
||||
setAwsRegion(speechCredential.data.aws_region || '');
|
||||
setUseForTts(speechCredential.data.use_for_tts || false);
|
||||
setUseForStt(speechCredential.data.use_for_stt || false);
|
||||
setOriginalTtsValue(speechCredential.data.use_for_tts || false);
|
||||
setOriginalSttValue(speechCredential.data.use_for_stt || false);
|
||||
}
|
||||
setShowLoader(false);
|
||||
} catch (err) {
|
||||
isMounted = false;
|
||||
handleErrors({
|
||||
err,
|
||||
history,
|
||||
dispatch,
|
||||
redirect: '/internal/speech-services',
|
||||
fallbackMessage: 'That speech service does not exist',
|
||||
preferFallback: true,
|
||||
});
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const handleFileUpload = async (e) => {
|
||||
setErrorMessage('');
|
||||
setServiceKey('');
|
||||
setDisplayedServiceKey('');
|
||||
|
||||
const file = e.target.files[0];
|
||||
|
||||
if (!file) {
|
||||
setValidServiceKey(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileAsText = await file.text();
|
||||
|
||||
try {
|
||||
const fileJson = JSON.parse(fileAsText);
|
||||
|
||||
if (!fileJson.client_email || !fileJson.private_key) {
|
||||
setValidServiceKey(false);
|
||||
setErrorMessage('Invalid service key file, missing data.');
|
||||
return;
|
||||
}
|
||||
|
||||
setValidServiceKey(true);
|
||||
setServiceKey(fileJson);
|
||||
setDisplayedServiceKey(JSON.stringify(fileJson, null, 2));
|
||||
|
||||
} catch (err) {
|
||||
setValidServiceKey(false);
|
||||
setErrorMessage('Invalid service key file, could not parse as JSON.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidVendorGoogle(false);
|
||||
setInvalidVendorAws(false);
|
||||
setInvalidVendorMs(false);
|
||||
setInvalidVendorWellSaid(false);
|
||||
setInvalidAccessKeyId(false);
|
||||
setInvalidSecretAccessKey(false);
|
||||
setInvalidUseForTts(false);
|
||||
setInvalidUseForStt(false);
|
||||
setInvalidApiKey(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if (!vendor) {
|
||||
errorMessages.push('Please select a vendor.');
|
||||
setInvalidVendorGoogle(true);
|
||||
setInvalidVendorAws(true);
|
||||
setInvalidVendorMs(true);
|
||||
setInvalidVendorWellSaid(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refVendorGoogle.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'google' && !serviceKey) {
|
||||
errorMessages.push('Please upload a service key file.');
|
||||
}
|
||||
|
||||
if (vendor === 'aws' && !accessKeyId) {
|
||||
errorMessages.push('Please provide an access key ID.');
|
||||
setInvalidAccessKeyId(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refAccessKeyId.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'aws' && !secretAccessKey) {
|
||||
errorMessages.push('Please provide a secret access key.');
|
||||
setInvalidSecretAccessKey(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refSecretAccessKey.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'aws' && !awsregion) {
|
||||
errorMessages.push('Please select a region.');
|
||||
setInvalidAwsRegion(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refAwsRegion.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'microsoft' && !apiKey) {
|
||||
errorMessages.push('Please provide an API key.');
|
||||
setInvalidApiKey(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refApiKey.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'microsoft' && !region) {
|
||||
errorMessages.push('Please select a region.');
|
||||
setInvalidRegion(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refRegion.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vendor === 'wellsaid' && !apiKey) {
|
||||
errorMessages.push('Please provide an API key.');
|
||||
setInvalidApiKey(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refApiKey.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//===============================================
|
||||
// Submit
|
||||
//===============================================
|
||||
const method = type === 'add'
|
||||
? 'post'
|
||||
: 'put';
|
||||
|
||||
const url = type === 'add'
|
||||
? `/ServiceProviders/${currentServiceProvider}/SpeechCredentials`
|
||||
: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}`;
|
||||
|
||||
const postResults = await axios({
|
||||
method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
data: {
|
||||
vendor,
|
||||
service_key: vendor === 'google' ? JSON.stringify(serviceKey) : null,
|
||||
access_key_id: vendor === 'aws' ? accessKeyId : null,
|
||||
secret_access_key: vendor === 'aws' ? secretAccessKey : null,
|
||||
aws_region: vendor === 'aws' ? awsregion : null,
|
||||
api_key: ['microsoft', 'wellsaid'].includes(vendor) ? apiKey : null,
|
||||
region: vendor === 'microsoft' ? region : null,
|
||||
use_for_tts: useForTts,
|
||||
use_for_stt: useForStt,
|
||||
service_provider_sid: accountSid ? null : currentServiceProvider,
|
||||
account_sid: accountSid || null,
|
||||
}
|
||||
});
|
||||
|
||||
if (type === 'add') {
|
||||
if (!postResults.data || !postResults.data.sid) {
|
||||
throw new Error('Error retrieving response data');
|
||||
}
|
||||
|
||||
speech_service_sid = postResults.data.sid;
|
||||
}
|
||||
|
||||
//===============================================
|
||||
// Test speech credentials
|
||||
//===============================================
|
||||
if (useForTts || useForStt) {
|
||||
const testResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}/test`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (useForTts && testResults.data.tts.status === 'not tested') {
|
||||
errorMessages.push('text-to-speech was not tested, please try again.');
|
||||
}
|
||||
|
||||
if (useForStt && testResults.data.stt.status === 'not tested') {
|
||||
errorMessages.push('speech-to-text was not tested, please try again.');
|
||||
}
|
||||
|
||||
const ttsReason = (useForTts && testResults.data.tts.status === 'fail')
|
||||
? testResults.data.tts.reason
|
||||
: null;
|
||||
|
||||
const sttReason = (useForStt && testResults.data.stt.status === 'fail')
|
||||
? testResults.data.stt.reason
|
||||
: null;
|
||||
|
||||
if (ttsReason && (ttsReason === sttReason)) {
|
||||
errorMessages.push(ttsReason);
|
||||
} else {
|
||||
if (ttsReason) {
|
||||
errorMessages.push(`Text-to-speech error: ${ttsReason}`);
|
||||
}
|
||||
|
||||
if (sttReason) {
|
||||
errorMessages.push(`Speech-to-text error: ${sttReason}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
}
|
||||
|
||||
if (errorMessages.length) {
|
||||
if (type === 'add') {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speech_service_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'edit') {
|
||||
await axios({
|
||||
method,
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
data: {
|
||||
use_for_tts: originalTtsValue,
|
||||
use_for_stt: originalSttValue,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//===============================================
|
||||
// If successful, go to speech services
|
||||
//===============================================
|
||||
isMounted = false;
|
||||
if (accountSid) {
|
||||
history.push(`/internal/speech-services?account_sid=${accountSid}`);
|
||||
} else {
|
||||
history.push('/internal/speech-services');
|
||||
}
|
||||
const dispatchMessage = type === 'add'
|
||||
? 'Speech service created successfully'
|
||||
: 'Speech service updated successfully';
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: dispatchMessage
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
isMounted = false;
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
setErrorMessage(
|
||||
(err.response && err.response.data && err.response.data.msg) ||
|
||||
err.message || 'Something went wrong, please try again.'
|
||||
);
|
||||
console.error(err.response || err);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setShowLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
showLoader ? (
|
||||
<Loader height={props.type === 'add' ? '424px' : '376px'} />
|
||||
) : (
|
||||
<Form
|
||||
large
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Label htmlFor="vendor">Vendor</Label>
|
||||
<Select
|
||||
name="vendor"
|
||||
id="vendor"
|
||||
value={vendor}
|
||||
onChange={e => setVendor(e.target.value)}
|
||||
{...[refVendorGoogle, refVendorAws, refVendorMs, refVendorWellSaid]}
|
||||
invalid={[invalidVendorGoogle, invalidVendorAws, invalidVendorMs, invalidVendorWellSaid].includes(true)}
|
||||
>
|
||||
<option value="">
|
||||
Select a Vendor
|
||||
</option>
|
||||
<option value="google">Google</option>
|
||||
<option value="aws">AWS</option>
|
||||
<option value="microsoft">Microsoft</option>
|
||||
<option value="wellsaid">WellSaid</option>
|
||||
</Select>
|
||||
|
||||
<Label htmlFor="account">Used by</Label>
|
||||
<Select
|
||||
name="account"
|
||||
id="account"
|
||||
value={accountSid}
|
||||
onChange={e => setAccountSid(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
All accounts
|
||||
</option>
|
||||
{accounts.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
|
||||
<option
|
||||
key={a.account_sid}
|
||||
value={a.account_sid}
|
||||
>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{['google', 'aws', 'microsoft', 'wellsaid'].includes(vendor) ? (
|
||||
<>
|
||||
<div />
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
name="useForTts"
|
||||
id="useForTts"
|
||||
label="Use for text-to-speech"
|
||||
checked={useForTts}
|
||||
onChange={e => setUseForTts(e.target.checked)}
|
||||
invalid={invalidUseForTts}
|
||||
ref={refUseForTts}
|
||||
/>
|
||||
<div />
|
||||
<Checkbox
|
||||
noLeftMargin
|
||||
name="useForStt"
|
||||
id="useForStt"
|
||||
label="Use for speech-to-text"
|
||||
disabled={'wellsaid' === vendor}
|
||||
checked={useForStt}
|
||||
onChange={e => setUseForStt(e.target.checked)}
|
||||
invalid={invalidUseForStt}
|
||||
ref={refUseForStt}
|
||||
/>
|
||||
</>
|
||||
) :
|
||||
(
|
||||
null
|
||||
)}
|
||||
|
||||
{vendor === 'google' ? (
|
||||
<>
|
||||
<Label htmlFor="serviceKey">Service Key</Label>
|
||||
{type === 'add' && (
|
||||
<FileUpload
|
||||
id="serviceKey"
|
||||
onChange={handleFileUpload}
|
||||
validFile={validServiceKey}
|
||||
/>
|
||||
)}
|
||||
{displayedServiceKey && (
|
||||
<>
|
||||
{type === 'add' && (
|
||||
<span></span>
|
||||
)}
|
||||
<Code>{displayedServiceKey}</Code>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : vendor === 'aws' ? (
|
||||
<>
|
||||
<Label htmlFor="accessKeyId">Access Key ID</Label>
|
||||
<Input
|
||||
name="accessKeyId"
|
||||
id="accessKeyId"
|
||||
value={accessKeyId}
|
||||
onChange={e => setAccessKeyId(e.target.value)}
|
||||
placeholder=""
|
||||
invalid={invalidAccessKeyId}
|
||||
ref={refAccessKeyId}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
|
||||
<Label htmlFor="secretAccessKey">Secret Access Key</Label>
|
||||
<PasswordInput
|
||||
allowShowPassword
|
||||
name="secretAccessKey"
|
||||
id="secretAccessKey"
|
||||
password={secretAccessKey}
|
||||
setPassword={setSecretAccessKey}
|
||||
setErrorMessage={setErrorMessage}
|
||||
invalid={invalidSecretAccessKey}
|
||||
ref={refSecretAccessKey}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
|
||||
<Label htmlFor="regions">Region</Label>
|
||||
<Select
|
||||
name="regions"
|
||||
id="regions"
|
||||
value={awsregion}
|
||||
onChange={e => setAwsRegion(e.target.value)}
|
||||
ref={refAwsRegion}
|
||||
invalid={invalidAwsRegion}
|
||||
>
|
||||
<option value="">
|
||||
Select a region
|
||||
</option>
|
||||
{AwsRegions.map(r => (
|
||||
<option
|
||||
key={r.value}
|
||||
value={r.value}
|
||||
>
|
||||
{r.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
) : vendor === 'microsoft' ? (
|
||||
<>
|
||||
<Label htmlFor="apiKey">API Key</Label>
|
||||
<Input
|
||||
name="apiKey"
|
||||
id="apiKey"
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
placeholder=""
|
||||
invalid={invalidApiKey}
|
||||
ref={refApiKey}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
|
||||
<Label htmlFor="region">Region</Label>
|
||||
<Select
|
||||
name="region"
|
||||
id="region"
|
||||
value={region}
|
||||
onChange={e => setRegion(e.target.value)}
|
||||
ref={refRegion}
|
||||
invalid={invalidRegion}
|
||||
>
|
||||
<option value="">
|
||||
All regions
|
||||
</option>
|
||||
{MicrosoftAzureRegions.map(r => (
|
||||
<option
|
||||
key={r.value}
|
||||
value={r.value}
|
||||
>
|
||||
{r.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
) : vendor === 'wellsaid' ? (
|
||||
<>
|
||||
<Label htmlFor="apiKey">API Key</Label>
|
||||
<Input
|
||||
name="apiKey"
|
||||
id="apiKey"
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
placeholder=""
|
||||
invalid={invalidApiKey}
|
||||
ref={refApiKey}
|
||||
disabled={type === 'edit'}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
|
||||
<StyledButtonGroup flexEnd spaced type={type}>
|
||||
<Button
|
||||
rounded="true"
|
||||
gray
|
||||
type="button"
|
||||
onClick={() => {
|
||||
history.push('/internal/speech-services');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: type === 'add' ? 'New speech service canceled' : 'Changes canceled',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button rounded="true" disabled={!vendor}>
|
||||
{type === 'add'
|
||||
? 'Add Speech Service'
|
||||
: 'Save'
|
||||
}
|
||||
</Button>
|
||||
</StyledButtonGroup>
|
||||
</Form>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeechServicesAddEdit;
|
||||
@@ -0,0 +1,69 @@
|
||||
import React, { useEffect, forwardRef } from "react";
|
||||
|
||||
import { Selector } from "src/components/forms";
|
||||
|
||||
import type { Account } from "src/api/types";
|
||||
import { hasLength } from "src/utils";
|
||||
|
||||
type AccountSelectProps = {
|
||||
label?: string;
|
||||
account: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
accounts?: Account[];
|
||||
defaultOption?: boolean;
|
||||
|
||||
/** Native select element attributes we support */
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type SelectorRef = HTMLSelectElement;
|
||||
|
||||
export const AccountSelect = forwardRef<SelectorRef, AccountSelectProps>(
|
||||
(
|
||||
{
|
||||
label = "Account",
|
||||
account: [accountSid, setAccountSid],
|
||||
accounts,
|
||||
required = true,
|
||||
defaultOption,
|
||||
...restProps
|
||||
}: AccountSelectProps,
|
||||
ref
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (hasLength(accounts) && !accountSid && !defaultOption) {
|
||||
setAccountSid(accounts[0].account_sid);
|
||||
}
|
||||
}, [accounts, accountSid, defaultOption]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor="account_sid">
|
||||
{label} {required && <span>*</span>}
|
||||
</label>
|
||||
<Selector
|
||||
ref={ref}
|
||||
id="account_sid"
|
||||
name="account_sid"
|
||||
required={required}
|
||||
value={accountSid}
|
||||
options={(defaultOption
|
||||
? [{ name: "All accounts", value: "" }]
|
||||
: []
|
||||
).concat(
|
||||
hasLength(accounts)
|
||||
? accounts.map((account) => ({
|
||||
name: account.name,
|
||||
value: account.account_sid,
|
||||
}))
|
||||
: []
|
||||
)}
|
||||
onChange={(e) => setAccountSid(e.target.value)}
|
||||
{...restProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
AccountSelect.displayName = "AccountSelect";
|
||||
@@ -0,0 +1,75 @@
|
||||
import React, { useEffect, forwardRef } from "react";
|
||||
|
||||
import { Selector } from "src/components/forms";
|
||||
import { hasLength } from "src/utils";
|
||||
|
||||
import type { Application } from "src/api/types";
|
||||
import type { IMessage } from "src/store/types";
|
||||
|
||||
type ApplicationSelectProps = {
|
||||
id?: string;
|
||||
label?: IMessage;
|
||||
application: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
applications?: Application[];
|
||||
defaultOption?: string;
|
||||
|
||||
/** Native select element attributes we support */
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type SelectorRef = HTMLSelectElement;
|
||||
|
||||
export const ApplicationSelect = forwardRef<
|
||||
SelectorRef,
|
||||
ApplicationSelectProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
id = "application_sid",
|
||||
label = "Application",
|
||||
application: [applicationSid, setApplicationSid],
|
||||
applications,
|
||||
required = false,
|
||||
defaultOption,
|
||||
...restProps
|
||||
}: ApplicationSelectProps,
|
||||
ref
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (hasLength(applications) && !applicationSid && !defaultOption) {
|
||||
setApplicationSid(applications[0].application_sid);
|
||||
}
|
||||
}, [applications, applicationSid, defaultOption]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={id}>
|
||||
{label} {required && <span>*</span>}
|
||||
</label>
|
||||
<Selector
|
||||
ref={ref}
|
||||
id={id}
|
||||
name={id}
|
||||
required={required}
|
||||
value={applicationSid}
|
||||
options={(defaultOption
|
||||
? [{ name: defaultOption, value: "" }]
|
||||
: []
|
||||
).concat(
|
||||
hasLength(applications)
|
||||
? applications.map((application) => ({
|
||||
name: application.name,
|
||||
value: application.application_sid,
|
||||
}))
|
||||
: []
|
||||
)}
|
||||
onChange={(e) => setApplicationSid(e.target.value)}
|
||||
{...restProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ApplicationSelect.displayName = "ApplicationSelect";
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useState, useEffect, forwardRef } from "react";
|
||||
import { classNames } from "jambonz-ui";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type CheckzoneProps = {
|
||||
id?: string;
|
||||
name: string;
|
||||
label: string;
|
||||
hidden?: boolean;
|
||||
children: React.ReactNode;
|
||||
initialCheck: boolean;
|
||||
handleChecked?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
type CheckzoneRef = HTMLInputElement;
|
||||
|
||||
/** The forwarded ref is so forms can still focus() this checkbox field if necessary... */
|
||||
export const Checkzone = forwardRef<CheckzoneRef, CheckzoneProps>(
|
||||
(
|
||||
{
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
hidden = false,
|
||||
children,
|
||||
initialCheck,
|
||||
handleChecked,
|
||||
}: CheckzoneProps,
|
||||
ref
|
||||
) => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
const classesTop = classNames({
|
||||
checkzone: true,
|
||||
"checkzone--hidden": hidden,
|
||||
});
|
||||
const classesIn = classNames({
|
||||
checkzone__managed: true,
|
||||
active: checked,
|
||||
});
|
||||
|
||||
/** Handle initial checked condition */
|
||||
useEffect(() => {
|
||||
setChecked(initialCheck);
|
||||
}, [initialCheck]);
|
||||
|
||||
return (
|
||||
<div className={classesTop}>
|
||||
<label>
|
||||
<input
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
id={id || name}
|
||||
onChange={(e) => {
|
||||
setChecked(e.target.checked);
|
||||
|
||||
if (handleChecked) {
|
||||
handleChecked(e);
|
||||
}
|
||||
}}
|
||||
checked={checked}
|
||||
/>
|
||||
<div>{label}</div>
|
||||
</label>
|
||||
<div className={classesIn}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Checkzone.displayName = "Checkzone";
|
||||
@@ -0,0 +1,57 @@
|
||||
@use "src/styles/vars";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.checkzone {
|
||||
padding: ui-vars.$px02;
|
||||
border-radius: ui-vars.$px01;
|
||||
border: 2px solid vars.$jeangrey;
|
||||
width: 100%;
|
||||
max-width: vars.$widthinput;
|
||||
|
||||
> label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
margin-right: ui-vars.$px02;
|
||||
}
|
||||
}
|
||||
|
||||
&__managed {
|
||||
margin-top: ui-vars.$px02;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.25;
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-top: ui-vars.$px02;
|
||||
}
|
||||
|
||||
> label + * {
|
||||
margin-top: ui-vars.$px01;
|
||||
}
|
||||
|
||||
&.active {
|
||||
cursor: auto;
|
||||
opacity: 1;
|
||||
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
.checkzone__managed {
|
||||
display: none;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useState, forwardRef } from "react";
|
||||
import { classNames } from "jambonz-ui";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type FileProps = JSX.IntrinsicElements["input"] & {
|
||||
handleFile: (file: File) => void;
|
||||
};
|
||||
|
||||
type FileRef = HTMLInputElement;
|
||||
|
||||
/** The forwarded ref is so forms can still focus() this input if necessary... */
|
||||
/** Disabling the cosmetic text input seems the best way to remove it from the field... */
|
||||
/** Worth noting that tabIndex -1 with readOnly works as well, but disabled nukes it! */
|
||||
/** Passing rest props for things like `required` etc for form handling / validation */
|
||||
export const FileUpload = forwardRef<FileRef, FileProps>(
|
||||
(
|
||||
{
|
||||
id,
|
||||
name,
|
||||
handleFile,
|
||||
placeholder = "No file chosen",
|
||||
disabled,
|
||||
...restProps
|
||||
}: FileProps,
|
||||
ref
|
||||
) => {
|
||||
const [fileName, setFileName] = useState("");
|
||||
const [focus, setFocus] = useState(false);
|
||||
const classes = {
|
||||
"file-upload": true,
|
||||
focused: focus,
|
||||
disabled: disabled ? true : false,
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length) {
|
||||
setFileName(e.target.files[0].name);
|
||||
handleFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(classes)}>
|
||||
<div className="file-upload__wrap inpbtn">
|
||||
<input
|
||||
ref={ref}
|
||||
id={id}
|
||||
name={name}
|
||||
type="file"
|
||||
onChange={handleChange}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
disabled={disabled}
|
||||
{...restProps}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={fileName}
|
||||
placeholder={placeholder}
|
||||
disabled
|
||||
/>
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
title={placeholder}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icons.FilePlus />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FileUpload.displayName = "FileUpload";
|
||||
@@ -0,0 +1,45 @@
|
||||
@use "src/styles/vars";
|
||||
@use "src/styles/mixins";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.file-upload {
|
||||
&__wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.form & {
|
||||
max-width: vars.$widthinput;
|
||||
}
|
||||
}
|
||||
|
||||
&.focused {
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
border-color: ui-vars.$dark;
|
||||
color: ui-vars.$dark;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
@include mixins.disabled();
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Passwd } from "./passwd";
|
||||
import { Message } from "./message";
|
||||
import { Selector } from "./selector";
|
||||
import { Checkzone } from "./checkzone";
|
||||
import { FileUpload } from "./file-upload";
|
||||
import { AccountSelect } from "./account-select";
|
||||
import { ApplicationSelect } from "./application-select";
|
||||
import { LocalLimits, useLocalLimitsRef } from "./local-limits";
|
||||
|
||||
export {
|
||||
Passwd,
|
||||
Message,
|
||||
Selector,
|
||||
Checkzone,
|
||||
FileUpload,
|
||||
AccountSelect,
|
||||
ApplicationSelect,
|
||||
LocalLimits,
|
||||
useLocalLimitsRef,
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
import { LIMITS } from "src/api/constants";
|
||||
import { hasLength } from "src/utils";
|
||||
|
||||
import type { Limit, LimitCategories } from "src/api/types";
|
||||
|
||||
type LocalLimitRef = {
|
||||
[key in LimitCategories]: HTMLInputElement;
|
||||
};
|
||||
|
||||
type LocalLimitsProps = {
|
||||
data: Limit[] | undefined;
|
||||
limits: [Limit[], React.Dispatch<React.SetStateAction<Limit[]>>];
|
||||
inputRef?: React.MutableRefObject<LocalLimitRef>;
|
||||
};
|
||||
|
||||
/** Simple wrapper hook since this ref is so specific */
|
||||
export const useLocalLimitsRef = () => {
|
||||
return useRef<LocalLimitRef>({} as LocalLimitRef);
|
||||
};
|
||||
|
||||
export const LocalLimits = ({
|
||||
data,
|
||||
limits: [localLimits, setLocalLimits],
|
||||
inputRef,
|
||||
}: LocalLimitsProps) => {
|
||||
const updateLimitValue = (category: string) => {
|
||||
if (hasLength(localLimits)) {
|
||||
const limit = localLimits.find((l) => l.category === category);
|
||||
|
||||
return limit ? limit.quantity : "";
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasLength(data)) {
|
||||
setLocalLimits(data);
|
||||
} else {
|
||||
setLocalLimits([]);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{LIMITS.map(({ category, label }) => {
|
||||
return (
|
||||
<React.Fragment key={category}>
|
||||
<label htmlFor={category}>{label}</label>
|
||||
<input
|
||||
ref={(el: HTMLInputElement) => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current[category] = el;
|
||||
}
|
||||
}}
|
||||
id={category}
|
||||
type="number"
|
||||
name={category}
|
||||
placeholder="Enter quantity (0=unlimited)"
|
||||
min="0"
|
||||
value={updateLimitValue(category)}
|
||||
onChange={(e) => {
|
||||
const limit = localLimits.find((l) => l.category === category);
|
||||
const value = e.target.value ? Number(e.target.value) : "";
|
||||
|
||||
if (limit) {
|
||||
setLocalLimits(
|
||||
localLimits.map((l) =>
|
||||
l.category === category ? { ...l, quantity: value } : l
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setLocalLimits([
|
||||
...localLimits,
|
||||
{ category, quantity: value },
|
||||
]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import type { IMessage } from "src/store/types";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type MsgProps = {
|
||||
message: IMessage;
|
||||
};
|
||||
|
||||
export const Message = ({ message }: MsgProps) => {
|
||||
return (
|
||||
<div className="msg">
|
||||
<Icons.AlertCircle />
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
@use "src/styles/vars";
|
||||
@use "src/styles/mixins";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.msg {
|
||||
@include ui-mixins.ms();
|
||||
padding: ui-vars.$px01 ui-vars.$px02 ui-vars.$px01 56px;
|
||||
border-radius: ui-vars.$px01;
|
||||
border: 2px solid ui-vars.$jambonz;
|
||||
background-color: ui-vars.$pink;
|
||||
color: ui-vars.$jambonz;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
@include mixins.mobile() {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
> svg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
> svg:first-child {
|
||||
left: ui-vars.$px02;
|
||||
}
|
||||
|
||||
> svg:last-child {
|
||||
right: ui-vars.$px02;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useState, forwardRef } from "react";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type PasswdProps = JSX.IntrinsicElements["input"] & {
|
||||
locked?: boolean;
|
||||
/** This is optional in case an onChange override is necessary... */
|
||||
setValue?: React.Dispatch<React.SetStateAction<string>>;
|
||||
};
|
||||
|
||||
type PasswdRef = HTMLInputElement;
|
||||
|
||||
/** The restProps spread at the end in case an onChange override is necessary... */
|
||||
/** The forwarded ref is so forms can still focus() this input field if necessary... */
|
||||
export const Passwd = forwardRef<PasswdRef, PasswdProps>(
|
||||
(
|
||||
{
|
||||
name,
|
||||
value,
|
||||
setValue,
|
||||
placeholder,
|
||||
locked = false,
|
||||
...restProps
|
||||
}: PasswdProps,
|
||||
ref
|
||||
) => {
|
||||
const [reveal, setReveal] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="passwd">
|
||||
<input
|
||||
ref={ref}
|
||||
type={reveal ? "text" : "password"}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
if (setValue) {
|
||||
setValue(e.target.value);
|
||||
}
|
||||
}}
|
||||
{...restProps}
|
||||
/>
|
||||
{!locked && (
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
onClick={() => setReveal(!reveal)}
|
||||
>
|
||||
{reveal ? <Icons.EyeOff /> : <Icons.Eye />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Passwd.displayName = "Passwd";
|
||||
@@ -0,0 +1,32 @@
|
||||
@use "src/styles/vars";
|
||||
@use "src/styles/mixins";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
|
||||
.passwd {
|
||||
position: relative;
|
||||
|
||||
.form & {
|
||||
max-width: vars.$widthinput;
|
||||
}
|
||||
|
||||
> input {
|
||||
width: 100%;
|
||||
|
||||
&[disabled] {
|
||||
@include mixins.disabled();
|
||||
}
|
||||
}
|
||||
|
||||
> button[type="button"] {
|
||||
position: absolute;
|
||||
right: ui-vars.$px02;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
stroke: ui-vars.$jambonz;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React, { useState, forwardRef } from "react";
|
||||
import { classNames } from "jambonz-ui";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
export interface SelectorOption {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
type SelectorProps = JSX.IntrinsicElements["select"] & {
|
||||
options: SelectorOption[];
|
||||
};
|
||||
|
||||
type SelectorRef = HTMLSelectElement;
|
||||
|
||||
/** The forwarded ref is so forms can still focus() this select menu if necessary... */
|
||||
export const Selector = forwardRef<SelectorRef, SelectorProps>(
|
||||
(
|
||||
{ id, name, value, options, disabled, ...restProps }: SelectorProps,
|
||||
ref
|
||||
) => {
|
||||
const [focus, setFocus] = useState(false);
|
||||
const classes = {
|
||||
selector: true,
|
||||
focused: focus,
|
||||
disabled: disabled ? true : false,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(classes)}>
|
||||
<select
|
||||
ref={ref}
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
disabled={disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span>
|
||||
<Icons.ChevronUp />
|
||||
<Icons.ChevronDown />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Selector.displayName = "Selector";
|
||||
@@ -0,0 +1,75 @@
|
||||
@use "src/styles/vars";
|
||||
@use "src/styles/mixins";
|
||||
@use "jambonz-ui/src/styles/index";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.selector {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: vars.$widthinput;
|
||||
|
||||
&.disabled {
|
||||
@include mixins.disabled();
|
||||
}
|
||||
|
||||
&.focused {
|
||||
select {
|
||||
border-color: ui-vars.$dark;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
background-color: ui-vars.$dark;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
@include ui-mixins.m();
|
||||
appearance: none;
|
||||
padding: ui-vars.$px01 ui-vars.$px02;
|
||||
border-radius: ui-vars.$px01;
|
||||
border: 2px solid ui-vars.$grey;
|
||||
background-color: ui-vars.$white;
|
||||
width: 100%;
|
||||
max-width: vars.$widthinput;
|
||||
|
||||
&:focus {
|
||||
border-color: ui-vars.$dark;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
@include mixins.disabled();
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
height: 100%;
|
||||
width: 50px;
|
||||
background-color: ui-vars.$grey;
|
||||
border-radius: ui-vars.$px01;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
|
||||
svg {
|
||||
stroke: ui-vars.$white;
|
||||
|
||||
&:first-child {
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
X,
|
||||
Eye,
|
||||
Info,
|
||||
Menu,
|
||||
List,
|
||||
Edit,
|
||||
Plus,
|
||||
Grid,
|
||||
Phone,
|
||||
Users,
|
||||
Edit3,
|
||||
Trash,
|
||||
LogIn,
|
||||
LogOut,
|
||||
EyeOff,
|
||||
Server,
|
||||
Trash2,
|
||||
GitHub,
|
||||
Filter,
|
||||
XCircle,
|
||||
Settings,
|
||||
FilePlus,
|
||||
Activity,
|
||||
ChevronUp,
|
||||
Clipboard,
|
||||
RefreshCw,
|
||||
ArrowRight,
|
||||
PlusCircle,
|
||||
HelpCircle,
|
||||
ChevronDown,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
MessageCircle,
|
||||
PhoneOutgoing,
|
||||
PhoneIncoming,
|
||||
MoreHorizontal,
|
||||
} from "react-feather";
|
||||
|
||||
import type { Icon } from "react-feather";
|
||||
|
||||
export interface IconMap {
|
||||
[key: string]: Icon;
|
||||
}
|
||||
|
||||
export const Icons: IconMap = {
|
||||
X,
|
||||
Eye,
|
||||
Info,
|
||||
Menu,
|
||||
List,
|
||||
Edit,
|
||||
Plus,
|
||||
Grid,
|
||||
Phone,
|
||||
Users,
|
||||
Edit3,
|
||||
Trash,
|
||||
LogIn,
|
||||
LogOut,
|
||||
EyeOff,
|
||||
Server,
|
||||
Trash2,
|
||||
GitHub,
|
||||
Filter,
|
||||
XCircle,
|
||||
Settings,
|
||||
FilePlus,
|
||||
Activity,
|
||||
ChevronUp,
|
||||
Clipboard,
|
||||
RefreshCw,
|
||||
ArrowRight,
|
||||
PlusCircle,
|
||||
HelpCircle,
|
||||
ChevronDown,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
MessageCircle,
|
||||
PhoneOutgoing,
|
||||
PhoneIncoming,
|
||||
MoreHorizontal,
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Icons } from "./icons";
|
||||
import { Toast } from "./toast";
|
||||
import { Modal, ModalClose, ModalForm } from "./modal";
|
||||
import { RequireAuth } from "./require-auth";
|
||||
import { AccessControl } from "./access-control";
|
||||
import { Obscure } from "./obscure";
|
||||
import { ClipBoard } from "./clipboard";
|
||||
import { Section } from "./section";
|
||||
import { Tooltip } from "./tooltip";
|
||||
import { Spinner } from "./spinner";
|
||||
import { AccountFilter } from "./account-filter";
|
||||
import { SelectFilter } from "./select-filter";
|
||||
import { Pagination } from "./pagination";
|
||||
import { ApplicationFilter } from "./application-filter";
|
||||
import { SearchFilter } from "./search-filter";
|
||||
|
||||
export {
|
||||
Icons,
|
||||
Toast,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalForm,
|
||||
RequireAuth,
|
||||
AccessControl,
|
||||
Obscure,
|
||||
ClipBoard,
|
||||
Section,
|
||||
Tooltip,
|
||||
Spinner,
|
||||
AccountFilter,
|
||||
SelectFilter,
|
||||
Pagination,
|
||||
ApplicationFilter,
|
||||
SearchFilter,
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Button, ButtonGroup } from "jambonz-ui";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type ModalProps = {
|
||||
disabled?: boolean;
|
||||
children: React.ReactNode;
|
||||
handleSubmit: (e: React.FormEvent) => void;
|
||||
handleCancel: (e: React.FormEvent) => void;
|
||||
};
|
||||
|
||||
type CloseProps = {
|
||||
children: React.ReactNode;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
const portal: Element = document.getElementById("modal")!;
|
||||
|
||||
export const Modal = ({
|
||||
disabled,
|
||||
children,
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
}: ModalProps) => {
|
||||
return ReactDOM.createPortal(
|
||||
<div className="modal">
|
||||
<div className="modal__box">
|
||||
<div className="modal__stuff">{children}</div>
|
||||
<ButtonGroup right>
|
||||
<Button
|
||||
type="button"
|
||||
small
|
||||
subStyle="grey"
|
||||
onClick={handleCancel}
|
||||
disabled={disabled}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
small
|
||||
onClick={handleSubmit}
|
||||
disabled={disabled}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>,
|
||||
portal
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalForm = ({
|
||||
disabled,
|
||||
children,
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
}: ModalProps) => {
|
||||
return ReactDOM.createPortal(
|
||||
<div className="modal">
|
||||
<form
|
||||
className="form modal__box"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit(e);
|
||||
}}
|
||||
>
|
||||
<div className="modal__stuff">{children}</div>
|
||||
<ButtonGroup right>
|
||||
<Button
|
||||
small
|
||||
type="button"
|
||||
subStyle="grey"
|
||||
onClick={handleCancel}
|
||||
disabled={disabled}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" small disabled={disabled}>
|
||||
Save
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</form>
|
||||
</div>,
|
||||
portal
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalClose = ({ children, handleClose }: CloseProps) => {
|
||||
return ReactDOM.createPortal(
|
||||
<div className="modal">
|
||||
<div className="modal__box">
|
||||
<div className="modal__stuff">{children}</div>
|
||||
<ButtonGroup right>
|
||||
<Button type="button" small subStyle="grey" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>,
|
||||
portal
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
@use "src/styles/vars";
|
||||
@use "src/styles/mixins";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: vars.$zindex00;
|
||||
padding-left: ui-vars.$px02;
|
||||
padding-right: ui-vars.$px02;
|
||||
|
||||
&__stuff {
|
||||
> * + * {
|
||||
margin-top: ui-vars.$px02;
|
||||
}
|
||||
}
|
||||
|
||||
&__box {
|
||||
padding: ui-vars.$px03;
|
||||
width: 100%;
|
||||
max-width: vars.$widthinput;
|
||||
border-radius: ui-vars.$px02;
|
||||
box-shadow: 0px ui-vars.$px00 ui-vars.$px00 rgba(0, 0, 0, 0.25);
|
||||
background-color: ui-vars.$white;
|
||||
|
||||
@include mixins.small() {
|
||||
padding: ui-vars.$px02;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-top: ui-vars.$px03;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
import { getObscured } from "src/utils";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type ObscureProps = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const Obscure = ({ text }: ObscureProps) => {
|
||||
const [reveal, setReveal] = useState(false);
|
||||
const obscured = getObscured(text);
|
||||
|
||||
return (
|
||||
<div className="obscure">
|
||||
<span>{reveal ? text : obscured}</span>
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
onClick={() => setReveal(!reveal)}
|
||||
>
|
||||
{reveal ? <Icons.EyeOff /> : <Icons.Eye />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
@use "src/styles/vars";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.obscure {
|
||||
@include ui-mixins.ms();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: ui-vars.$font-mono;
|
||||
|
||||
> span {
|
||||
@media (max-width: vars.$gridbreak2) {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
> button[type="button"] {
|
||||
cursor: pointer;
|
||||
margin-left: ui-vars.$px02;
|
||||
|
||||
svg {
|
||||
stroke: ui-vars.$jambonz;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import SetupTemplate from '../templates/SetupTemplate';
|
||||
import InternalTemplate from '../templates/InternalTemplate';
|
||||
import Link from '../elements/Link';
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 4rem;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const InvalidRoute = () => {
|
||||
const [ isLoggedIn, setIsLoggedIn ] = useState(false);
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem('token')) {
|
||||
setIsLoggedIn(true);
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
return (
|
||||
isLoggedIn ? (
|
||||
<InternalTemplate title="Invalid Route">
|
||||
<Container>
|
||||
That page doesn't exist.
|
||||
</Container>
|
||||
</InternalTemplate>
|
||||
) : (
|
||||
<SetupTemplate title="Invalid Route">
|
||||
<Container>
|
||||
<p>That page doesn't exist.</p>
|
||||
<p><Link to="/">Log In</Link></p>
|
||||
</Container>
|
||||
</SetupTemplate>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default InvalidRoute;
|
||||
@@ -1,228 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import SetupTemplate from '../templates/SetupTemplate';
|
||||
import Form from '../elements/Form';
|
||||
import Button from '../elements/Button';
|
||||
import Input from '../elements/Input';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import FormError from '../blocks/FormError';
|
||||
import { APP_API_BASE_URL } from "../../constants";
|
||||
|
||||
const Login = props => {
|
||||
let history = useHistory();
|
||||
useEffect(() => {
|
||||
document.title = `Login | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
// Refs
|
||||
const refUsername = useRef(null);
|
||||
const refPassword = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ username, setUsername ] = useState('');
|
||||
const [ password, setPassword ] = useState('');
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidUsername, setInvalidUsername ] = useState(false);
|
||||
const [ invalidPassword, setInvalidPassword ] = useState(false);
|
||||
|
||||
const handleSubmit = async e => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidUsername(false);
|
||||
setInvalidPassword(false);
|
||||
|
||||
if (!username && !password) {
|
||||
setErrorMessage('Username and password are required');
|
||||
setInvalidUsername(true);
|
||||
setInvalidPassword(true);
|
||||
refUsername.current.focus();
|
||||
return;
|
||||
}
|
||||
if (!username) {
|
||||
setErrorMessage('Username is required');
|
||||
setInvalidUsername(true);
|
||||
refUsername.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
setErrorMessage('Password is required');
|
||||
setInvalidPassword(true);
|
||||
refPassword.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Log in
|
||||
const response = await axios({
|
||||
method: 'post',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/login',
|
||||
data: { username, password },
|
||||
});
|
||||
|
||||
// New account, password change required
|
||||
if (response.data.force_change) {
|
||||
// `user_sid` and `old_password` are needed for the new password form.
|
||||
// They're saved to sessionStorage so that the data does not persist.
|
||||
sessionStorage.setItem('user_sid', response.data.user_sid);
|
||||
sessionStorage.setItem('old_password', password);
|
||||
localStorage.setItem('token', response.data.token);
|
||||
history.push('/create-password');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save API key
|
||||
if (response.data.token) {
|
||||
localStorage.setItem('token', response.data.token);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Get account data
|
||||
//-----------------------------------------------------------------------------
|
||||
const serviceProvidersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/serviceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const voipCarriersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/voipCarriers',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const promiseAllValues = await Promise.all([
|
||||
serviceProvidersPromise,
|
||||
accountsPromise,
|
||||
applicationsPromise,
|
||||
voipCarriersPromise,
|
||||
]);
|
||||
|
||||
const serviceProviders = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[1].data;
|
||||
const applications = promiseAllValues[2].data;
|
||||
const voipCarriers = promiseAllValues[3].data;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Determine where to route user
|
||||
//-----------------------------------------------------------------------------
|
||||
if (
|
||||
(serviceProviders.length > 1) ||
|
||||
(accounts.length > 1) ||
|
||||
(accounts.length < 1) ||
|
||||
(applications.length > 1) ||
|
||||
(voipCarriers.length > 0)
|
||||
) {
|
||||
history.push('/internal/accounts');
|
||||
return;
|
||||
}
|
||||
|
||||
// const { sip_realm, registration_hook } = accounts[0];
|
||||
|
||||
// if (
|
||||
// (!sip_realm || !registration_hook) &&
|
||||
// !applications.length
|
||||
// ) {
|
||||
// history.push('/configure-account');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!applications.length) {
|
||||
// history.push('/create-application');
|
||||
// return;
|
||||
// }
|
||||
|
||||
history.push('/internal/accounts');
|
||||
|
||||
} catch (err) {
|
||||
// 400 --> one or both fields are empty (prevented by above error checking)
|
||||
// 403 --> username or password are incorrect
|
||||
if (
|
||||
err.response
|
||||
&& err.response.status
|
||||
&& err.response.status > 399
|
||||
&& err.response.status < 500
|
||||
) {
|
||||
setErrorMessage('Login credentials are incorrect');
|
||||
} else {
|
||||
setErrorMessage(
|
||||
(err.response && err.response.data && err.response.data.msg) ||
|
||||
'Something went wrong, please try again.'
|
||||
);
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SetupTemplate title="Log In">
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
large
|
||||
fullWidth
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
ref={refUsername}
|
||||
invalid={invalidUsername}
|
||||
autoFocus
|
||||
/>
|
||||
<PasswordInput
|
||||
large
|
||||
allowShowPassword
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="Password"
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
setErrorMessage={setErrorMessage}
|
||||
ref={refPassword}
|
||||
invalid={invalidPassword}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<FormError message={errorMessage} />
|
||||
)}
|
||||
<Button
|
||||
large
|
||||
fullWidth
|
||||
>
|
||||
Log In
|
||||
</Button>
|
||||
</Form>
|
||||
</SetupTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,230 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import AccountForm from '../../forms/AccountForm';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const AccountsAddEdit = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
let { account_sid } = useParams();
|
||||
const pageTitle = account_sid ? 'Edit Account' : 'Add Account';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
//=============================================================================
|
||||
// Get API keys
|
||||
//=============================================================================
|
||||
const getApiKeys = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const results = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${account_sid}/ApiKeys`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const simplifiedApiKeys = results.data.map(a => {
|
||||
const { token } = a;
|
||||
const maskLength = token.length - 4;
|
||||
const maskedPortion = token.substring(0, maskLength).replace(/[a-zA-Z0-9]/g, '*');
|
||||
const revealedPortion = token.substring(maskLength);
|
||||
const maskedToken = `${maskedPortion}${revealedPortion}`;
|
||||
|
||||
const { last_used } = a;
|
||||
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: a.api_key_sid,
|
||||
token: {
|
||||
type: 'masked',
|
||||
masked: maskedToken,
|
||||
revealed: token,
|
||||
},
|
||||
last_used: {
|
||||
type: 'normal',
|
||||
content: lastUsedString,
|
||||
},
|
||||
};
|
||||
});
|
||||
return(simplifiedApiKeys);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get API key data',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Create API key
|
||||
//=============================================================================
|
||||
const createApiKey = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const result = await axios({
|
||||
method: 'post',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Apikeys',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data: {
|
||||
"account_sid": account_sid,
|
||||
}
|
||||
});
|
||||
return result.data.token;
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to create API key',
|
||||
});
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Delete API key
|
||||
//=============================================================================
|
||||
const formatApiKeyToDelete = apiKey => {
|
||||
const items = [
|
||||
{ name: 'API Key:' , content: apiKey.token.masked || '[none]' },
|
||||
{ name: 'Last Used:' , content: apiKey.last_used.content || 'Never used' },
|
||||
];
|
||||
return items;
|
||||
};
|
||||
const deleteApiKey = async apiKeyToDelete => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Apikeys/${apiKeyToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete API key');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
breadcrumbs={[
|
||||
{ name: 'Accounts', url: '/internal/accounts' },
|
||||
{ name: pageTitle },
|
||||
]}
|
||||
additionalTable={account_sid && (
|
||||
<TableContent
|
||||
name="API key"
|
||||
getContent={getApiKeys}
|
||||
columns={[
|
||||
{ header: 'Account API Keys', key: 'token', width: '27rem', fontWeight: 'normal' },
|
||||
{ header: 'Last Used', key: 'last_used', width: '10rem' },
|
||||
]}
|
||||
addContent={createApiKey}
|
||||
formatContentToDelete={formatApiKeyToDelete}
|
||||
deleteContent={deleteApiKey}
|
||||
rowsHaveDeleteButtons
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<AccountForm
|
||||
type={account_sid ? 'edit' : 'add'}
|
||||
account_sid={account_sid}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountsAddEdit;
|
||||
@@ -1,235 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const AccountsList = () => {
|
||||
let history = useHistory();
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
useEffect(() => {
|
||||
document.title = `Accounts | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
//=============================================================================
|
||||
// Get accounts
|
||||
//=============================================================================
|
||||
const getAccounts = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(!currentServiceProvider) return [];
|
||||
const results = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const simplifiedAccounts = results.data.map(a => ({
|
||||
sid: a.account_sid,
|
||||
name: a.name,
|
||||
sip_realm: a.sip_realm,
|
||||
url_reg: a.registration_hook && a.registration_hook.url,
|
||||
url_queue: a.queue_event_hook && a.queue_event_hook.url,
|
||||
subspace_enabled: a.subspace_sip_teleport_id ? 'Enabled' : ''
|
||||
}));
|
||||
return(simplifiedAccounts);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get account data',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Delete account
|
||||
//=============================================================================
|
||||
const formatAccountToDelete = account => {
|
||||
const items = [
|
||||
{ name: 'Name:' , content: account.name || '[none]' },
|
||||
{ name: 'SIP Realm:' , content: account.sip_realm || '[none]' },
|
||||
{ name: 'Registration Webhook:' , content: account.url_reg || '[none]' },
|
||||
];
|
||||
return items;
|
||||
};
|
||||
const deleteAccount = async accountToDelete => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if any application, phone number, or MS Teams tenant uses this account
|
||||
// or if the account has any API keys
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const phoneNumbersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/PhoneNumbers',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const msTeamsTenantsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/MicrosoftTeamsTenants',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const apiKeysPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${accountToDelete.sid}/ApiKeys`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const promiseAllValues = await Promise.all([
|
||||
applicationsPromise,
|
||||
phoneNumbersPromise,
|
||||
msTeamsTenantsPromise,
|
||||
apiKeysPromise,
|
||||
]);
|
||||
const applications = promiseAllValues[0].data;
|
||||
const phoneNumbers = promiseAllValues[1].data;
|
||||
const msTeamsTenants = promiseAllValues[2].data;
|
||||
const apiKeys = promiseAllValues[3].data;
|
||||
|
||||
const accountApps = applications.filter(app => (
|
||||
app.account_sid === accountToDelete.sid
|
||||
));
|
||||
const accountPhoneNumbers = phoneNumbers.filter(p => (
|
||||
p.account_sid === accountToDelete.sid
|
||||
));
|
||||
const accountMsTeamsTenants = msTeamsTenants.filter(tenant => (
|
||||
tenant.account_sid === accountToDelete.sid
|
||||
));
|
||||
let errorMessages = [];
|
||||
for (const app of accountApps) {
|
||||
errorMessages.push(`Application: ${app.name}`);
|
||||
}
|
||||
for (const num of accountPhoneNumbers) {
|
||||
errorMessages.push(`Phone Number: ${num.number}`);
|
||||
}
|
||||
for (const tenant of accountMsTeamsTenants) {
|
||||
errorMessages.push(`Microsoft Teams Tenant: ${tenant.tenant_fqdn}`);
|
||||
}
|
||||
for (const apiKey of apiKeys) {
|
||||
const maskLength = apiKey.token.length - 4;
|
||||
const maskedPortion = apiKey.token.substring(0, maskLength).replace(/[a-zA-Z0-9]/g, '*');
|
||||
const revealedPortion = apiKey.token.substring(maskLength);
|
||||
const maskedToken = `${maskedPortion}${revealedPortion}`;
|
||||
errorMessages.push(`API Key: ${maskedToken}`);
|
||||
}
|
||||
if (errorMessages.length) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p style={{ margin: '0.5rem 0' }}>
|
||||
This account cannot be deleted because it is in use by:
|
||||
</p>
|
||||
<ul style={{ margin: '0.5rem 0' }}>
|
||||
{errorMessages.map((err, i) => (
|
||||
<li key={i}>{err}</li>
|
||||
))}
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// Delete account
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${accountToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete account');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate
|
||||
title="Accounts"
|
||||
addButtonText="Add an Account"
|
||||
addButtonLink="/internal/accounts/add"
|
||||
>
|
||||
<TableContent
|
||||
name="account"
|
||||
urlParam="accounts"
|
||||
getContent={getAccounts}
|
||||
columns={[
|
||||
{ header: 'Name', key: 'name' },
|
||||
{ header: 'AccountSid', key: 'sid' },
|
||||
{ header: 'SIP Realm', key: 'sip_realm' },
|
||||
{ header: 'Registration Webhook', key: 'url_reg' },
|
||||
{ header: 'Queue Event Webhook', key: 'url_queue' }
|
||||
]}
|
||||
formatContentToDelete={formatAccountToDelete}
|
||||
deleteContent={deleteAccount}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountsList;
|
||||
@@ -1,259 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
import styled from "styled-components/macro";
|
||||
import { NotificationDispatchContext } from "../../../contexts/NotificationContext";
|
||||
import InternalTemplate from "../../templates/InternalTemplate";
|
||||
import Button from "../../../components/elements/Button";
|
||||
import InputGroup from "../../../components/elements/InputGroup";
|
||||
import Label from "../../../components/elements/Label";
|
||||
import Select from "../../../components/elements/Select";
|
||||
import AntdTable from "../../../components/blocks/AntdTable";
|
||||
import handleErrors from "../../../helpers/handleErrors";
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
& > span {
|
||||
height: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const AlertsIndex = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const jwt = localStorage.getItem("token");
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Table props
|
||||
const [alertsData, setAlertsData] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowCount, setRowCount] = useState(25);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Filter values
|
||||
const [account, setAccount] = useState("");
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
const [attemptedAt, setAttemptedAt] = useState("today");
|
||||
|
||||
//=============================================================================
|
||||
// Define Table props
|
||||
//=============================================================================
|
||||
const Columns = [
|
||||
{
|
||||
title: "Date",
|
||||
dataIndex: "time",
|
||||
key: "time",
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
title: "Message",
|
||||
dataIndex: "message",
|
||||
key: "message",
|
||||
width: 250,
|
||||
},
|
||||
];
|
||||
const { height } = window.screen;
|
||||
|
||||
const renderPagination = (page, type, originElement) => {
|
||||
let node = originElement;
|
||||
|
||||
switch (type) {
|
||||
case "page":
|
||||
node = <StyledButton gray={currentPage !== page}>{page}</StyledButton>;
|
||||
break;
|
||||
case "prev":
|
||||
node = <StyledButton>{`<`}</StyledButton>;
|
||||
break;
|
||||
case "next":
|
||||
node = <StyledButton>{`>`}</StyledButton>;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
//=============================================================================
|
||||
// Get alerts
|
||||
//=============================================================================
|
||||
const getAlerts = async () => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
let filter = {
|
||||
page: currentPage,
|
||||
count: rowCount,
|
||||
};
|
||||
|
||||
if (!account) {
|
||||
setAlertsData([]);
|
||||
setTotalCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
switch (attemptedAt) {
|
||||
case "today":
|
||||
filter.start = moment().startOf("date").toISOString();
|
||||
break;
|
||||
case "7d":
|
||||
filter.days = 7;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
const alerts = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${account}/Alerts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
params: {
|
||||
...filter,
|
||||
},
|
||||
});
|
||||
|
||||
if (isMounted) {
|
||||
const { total, data } = alerts.data;
|
||||
const simplififedAlerts = data.map((alert, index) => ({
|
||||
...alert,
|
||||
id: index,
|
||||
time: alert.time
|
||||
? moment(alert.time).format("YYYY MM.DD hh:mm a")
|
||||
: "",
|
||||
message: alert.message,
|
||||
}));
|
||||
|
||||
setAlertsData(simplififedAlerts);
|
||||
setTotalCount(total);
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage === 1) {
|
||||
getAlerts();
|
||||
} else {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [account, attemptedAt]);
|
||||
|
||||
useEffect(() => {
|
||||
getAlerts();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPage, rowCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
let isMounted = true;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
if (accountResponse.data.length > 0) {
|
||||
setAccount(accountResponse.data[0].account_sid);
|
||||
} else {
|
||||
setAccount("");
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
return (
|
||||
<InternalTemplate title="Alerts">
|
||||
<StyledInputGroup flexEnd space>
|
||||
<Label indented htmlFor="account">
|
||||
Account
|
||||
</Label>
|
||||
<AccountSelect
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={(e) => setAccount(e.target.value)}
|
||||
>
|
||||
{accountList.map((acc) => (
|
||||
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
|
||||
))}
|
||||
</AccountSelect>
|
||||
<Label middle htmlFor="daterange">
|
||||
Date
|
||||
</Label>
|
||||
<Select
|
||||
name="daterange"
|
||||
id="daterange"
|
||||
value={attemptedAt}
|
||||
onChange={(e) => setAttemptedAt(e.target.value)}
|
||||
>
|
||||
<option value="today">today</option>
|
||||
<option value="7d">last 7d</option>
|
||||
</Select>
|
||||
</StyledInputGroup>
|
||||
<AntdTable
|
||||
dataSource={alertsData}
|
||||
columns={Columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: ["bottomCenter"],
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(page);
|
||||
setRowCount(size);
|
||||
},
|
||||
showTotal: (total) => `Total: ${total} records`,
|
||||
current: currentPage,
|
||||
total: totalCount,
|
||||
pageSize: rowCount,
|
||||
pageSizeOptions: [25, 50, 100],
|
||||
showSizeChanger: true,
|
||||
itemRender: renderPagination,
|
||||
showLessItems: true,
|
||||
}}
|
||||
scroll={{ y: Math.max(height - 660, 200) }}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertsIndex;
|
||||
@@ -1,29 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import ApplicationForm from '../../forms/ApplicationForm';
|
||||
|
||||
const ApplicationsAddEdit = () => {
|
||||
let { application_sid } = useParams();
|
||||
const pageTitle = application_sid ? 'Edit Application' : 'Add Application';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
breadcrumbs={[
|
||||
{ name: 'Applications', url: '/internal/applications' },
|
||||
{ name: pageTitle },
|
||||
]}
|
||||
>
|
||||
<ApplicationForm
|
||||
type={application_sid ? 'edit' : 'add'}
|
||||
application_sid={application_sid}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplicationsAddEdit;
|
||||
@@ -1,294 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useEffect, useContext, useState, useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import styled from "styled-components/macro";
|
||||
import Select from "../../../components/elements/Select";
|
||||
import InputGroup from "../../../components/elements/InputGroup";
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import handleErrors from "../../../helpers/handleErrors";
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const FilterLabel = styled.span`
|
||||
color: #231f20;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
margin-left: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const ApplicationsList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const jwt = localStorage.getItem('token');
|
||||
const [account, setAccount] = useState("");
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Applications | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
try {
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
if (accountResponse.data.length > 0) {
|
||||
setAccount(accountResponse.data[0].account_sid);
|
||||
} else {
|
||||
setAccount("");
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
//=============================================================================
|
||||
// Get applications
|
||||
//=============================================================================
|
||||
const getApplications = useCallback(async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!account) {
|
||||
return [];
|
||||
}
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${account}/Applications`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const promiseAllValues = await Promise.all([
|
||||
applicationsPromise,
|
||||
]);
|
||||
|
||||
const applications = promiseAllValues[0].data;
|
||||
|
||||
const simplifiedApplications = applications.map(app => {
|
||||
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,
|
||||
messaging_hook_url: app.messaging_hook && app.messaging_hook.url,
|
||||
account: app.account
|
||||
};
|
||||
});
|
||||
return(simplifiedApplications);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get application data',
|
||||
});
|
||||
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: 'Messaging Webhook:', content: app.messaging_hook_url || '[none]' },
|
||||
];
|
||||
};
|
||||
const deleteApplication = async applicationToDelete => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// check if any account or Microsoft Teams Tenant uses this application
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const msTeamsTenantsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/MicrosoftTeamsTenants',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const promiseAllValues = await Promise.all([
|
||||
accountsPromise,
|
||||
msTeamsTenantsPromise,
|
||||
]);
|
||||
|
||||
const accounts = promiseAllValues[0].data;
|
||||
const msTeamsTenants = promiseAllValues[1].data;
|
||||
|
||||
const appAccounts = accounts.filter(acc => (
|
||||
acc.device_calling_application_sid === applicationToDelete.sid
|
||||
));
|
||||
const appMsTeamsTenants = msTeamsTenants.filter(tenant => (
|
||||
tenant.application_sid === applicationToDelete.sid
|
||||
));
|
||||
let errorMessages = [];
|
||||
for (const account of appAccounts) {
|
||||
errorMessages.push(`Account: ${account.name}`);
|
||||
}
|
||||
for (const tenant of appMsTeamsTenants) {
|
||||
errorMessages.push(`Microsoft Teams Tenant: ${tenant.tenant_fqdn}`);
|
||||
}
|
||||
if (errorMessages.length) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p style={{ margin: '0.5rem 0' }}>
|
||||
This application cannot be deleted because it is in use by:
|
||||
</p>
|
||||
<ul style={{ margin: '0.5rem 0' }}>
|
||||
{errorMessages.map((err, i) => (
|
||||
<li key={i}>{err}</li>
|
||||
))}
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// Delete application
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Applications/${applicationToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete application');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate
|
||||
title="Applications"
|
||||
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"
|
||||
getContent={getApplications}
|
||||
columns={[
|
||||
{ header: 'Name', key: 'name' },
|
||||
{ header: 'Account', key: 'account' },
|
||||
{ header: 'Calling Webhook', key: 'call_hook_url' },
|
||||
{ header: 'Call Status Webhook', key: 'status_hook_url' },
|
||||
]}
|
||||
formatContentToDelete={formatApplicationToDelete}
|
||||
deleteContent={deleteApplication}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplicationsList;
|
||||
@@ -1,30 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import CarrierForm from '../../forms/CarrierForm';
|
||||
|
||||
const CarriersAddEdit = () => {
|
||||
let { voip_carrier_sid } = useParams();
|
||||
const pageTitle = voip_carrier_sid ? 'Edit Carrier' : 'Add Carrier';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
breadcrumbs={[
|
||||
{ name: 'Carriers', url: '/internal/carriers' },
|
||||
{ name: pageTitle },
|
||||
]}
|
||||
>
|
||||
<CarrierForm
|
||||
type={voip_carrier_sid ? 'edit' : 'add'}
|
||||
voip_carrier_sid={voip_carrier_sid}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default CarriersAddEdit;
|
||||
@@ -1,282 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useEffect, useContext, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import sortSipGateways from '../../../helpers/sortSipGateways';
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import InputGroup from '../../../components/elements/InputGroup';
|
||||
import Select from '../../../components/elements/Select';
|
||||
import handleErrors from '../../../helpers/handleErrors';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const FilterLabel = styled.span`
|
||||
color: #231f20;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
margin-left: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const CarriersList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const location = useLocation();
|
||||
const locationAccountSid = new URLSearchParams(location.search).get('account_sid');
|
||||
|
||||
const [accountSid, setAccountSid] = useState('');
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Carriers | Jambonz | Open Source CPAAS`;
|
||||
}, []);
|
||||
|
||||
//=============================================================================
|
||||
// Get accounts
|
||||
//=============================================================================
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
try {
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (locationAccountSid) {
|
||||
setAccountSid(locationAccountSid);
|
||||
}
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
//=============================================================================
|
||||
// Get sip trunks
|
||||
//=============================================================================
|
||||
const getCarriers = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(!currentServiceProvider) return [];
|
||||
// Get all SIP trunks
|
||||
const trunkResults = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/VoipCarriers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const trunkResultsFiltered = accountSid ?
|
||||
trunkResults.data.filter(t => t.account_sid === accountSid) :
|
||||
trunkResults.data.filter(t => t.account_sid === null);
|
||||
|
||||
// Add appropriate gateways to each trunk
|
||||
const trunkMap = {};
|
||||
for (const t of trunkResultsFiltered) {
|
||||
const gws = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/SipGateways?voip_carrier_sid=${t.voip_carrier_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
trunkMap[t.voip_carrier_sid] = gws.data;
|
||||
}
|
||||
|
||||
const trunksWithGateways = trunkResultsFiltered.map(t => {
|
||||
const gateways = trunkMap[t.voip_carrier_sid] || [];
|
||||
sortSipGateways(gateways);
|
||||
return {
|
||||
...t,
|
||||
gateways,
|
||||
};
|
||||
});
|
||||
|
||||
const simplifiedCarriers = trunksWithGateways.map(t => ({
|
||||
sid: t.voip_carrier_sid,
|
||||
name: t.name,
|
||||
status: t.is_active === 1 ? "active" : "inactive",
|
||||
gatewaysConcat: `${
|
||||
t.gateways.filter((item) => item.inbound === 1).length
|
||||
} inbound, ${
|
||||
t.gateways.filter((item) => item.outbound === 1).length
|
||||
} outbound`,
|
||||
gatewaysList: t.gateways.map(g => `${g.ipv4}:${g.port}`),
|
||||
gatewaysSid: t.gateways.map(g => g.sip_gateway_sid),
|
||||
}));
|
||||
return(simplifiedCarriers);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get SIP trunk data',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Delete sip trunk
|
||||
//=============================================================================
|
||||
const formatCarrierToDelete = trunk => {
|
||||
const gatewayName = trunk.gatewaysList.length > 1
|
||||
? 'SIP Gateways:'
|
||||
: 'SIP Gateway:';
|
||||
|
||||
return [
|
||||
{ name: 'Name:', content: trunk.name || '[none]' },
|
||||
{ name: 'Status:', content: trunk.status || '[none]' },
|
||||
{ name: gatewayName, content: trunk.gatewaysConcat || '[none]' },
|
||||
];
|
||||
};
|
||||
const deleteCarrier = async carrierToDelete => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
// delete associated gateways
|
||||
for (const sid of carrierToDelete.gatewaysSid) {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/SipGateways/${sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
// delete sip trunk
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/VoipCarriers/${carrierToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete SIP trunk');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate
|
||||
title="Carriers"
|
||||
addButtonText="Add a Carrier"
|
||||
addButtonLink="/internal/carriers/add"
|
||||
>
|
||||
<StyledInputGroup flexEnd space>
|
||||
<FilterLabel htmlFor="account">Used By:</FilterLabel>
|
||||
<AccountSelect
|
||||
name="account"
|
||||
id="account"
|
||||
value={accountSid}
|
||||
onChange={e => setAccountSid(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
All accounts
|
||||
</option>
|
||||
{accountList.map((acc) => (
|
||||
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
|
||||
))}
|
||||
</AccountSelect>
|
||||
</StyledInputGroup>
|
||||
<TableContent
|
||||
name="Carrier"
|
||||
urlParam="carriers"
|
||||
getContent={getCarriers}
|
||||
columns={[
|
||||
{ header: 'Name', key: 'name' },
|
||||
{ header: 'Status', key: 'status' },
|
||||
{ header: 'Gateways', key: 'gatewaysConcat' },
|
||||
]}
|
||||
formatContentToDelete={formatCarrierToDelete}
|
||||
deleteContent={deleteCarrier}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default CarriersList;
|
||||
@@ -1,29 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import MsTeamsTenantForm from '../../forms/MsTeamsTenantForm';
|
||||
|
||||
const MsTeamsTenantsAddEdit = () => {
|
||||
let { ms_teams_tenant_sid } = useParams();
|
||||
const pageTitle = ms_teams_tenant_sid ? 'Edit Microsoft Teams Tenant' : 'Add Microsoft Teams Tenant';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
breadcrumbs={[
|
||||
{ name: 'Microsoft Teams Tenants', url: '/internal/ms-teams-tenants' },
|
||||
{ name: pageTitle },
|
||||
]}
|
||||
>
|
||||
<MsTeamsTenantForm
|
||||
type={ms_teams_tenant_sid ? 'edit' : 'add'}
|
||||
ms_teams_tenant_sid={ms_teams_tenant_sid}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default MsTeamsTenantsAddEdit;
|
||||
@@ -1,168 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const MsTeamsTenantsList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
useEffect(() => {
|
||||
document.title = `Microsoft Teams Tenants | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
//=============================================================================
|
||||
// Get data
|
||||
//=============================================================================
|
||||
const getMsTeamsTenants = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const msTeamsTenantsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/MicrosoftTeamsTenants',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const promiseAllValues = await Promise.all([
|
||||
msTeamsTenantsPromise,
|
||||
accountsPromise,
|
||||
applicationsPromise,
|
||||
]);
|
||||
const msTeamsTenants = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[1].data;
|
||||
const applications = promiseAllValues[2].data;
|
||||
|
||||
const combinedData = msTeamsTenants.map(team => {
|
||||
const account = accounts.filter(a => a.account_sid === team.account_sid );
|
||||
const application = applications.filter(a => a.application_sid === team.application_sid);
|
||||
return {
|
||||
sid: team.ms_teams_tenant_sid,
|
||||
tenant_fqdn: team.tenant_fqdn,
|
||||
account: account[0] && account[0].name,
|
||||
application: application[0] && application[0].name,
|
||||
};
|
||||
});
|
||||
return(combinedData);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get Microsoft Teams Tenant data',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Delete Microsoft Teams Tenant
|
||||
//=============================================================================
|
||||
const formatTenantsToDelete = team => {
|
||||
return [
|
||||
{ name: 'Domain Name:', content: team.tenant_fqdn || '[none]' },
|
||||
{ name: 'Account:', content: team.account || '[none]' },
|
||||
{ name: 'Application:', content: team.application || '[none]' },
|
||||
];
|
||||
};
|
||||
const deleteTenant = async tenant => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/MicrosoftTeamsTenants/${tenant.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete Microsoft Teams Tenant');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate
|
||||
title="Microsoft Teams Tenants"
|
||||
addButtonText="Add a Microsoft Teams Tenant"
|
||||
addButtonLink="/internal/ms-teams-tenants/add"
|
||||
>
|
||||
<TableContent
|
||||
name="tenant"
|
||||
urlParam="ms-teams-tenants"
|
||||
getContent={getMsTeamsTenants}
|
||||
columns={[
|
||||
{ header: 'Domain Name', key: 'tenant_fqdn' },
|
||||
{ header: 'Account', key: 'account' },
|
||||
{ header: 'Application', key: 'application' },
|
||||
]}
|
||||
formatContentToDelete={formatTenantsToDelete}
|
||||
deleteContent={deleteTenant}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default MsTeamsTenantsList;
|
||||
@@ -1,29 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import PhoneNumberForm from '../../forms/PhoneNumberForm';
|
||||
|
||||
const PhoneNumbersAddEdit = () => {
|
||||
let { phone_number_sid } = useParams();
|
||||
const pageTitle = phone_number_sid ? 'Edit Phone Number' : 'Add Phone Number';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
breadcrumbs={[
|
||||
{ name: 'Phone Numbers', url: '/internal/phone-numbers' },
|
||||
{ name: pageTitle },
|
||||
]}
|
||||
>
|
||||
<PhoneNumberForm
|
||||
type={phone_number_sid ? 'edit' : 'add'}
|
||||
phone_number_sid={phone_number_sid}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhoneNumbersAddEdit;
|
||||
@@ -1,259 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import phoneNumberFormat from '../../../helpers/phoneNumberFormat';
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const PhoneNumbersList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Phone Number Routing | Jambonz | Open Source CPAAS`;
|
||||
}, []);
|
||||
|
||||
//=============================================================================
|
||||
// Get phone numbers
|
||||
//=============================================================================
|
||||
const getPhoneNumbers = useCallback(async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(!currentServiceProvider) return [];
|
||||
const phoneNumbersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/PhoneNumbers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Applications`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const sipTrunksPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/VoipCarriers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const promiseAllValues = await Promise.all([
|
||||
phoneNumbersPromise,
|
||||
accountsPromise,
|
||||
applicationsPromise,
|
||||
sipTrunksPromise,
|
||||
]);
|
||||
const phoneNumbers = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[1].data;
|
||||
const applications = promiseAllValues[2].data;
|
||||
const sipTrunks = promiseAllValues[3].data;
|
||||
|
||||
// sort all applications and store to state for use in bulk editing
|
||||
const allApplications = [...applications, ];
|
||||
allApplications.sort((a, b) => {
|
||||
let valA = (a.name && a.name.toLowerCase()) || '';
|
||||
let valB = (b.name && b.name.toLowerCase()) || '';
|
||||
const result = valA > valB ? 1 : valA < valB ? -1 : 0;
|
||||
return result;
|
||||
});
|
||||
const applicationsForBulk = allApplications.map(app => ({
|
||||
name: app.name,
|
||||
application_sid: app.application_sid,
|
||||
}));
|
||||
applicationsForBulk.push({
|
||||
name: '- None -',
|
||||
application_sid: null,
|
||||
});
|
||||
setApplications(applicationsForBulk);
|
||||
|
||||
const combinedData = phoneNumbers.map((p, i) => {
|
||||
const account = accounts.filter(a => a.account_sid === p.account_sid );
|
||||
const application = applications.filter(a => a.application_sid === p.application_sid );
|
||||
const sipTrunk = sipTrunks.filter(a => a.voip_carrier_sid === p.voip_carrier_sid );
|
||||
return {
|
||||
sid: p.phone_number_sid,
|
||||
number: phoneNumberFormat(p.number),
|
||||
account: account[0] && account[0].name,
|
||||
application: application[0] && application[0].name,
|
||||
sipTrunk: sipTrunk[0] && sipTrunk[0].name,
|
||||
};
|
||||
});
|
||||
return(combinedData);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get phone number data',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
}
|
||||
}, [currentServiceProvider, dispatch, history]);
|
||||
|
||||
//=============================================================================
|
||||
// Delete phone number
|
||||
//=============================================================================
|
||||
const formatPhoneNumberToDelete = p => {
|
||||
return [
|
||||
{ name: 'Number:', content: p.number || '[none]' },
|
||||
{ name: 'SIP Trunk:', content: p.sipTrunk || '[none]' },
|
||||
{ name: 'Account:', content: p.account || '[none]' },
|
||||
{ name: 'Application:', content: p.application || '[none]' },
|
||||
];
|
||||
};
|
||||
const deletePhoneNumber = async phoneNumber => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/PhoneNumbers/${phoneNumber.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete phone number');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Bulk Edit Applications
|
||||
//=============================================================================
|
||||
const [ applications, setApplications ] = useState([]);
|
||||
const handleBulkEditApplications = async (phoneNumberSids, application) => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
for (const sid of phoneNumberSids) {
|
||||
await axios({
|
||||
method: 'put',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/PhoneNumbers/${sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data: {
|
||||
application_sid: application.application_sid,
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
} 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);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate
|
||||
title="Phone Number Routing"
|
||||
addButtonText="Add a Phone Number"
|
||||
addButtonLink="/internal/phone-numbers/add"
|
||||
>
|
||||
<TableContent
|
||||
withCheckboxes
|
||||
name="phone number"
|
||||
urlParam="phone-numbers"
|
||||
getContent={getPhoneNumbers}
|
||||
columns={[
|
||||
{ header: 'Number', key: 'number' },
|
||||
{ header: 'SIP Trunk', key: 'sipTrunk' },
|
||||
{ header: 'Account', key: 'account' },
|
||||
{ header: 'Application', key: 'application' },
|
||||
]}
|
||||
formatContentToDelete={formatPhoneNumberToDelete}
|
||||
deleteContent={deletePhoneNumber}
|
||||
bulkMenuItems={applications}
|
||||
bulkAction={handleBulkEditApplications}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhoneNumbersList;
|
||||
@@ -1,494 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
import styled from "styled-components/macro";
|
||||
import { NotificationDispatchContext } from "../../../contexts/NotificationContext";
|
||||
import InternalTemplate from "../../templates/InternalTemplate";
|
||||
import AntdTable from "../../../components/blocks/AntdTable";
|
||||
import phoneNumberFormat from "../../../helpers/phoneNumberFormat";
|
||||
import timeFormat from "../../../helpers/timeFormat";
|
||||
import Label from "../../../components/elements/Label";
|
||||
import Button from "../../../components/elements/Button";
|
||||
import InputGroup from "../../../components/elements/InputGroup";
|
||||
import Select from "../../../components/elements/Select";
|
||||
import handleErrors from "../../../helpers/handleErrors";
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const FilterLabel = styled.span`
|
||||
color: #231f20;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
margin-left: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const ExpandedSection = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
& > span {
|
||||
height: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const StyledPcapLink = styled.a`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
outline: 0;
|
||||
height: 36px;
|
||||
padding: 10px 26px 8px;
|
||||
border-radius: 0.25rem;
|
||||
background: #D91C5C;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
background: #BD164E;
|
||||
}
|
||||
`;
|
||||
|
||||
const PcapButton = ({call_data, account_sid, jwt_token}) => {
|
||||
const [pcap, setPcap] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${account_sid}/RecentCalls/${call_data.sip_callid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt_token}`,
|
||||
},
|
||||
}).then((result_1) => {
|
||||
if (result_1.status === 200 && result_1.data.total > 0) {
|
||||
axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${account_sid}/RecentCalls/${call_data.sip_callid}/pcap`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt_token}`,
|
||||
},
|
||||
responseType: "blob",
|
||||
}).then((result_2) => {
|
||||
setPcap({
|
||||
dataUrl: URL.createObjectURL(result_2.data),
|
||||
fileName: `callid-${call_data.sip_callid}.pcap`,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [call_data, account_sid, jwt_token, setPcap]);
|
||||
|
||||
if (pcap) {
|
||||
return (
|
||||
<Label>
|
||||
<StyledPcapLink
|
||||
href={pcap.dataUrl}
|
||||
download={pcap.fileName}
|
||||
>
|
||||
Download pcap
|
||||
</StyledPcapLink>
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const RecentCallsIndex = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const jwt = localStorage.getItem("token");
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
|
||||
// Table props
|
||||
const [recentCallsData, setRecentCallsData] = useState([]);
|
||||
const [rawData, setRawData] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowCount, setRowCount] = useState(25);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
|
||||
|
||||
// Filter values
|
||||
const [account, setAccount] = useState("");
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
const [attemptedAt, setAttemptedAt] = useState("today");
|
||||
const [dirFilter, setDirFilter] = useState("io");
|
||||
const [answered, setAnswered] = useState("all");
|
||||
// width
|
||||
const { height } = window.screen;
|
||||
|
||||
//=============================================================================
|
||||
// Define Table props
|
||||
//=============================================================================
|
||||
const Columns = [
|
||||
{
|
||||
title: "Date",
|
||||
dataIndex: "attempted_at",
|
||||
key: "attempted_at",
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
title: "Direction",
|
||||
dataIndex: "direction",
|
||||
key: "direction",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "From",
|
||||
dataIndex: "from",
|
||||
key: "from",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "To",
|
||||
dataIndex: "to",
|
||||
key: "to",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "Trunk",
|
||||
dataIndex: "trunk",
|
||||
key: "trunk",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "Duration",
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
width: 150,
|
||||
},
|
||||
];
|
||||
|
||||
//=============================================================================
|
||||
// Get recent calls
|
||||
//=============================================================================
|
||||
const handleFilterChange = () => {
|
||||
let filter = {
|
||||
page: currentPage,
|
||||
count: rowCount,
|
||||
};
|
||||
if (attemptedAt) {
|
||||
switch (attemptedAt) {
|
||||
case "today":
|
||||
filter.start = moment().startOf("date").toISOString();
|
||||
break;
|
||||
case "7d":
|
||||
filter.days = 7;
|
||||
break;
|
||||
case "14d":
|
||||
filter.days = 14;
|
||||
break;
|
||||
case "30d":
|
||||
filter.days = 30;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if (dirFilter === "inbound") {
|
||||
filter.direction = "inbound";
|
||||
} else if (dirFilter === "outbound") {
|
||||
filter.direction = "outbound";
|
||||
}
|
||||
|
||||
if (answered && answered !== "all") {
|
||||
filter.answered = answered === "answered" ? "true" : "false";
|
||||
}
|
||||
|
||||
getRecentCallsData(filter);
|
||||
};
|
||||
|
||||
const getRecentCallsData = async (filter = {}) => {
|
||||
let isMounted = true;
|
||||
|
||||
if (!account) {
|
||||
setRecentCallsData([]);
|
||||
setTotalCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Accounts/${account}/RecentCalls`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
params: {
|
||||
...filter,
|
||||
},
|
||||
});
|
||||
|
||||
if (isMounted) {
|
||||
const { total, data } = result.data;
|
||||
|
||||
setRawData([...data]);
|
||||
|
||||
const recentCalls = data.map((item, index) => ({
|
||||
key: index,
|
||||
...item,
|
||||
attempted_at: item.attempted_at
|
||||
? moment(item.attempted_at).format("YYYY MM.DD hh:mm a")
|
||||
: "",
|
||||
from: phoneNumberFormat(item.from),
|
||||
to: phoneNumberFormat(item.to),
|
||||
status: item.answered ? "answered" : item.termination_reason,
|
||||
duration: timeFormat(item.duration),
|
||||
trace_id: item.trace_id
|
||||
}));
|
||||
|
||||
setRecentCallsData(recentCalls);
|
||||
setTotalCount(total);
|
||||
setExpandedRowKeys([]);
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderExpandedRow = (data) => {
|
||||
const fields = [
|
||||
"direction",
|
||||
"attempted_at",
|
||||
"answered_at",
|
||||
"terminated_at",
|
||||
"duration",
|
||||
"answered",
|
||||
"from",
|
||||
"to",
|
||||
"termination_reason",
|
||||
"call_sid",
|
||||
"sip_callid",
|
||||
"host",
|
||||
"remote_host",
|
||||
"sip_status",
|
||||
"trunk",
|
||||
"trace_id"
|
||||
];
|
||||
|
||||
return (
|
||||
<ExpandedSection>
|
||||
{fields.map((field, index) => {
|
||||
if (!rawData || !rawData[data.key]) {
|
||||
return null;
|
||||
}
|
||||
let label = rawData[data.key][field];
|
||||
|
||||
if (typeof label === "boolean") {
|
||||
label = label ? "true" : "false";
|
||||
}
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<Label>{`${field}:`}</Label>
|
||||
<Label>{label}</Label>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<PcapButton call_data={data} account_sid={account} jwt_token={jwt} />
|
||||
</ExpandedSection>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPagination = (page, type, originElement) => {
|
||||
let node = originElement;
|
||||
|
||||
switch (type) {
|
||||
case "page":
|
||||
node = <StyledButton gray={currentPage !== page}>{page}</StyledButton>;
|
||||
break;
|
||||
case "prev":
|
||||
node = <StyledButton>{`<`}</StyledButton>;
|
||||
break;
|
||||
case "next":
|
||||
node = <StyledButton>{`>`}</StyledButton>;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
const handleExpandChange = (expanded, record) => {
|
||||
if (expanded) {
|
||||
setExpandedRowKeys((prev) => [...prev, record.key]);
|
||||
} else {
|
||||
setExpandedRowKeys((prev) => [
|
||||
...prev.filter((item) => item !== record.key),
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage === 1) {
|
||||
handleFilterChange();
|
||||
} else {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [account, attemptedAt, dirFilter, answered]);
|
||||
|
||||
useEffect(() => {
|
||||
handleFilterChange();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPage, rowCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
let isMounted = true;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
if (accountResponse.data.length > 0) {
|
||||
setAccount(accountResponse.data[0].account_sid);
|
||||
} else {
|
||||
setAccount("");
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate title="Recent Calls">
|
||||
<StyledInputGroup flexEnd space>
|
||||
<FilterLabel htmlFor="account">Account:</FilterLabel>
|
||||
<AccountSelect
|
||||
name="account"
|
||||
id="account"
|
||||
value={account}
|
||||
onChange={(e) => setAccount(e.target.value)}
|
||||
>
|
||||
{accountList.map((acc) => (
|
||||
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
|
||||
))}
|
||||
</AccountSelect>
|
||||
<FilterLabel htmlFor="daterange">Date:</FilterLabel>
|
||||
<Select
|
||||
name="daterange"
|
||||
id="daterange"
|
||||
value={attemptedAt}
|
||||
onChange={(e) => setAttemptedAt(e.target.value)}
|
||||
>
|
||||
<option value="today">today</option>
|
||||
<option value="7d">last 7d</option>
|
||||
<option value="14d">last 14d</option>
|
||||
<option value="30d">last 30d</option>
|
||||
</Select>
|
||||
<FilterLabel htmlFor="direction">Direction:</FilterLabel>
|
||||
<Select
|
||||
name="direction"
|
||||
id="direction"
|
||||
value={dirFilter}
|
||||
onChange={(e) => setDirFilter(e.target.value)}
|
||||
>
|
||||
<option value="io">either</option>
|
||||
<option value="inbound">inbound only</option>
|
||||
<option value="outbound">outbound only</option>
|
||||
</Select>
|
||||
<FilterLabel htmlFor="status">Status:</FilterLabel>
|
||||
<Select
|
||||
name="status"
|
||||
id="status"
|
||||
value={answered}
|
||||
onChange={(e) => setAnswered(e.target.value)}
|
||||
>
|
||||
<option value="all">all</option>
|
||||
<option value="answered">answered</option>
|
||||
<option value="not-answered">not answered</option>
|
||||
</Select>
|
||||
</StyledInputGroup>
|
||||
<AntdTable
|
||||
dataSource={recentCallsData}
|
||||
columns={Columns}
|
||||
rowKey="key"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: ["bottomCenter"],
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(page);
|
||||
setRowCount(size);
|
||||
},
|
||||
showTotal: (total) => `Total: ${total} records`,
|
||||
current: currentPage,
|
||||
total: totalCount,
|
||||
pageSize: rowCount,
|
||||
pageSizeOptions: [25, 50, 100],
|
||||
showSizeChanger: true,
|
||||
itemRender: renderPagination,
|
||||
showLessItems: true,
|
||||
}}
|
||||
scroll={{ y: Math.max(height - 580, 200) }}
|
||||
expandable={{
|
||||
expandedRowRender: renderExpandedRow,
|
||||
}}
|
||||
expandedRowKeys={expandedRowKeys}
|
||||
onExpand={handleExpandChange}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecentCallsIndex;
|
||||
@@ -1,225 +0,0 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import SettingsForm from '../../forms/SettingsForm';
|
||||
import TableContent from '../../blocks/TableContent.js';
|
||||
import axios from 'axios';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
|
||||
|
||||
const Settings = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
let service_provider_sid = useContext(ServiceProviderValueContext);
|
||||
const pageTitle = 'Settings';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
//=============================================================================
|
||||
// Get API keys
|
||||
//=============================================================================
|
||||
const getApiKeys = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const results = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${service_provider_sid}/ApiKeys`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const simplifiedApiKeys = results.data.map(a => {
|
||||
const { token } = a;
|
||||
const maskLength = token.length - 4;
|
||||
const maskedPortion = token.substring(0, maskLength).replace(/[a-zA-Z0-9]/g, '*');
|
||||
const revealedPortion = token.substring(maskLength);
|
||||
const maskedToken = `${maskedPortion}${revealedPortion}`;
|
||||
|
||||
const { last_used } = a;
|
||||
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: a.api_key_sid,
|
||||
token: {
|
||||
type: 'masked',
|
||||
masked: maskedToken,
|
||||
revealed: token,
|
||||
},
|
||||
last_used: {
|
||||
type: 'normal',
|
||||
content: lastUsedString,
|
||||
},
|
||||
};
|
||||
});
|
||||
return (simplifiedApiKeys);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get API key data',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Create API key
|
||||
//=============================================================================
|
||||
const createApiKey = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const result = await axios({
|
||||
method: 'post',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/ApiKeys',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data: {
|
||||
"service_provider_sid": service_provider_sid,
|
||||
}
|
||||
});
|
||||
return result.data.token;
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to create API key',
|
||||
});
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Delete API key
|
||||
//=============================================================================
|
||||
const formatApiKeyToDelete = apiKey => {
|
||||
const items = [
|
||||
{ name: 'API Key:', content: apiKey.token.masked || '[none]' },
|
||||
{ name: 'Last Used:', content: apiKey.last_used.content || 'Never used' },
|
||||
];
|
||||
return items;
|
||||
};
|
||||
const deleteApiKey = async apiKeyToDelete => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Apikeys/${apiKeyToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.log(err.response || err);
|
||||
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete API key');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
breadcrumbs={[
|
||||
{ name: 'Service Provider', url: '/internal/settings' },
|
||||
{ name: pageTitle },
|
||||
]}
|
||||
additionalTable={service_provider_sid && (
|
||||
<TableContent
|
||||
name="API key"
|
||||
getContent={getApiKeys}
|
||||
columns={[
|
||||
{ header: 'Service Provider API Keys', key: 'token', width: '27rem', fontWeight: 'normal' },
|
||||
{ header: 'Last Used', key: 'last_used', width: '10rem' },
|
||||
]}
|
||||
addContent={createApiKey}
|
||||
formatContentToDelete={formatApiKeyToDelete}
|
||||
deleteContent={deleteApiKey}
|
||||
rowsHaveDeleteButtons
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<SettingsForm />
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -1,29 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import SpeechForm from '../../forms/SpeechForm';
|
||||
|
||||
const SpeechServicesAddEdit = () => {
|
||||
let { speech_service_sid } = useParams();
|
||||
const pageTitle = speech_service_sid ? 'Edit Speech Service' : 'Add Speech Service';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
breadcrumbs={[
|
||||
{ name: 'Speech Services', url: '/internal/speech-services' },
|
||||
{ name: pageTitle },
|
||||
]}
|
||||
>
|
||||
<SpeechForm
|
||||
type={speech_service_sid ? 'edit' : 'add'}
|
||||
speech_service_sid={speech_service_sid}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeechServicesAddEdit;
|
||||
@@ -1,309 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import handleErrors from '../../../helpers/handleErrors';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import TableContent from '../../../components/blocks/TableContent';
|
||||
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
|
||||
import InputGroup from '../../../components/elements/InputGroup';
|
||||
import Select from '../../../components/elements/Select';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const FilterLabel = styled.span`
|
||||
color: #231f20;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
margin-left: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled(InputGroup)`
|
||||
padding: 1rem 1rem 0;
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccountSelect = styled(Select)`
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
const SpeechServicesList = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
const currentServiceProvider = useContext(ServiceProviderValueContext);
|
||||
const jwt = localStorage.getItem('token');
|
||||
const location = useLocation();
|
||||
const locationAccountSid = new URLSearchParams(location.search).get('account_sid');
|
||||
|
||||
const [accountSid, setAccountSid] = useState('');
|
||||
const [accountList, setAccountList] = useState([]);
|
||||
|
||||
//=============================================================================
|
||||
// Get accounts
|
||||
//=============================================================================
|
||||
useEffect(() => {
|
||||
if (currentServiceProvider) {
|
||||
const getAccounts = async () => {
|
||||
try {
|
||||
const accountResponse = await axios({
|
||||
method: "get",
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
if (locationAccountSid) {
|
||||
setAccountSid(locationAccountSid);
|
||||
}
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch });
|
||||
}
|
||||
};
|
||||
|
||||
getAccounts();
|
||||
} else {
|
||||
setAccountList([]);
|
||||
}
|
||||
}, [currentServiceProvider]);
|
||||
|
||||
//=============================================================================
|
||||
// Get speech services
|
||||
//=============================================================================
|
||||
const getSpeechServices = async () => {
|
||||
try {
|
||||
if (!jwt) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!currentServiceProvider) return [];
|
||||
|
||||
const speechApiUrl = accountSid ?
|
||||
`/Accounts/${accountSid}/SpeechCredentials` :
|
||||
`/ServiceProviders/${currentServiceProvider}/SpeechCredentials`;
|
||||
const speechServices = await axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: speechApiUrl,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
|
||||
const credentialTestPromises = speechServices.data.map(s => {
|
||||
if (s.use_for_stt || s.use_for_tts) {
|
||||
return axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${s.speech_credential_sid}/test`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const testResposes = await Promise.all(credentialTestPromises);
|
||||
|
||||
const cleanedUpSpeechServices = speechServices.data.map((s, i) => {
|
||||
const testResults = testResposes[i] && testResposes[i].data;
|
||||
|
||||
let content = null;
|
||||
let title = null;
|
||||
|
||||
if (s.use_for_tts && s.use_for_stt) {
|
||||
|
||||
if (testResults.tts.status === 'ok' && testResults.stt.status === 'ok') {
|
||||
content = 'ok';
|
||||
title = 'Connection test successful';
|
||||
} else {
|
||||
content = 'fail';
|
||||
|
||||
if (testResults.tts.reason && testResults.stt.reason) {
|
||||
|
||||
if (testResults.tts.reason === testResults.stt.reason) {
|
||||
title = testResults.tts.reason;
|
||||
} else {
|
||||
title = `TTS: ${testResults.tts.reason}. STT: ${testResults.stt.reason}`;
|
||||
}
|
||||
|
||||
} else if (testResults.tts.reason) {
|
||||
title = `TTS: ${testResults.tts.reason}`;
|
||||
|
||||
} else if (testResults.stt.reason) {
|
||||
title = `STT: ${testResults.stt.reason}`;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (s.use_for_tts) {
|
||||
|
||||
content = testResults.tts.status;
|
||||
title = testResults.tts.status === 'ok'
|
||||
? 'Connection test successful'
|
||||
: testResults.tts.reason;
|
||||
|
||||
} else if (s.use_for_stt) {
|
||||
|
||||
content = testResults.stt.status;
|
||||
title = testResults.stt.status === 'ok'
|
||||
? 'Connection test successful'
|
||||
: testResults.stt.reason;
|
||||
|
||||
}
|
||||
|
||||
const { last_used } = s;
|
||||
let lastUsedString = 'Never used';
|
||||
if (last_used) {
|
||||
const currentDate = new Date();
|
||||
const lastUsedDate = new Date(last_used);
|
||||
currentDate.setHours(0,0,0,0);
|
||||
lastUsedDate.setHours(0,0,0,0);
|
||||
const daysDifference = Math.round((currentDate - lastUsedDate) / 1000 / 60 / 60 / 24);
|
||||
lastUsedString = daysDifference > 1
|
||||
? `${daysDifference} days ago`
|
||||
: daysDifference === 1
|
||||
? 'Yesterday'
|
||||
: daysDifference === 0
|
||||
? 'Today'
|
||||
: 'Never used';
|
||||
}
|
||||
return {
|
||||
sid: s.speech_credential_sid,
|
||||
vendor: s.vendor,
|
||||
usage: (s.use_for_tts && s.use_for_stt) ? 'TTS/STT'
|
||||
: s.use_for_tts ? 'TTS'
|
||||
: s.use_for_stt ? 'STT'
|
||||
: 'Not in use',
|
||||
last_used: lastUsedString,
|
||||
status: {
|
||||
type: 'status',
|
||||
content,
|
||||
title,
|
||||
},
|
||||
};
|
||||
});
|
||||
return(cleanedUpSpeechServices);
|
||||
} catch (err) {
|
||||
handleErrors({ err, history, dispatch, fallbackMessage: 'Unable to get speech services' });
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Delete speech service
|
||||
//=============================================================================
|
||||
const formatSpeechServiceToDelete = s => {
|
||||
return [
|
||||
{ name: 'Vendor', content: s.vendor || '[none]' },
|
||||
{ name: 'Usage', content: s.usage || '[none]' },
|
||||
{ name: 'Last Used', content: s.last_used || 'Never' },
|
||||
];
|
||||
};
|
||||
const deleteSpeechService = async speechServiceToDelete => {
|
||||
try {
|
||||
if (!jwt) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete speech service
|
||||
await axios({
|
||||
method: 'delete',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials/${speechServiceToDelete.sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again.',
|
||||
});
|
||||
} else {
|
||||
console.error(err.response || err);
|
||||
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete speech service');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Render
|
||||
//=============================================================================
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="normalTable"
|
||||
title="Speech Services"
|
||||
addButtonText="Add Speech Service"
|
||||
addButtonLink="/internal/speech-services/add"
|
||||
>
|
||||
<StyledInputGroup flexEnd space>
|
||||
<FilterLabel htmlFor="account">Used By:</FilterLabel>
|
||||
<AccountSelect
|
||||
name="account"
|
||||
id="account"
|
||||
value={accountSid}
|
||||
onChange={e => setAccountSid(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
All accounts
|
||||
</option>
|
||||
{accountList.map((acc) => (
|
||||
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
|
||||
))}
|
||||
</AccountSelect>
|
||||
</StyledInputGroup>
|
||||
<TableContent
|
||||
normalTable
|
||||
name="speech service"
|
||||
urlParam="speech-services"
|
||||
getContent={getSpeechServices}
|
||||
columns={[
|
||||
{ header: 'Vendor', key: 'vendor', bold: true },
|
||||
{ header: 'Usage', key: 'usage', },
|
||||
{ header: 'Last Used', key: 'last_used', },
|
||||
{ header: 'Status', key: 'status', textAlign: 'center' },
|
||||
]}
|
||||
formatContentToDelete={formatSpeechServiceToDelete}
|
||||
deleteContent={deleteSpeechService}
|
||||
/>
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeechServicesList;
|
||||
@@ -1,22 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import SetupTemplate from '../../templates/SetupTemplate';
|
||||
import AccountForm from '../../forms/AccountForm';
|
||||
|
||||
const ConfigureAccount = () => {
|
||||
useEffect(() => {
|
||||
document.title = `Configure Account | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
return (
|
||||
<SetupTemplate
|
||||
wide
|
||||
title="Configure Account"
|
||||
progress={1}
|
||||
>
|
||||
<AccountForm
|
||||
type="setup"
|
||||
/>
|
||||
</SetupTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigureAccount;
|
||||
@@ -1,24 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import SetupTemplate from '../../templates/SetupTemplate';
|
||||
import CarrierForm from '../../forms/CarrierForm';
|
||||
import Sbcs from '../../blocks/Sbcs';
|
||||
|
||||
const ConfigureSipTrunk = () => {
|
||||
useEffect(() => {
|
||||
document.title = `Configure SIP Trunk | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
return (
|
||||
<SetupTemplate
|
||||
wide
|
||||
title="Configure SIP Trunk"
|
||||
subtitle={<Sbcs centered />}
|
||||
progress={3}
|
||||
>
|
||||
<CarrierForm
|
||||
type="setup"
|
||||
/>
|
||||
</SetupTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigureSipTrunk;
|
||||
@@ -1,22 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import SetupTemplate from '../../templates/SetupTemplate';
|
||||
import ApplicationForm from '../../forms/ApplicationForm';
|
||||
|
||||
const CreateApplication = () => {
|
||||
useEffect(() => {
|
||||
document.title = `Create Application | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
return (
|
||||
<SetupTemplate
|
||||
wide
|
||||
title="Create Application"
|
||||
progress={2}
|
||||
>
|
||||
<ApplicationForm
|
||||
type="setup"
|
||||
/>
|
||||
</SetupTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateApplication;
|
||||
@@ -1,339 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import SetupTemplate from '../../templates/SetupTemplate';
|
||||
import Form from '../../elements/Form';
|
||||
import Button from '../../elements/Button';
|
||||
import Input from '../../elements/Input';
|
||||
import FormError from '../../blocks/FormError';
|
||||
import Loader from '../../blocks/Loader';
|
||||
import { APP_API_BASE_URL } from "../../../constants";
|
||||
|
||||
const CreatePassword = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
useEffect(() => {
|
||||
document.title = `Create Password | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
// Refs
|
||||
const refPassword = useRef(null);
|
||||
const refPasswordConfirm = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ password, setPassword ] = useState('');
|
||||
const [ passwordConfirm, setPasswordConfirm ] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidPassword, setInvalidPassword ] = useState(false);
|
||||
const [ invalidPasswordConfirm, setInvalidPasswordConfirm ] = useState(false);
|
||||
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
// Handle Password visibility
|
||||
// (not using PasswordInput because need to manage if password is visible here
|
||||
// because showing password hides passwordConfirm input)
|
||||
const [ showPassword, setShowPassword ] = useState(false);
|
||||
const toggleShowPassword = () => setShowPassword(!showPassword);
|
||||
|
||||
useEffect(() => {
|
||||
const getAPIData = async () => {
|
||||
try {
|
||||
if (!sessionStorage.getItem('user_sid')) {
|
||||
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'The create password page can only be used once.',
|
||||
});
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Get account data
|
||||
//-----------------------------------------------------------------------------
|
||||
const serviceProvidersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/serviceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const voipCarriersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: '/voipCarriers',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const promiseAllValues = await Promise.all([
|
||||
serviceProvidersPromise,
|
||||
accountsPromise,
|
||||
applicationsPromise,
|
||||
voipCarriersPromise,
|
||||
]);
|
||||
|
||||
const serviceProviders = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[1].data;
|
||||
const applications = promiseAllValues[2].data;
|
||||
const voipCarriers = promiseAllValues[3].data;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Determine where to route user
|
||||
//-----------------------------------------------------------------------------
|
||||
if (
|
||||
(serviceProviders.length > 1) ||
|
||||
(accounts.length > 1) ||
|
||||
(accounts.length < 1) ||
|
||||
(applications.length > 1) ||
|
||||
(voipCarriers.length > 0)
|
||||
) {
|
||||
history.push('/internal/accounts');
|
||||
return;
|
||||
}
|
||||
|
||||
// const { sip_realm, registration_hook } = accounts[0];
|
||||
|
||||
// if (
|
||||
// (!sip_realm || !registration_hook) &&
|
||||
// !applications.length
|
||||
// ) {
|
||||
// history.push('/configure-account');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!applications.length) {
|
||||
// history.push('/create-application');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!voipCarriers.length) {
|
||||
// history.push('/configure-sip-trunk');
|
||||
// return;
|
||||
// }
|
||||
|
||||
history.push('/internal/accounts');
|
||||
}
|
||||
} 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((err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.');
|
||||
console.log(err.response || err);
|
||||
}
|
||||
} finally {
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
getAPIData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async e => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidPassword(false);
|
||||
setInvalidPasswordConfirm(false);
|
||||
|
||||
if (!password) {
|
||||
setErrorMessage('Please provide a password');
|
||||
setInvalidPassword(true);
|
||||
if (!passwordConfirm) {
|
||||
setInvalidPasswordConfirm(true);
|
||||
}
|
||||
refPassword.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showPassword && !passwordConfirm) {
|
||||
setErrorMessage('Both fields are required');
|
||||
setInvalidPasswordConfirm(true);
|
||||
refPasswordConfirm.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showPassword && password !== passwordConfirm) {
|
||||
setErrorMessage('Passwords do not match');
|
||||
setInvalidPassword(true);
|
||||
setInvalidPasswordConfirm(true);
|
||||
refPassword.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(password.length < 6) ||
|
||||
(!/\d/.test(password)) ||
|
||||
(!/[a-zA-Z]/.test(password))
|
||||
) {
|
||||
setErrorMessage(
|
||||
<div>
|
||||
Password must:
|
||||
<ul>
|
||||
<li>Be at least 6 characters</li>
|
||||
<li>Contain at least one letter</li>
|
||||
<li>Contain at least one number</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
setInvalidPassword(true);
|
||||
setInvalidPasswordConfirm(true);
|
||||
refPassword.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const user_sid = sessionStorage.getItem('user_sid');
|
||||
const old_password = sessionStorage.getItem('old_password');
|
||||
|
||||
if (!old_password) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in again.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios({
|
||||
method: 'put',
|
||||
baseURL: APP_API_BASE_URL,
|
||||
url: `/Users/${user_sid}`,
|
||||
data: {
|
||||
old_password,
|
||||
new_password: password,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
}
|
||||
});
|
||||
|
||||
sessionStorage.removeItem('user_sid');
|
||||
sessionStorage.removeItem('old_password');
|
||||
|
||||
if (response.data.user_sid) {
|
||||
localStorage.setItem('token', response.data.token);
|
||||
}
|
||||
|
||||
// history.push('/configure-account');
|
||||
history.push('/internal/accounts');
|
||||
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
setErrorMessage('something went wrong, please log in and try again');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
showLoader ? (
|
||||
<Loader height="309px" />
|
||||
) : (
|
||||
<SetupTemplate
|
||||
title="Create Password"
|
||||
subtitle="You must create a new password"
|
||||
>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
large
|
||||
allowShowPassword={showPassword}
|
||||
showPassword={showPassword}
|
||||
toggleShowPassword={toggleShowPassword}
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="New Password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (!showPassword && e.getModifierState('CapsLock')) {
|
||||
setErrorMessage('CAPSLOCK is enabled!');
|
||||
} else {
|
||||
setErrorMessage('');
|
||||
}
|
||||
}}
|
||||
ref={refPassword}
|
||||
invalid={invalidPassword}
|
||||
autoFocus
|
||||
/>
|
||||
{!showPassword && (
|
||||
<Input
|
||||
large
|
||||
allowShowPassword
|
||||
showPassword={showPassword}
|
||||
toggleShowPassword={toggleShowPassword}
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
id="confirmPassword"
|
||||
placeholder="Confirm New Password"
|
||||
value={passwordConfirm}
|
||||
onChange={e => setPasswordConfirm(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (!showPassword && e.getModifierState('CapsLock')) {
|
||||
setErrorMessage('CAPSLOCK is enabled!');
|
||||
} else {
|
||||
setErrorMessage('');
|
||||
}
|
||||
}}
|
||||
ref={refPasswordConfirm}
|
||||
invalid={invalidPasswordConfirm}
|
||||
/>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<FormError message={errorMessage} />
|
||||
)}
|
||||
<Button
|
||||
large
|
||||
fullWidth
|
||||
>
|
||||
Create Password
|
||||
</Button>
|
||||
</Form>
|
||||
</SetupTemplate>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePassword;
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
|
||||
import SetupTemplate from '../../templates/SetupTemplate';
|
||||
import Button from '../../elements/Button';
|
||||
|
||||
const SetupComplete = () => {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
useEffect(() => {
|
||||
document.title = `Setup Complete | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
}
|
||||
}, [history, dispatch]);
|
||||
|
||||
return (
|
||||
<SetupTemplate
|
||||
title="Setup Complete!"
|
||||
progress={4}
|
||||
>
|
||||
<Button
|
||||
large
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
history.push('/internal/accounts');
|
||||
}}
|
||||
>
|
||||
Continue to account
|
||||
</Button>
|
||||
</SetupTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupComplete;
|
||||
@@ -0,0 +1,119 @@
|
||||
import React from "react";
|
||||
import { Icon } from "jambonz-ui";
|
||||
|
||||
import { Icons } from "../icons";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type PaginationProps = {
|
||||
pageNumber: number;
|
||||
setPageNumber: React.Dispatch<React.SetStateAction<number>>;
|
||||
maxPageNumber: number;
|
||||
|
||||
/** Sort of "secret" props... */
|
||||
nextTo?: number;
|
||||
jumpNum?: number;
|
||||
showMin?: number;
|
||||
};
|
||||
|
||||
export const Pagination = ({
|
||||
pageNumber,
|
||||
setPageNumber,
|
||||
maxPageNumber,
|
||||
nextTo = 1,
|
||||
jumpNum = 3,
|
||||
showMin = 4,
|
||||
}: PaginationProps) => {
|
||||
const handleSetPageNumber = (num: number) => {
|
||||
setPageNumber(Math.max(1, Math.min(maxPageNumber, num)));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pagination">
|
||||
<button
|
||||
type="button"
|
||||
disabled={pageNumber === 1}
|
||||
onClick={() => handleSetPageNumber(pageNumber - 1)}
|
||||
>
|
||||
<Icon>
|
||||
<Icons.ChevronLeft />
|
||||
</Icon>
|
||||
</button>
|
||||
{Array(maxPageNumber)
|
||||
.fill(0)
|
||||
.map((_, index) => {
|
||||
const num = index + 1;
|
||||
|
||||
if (
|
||||
pageNumber === num ||
|
||||
num === 1 ||
|
||||
num === maxPageNumber ||
|
||||
(pageNumber > num && pageNumber <= num + nextTo) ||
|
||||
(pageNumber < num && pageNumber >= num - nextTo) ||
|
||||
(pageNumber <= showMin && num <= showMin) ||
|
||||
(maxPageNumber - pageNumber + 1 <= showMin &&
|
||||
maxPageNumber - num + 1 <= showMin)
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSetPageNumber(num)}
|
||||
key={num}
|
||||
>
|
||||
{num === pageNumber ? (
|
||||
<Icon>{num}</Icon>
|
||||
) : (
|
||||
<Icon subStyle="grey">{num}</Icon>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
pageNumber >= showMin
|
||||
? num === pageNumber + nextTo + 1
|
||||
: num === showMin + 1
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
key="jump_right"
|
||||
type="button"
|
||||
onClick={() => handleSetPageNumber(pageNumber + jumpNum)}
|
||||
>
|
||||
<Icon subStyle="grey">
|
||||
<Icons.MoreHorizontal />
|
||||
</Icon>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
maxPageNumber - pageNumber + 1 >= showMin
|
||||
? num === pageNumber - nextTo - 1
|
||||
: maxPageNumber - num + 1 === showMin + 1
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
key="jump_left"
|
||||
type="button"
|
||||
onClick={() => handleSetPageNumber(pageNumber - jumpNum)}
|
||||
>
|
||||
<Icon subStyle="grey">
|
||||
<Icons.MoreHorizontal />
|
||||
</Icon>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
disabled={pageNumber === maxPageNumber}
|
||||
onClick={() => handleSetPageNumber(pageNumber + 1)}
|
||||
>
|
||||
<Icon>
|
||||
<Icons.ChevronRight />
|
||||
</Icon>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
@use "src/styles/vars";
|
||||
@use "src/styles/mixins";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
grid-gap: ui-vars.$px01;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ico {
|
||||
color: ui-vars.$white;
|
||||
@include mixins.icosize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useAuth } from "src/router/auth";
|
||||
import { toastError } from "src/store";
|
||||
import { ROUTE_LOGIN } from "src/router/routes";
|
||||
import { MSG_MUST_LOGIN } from "src/constants";
|
||||
|
||||
/**
|
||||
* Wrapper component that enforces valid authorization to the app
|
||||
*/
|
||||
export const RequireAuth = ({ children }: { children: React.ReactNode }) => {
|
||||
const { authorized } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
/** Simply not authorized -- e.g. no token */
|
||||
useEffect(() => {
|
||||
if (!authorized) {
|
||||
toastError(MSG_MUST_LOGIN);
|
||||
navigate(ROUTE_LOGIN);
|
||||
}
|
||||
}, [authorized]);
|
||||
|
||||
if (!authorized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { classNames } from "jambonz-ui";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type SearchFilterProps = JSX.IntrinsicElements["input"] & {
|
||||
filter: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
};
|
||||
|
||||
export const SearchFilter = ({
|
||||
placeholder,
|
||||
filter: [filterValue, setFilterValue],
|
||||
}: SearchFilterProps) => {
|
||||
const [focus, setFocus] = useState(false);
|
||||
const [appearance, setAppearance] = useState(false);
|
||||
const classes = {
|
||||
"search-filter": true,
|
||||
focused: focus,
|
||||
appearance,
|
||||
};
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterValue(e.target.value.toLowerCase());
|
||||
|
||||
if (e.target.value) {
|
||||
setAppearance(true);
|
||||
} else {
|
||||
setAppearance(false);
|
||||
}
|
||||
},
|
||||
[setFilterValue]
|
||||
);
|
||||
|
||||
const handleActive = useCallback(() => {
|
||||
if (filterValue) {
|
||||
setAppearance(true);
|
||||
}
|
||||
}, [filterValue]);
|
||||
|
||||
const handleInactive = () => {
|
||||
setAppearance(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(classes)}>
|
||||
<Icons.Filter />
|
||||
<input
|
||||
type="search"
|
||||
name="search_filter"
|
||||
placeholder={placeholder}
|
||||
value={filterValue}
|
||||
onChange={handleChange}
|
||||
onFocus={() => {
|
||||
setFocus(true);
|
||||
handleActive();
|
||||
}}
|
||||
onMouseEnter={handleActive}
|
||||
onMouseLeave={handleInactive}
|
||||
onBlur={() => {
|
||||
setFocus(false);
|
||||
handleInactive();
|
||||
}}
|
||||
/>
|
||||
<Icons.XCircle />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
@use "src/styles/vars";
|
||||
@use "src/styles/mixins";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.search-filter {
|
||||
position: relative;
|
||||
|
||||
&.appearance {
|
||||
svg:last-child {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.focused {
|
||||
svg:first-child {
|
||||
color: ui-vars.$dark;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="search"] {
|
||||
@include ui-mixins.mxs();
|
||||
font-family: inherit;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid ui-vars.$grey;
|
||||
border-radius: ui-vars.$px01;
|
||||
padding: ui-vars.$px00 ui-vars.$px01 ui-vars.$px00 32px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&::-webkit-search-cancel-button {
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: vars.$jeangrey;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: ui-vars.$dark;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
color: vars.$jeangrey;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
&:first-child {
|
||||
left: ui-vars.$px01;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
right: ui-vars.$px01;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { classNames } from "jambonz-ui";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type SectionProps = {
|
||||
slim?: boolean;
|
||||
clean?: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Section = ({
|
||||
children,
|
||||
slim = false,
|
||||
clean = false,
|
||||
}: SectionProps) => {
|
||||
const classes = classNames({
|
||||
sec: true,
|
||||
"sec--slim": slim,
|
||||
"sec--clean": clean,
|
||||
});
|
||||
|
||||
return <section className={classes}>{children}</section>;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
@use "src/styles/vars";
|
||||
@use "jambonz-ui/src/styles/index";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.sec {
|
||||
margin-top: ui-vars.$px03;
|
||||
width: 100%;
|
||||
padding: ui-vars.$px03;
|
||||
border-radius: ui-vars.$px01;
|
||||
box-shadow: 0 0 ui-vars.$px01 rgba(0, 0, 0, 0.25);
|
||||
|
||||
&--slim {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&--clean {
|
||||
box-shadow: none;
|
||||
margin-top: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useState } from "react";
|
||||
import { classNames } from "jambonz-ui";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import type { SelectorOption } from "./forms/selector";
|
||||
|
||||
type SelectFilterProps = {
|
||||
id: string;
|
||||
label?: string;
|
||||
options: SelectorOption[];
|
||||
filter: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
handleSelect?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
};
|
||||
|
||||
export const SelectFilter = ({
|
||||
id,
|
||||
label,
|
||||
options,
|
||||
filter: [filterValue, setFilterValue],
|
||||
handleSelect,
|
||||
}: SelectFilterProps) => {
|
||||
const [focus, setFocus] = useState(false);
|
||||
const classes = {
|
||||
smsel: true,
|
||||
"smsel--filter": true,
|
||||
"select-filter": true,
|
||||
focused: focus,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(classes)}>
|
||||
{label && <label htmlFor={id}>{label}:</label>}
|
||||
<div>
|
||||
<select
|
||||
id={id}
|
||||
name={id}
|
||||
value={filterValue}
|
||||
onChange={(e) => {
|
||||
setFilterValue(e.target.value);
|
||||
|
||||
if (handleSelect) {
|
||||
handleSelect(e);
|
||||
}
|
||||
}}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span>
|
||||
<Icons.ChevronUp />
|
||||
<Icons.ChevronDown />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { classNames } from "jambonz-ui";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
type SpinnerProps = {
|
||||
small?: boolean;
|
||||
};
|
||||
|
||||
export const Spinner = ({ small = false }: SpinnerProps) => {
|
||||
const classes = {
|
||||
spinner: true,
|
||||
"spinner--small": small,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(classes)}>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
@use "src/styles/vars";
|
||||
@use "jambonz-ui/src/styles/vars" as ui-vars;
|
||||
|
||||
/** https://loading.io/css/ */
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: ui-vars.$px04;
|
||||
height: ui-vars.$px04;
|
||||
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 8px solid ui-vars.$jambonz;
|
||||
border-radius: 50%;
|
||||
animation: spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: ui-vars.$jambonz transparent transparent transparent;
|
||||
}
|
||||
|
||||
&--small {
|
||||
width: ui-vars.$px02;
|
||||
height: ui-vars.$px02;
|
||||
|
||||
div {
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
|
||||
div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
|
||||
div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user