mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-07-04 19:21:58 +00:00
feat: clients (#272)
* feat: clients * fix typo * fix reivew comments * add error message if account miss realm or device calling app * fix: remove Showed By
This commit is contained in:
@@ -278,3 +278,4 @@ export const API_LCRS = `${API_BASE_URL}/Lcrs`;
|
||||
export const API_LCR_ROUTES = `${API_BASE_URL}/LcrRoutes`;
|
||||
export const API_LCR_CARRIER_SET_ENTRIES = `${API_BASE_URL}/LcrCarrierSetEntries`;
|
||||
export const API_TTS_CACHE = `${API_BASE_URL}/TtsCache`;
|
||||
export const API_CLIENTS = `${API_BASE_URL}/Clients`;
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
API_LCR_CARRIER_SET_ENTRIES,
|
||||
API_LCRS,
|
||||
API_TTS_CACHE,
|
||||
API_CLIENTS,
|
||||
} from "./constants";
|
||||
import { ROUTE_LOGIN } from "src/router/routes";
|
||||
import {
|
||||
@@ -72,6 +73,7 @@ import type {
|
||||
LcrCarrierSetEntry,
|
||||
BucketCredential,
|
||||
BucketCredentialTestResult,
|
||||
Client,
|
||||
} from "./types";
|
||||
import { StatusCodes } from "./types";
|
||||
import { JaegerRoot } from "./jaeger-types";
|
||||
@@ -407,6 +409,10 @@ export const postLcrCarrierSetEntry = (
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const postClient = (payload: Partial<Client>) => {
|
||||
return postFetch<SidResponse, Partial<Client>>(API_CLIENTS, payload);
|
||||
};
|
||||
/** Named wrappers for `putFetch` */
|
||||
|
||||
export const putUser = (sid: string, payload: Partial<UserUpdatePayload>) => {
|
||||
@@ -520,6 +526,12 @@ export const putLcrCarrierSetEntries = (
|
||||
);
|
||||
};
|
||||
|
||||
export const putClient = (sid: string, payload: Partial<Client>) => {
|
||||
return putFetch<EmptyResponse, Partial<Client>>(
|
||||
`${API_CLIENTS}/${sid}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
/** Named wrappers for `deleteFetch` */
|
||||
|
||||
export const deleteUser = (sid: string) => {
|
||||
@@ -599,6 +611,9 @@ export const deleteAccountTtsCache = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_BASE_URL}/Accounts/${sid}/TtsCache`);
|
||||
};
|
||||
|
||||
export const deleteClient = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_CLIENTS}/${sid}`);
|
||||
};
|
||||
/** Named wrappers for `getFetch` */
|
||||
|
||||
export const getUser = (sid: string) => {
|
||||
@@ -637,6 +652,14 @@ export const getLcrCarrierSetEtries = (sid: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getClients = () => {
|
||||
return getFetch<Client[]>(API_CLIENTS);
|
||||
};
|
||||
|
||||
export const getClient = (sid: string) => {
|
||||
return getFetch<Client[]>(`${API_CLIENTS}/${sid}`);
|
||||
};
|
||||
|
||||
/** Wrappers for APIs that can have a mock dev server response */
|
||||
|
||||
export const getRecentCalls = (sid: string, query: Partial<CallQuery>) => {
|
||||
|
||||
@@ -453,6 +453,14 @@ export interface LcrCarrierSetEntry {
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
client_sid?: null | string;
|
||||
account_sid: null | string;
|
||||
username: null | string;
|
||||
password?: null | string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface PageQuery {
|
||||
page: number;
|
||||
count: number;
|
||||
|
||||
@@ -38,7 +38,7 @@ export const AccountFilter = ({
|
||||
|
||||
return (
|
||||
<div className={classNames(classes)}>
|
||||
<label htmlFor="account_filter">{label}:</label>
|
||||
{label && <label htmlFor="account_filter">{label}:</label>}
|
||||
<div>
|
||||
<select
|
||||
id="account_filter"
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
Download,
|
||||
Smartphone,
|
||||
} from "react-feather";
|
||||
|
||||
import type { Icon } from "react-feather";
|
||||
@@ -104,4 +105,5 @@ export const Icons: IconMap = {
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
Download,
|
||||
Smartphone,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ROUTE_INTERNAL_PHONE_NUMBERS,
|
||||
ROUTE_INTERNAL_MS_TEAMS_TENANTS,
|
||||
ROUTE_INTERNAL_LEST_COST_ROUTING,
|
||||
ROUTE_INTERNAL_CLIENTS,
|
||||
} from "src/router/routes";
|
||||
import { Icons } from "src/components";
|
||||
import { Scope, UserData } from "src/store/types";
|
||||
@@ -53,6 +54,11 @@ export const naviTop: NaviItem[] = [
|
||||
scope: Scope.account,
|
||||
restrict: true,
|
||||
},
|
||||
{
|
||||
label: "Clients",
|
||||
icon: Icons.Smartphone,
|
||||
route: () => ROUTE_INTERNAL_CLIENTS,
|
||||
},
|
||||
{
|
||||
label: "Applications",
|
||||
icon: Icons.Grid,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { H1 } from "@jambonz/ui-kit";
|
||||
import React from "react";
|
||||
import ClientsForm from "./form";
|
||||
|
||||
export const ClientsAdd = () => {
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Add client</H1>
|
||||
<ClientsForm />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientsAdd;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { P } from "@jambonz/ui-kit";
|
||||
import React from "react";
|
||||
import { Client } from "src/api/types";
|
||||
import { Modal } from "src/components";
|
||||
|
||||
type ClientsDeleteProps = {
|
||||
client: Client;
|
||||
handleCancel: () => void;
|
||||
handleSubmit: () => void;
|
||||
};
|
||||
export const ClientsDelete = ({
|
||||
client,
|
||||
handleCancel,
|
||||
handleSubmit,
|
||||
}: ClientsDeleteProps) => {
|
||||
return (
|
||||
<>
|
||||
<Modal handleCancel={handleCancel} handleSubmit={handleSubmit}>
|
||||
<P>
|
||||
Are you sure you want to delete the client{" "}
|
||||
<strong>{client.username}</strong>?
|
||||
</P>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientsDelete;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { H1 } from "@jambonz/ui-kit";
|
||||
import React, { useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useApiData } from "src/api";
|
||||
import { Client } from "src/api/types";
|
||||
import { toastError } from "src/store";
|
||||
import ClientsForm from "./form";
|
||||
|
||||
export const ClientsEdit = () => {
|
||||
const params = useParams();
|
||||
const [data, refetch, error] = useApiData<Client>(
|
||||
`Clients/${params.client_sid}`
|
||||
);
|
||||
|
||||
/** Handle error toast at top level... */
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toastError(error.msg);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Edit client</H1>
|
||||
<ClientsForm client={{ data, refetch, error }} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientsEdit;
|
||||
@@ -0,0 +1,229 @@
|
||||
import { Button, ButtonGroup, MS } from "@jambonz/ui-kit";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
deleteClient,
|
||||
postClient,
|
||||
putClient,
|
||||
useApiData,
|
||||
useServiceProviderData,
|
||||
} from "src/api";
|
||||
import { DEFAULT_PSWD_SETTINGS, USER_ACCOUNT } from "src/api/constants";
|
||||
import {
|
||||
Account,
|
||||
Client,
|
||||
PasswordSettings,
|
||||
UseApiDataMap,
|
||||
} from "src/api/types";
|
||||
import { Section } from "src/components";
|
||||
import { AccountSelect, Message, Passwd } from "src/components/forms";
|
||||
import { MSG_REQUIRED_FIELDS } from "src/constants";
|
||||
import { ROUTE_INTERNAL_CLIENTS } from "src/router/routes";
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import ClientsDelete from "./delete";
|
||||
import { hasValue, isValidPasswd } from "src/utils";
|
||||
import { IMessage } from "src/store/types";
|
||||
|
||||
type ClientsFormProps = {
|
||||
client?: UseApiDataMap<Client>;
|
||||
};
|
||||
|
||||
export const ClientsForm = ({ client }: ClientsFormProps) => {
|
||||
const user = useSelectState("user");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [pwdSettings] =
|
||||
useApiData<PasswordSettings>("PasswordSettings") || DEFAULT_PSWD_SETTINGS;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [modal, setModal] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!client) {
|
||||
if (!passwdCheck()) return;
|
||||
|
||||
postClient({
|
||||
account_sid: accountSid,
|
||||
username: username,
|
||||
password: password,
|
||||
is_active: isActive,
|
||||
})
|
||||
.then(() => {
|
||||
toastSuccess("Client created successfully");
|
||||
navigate(ROUTE_INTERNAL_CLIENTS);
|
||||
})
|
||||
.catch((error: { msg: IMessage }) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
} else {
|
||||
putClient(client.data?.client_sid || "", {
|
||||
account_sid: accountSid,
|
||||
username: username,
|
||||
...(password && { password: password }),
|
||||
is_active: isActive,
|
||||
})
|
||||
.then(() => {
|
||||
toastSuccess("Client updated successfully");
|
||||
navigate(ROUTE_INTERNAL_CLIENTS);
|
||||
})
|
||||
.catch((error: { msg: IMessage }) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const passwdCheck = () => {
|
||||
if (pwdSettings && !isValidPasswd(password, pwdSettings)) {
|
||||
toastError("Invalid password.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setModal(false);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (client) {
|
||||
deleteClient(client.data?.client_sid || "")
|
||||
.then(() => {
|
||||
toastSuccess("Client deleted successfully");
|
||||
navigate(ROUTE_INTERNAL_CLIENTS);
|
||||
})
|
||||
.catch((error: { msg: IMessage }) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (client && client.data) {
|
||||
if (client.data.username) {
|
||||
setUsername(client.data.username);
|
||||
}
|
||||
|
||||
if (client.data.account_sid) {
|
||||
setAccountSid(client.data.account_sid);
|
||||
}
|
||||
|
||||
setIsActive(client.data.is_active);
|
||||
}
|
||||
}, [client]);
|
||||
|
||||
useEffect(() => {
|
||||
const acc = accounts?.find((a) => a.account_sid === accountSid);
|
||||
if (!accountSid || !accounts || !acc) return;
|
||||
if (!acc?.sip_realm) {
|
||||
setErrorMessage(`Sip realm is not set for the account.`);
|
||||
} else if (!acc?.device_calling_application_sid) {
|
||||
setErrorMessage(`Device calling application is not set for the account.`);
|
||||
} else {
|
||||
setErrorMessage("");
|
||||
}
|
||||
}, [accountSid]);
|
||||
return (
|
||||
<>
|
||||
<Section slim>
|
||||
<form className="form form--internal" onSubmit={handleSubmit}>
|
||||
<fieldset>
|
||||
<MS>{MSG_REQUIRED_FIELDS}</MS>
|
||||
{errorMessage && <Message message={errorMessage} />}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<div className="multi">
|
||||
<div className="inp">
|
||||
<label htmlFor="lcr_name">
|
||||
User Name<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="client_username"
|
||||
name="client_username"
|
||||
type="text"
|
||||
placeholder="user name"
|
||||
value={username}
|
||||
required={true}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label htmlFor="is_active" className="chk">
|
||||
<input
|
||||
id="is_active"
|
||||
name="is_active"
|
||||
type="checkbox"
|
||||
checked={isActive}
|
||||
onChange={(e) => setIsActive(e.target.checked)}
|
||||
/>
|
||||
<div>Active</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label htmlFor="password">
|
||||
Password{!hasValue(client) && <span>*</span>}
|
||||
</label>
|
||||
<Passwd
|
||||
id="password"
|
||||
required={!hasValue(client)}
|
||||
name="password"
|
||||
value={password}
|
||||
placeholder="Password"
|
||||
setValue={setPassword}
|
||||
/>
|
||||
</fieldset>
|
||||
{user?.scope !== USER_ACCOUNT && (
|
||||
<fieldset>
|
||||
<AccountSelect
|
||||
accounts={accounts}
|
||||
account={[accountSid, setAccountSid]}
|
||||
label="Used by"
|
||||
required={true}
|
||||
defaultOption={false}
|
||||
disabled={hasValue(client)}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
<fieldset>
|
||||
<ButtonGroup left className={client && "btns--spaced"}>
|
||||
<Button
|
||||
small
|
||||
subStyle="grey"
|
||||
as={Link}
|
||||
to={ROUTE_INTERNAL_CLIENTS}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" small disabled={errorMessage !== ""}>
|
||||
Save
|
||||
</Button>
|
||||
{client && client.data && (
|
||||
<Button
|
||||
small
|
||||
type="button"
|
||||
subStyle="grey"
|
||||
onClick={() => setModal(true)}
|
||||
>
|
||||
Delete User
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Section>
|
||||
{client && client.data && modal && (
|
||||
<ClientsDelete
|
||||
client={client.data}
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientsForm;
|
||||
@@ -0,0 +1,175 @@
|
||||
import { Button, H1, Icon, M } from "@jambonz/ui-kit";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { deleteClient, useApiData, useServiceProviderData } from "src/api";
|
||||
import { Account, Client } from "src/api/types";
|
||||
import {
|
||||
AccountFilter,
|
||||
Icons,
|
||||
ScopedAccess,
|
||||
SearchFilter,
|
||||
Section,
|
||||
Spinner,
|
||||
} from "src/components";
|
||||
import { ROUTE_INTERNAL_CLIENTS } from "src/router/routes";
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import { Scope } from "src/store/types";
|
||||
import { hasLength, hasValue, useFilteredResults } from "src/utils";
|
||||
import ClientsDelete from "./delete";
|
||||
import { USER_ACCOUNT } from "src/api/constants";
|
||||
|
||||
export const Clients = () => {
|
||||
const user = useSelectState("user");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [clients, refetch] = useApiData<Client[]>("Clients");
|
||||
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [filter, setFilter] = useState("");
|
||||
const [client, setClient] = useState<Client | null>();
|
||||
|
||||
const tmpFilteredClients = useMemo(() => {
|
||||
if (user?.account_sid && user?.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
return clients;
|
||||
}
|
||||
|
||||
return clients
|
||||
? clients.filter((c) => {
|
||||
return accountSid ? c.account_sid === accountSid : true;
|
||||
})
|
||||
: [];
|
||||
}, [accountSid, clients]);
|
||||
|
||||
const filteredClients = useFilteredResults(filter, tmpFilteredClients);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (client) {
|
||||
deleteClient(client.client_sid || "")
|
||||
.then(() => {
|
||||
toastSuccess(
|
||||
<>
|
||||
Deleted outbound call route <strong>{client.username}</strong>
|
||||
</>
|
||||
);
|
||||
setClient(null);
|
||||
refetch();
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="mast">
|
||||
<H1 className="h2">Clients</H1>
|
||||
<Link to={`${ROUTE_INTERNAL_CLIENTS}/add`} title="Add a client">
|
||||
{" "}
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
<section className="filters filters--spaced">
|
||||
<SearchFilter
|
||||
placeholder="Filter clients"
|
||||
filter={[filter, setFilter]}
|
||||
/>
|
||||
<ScopedAccess user={user} scope={Scope.admin}>
|
||||
<AccountFilter
|
||||
account={[accountSid, setAccountSid]}
|
||||
accounts={accounts}
|
||||
label=""
|
||||
defaultOption
|
||||
/>
|
||||
</ScopedAccess>
|
||||
</section>
|
||||
<Section {...(hasLength(filteredClients) && { slim: true })}>
|
||||
<div className="list">
|
||||
{!hasValue(filteredClients) && hasLength(accounts) ? (
|
||||
<Spinner />
|
||||
) : hasLength(filteredClients) ? (
|
||||
filteredClients.map((c) => (
|
||||
<div className="item" key={c.client_sid}>
|
||||
<div className="item__info">
|
||||
<div className="item__title">
|
||||
<Link
|
||||
to={`${ROUTE_INTERNAL_CLIENTS}/${c.client_sid}/edit`}
|
||||
title="Edit outbound call routes"
|
||||
className="i"
|
||||
>
|
||||
<strong>{c.username}</strong>
|
||||
<Icons.ArrowRight />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="item__meta">
|
||||
<div>
|
||||
<div
|
||||
className={`i txt--${c.is_active ? "teal" : "grey"}`}
|
||||
>
|
||||
{c.is_active ? (
|
||||
<Icons.CheckCircle />
|
||||
) : (
|
||||
<Icons.XCircle />
|
||||
)}
|
||||
<span>{c.is_active ? "Active" : "Inactive"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={`i txt--${c.account_sid ? "teal" : "grey"}`}
|
||||
>
|
||||
<Icons.Activity />
|
||||
<span>
|
||||
{
|
||||
accounts?.find(
|
||||
(acct) => acct.account_sid === c.account_sid
|
||||
)?.name
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="item__actions">
|
||||
<Link
|
||||
to={`${ROUTE_INTERNAL_CLIENTS}/${c.client_sid}/edit`}
|
||||
title="Edit Client"
|
||||
>
|
||||
<Icons.Edit3 />
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
title="Delete client"
|
||||
onClick={() => setClient(c)}
|
||||
className="btnty"
|
||||
>
|
||||
<Icons.Trash />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<M>No Clients.</M>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
<Section clean>
|
||||
<Button small as={Link} to={`${ROUTE_INTERNAL_CLIENTS}/add`}>
|
||||
Add client
|
||||
</Button>
|
||||
</Section>
|
||||
{client && (
|
||||
<ClientsDelete
|
||||
client={client}
|
||||
handleCancel={() => setClient(null)}
|
||||
handleSubmit={handleDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Clients;
|
||||
@@ -183,7 +183,7 @@ export const Lcrs = () => {
|
||||
<div className="item__actions">
|
||||
<Link
|
||||
to={`${ROUTE_INTERNAL_LEST_COST_ROUTING}/${lcr.lcr_sid}/edit`}
|
||||
title="Edit carrier"
|
||||
title="Edit Client"
|
||||
>
|
||||
<Icons.Edit3 />
|
||||
</Link>
|
||||
|
||||
@@ -42,6 +42,9 @@ import MSTeamsTenantsEdit from "src/containers/internal/views/ms-teams-tenants/e
|
||||
import Lcrs from "src/containers/internal/views/least-cost-routing";
|
||||
import LcrsAdd from "src/containers/internal/views/least-cost-routing/add";
|
||||
import LcrsEdit from "src/containers/internal/views/least-cost-routing/edit";
|
||||
import Clients from "src/containers/internal/views/clients";
|
||||
import ClientsAdd from "src/containers/internal/views/clients/add";
|
||||
import ClientsEdit from "src/containers/internal/views/clients/edit";
|
||||
|
||||
export const Router = () => {
|
||||
const toast = useSelectState("toast");
|
||||
@@ -138,6 +141,13 @@ export const Router = () => {
|
||||
element={<LcrsEdit />}
|
||||
/>
|
||||
|
||||
<Route path="clients" element={<Clients />} />
|
||||
<Route path="clients/add" element={<ClientsAdd />} />
|
||||
<Route
|
||||
path="clients/:client_sid/edit"
|
||||
element={<ClientsEdit />}
|
||||
/>
|
||||
|
||||
{/* 404 page not found */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
|
||||
@@ -4,6 +4,7 @@ export const ROUTE_FORGOT_PASSWORD = "/forgot-password";
|
||||
export const ROUTE_INTERNAL_USERS = "/internal/users";
|
||||
export const ROUTE_INTERNAL_SETTINGS = "/internal/settings";
|
||||
export const ROUTE_INTERNAL_ACCOUNTS = "/internal/accounts";
|
||||
export const ROUTE_INTERNAL_CLIENTS = "/internal/clients";
|
||||
export const ROUTE_INTERNAL_APPLICATIONS = "/internal/applications";
|
||||
export const ROUTE_INTERNAL_RECENT_CALLS = "/internal/recent-calls";
|
||||
export const ROUTE_INTERNAL_ALERTS = "/internal/alerts";
|
||||
|
||||
Reference in New Issue
Block a user