mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2025-12-19 05:37:43 +00:00
Compare commits
36 Commits
v0.9.4-rc3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c33eb46ce0 | ||
|
|
f003c158dc | ||
|
|
b1ddaf230d | ||
|
|
0260b1ec8b | ||
|
|
1c1f97f045 | ||
|
|
e6c5a18c87 | ||
|
|
19742ab67e | ||
|
|
53d0c0b510 | ||
|
|
7a0eb71bae | ||
|
|
6aae8d9930 | ||
|
|
a70a1bf614 | ||
|
|
975a787f1e | ||
|
|
46e220f28b | ||
|
|
6836a99635 | ||
|
|
f7f4a2e7b1 | ||
|
|
f1f8a7d808 | ||
|
|
9dd9cf867a | ||
|
|
a372c09bc6 | ||
|
|
031e5e923e | ||
|
|
e02904f7f3 | ||
|
|
7eaf25d13f | ||
|
|
6e4d663337 | ||
|
|
c0a40dd784 | ||
|
|
536bf0f471 | ||
|
|
aaf1ede5c2 | ||
|
|
24d646f705 | ||
|
|
c648afcb1a | ||
|
|
4eca59d9bd | ||
|
|
4a293ae7da | ||
|
|
03e52e3dc5 | ||
|
|
9ab592a898 | ||
|
|
1723326890 | ||
|
|
504825d699 | ||
|
|
e65d9b9db6 | ||
|
|
10818493bc | ||
|
|
844eec953c |
2
.env
2
.env
@@ -1,4 +1,4 @@
|
||||
#VITE_API_BASE_URL=http://127.0.0.1:3000/v1
|
||||
# VITE_API_BASE_URL=http://127.0.0.1:3000/v1
|
||||
#VITE_DEV_BASE_URL=http://127.0.0.1:3000/v1
|
||||
|
||||
## enables choosing units and lisenced account call limits
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18.15-alpine3.16 as builder
|
||||
FROM node:20-alpine AS builder
|
||||
RUN apk update && apk add --no-cache python3 make g++
|
||||
COPY . /opt/app
|
||||
WORKDIR /opt/app/
|
||||
@@ -6,7 +6,7 @@ RUN npm install
|
||||
RUN npm run build
|
||||
RUN npm prune
|
||||
|
||||
FROM node:18.14.1-alpine as webapp
|
||||
FROM node:20-alpine AS webapp
|
||||
RUN apk add curl
|
||||
WORKDIR /opt/app
|
||||
COPY . /opt/app
|
||||
|
||||
3113
package-lock.json
generated
3113
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "jambonz-webapp",
|
||||
"description": "A simple provisioning web app for jambonz",
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.5",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
"node": ">=18"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
@@ -41,7 +41,7 @@
|
||||
"deploy": "npm i && npm run build && npm run pm2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/ui-kit": "^0.0.21",
|
||||
"@jambonz/ui-kit": "^0.0.22",
|
||||
"@stripe/react-stripe-js": "^2.6.2",
|
||||
"@stripe/stripe-js": "^3.2.0",
|
||||
"dayjs": "^1.11.10",
|
||||
@@ -71,16 +71,16 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"express": "^4.19.2",
|
||||
"express": "^5.1.0",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.2",
|
||||
"nanoid": "^5.0.7",
|
||||
"lint-staged": "^16.1.2",
|
||||
"nanoid": "^5.1.5",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.74.1",
|
||||
"serve": "^14.2.1",
|
||||
"sass": "^1.89.2",
|
||||
"serve": "^14.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.4",
|
||||
"vite": "^5.2.8"
|
||||
"vite": "^6.4.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": "eslint --max-warnings=0",
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
Currency,
|
||||
ElevenLabsOptions,
|
||||
GoogleCustomVoice,
|
||||
InworldOptions,
|
||||
LimitField,
|
||||
LimitUnitOption,
|
||||
PasswordSettings,
|
||||
@@ -92,10 +93,6 @@ export const DISABLE_ADDITIONAL_SPEECH_VENDORS: boolean =
|
||||
export const AWS_REGION: string =
|
||||
window.JAMBONZ?.AWS_REGION || import.meta.env.VITE_APP_AWS_REGION;
|
||||
|
||||
export const ENABLE_PHONE_NUMBER_LAZY_LOAD: boolean =
|
||||
window.JAMBONZ?.ENABLE_PHONE_NUMBER_LAZY_LOAD === "true" ||
|
||||
JSON.parse(import.meta.env.VITE_APP_ENABLE_PHONE_NUMBER_LAZY_LOAD || "false");
|
||||
|
||||
export const DEFAULT_SERVICE_PROVIDER_SID: string =
|
||||
window.JAMBONZ?.DEFAULT_SERVICE_PROVIDER_SID ||
|
||||
import.meta.env.VITE_APP_DEFAULT_SERVICE_PROVIDER_SID;
|
||||
@@ -134,7 +131,7 @@ export const DEFAULT_WEBHOOK: WebHook = {
|
||||
};
|
||||
|
||||
/** Default SIP/SMPP Gateways */
|
||||
export const DEFAULT_SIP_GATEWAY: SipGateway = {
|
||||
export const DEFAULT_SIP_INBOUND_GATEWAY: SipGateway = {
|
||||
voip_carrier_sid: "",
|
||||
ipv4: "",
|
||||
port: 5060,
|
||||
@@ -248,6 +245,14 @@ export const VERBIO_STT_MODELS = [
|
||||
|
||||
export const DEFAULT_VERBIO_MODEL = "V1";
|
||||
|
||||
// ASSEMBLYAI
|
||||
export const ASSEMBLYAI_STT_VERSIONS = [
|
||||
{ name: "V2", value: "v2" },
|
||||
{ name: "V3", value: "v3" },
|
||||
];
|
||||
|
||||
export const DEFAULT_ASSEMBLYAI_STT_VERSION = "v2";
|
||||
|
||||
export const ADDITIONAL_SPEECH_VENDORS: Lowercase<Vendor>[] = ["speechmatics"];
|
||||
|
||||
// Google Custom Voice reported usage options
|
||||
@@ -281,6 +286,14 @@ export const DEFAULT_RIMELABS_OPTIONS: Partial<RimelabsOptions> = {
|
||||
reduceLatency: true,
|
||||
};
|
||||
|
||||
export const DEFAULT_INWORLD_OPTIONS: Partial<InworldOptions> = {
|
||||
audioConfig: {
|
||||
pitch: 0.0,
|
||||
speakingRate: 1.0,
|
||||
},
|
||||
temperature: 0.8,
|
||||
};
|
||||
|
||||
// PlayHT options
|
||||
export const DEFAULT_PLAYHT_OPTIONS: Partial<PlayHTOptions> = {
|
||||
quality: "medium",
|
||||
@@ -335,6 +348,12 @@ export const DTMF_TYPE_SELECTION: SelectorOptions[] = [
|
||||
{ name: "Tones", value: "tones" },
|
||||
];
|
||||
|
||||
export const TRUNK_TYPE_SELECTION: SelectorOptions[] = [
|
||||
{ name: "IP Trunk", value: "static_ip" },
|
||||
{ name: "Auth Trunk", value: "auth" },
|
||||
{ name: "Registration Trunk", value: "reg" },
|
||||
];
|
||||
|
||||
/** Available webhook methods */
|
||||
export const WEBHOOK_METHODS: WebhookOption[] = [
|
||||
{
|
||||
@@ -405,6 +424,11 @@ export const CurrencySymbol: Currency = {
|
||||
usd: "$",
|
||||
};
|
||||
|
||||
export const DEEPGRAM_STT_ENPOINT = [
|
||||
{ name: "US (Default)", value: "" },
|
||||
{ name: "EU-hosted", value: "api.eu.deepgram.com" },
|
||||
];
|
||||
|
||||
/** User scope values values */
|
||||
export const USER_ADMIN = "admin";
|
||||
export const USER_SP = "service_provider";
|
||||
|
||||
@@ -97,6 +97,8 @@ import type {
|
||||
SpeechSupportedLanguagesAndVoices,
|
||||
AppEnv,
|
||||
PhoneNumberQuery,
|
||||
ApplicationQuery,
|
||||
VoipCarrierQuery,
|
||||
} from "./types";
|
||||
import { Availability, StatusCodes } from "./types";
|
||||
import { JaegerRoot } from "./jaeger-types";
|
||||
@@ -821,6 +823,28 @@ export const getAppEnvSchema = (url: string) => {
|
||||
return getFetch<AppEnv>(`${API_APP_ENV}?url=${url}`);
|
||||
};
|
||||
|
||||
export const getApplications = (
|
||||
sid: string,
|
||||
query: Partial<ApplicationQuery>,
|
||||
) => {
|
||||
const qryStr = getQuery<Partial<ApplicationQuery>>(query);
|
||||
|
||||
return getFetch<PagedResponse<Application>>(
|
||||
`${API_ACCOUNTS}/${sid}/Applications?${qryStr}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const getSPVoipCarriers = (
|
||||
sid: string,
|
||||
query: Partial<VoipCarrierQuery>,
|
||||
) => {
|
||||
const qryStr = getQuery<Partial<VoipCarrierQuery>>(query);
|
||||
|
||||
return getFetch<PagedResponse<Carrier>>(
|
||||
`${API_SERVICE_PROVIDERS}/${sid}/VoipCarriers?${qryStr}`,
|
||||
);
|
||||
};
|
||||
|
||||
/** Wrappers for APIs that can have a mock dev server response */
|
||||
|
||||
export const getMe = () => {
|
||||
@@ -903,7 +927,7 @@ export const getPrice = () => {
|
||||
export const getPhoneNumbers = (query: Partial<PhoneNumberQuery>) => {
|
||||
const qryStr = getQuery<Partial<PhoneNumberQuery>>(query);
|
||||
|
||||
return getFetch<PhoneNumber[]>(`${API_PHONE_NUMBERS}?${qryStr}`);
|
||||
return getFetch<PagedResponse<PhoneNumber>>(`${API_PHONE_NUMBERS}?${qryStr}`);
|
||||
};
|
||||
|
||||
export const getSpeechSupportedLanguagesAndVoices = (
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { Language, Model, Vendor, VoiceLanguage } from "src/vendor/types";
|
||||
import type {
|
||||
JambonzResourceOptions,
|
||||
Language,
|
||||
Model,
|
||||
Vendor,
|
||||
VoiceLanguage,
|
||||
} from "src/vendor/types";
|
||||
|
||||
/** Simple types */
|
||||
|
||||
@@ -413,6 +419,7 @@ export interface SpeechCredential {
|
||||
custom_stt_endpoint: null | string;
|
||||
client_id: null | string;
|
||||
client_secret: null | string;
|
||||
client_key: null | string;
|
||||
secret: null | string;
|
||||
nuance_tts_uri: null | string;
|
||||
nuance_stt_uri: null | string;
|
||||
@@ -429,8 +436,10 @@ export interface SpeechCredential {
|
||||
label: null | string;
|
||||
cobalt_server_uri: null | string;
|
||||
model_id: null | string;
|
||||
stt_model_id: null | string;
|
||||
voice_engine: null | string;
|
||||
engine_version: null | string;
|
||||
service_version: null | string;
|
||||
model: null | string;
|
||||
options: null | string;
|
||||
deepgram_stt_uri: null | string;
|
||||
@@ -438,6 +447,10 @@ export interface SpeechCredential {
|
||||
deepgram_stt_use_tls: number;
|
||||
speechmatics_stt_uri: null | string;
|
||||
playht_tts_uri: null | string;
|
||||
resemble_tts_uri: null | string;
|
||||
resemble_tts_use_tls: number;
|
||||
api_uri: null | string;
|
||||
houndify_server_uri: null | string;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
@@ -457,6 +470,8 @@ export interface CarrierRegisterStatus {
|
||||
|
||||
export type DtmfType = "rfc2833" | "tones" | "info";
|
||||
|
||||
export type TrunkType = "static_ip" | "auth" | "reg";
|
||||
|
||||
export interface Carrier {
|
||||
voip_carrier_sid: string;
|
||||
name: string;
|
||||
@@ -485,6 +500,7 @@ export interface Carrier {
|
||||
register_status: CarrierRegisterStatus;
|
||||
dtmf_type: DtmfType;
|
||||
outbound_sip_proxy: string | null;
|
||||
trunk_type: TrunkType;
|
||||
}
|
||||
|
||||
export interface PredefinedCarrier extends Carrier {
|
||||
@@ -557,12 +573,14 @@ export interface Client {
|
||||
|
||||
export interface PageQuery {
|
||||
page: number;
|
||||
page_size?: number;
|
||||
count: number;
|
||||
start?: string;
|
||||
days?: number;
|
||||
}
|
||||
|
||||
export interface PhoneNumberQuery extends PageQuery {
|
||||
service_provider_sid?: string;
|
||||
account_sid?: string;
|
||||
filter?: string;
|
||||
}
|
||||
@@ -572,6 +590,15 @@ export interface CallQuery extends PageQuery {
|
||||
answered?: string;
|
||||
}
|
||||
|
||||
export interface ApplicationQuery extends PageQuery {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface VoipCarrierQuery extends PageQuery {
|
||||
name?: string;
|
||||
account_sid?: string;
|
||||
}
|
||||
|
||||
export interface GoogleCustomVoicesQuery {
|
||||
speech_credential_sid?: string;
|
||||
label?: string;
|
||||
@@ -769,6 +796,16 @@ export interface RimelabsOptions {
|
||||
reduceLatency: boolean;
|
||||
}
|
||||
|
||||
export interface InworldOptions {
|
||||
audioConfig: {
|
||||
bitRate?: number;
|
||||
sampleRateHertz?: number;
|
||||
pitch?: number;
|
||||
speakingRate?: number;
|
||||
};
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
export type CartesiaEmotions =
|
||||
| "anger:lowest"
|
||||
| "anger:low"
|
||||
@@ -799,6 +836,9 @@ export interface AppEnvProperty {
|
||||
default?: string | number | boolean;
|
||||
obscure?: boolean;
|
||||
uiHint?: "input" | "textarea" | "filepicker";
|
||||
enum?: string[];
|
||||
jambonzResource?: "carriers";
|
||||
jambonzResourceOptions?: JambonzResourceOptions[];
|
||||
}
|
||||
|
||||
export interface AppEnv {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
import { useToast } from "../toast/toast-provider";
|
||||
|
||||
type ClipBoardProps = {
|
||||
id?: string;
|
||||
@@ -13,6 +13,7 @@ type ClipBoardProps = {
|
||||
const hasClipboard = typeof navigator.clipboard !== "undefined";
|
||||
|
||||
export const ClipBoard = ({ text, id = "", name = "" }: ClipBoardProps) => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const handleClick = () => {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
}
|
||||
|
||||
select {
|
||||
@include ui-mixins.m();
|
||||
appearance: none;
|
||||
padding: ui-vars.$px01 ui-vars.$px02;
|
||||
border-radius: ui-vars.$px01;
|
||||
@@ -33,6 +32,7 @@
|
||||
background-color: ui-vars.$white;
|
||||
width: 100%;
|
||||
max-width: vars.$widthinput;
|
||||
@include ui-mixins.m();
|
||||
|
||||
&:focus {
|
||||
border-color: ui-vars.$dark;
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
}
|
||||
|
||||
@mixin typeahead-input {
|
||||
@include ui-mixins.m();
|
||||
appearance: none;
|
||||
padding: ui-vars.$px01 ui-vars.$px02;
|
||||
border-radius: ui-vars.$px01;
|
||||
@@ -37,6 +36,7 @@
|
||||
max-width: vars.$widthtypeaheadinput;
|
||||
transition: border-color 0.2s ease;
|
||||
font-family: inherit;
|
||||
@include ui-mixins.m();
|
||||
|
||||
&:focus {
|
||||
border-color: ui-vars.$dark;
|
||||
@@ -84,7 +84,6 @@
|
||||
}
|
||||
|
||||
@mixin typeahead-dropdown {
|
||||
@include ui-mixins.m();
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
@@ -93,6 +92,7 @@
|
||||
border: 1px solid ui-vars.$dark;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
@include ui-mixins.m();
|
||||
}
|
||||
|
||||
@mixin typeahead-option {
|
||||
@@ -126,8 +126,8 @@
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
@include typeahead-input();
|
||||
width: 100%;
|
||||
@include typeahead-input();
|
||||
}
|
||||
|
||||
span {
|
||||
@@ -135,8 +135,8 @@
|
||||
}
|
||||
|
||||
.typeahead-dropdown {
|
||||
@include typeahead-dropdown();
|
||||
z-index: 1000;
|
||||
@include typeahead-dropdown();
|
||||
}
|
||||
|
||||
.typeahead-option {
|
||||
@@ -149,10 +149,10 @@
|
||||
width: auto;
|
||||
|
||||
input {
|
||||
@include typeahead-input();
|
||||
height: 34px;
|
||||
min-width: 370px;
|
||||
font-size: var(--mxs-size);
|
||||
@include typeahead-input();
|
||||
}
|
||||
|
||||
span {
|
||||
@@ -160,13 +160,13 @@
|
||||
}
|
||||
|
||||
.typeahead-dropdown {
|
||||
@include typeahead-dropdown();
|
||||
width: 100%;
|
||||
@include typeahead-dropdown();
|
||||
}
|
||||
|
||||
.typeahead-option {
|
||||
@include typeahead-option();
|
||||
font-size: var(--mxs-size);
|
||||
@include typeahead-option();
|
||||
}
|
||||
|
||||
.pointerevents {
|
||||
|
||||
@@ -2,15 +2,18 @@ import React from "react";
|
||||
import { H1 } from "@jambonz/ui-kit";
|
||||
|
||||
import { RequireAuth } from "./require-auth";
|
||||
import { ToastProvider } from "./toast/toast-provider";
|
||||
|
||||
/** Wrapper to pass different auth contexts */
|
||||
const RequireAuthTestWrapper = () => {
|
||||
return (
|
||||
<RequireAuth>
|
||||
<div className="auth-div">
|
||||
<H1>Protected Route</H1>
|
||||
</div>
|
||||
</RequireAuth>
|
||||
<ToastProvider>
|
||||
<RequireAuth>
|
||||
<div className="auth-div">
|
||||
<H1>Protected Route</H1>
|
||||
</div>
|
||||
</RequireAuth>
|
||||
</ToastProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,14 +2,15 @@ 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";
|
||||
import { useToast } from "./toast/toast-provider";
|
||||
|
||||
/**
|
||||
* Wrapper component that enforces valid authorization to the app
|
||||
*/
|
||||
export const RequireAuth = ({ children }: { children: React.ReactNode }) => {
|
||||
const { toastError } = useToast();
|
||||
const { authorized } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
96
src/components/toast/toast-provider.tsx
Normal file
96
src/components/toast/toast-provider.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { Toast } from "./index";
|
||||
import type { IMessage, Toast as ToastProps } from "src/store/types";
|
||||
import { TOAST_TIME } from "src/constants";
|
||||
|
||||
// Define the context type
|
||||
interface ToastContextType {
|
||||
toastSuccess: (message: IMessage) => void;
|
||||
toastError: (message: IMessage) => void;
|
||||
}
|
||||
|
||||
// Create the context with a default value
|
||||
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Provider component that makes toast functionality available to any
|
||||
* nested components that call useToast().
|
||||
*/
|
||||
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [toast, setToast] = useState<ToastProps | null>(null);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
// Clear any existing toasts and timeouts
|
||||
const clearToast = useCallback(() => {
|
||||
setToast(null);
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Show a toast with the specified type and message
|
||||
const showToast = useCallback(
|
||||
(type: "success" | "error", message: IMessage) => {
|
||||
clearToast();
|
||||
|
||||
setToast({ type, message });
|
||||
|
||||
// Auto-hide after specified time
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
setToast(null);
|
||||
}, TOAST_TIME);
|
||||
},
|
||||
[clearToast],
|
||||
);
|
||||
|
||||
// Exposed methods
|
||||
const toastSuccess = useCallback(
|
||||
(message: IMessage) => {
|
||||
showToast("success", message);
|
||||
},
|
||||
[showToast],
|
||||
);
|
||||
|
||||
const toastError = useCallback(
|
||||
(message: IMessage) => {
|
||||
showToast("error", message);
|
||||
},
|
||||
[showToast],
|
||||
);
|
||||
|
||||
// Context value
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
toastSuccess,
|
||||
toastError,
|
||||
}),
|
||||
[toastSuccess, toastError],
|
||||
);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={contextValue}>
|
||||
{children}
|
||||
{toast && <Toast type={toast.type} message={toast.message} />}
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useToast = () => {
|
||||
const context = useContext(ToastContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error("useToast must be used within a ToastProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import { P, Button } from "@jambonz/ui-kit";
|
||||
|
||||
import { toastSuccess, toastError } from "src/store";
|
||||
import { useApiData, postApiKey, deleteApiKey } from "src/api";
|
||||
import { Modal, ModalClose, Obscure, ClipBoard, Section } from "src/components";
|
||||
import { getHumanDateTime, hasLength } from "src/utils";
|
||||
|
||||
import type { ApiKey, TokenResponse } from "src/api/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type ApiKeyProps = {
|
||||
path: string;
|
||||
@@ -18,6 +18,7 @@ type ApiKeyProps = {
|
||||
};
|
||||
|
||||
export const ApiKeys = ({ path, post, label }: ApiKeyProps) => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const [apiKeys, apiKeysRefetcher] = useApiData<ApiKey[]>(path);
|
||||
const [deleteKey, setDeleteKey] = useState<ApiKey | null>(null);
|
||||
const [addedKey, setAddedKey] = useState<TokenResponse | null>(null);
|
||||
|
||||
@@ -5,13 +5,12 @@ import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { Icons, ModalForm } from "src/components";
|
||||
import { naviTop, naviByo } from "./items";
|
||||
import { UserMe } from "../user-me";
|
||||
import { useSelectState, useDispatch } from "src/store";
|
||||
import {
|
||||
useSelectState,
|
||||
useDispatch,
|
||||
toastSuccess,
|
||||
toastError,
|
||||
} from "src/store";
|
||||
import { getActiveSP, setActiveSP } from "src/store/localStore";
|
||||
getActiveSP,
|
||||
removeAccountFilter,
|
||||
setActiveSP,
|
||||
} from "src/store/localStore";
|
||||
import { postServiceProviders } from "src/api";
|
||||
|
||||
import type { NaviItem } from "./items";
|
||||
@@ -22,6 +21,7 @@ import { Scope, UserData } from "src/store/types";
|
||||
import { USER_ADMIN } from "src/api/constants";
|
||||
import { ROUTE_LOGIN } from "src/router/routes";
|
||||
import { Lcr } from "src/api/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type CommonProps = {
|
||||
handleMenu: () => void;
|
||||
@@ -63,6 +63,7 @@ export const Navi = ({
|
||||
handleMenu,
|
||||
handleLogout,
|
||||
}: NaviProps) => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const user = useSelectState("user");
|
||||
@@ -166,6 +167,7 @@ export const Navi = ({
|
||||
onChange={(e) => {
|
||||
setSid(e.target.value);
|
||||
setActiveSP(e.target.value);
|
||||
removeAccountFilter();
|
||||
navigate(ROUTE_LOGIN);
|
||||
}}
|
||||
disabled={user?.scope !== USER_ADMIN}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useParams } from "react-router-dom";
|
||||
|
||||
import { ApiKeys } from "src/containers/internal/api-keys";
|
||||
import { useApiData } from "src/api";
|
||||
import { toastError, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import { AccountForm } from "./form";
|
||||
|
||||
import type { Account, Application, Limit, TtsCache } from "src/api/types";
|
||||
@@ -14,8 +14,10 @@ import {
|
||||
} from "src/router/routes";
|
||||
import { useScopedRedirect } from "src/utils";
|
||||
import { Scope } from "src/store/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const EditAccount = () => {
|
||||
const { toastError } = useToast();
|
||||
const params = useParams();
|
||||
const user = useSelectState("user");
|
||||
const [data, refetch, error] = useApiData<Account>(
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
|
||||
import { P, Button, ButtonGroup, MS, Icon, H1 } from "@jambonz/ui-kit";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import {
|
||||
putAccount,
|
||||
postAccount,
|
||||
@@ -75,6 +75,7 @@ import { EditBoard } from "src/components/editboard";
|
||||
import { ModalLoader } from "src/components/modal";
|
||||
import { useAuth } from "src/router/auth";
|
||||
import { Scope } from "src/store/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type AccountFormProps = {
|
||||
apps?: Application[];
|
||||
@@ -89,6 +90,7 @@ export const AccountForm = ({
|
||||
account,
|
||||
ttsCache,
|
||||
}: AccountFormProps) => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const user = useSelectState("user");
|
||||
@@ -289,6 +291,7 @@ export const AccountForm = ({
|
||||
endpoint: endpoint,
|
||||
access_key_id: bucketAccessKeyId,
|
||||
secret_access_key: bucketSecretAccessKey,
|
||||
...(bucketRegion && { region: bucketRegion }),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -437,6 +440,9 @@ export const AccountForm = ({
|
||||
access_key_id: bucketAccessKeyId || null,
|
||||
secret_access_key: bucketSecretAccessKey || null,
|
||||
...(hasLength(bucketTags) && { tags: bucketTags }),
|
||||
...(bucketRegion && {
|
||||
region: bucketRegion,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
...(!bucketCredentialChecked && {
|
||||
@@ -550,6 +556,10 @@ export const AccountForm = ({
|
||||
setBucketRegion(tmpBucketRegion);
|
||||
} else if (account.data.bucket_credential?.region) {
|
||||
setBucketRegion(account.data.bucket_credential?.region);
|
||||
} else if (
|
||||
account.data.bucket_credential?.vendor === BUCKET_VENDOR_S3_COMPATIBLE
|
||||
) {
|
||||
setBucketRegion("");
|
||||
}
|
||||
|
||||
if (tmpAzureConnectionString) {
|
||||
@@ -583,9 +593,7 @@ export const AccountForm = ({
|
||||
JSON.parse(account.data.bucket_credential?.service_key),
|
||||
);
|
||||
}
|
||||
setInitialCheckRecordAllCall(
|
||||
hasValue(bucketVendor) && bucketVendor.length !== 0,
|
||||
);
|
||||
setInitialCheckRecordAllCall(hasValue(account.data.bucket_credential));
|
||||
}
|
||||
}, [account]);
|
||||
|
||||
@@ -1102,6 +1110,18 @@ export const AccountForm = ({
|
||||
onChange={(e) => {
|
||||
setBucketVendor(e.target.value);
|
||||
setTmpBucketVendor(e.target.value);
|
||||
if (
|
||||
e.target.value === BUCKET_VENDOR_AWS &&
|
||||
!regions?.aws.find((r) => r.value === bucketRegion)
|
||||
) {
|
||||
setBucketRegion("us-east-1");
|
||||
setTmpBucketRegion("us-east-1");
|
||||
} else if (
|
||||
e.target.value === BUCKET_VENDOR_S3_COMPATIBLE
|
||||
) {
|
||||
setBucketRegion("");
|
||||
setTmpBucketRegion("");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -1122,6 +1142,17 @@ export const AccountForm = ({
|
||||
setTmpEndpoint(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="endpoint">Region (Optional)</label>
|
||||
<input
|
||||
id="aws_compatible_region"
|
||||
type="text"
|
||||
name="aws_compatible_region"
|
||||
value={bucketRegion}
|
||||
onChange={(e) => {
|
||||
setBucketRegion(e.target.value);
|
||||
setTmpBucketRegion(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<label htmlFor="bucket_name">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useServiceProviderData, deleteAccount } from "src/api";
|
||||
import { ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
|
||||
import { Section, Icons, Spinner, SearchFilter } from "src/components";
|
||||
import { DeleteAccount } from "./delete";
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import {
|
||||
hasLength,
|
||||
hasValue,
|
||||
@@ -17,8 +17,10 @@ import { USER_ACCOUNT } from "src/api/constants";
|
||||
|
||||
import { Scope } from "src/store/types";
|
||||
import type { Account } from "src/api/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const Accounts = () => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const user = useSelectState("user");
|
||||
const [accounts, refetch] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [account, setAccount] = useState<Account | null>(null);
|
||||
|
||||
@@ -10,11 +10,13 @@ import { postSubscriptions, useApiData } from "src/api";
|
||||
import { CurrentUserData, Subscription } from "src/api/types";
|
||||
import { Section } from "src/components";
|
||||
import { ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import { PaymentMethod } from "@stripe/stripe-js";
|
||||
import { ModalLoader } from "src/components/modal";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const ManagePaymentForm = () => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const user = useSelectState("user");
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
|
||||
@@ -19,10 +19,11 @@ import {
|
||||
useStripe,
|
||||
} from "@stripe/react-stripe-js";
|
||||
import { PaymentMethod } from "@stripe/stripe-js";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
import { ModalLoader } from "src/components/modal";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
const SubscriptionForm = () => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const [priceInfo] = useApiData<PriceInfo[]>("/Prices");
|
||||
const [userStripeInfo] = useApiData<StripeCustomerId>("/StripeCustomerId");
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
PER_PAGE_SELECTION,
|
||||
USER_ACCOUNT,
|
||||
} from "src/api/constants";
|
||||
import { toastError, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import { hasLength, hasValue } from "src/utils";
|
||||
import {
|
||||
AccountFilter,
|
||||
@@ -27,8 +27,10 @@ import {
|
||||
setLocation,
|
||||
} from "src/store/localStore";
|
||||
import AlertDetailItem from "./alert-detail-item";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const Alerts = () => {
|
||||
const { toastError } = useToast();
|
||||
const user = useSelectState("user");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
@@ -68,10 +70,10 @@ export const Alerts = () => {
|
||||
};
|
||||
|
||||
useMemo(() => {
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
if (!accountSid && user?.account_sid) setAccountSid(user?.account_sid);
|
||||
if (getQueryFilter()) {
|
||||
const [date] = getQueryFilter().split("/");
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
if (!accountSid && user?.account_sid) setAccountSid(user?.account_sid);
|
||||
setDateFilter(date);
|
||||
}
|
||||
}, [accountSid]);
|
||||
|
||||
@@ -3,15 +3,17 @@ import { H1 } from "@jambonz/ui-kit";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { useApiData } from "src/api";
|
||||
import { toastError, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import { ApplicationForm } from "./form";
|
||||
|
||||
import type { Application } from "src/api/types";
|
||||
import { useScopedRedirect } from "src/utils/use-scoped-redirect";
|
||||
import { Scope } from "src/store/types";
|
||||
import { ROUTE_INTERNAL_APPLICATIONS } from "src/router/routes";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const EditApplication = () => {
|
||||
const { toastError } = useToast();
|
||||
const params = useParams();
|
||||
const user = useSelectState("user");
|
||||
const [data, refetch, error] = useApiData<Application>(
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, ButtonGroup, MS } from "@jambonz/ui-kit";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import { ClipBoard, Section, Tooltip } from "src/components";
|
||||
import {
|
||||
Selector,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
useServiceProviderData,
|
||||
useApiData,
|
||||
getAppEnvSchema,
|
||||
getSPVoipCarriers,
|
||||
} from "src/api";
|
||||
import {
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
@@ -53,16 +54,23 @@ import type {
|
||||
AppEnv,
|
||||
} from "src/api/types";
|
||||
import { MSG_REQUIRED_FIELDS, MSG_WEBHOOK_FIELDS } from "src/constants";
|
||||
import { hasLength, isUserAccountScope, useRedirect } from "src/utils";
|
||||
import {
|
||||
hasLength,
|
||||
hasValue,
|
||||
isUserAccountScope,
|
||||
useRedirect,
|
||||
} from "src/utils";
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
import SpeechProviderSelection from "./speech-selection";
|
||||
import ObscureInput from "src/components/obscure-input";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type ApplicationFormProps = {
|
||||
application?: UseApiDataMap<Application>;
|
||||
};
|
||||
|
||||
export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const user = useSelectState("user");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
@@ -581,6 +589,51 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
setFallbackSpeechRecognizerLabel(tmp);
|
||||
};
|
||||
|
||||
const fetchAppEnvJambonzResources = async (appEnv: AppEnv) => {
|
||||
if (appEnv) {
|
||||
const promises = Object.entries(appEnv).map(async ([key, value]) => {
|
||||
const { jambonzResource } = value;
|
||||
switch (jambonzResource) {
|
||||
case "carriers":
|
||||
const carriers = await getSPVoipCarriers(
|
||||
currentServiceProvider?.service_provider_sid || "",
|
||||
{
|
||||
page: 1,
|
||||
page_size: 10000,
|
||||
...(user?.account_sid && {
|
||||
account_sid: user.account_sid,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (carriers.json.total) {
|
||||
return {
|
||||
key,
|
||||
jambonzResourceOptions: carriers.json.data.map((carrier) => ({
|
||||
name: carrier.name,
|
||||
value: carrier.name,
|
||||
})),
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return { key, jambonzResourceOptions: null };
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// Merge the results back into appEnv
|
||||
results.forEach(({ key, jambonzResourceOptions }) => {
|
||||
if (jambonzResourceOptions) {
|
||||
appEnv[key].jambonzResourceOptions = jambonzResourceOptions;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return appEnv;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (callWebhook && callWebhook.url) {
|
||||
// Clear any existing timeout to prevent multiple requests
|
||||
@@ -592,7 +645,26 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
appEnvTimeoutRef.current = setTimeout(() => {
|
||||
getAppEnvSchema(callWebhook.url)
|
||||
.then(({ json }) => {
|
||||
setAppEnv(json);
|
||||
// fetch app env jambonz_resource
|
||||
fetchAppEnvJambonzResources(json).then((updatedEnv) => {
|
||||
setAppEnv(updatedEnv);
|
||||
const defaultEnvVars = Object.keys(updatedEnv).reduce(
|
||||
(acc, key) => {
|
||||
const value = updatedEnv[key];
|
||||
if (value?.default) {
|
||||
return { ...acc, [key]: value.default };
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
setEnvVars((prev) => ({
|
||||
...defaultEnvVars,
|
||||
...(prev || {}),
|
||||
}));
|
||||
});
|
||||
// Default value
|
||||
})
|
||||
.catch((error) => {
|
||||
setMessage(error.msg);
|
||||
@@ -826,7 +898,9 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
? String(defaultValue)
|
||||
: "",
|
||||
onChange: (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
e: React.ChangeEvent<
|
||||
HTMLInputElement | HTMLSelectElement
|
||||
>,
|
||||
) => {
|
||||
// Convert to proper type based on schema
|
||||
let newValue;
|
||||
@@ -851,6 +925,15 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
type: isNumber ? "number" : "text",
|
||||
};
|
||||
|
||||
const isDropdown =
|
||||
(webhook.webhookEnv![key].type === "string" &&
|
||||
(webhook.webhookEnv![key].enum?.length ||
|
||||
0) > 0) ||
|
||||
hasLength(
|
||||
webhook.webhookEnv![key]
|
||||
.jambonzResourceOptions,
|
||||
);
|
||||
|
||||
const textAreaSpecificProps = {
|
||||
rows: 6,
|
||||
cols: 61,
|
||||
@@ -861,6 +944,23 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
.obscure
|
||||
? ObscureInput
|
||||
: webhook.webhookEnv![key].uiHint || "input";
|
||||
if (isDropdown) {
|
||||
const options =
|
||||
webhook.webhookEnv![key]
|
||||
.jambonzResourceOptions ||
|
||||
webhook.webhookEnv![key].enum!.map(
|
||||
(option) => ({
|
||||
name: option,
|
||||
value: option,
|
||||
}),
|
||||
);
|
||||
return (
|
||||
<Selector
|
||||
{...commonProps}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (componentType === "filepicker") {
|
||||
return (
|
||||
<>
|
||||
@@ -884,7 +984,8 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
}}
|
||||
placeholder="Choose a file"
|
||||
required={
|
||||
webhook.webhookEnv![key].required
|
||||
webhook.webhookEnv![key].required &&
|
||||
!hasValue(envVars?.[key])
|
||||
}
|
||||
/>
|
||||
{React.createElement("textarea", {
|
||||
@@ -922,6 +1023,9 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
serviceProviderSid={
|
||||
currentServiceProvider?.service_provider_sid || ""
|
||||
}
|
||||
application_speech_synthesis_voice={
|
||||
application?.data?.speech_synthesis_voice
|
||||
}
|
||||
accountSid={accountSid}
|
||||
credentials={credentials}
|
||||
ttsVendor={[synthVendor, setSynthVendor]}
|
||||
@@ -952,6 +1056,9 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
currentServiceProvider?.service_provider_sid || ""
|
||||
}
|
||||
accountSid={accountSid}
|
||||
application_speech_synthesis_voice={
|
||||
application?.data?.fallback_speech_synthesis_voice
|
||||
}
|
||||
credentials={credentials}
|
||||
ttsVendor={[
|
||||
fallbackSpeechSynthsisVendor,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { H1, M, Button, Icon } from "@jambonz/ui-kit";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { H1, M, Button, Icon, ButtonGroup, MS } from "@jambonz/ui-kit";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { deleteApplication, useServiceProviderData, useApiData } from "src/api";
|
||||
import {
|
||||
deleteApplication,
|
||||
useServiceProviderData,
|
||||
getApplications,
|
||||
} from "src/api";
|
||||
import {
|
||||
ROUTE_INTERNAL_APPLICATIONS,
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
@@ -13,35 +17,71 @@ import {
|
||||
Spinner,
|
||||
AccountFilter,
|
||||
SearchFilter,
|
||||
Pagination,
|
||||
SelectFilter,
|
||||
} from "src/components";
|
||||
import { DeleteApplication } from "./delete";
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import {
|
||||
isUserAccountScope,
|
||||
hasLength,
|
||||
hasValue,
|
||||
useFilteredResults,
|
||||
} from "src/utils";
|
||||
import { useSelectState } from "src/store";
|
||||
import { isUserAccountScope, hasLength, hasValue } from "src/utils";
|
||||
|
||||
import type { Application, Account } from "src/api/types";
|
||||
import { ScopedAccess } from "src/components/scoped-access";
|
||||
import { Scope } from "src/store/types";
|
||||
import { USER_ACCOUNT } from "src/api/constants";
|
||||
import { PER_PAGE_SELECTION, USER_ACCOUNT } from "src/api/constants";
|
||||
import { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const Applications = () => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const user = useSelectState("user");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [application, setApplication] = useState<Application | null>(null);
|
||||
const [apiUrl, setApiUrl] = useState("");
|
||||
const [applications, refetch] = useApiData<Application[]>(apiUrl);
|
||||
const [applications, setApplications] = useState<Application[] | null>(null);
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const filteredApplications = useFilteredResults<Application>(
|
||||
filter,
|
||||
applications,
|
||||
);
|
||||
const [applicationsTotal, setApplicationsTotal] = useState(0);
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [perPageFilter, setPerPageFilter] = useState("25");
|
||||
const [maxPageNumber, setMaxPageNumber] = useState(1);
|
||||
|
||||
// Track previous values to detect changes
|
||||
const prevValuesRef = useRef({
|
||||
accountSid: "",
|
||||
filter: "",
|
||||
pageNumber: 1,
|
||||
perPageFilter: "25",
|
||||
});
|
||||
|
||||
const fetchApplications = (resetPage = false) => {
|
||||
// Don't fetch if no account is selected
|
||||
if (!accountSid) return;
|
||||
|
||||
setApplications(null);
|
||||
|
||||
// Calculate the correct page to use
|
||||
const currentPage = resetPage ? 1 : pageNumber;
|
||||
|
||||
// If we're resetting the page, also update the state
|
||||
if (resetPage && pageNumber !== 1) {
|
||||
setPageNumber(1);
|
||||
}
|
||||
|
||||
getApplications(accountSid, {
|
||||
page: currentPage,
|
||||
page_size: Number(perPageFilter),
|
||||
...(filter && { name: filter }),
|
||||
})
|
||||
.then(({ json }) => {
|
||||
setApplications(json.data);
|
||||
setApplicationsTotal(json.total);
|
||||
setMaxPageNumber(Math.ceil(json.total / Number(perPageFilter)));
|
||||
})
|
||||
.catch((error) => {
|
||||
setApplications([]);
|
||||
toastError(error.msg);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (application) {
|
||||
@@ -53,8 +93,7 @@ export const Applications = () => {
|
||||
}
|
||||
deleteApplication(application.application_sid)
|
||||
.then(() => {
|
||||
// getApplications();
|
||||
refetch();
|
||||
fetchApplications(false);
|
||||
setApplication(null);
|
||||
toastSuccess(
|
||||
<>
|
||||
@@ -68,18 +107,44 @@ export const Applications = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial account
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (user?.account_sid && user.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
} else {
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
setAccountSid(
|
||||
getAccountFilter() || accountSid || accounts?.[0]?.account_sid || "",
|
||||
);
|
||||
}
|
||||
setLocation();
|
||||
}, [user, accounts]);
|
||||
|
||||
if (accountSid) {
|
||||
setApiUrl(`Accounts/${accountSid}/Applications`);
|
||||
}
|
||||
}, [accountSid, user]);
|
||||
// This single effect handles all data fetching triggers
|
||||
useEffect(() => {
|
||||
const accSid = accountSid || getAccountFilter() || "";
|
||||
|
||||
if (!accSid) return;
|
||||
|
||||
// Determine if the change requires a page reset
|
||||
const prevValues = prevValuesRef.current;
|
||||
const isFilterChange =
|
||||
prevValues.accountSid !== accountSid || prevValues.filter !== filter;
|
||||
|
||||
const isPageSizeChange =
|
||||
prevValues.perPageFilter !== perPageFilter &&
|
||||
prevValues.perPageFilter !== ""; // Skip initial render
|
||||
|
||||
// Update ref with current values for next comparison
|
||||
prevValuesRef.current = {
|
||||
accountSid: accSid,
|
||||
filter,
|
||||
pageNumber,
|
||||
perPageFilter,
|
||||
};
|
||||
|
||||
// Fetch data with page reset if needed
|
||||
fetchApplications(isFilterChange || isPageSizeChange);
|
||||
}, [accountSid, filter, pageNumber, perPageFilter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -100,6 +165,7 @@ export const Applications = () => {
|
||||
<SearchFilter
|
||||
placeholder="Filter applications"
|
||||
filter={[filter, setFilter]}
|
||||
delay={1000}
|
||||
/>
|
||||
<ScopedAccess user={user} scope={Scope.service_provider}>
|
||||
<AccountFilter
|
||||
@@ -108,12 +174,12 @@ export const Applications = () => {
|
||||
/>
|
||||
</ScopedAccess>
|
||||
</section>
|
||||
<Section {...(hasLength(filteredApplications) && { slim: true })}>
|
||||
<Section {...(hasLength(applications) && { slim: true })}>
|
||||
<div className="list">
|
||||
{!hasValue(applications) && hasLength(accounts) ? (
|
||||
<Spinner />
|
||||
) : hasLength(filteredApplications) ? (
|
||||
filteredApplications
|
||||
) : hasLength(applications) ? (
|
||||
applications
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((application) => {
|
||||
return (
|
||||
@@ -189,6 +255,26 @@ export const Applications = () => {
|
||||
</Button>
|
||||
</Section>
|
||||
)}
|
||||
<footer>
|
||||
<ButtonGroup>
|
||||
<MS>
|
||||
Total: {applicationsTotal} record
|
||||
{applicationsTotal === 1 ? "" : "s"}
|
||||
</MS>
|
||||
{hasLength(applications) && (
|
||||
<Pagination
|
||||
pageNumber={pageNumber}
|
||||
setPageNumber={setPageNumber}
|
||||
maxPageNumber={maxPageNumber}
|
||||
/>
|
||||
)}
|
||||
<SelectFilter
|
||||
id="page_filter"
|
||||
filter={[perPageFilter, setPerPageFilter]}
|
||||
options={PER_PAGE_SELECTION}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</footer>
|
||||
{application && (
|
||||
<DeleteApplication
|
||||
application={application}
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
} from "src/api/types";
|
||||
import { Selector } from "src/components/forms";
|
||||
import { SelectorOption } from "src/components/forms/selector";
|
||||
import { toastError, useSelectState } from "src/store";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
import { useSelectState } from "src/store";
|
||||
import { hasLength } from "src/utils";
|
||||
import {
|
||||
ELEVENLABS_LANG_EN,
|
||||
@@ -34,6 +35,10 @@ import {
|
||||
VENDOR_VOXIST,
|
||||
VENDOR_RIMELABS,
|
||||
VENDOR_OPENAI,
|
||||
VENDOR_INWORLD,
|
||||
VENDOR_DEEPGRAM_FLUX,
|
||||
VENDOR_RESEMBLE,
|
||||
VENDOR_HOUNDIFY,
|
||||
} from "src/vendor";
|
||||
import {
|
||||
LabelOptions,
|
||||
@@ -44,6 +49,7 @@ import {
|
||||
type SpeechProviderSelectionProbs = {
|
||||
accountSid: string;
|
||||
serviceProviderSid: string;
|
||||
application_speech_synthesis_voice: string | null | undefined;
|
||||
credentials: SpeechCredential[] | undefined;
|
||||
ttsVendor: [
|
||||
keyof SynthesisVendors,
|
||||
@@ -67,6 +73,7 @@ type SpeechProviderSelectionProbs = {
|
||||
export const SpeechProviderSelection = ({
|
||||
accountSid,
|
||||
serviceProviderSid,
|
||||
application_speech_synthesis_voice,
|
||||
credentials,
|
||||
ttsVendor: [synthVendor, setSynthVendor],
|
||||
ttsVendorOptions,
|
||||
@@ -80,6 +87,7 @@ export const SpeechProviderSelection = ({
|
||||
sttLabelOptions,
|
||||
sttLabel: [recogLabel, setRecogLabel],
|
||||
}: SpeechProviderSelectionProbs) => {
|
||||
const { toastError } = useToast();
|
||||
const user = useSelectState("user");
|
||||
const [
|
||||
synthesisSupportedLanguagesAndVoices,
|
||||
@@ -135,7 +143,12 @@ export const SpeechProviderSelection = ({
|
||||
ttsEffectTimer.current = setTimeout(() => {
|
||||
configSynthesis();
|
||||
}, 200);
|
||||
}, [synthVendor, synthLabel, serviceProviderSid]);
|
||||
}, [
|
||||
synthVendor,
|
||||
synthLabel,
|
||||
serviceProviderSid,
|
||||
application_speech_synthesis_voice,
|
||||
]);
|
||||
|
||||
// Get Recognizer languages and voices
|
||||
useEffect(() => {
|
||||
@@ -242,9 +255,14 @@ export const SpeechProviderSelection = ({
|
||||
// Extract model
|
||||
if (json.models && json.models.length) {
|
||||
setSynthesisModelOptions(json.models);
|
||||
if (synthVendor === VENDOR_DEEPGRAM) {
|
||||
if (
|
||||
synthVendor === VENDOR_DEEPGRAM &&
|
||||
(!application_speech_synthesis_voice ||
|
||||
!json.models.some(
|
||||
(m) => m.value === application_speech_synthesis_voice,
|
||||
))
|
||||
) {
|
||||
setSynthVoice(json.models[0].value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,6 +316,15 @@ export const SpeechProviderSelection = ({
|
||||
updateTtsVoice(newLang!.value, newLang!.voices[0].value);
|
||||
return;
|
||||
}
|
||||
if (synthVendor === VENDOR_INWORLD) {
|
||||
let newLang = json.tts.find((lang) => lang.value === "en");
|
||||
// If the new language doesn't map then default to the first one
|
||||
if (!newLang) {
|
||||
newLang = json.tts[0];
|
||||
}
|
||||
updateTtsVoice(newLang!.value, newLang!.voices[0].value);
|
||||
return;
|
||||
}
|
||||
/** Google and AWS have different language lists */
|
||||
/** If the new language doesn't map then default to "en-US" */
|
||||
let newLang = json.tts.find((lang) => lang.value === synthLang);
|
||||
@@ -344,6 +371,9 @@ export const SpeechProviderSelection = ({
|
||||
};
|
||||
|
||||
const configRecognizer = () => {
|
||||
if (recogVendor === VENDOR_DEEPGRAM_FLUX) {
|
||||
return;
|
||||
}
|
||||
getSpeechSupportedLanguagesAndVoices(
|
||||
serviceProviderSid,
|
||||
recogVendor,
|
||||
@@ -387,6 +417,7 @@ export const SpeechProviderSelection = ({
|
||||
toastError(error.msg);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<fieldset>
|
||||
@@ -403,6 +434,8 @@ export const SpeechProviderSelection = ({
|
||||
vendor.value !== VENDOR_SPEECHMATICS &&
|
||||
vendor.value !== VENDOR_CUSTOM &&
|
||||
vendor.value !== VENDOR_OPENAI &&
|
||||
vendor.value !== VENDOR_DEEPGRAM_FLUX &&
|
||||
vendor.value !== VENDOR_HOUNDIFY &&
|
||||
vendor.value !== VENDOR_COBALT,
|
||||
)}
|
||||
onChange={(e) => {
|
||||
@@ -557,6 +590,7 @@ export const SpeechProviderSelection = ({
|
||||
vendor.value != VENDOR_WELLSAID &&
|
||||
vendor.value != VENDOR_ELEVENLABS &&
|
||||
vendor.value != VENDOR_WHISPER &&
|
||||
vendor.value !== VENDOR_RESEMBLE &&
|
||||
vendor.value !== VENDOR_CUSTOM,
|
||||
)}
|
||||
onChange={(e) => {
|
||||
@@ -584,6 +618,7 @@ export const SpeechProviderSelection = ({
|
||||
)}
|
||||
{recogVendor &&
|
||||
!recogVendor.toString().startsWith(VENDOR_CUSTOM) &&
|
||||
recogVendor !== VENDOR_DEEPGRAM_FLUX &&
|
||||
recogLang && (
|
||||
<>
|
||||
<label htmlFor="recognizer_lang">Language</label>
|
||||
|
||||
@@ -2,11 +2,11 @@ import React, { useEffect, useState } from "react";
|
||||
import { P } from "@jambonz/ui-kit";
|
||||
|
||||
import { Modal, ModalClose } from "src/components";
|
||||
import { getFetch } from "src/api";
|
||||
import { getFetch, getLcrRoutes, getLcrs } from "src/api";
|
||||
import { API_PHONE_NUMBERS } from "src/api/constants";
|
||||
import { formatPhoneNumber, hasLength } from "src/utils";
|
||||
import { formatPhoneNumber, hasLength, hasValue } from "src/utils";
|
||||
|
||||
import type { Carrier, PhoneNumber } from "src/api/types";
|
||||
import type { Carrier, Lcr, PhoneNumber } from "src/api/types";
|
||||
|
||||
type DeleteProps = {
|
||||
carrier: Carrier;
|
||||
@@ -20,28 +20,64 @@ export const DeleteCarrier = ({
|
||||
handleSubmit,
|
||||
}: DeleteProps) => {
|
||||
const [phoneNumbers, setPhoneNumbers] = useState<PhoneNumber[]>();
|
||||
const [lcrs, setLcrs] = useState<Lcr[]>();
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
getFetch<PhoneNumber[]>(API_PHONE_NUMBERS).then(({ json }) => {
|
||||
Promise.all([
|
||||
getFetch<PhoneNumber[]>(API_PHONE_NUMBERS),
|
||||
new Promise<Lcr[]>((resolve, reject) => {
|
||||
getLcrs()
|
||||
.then(({ json }) => {
|
||||
Promise.all(
|
||||
json.map((lcr: Lcr) =>
|
||||
getLcrRoutes(lcr.lcr_sid!)
|
||||
.then(({ json }) => {
|
||||
if (
|
||||
json.some((route) =>
|
||||
route.lcr_carrier_set_entries?.some(
|
||||
(entry) =>
|
||||
entry.voip_carrier_sid === carrier.voip_carrier_sid,
|
||||
),
|
||||
)
|
||||
) {
|
||||
return lcr;
|
||||
}
|
||||
})
|
||||
.catch((error) => reject(error)),
|
||||
),
|
||||
)
|
||||
.then((lcrs) => {
|
||||
resolve(lcrs as Lcr[]);
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
}),
|
||||
]).then(([numbers, fetchedLcrs]) => {
|
||||
if (!ignore) {
|
||||
setPhoneNumbers(
|
||||
json.filter(
|
||||
numbers.json.filter(
|
||||
(phone) => phone.voip_carrier_sid === carrier.voip_carrier_sid,
|
||||
),
|
||||
);
|
||||
|
||||
// Only set LCRs if they are not empty
|
||||
setLcrs(fetchedLcrs.filter((p) => hasValue(p)));
|
||||
}
|
||||
});
|
||||
|
||||
return function cleanup() {
|
||||
ignore = true;
|
||||
};
|
||||
}, []);
|
||||
}, [carrier.voip_carrier_sid]);
|
||||
|
||||
const hasBlockingDependencies = hasLength(phoneNumbers) || hasLength(lcrs);
|
||||
|
||||
return (
|
||||
<>
|
||||
{phoneNumbers && !hasLength(phoneNumbers) && (
|
||||
{phoneNumbers && lcrs && !hasBlockingDependencies && (
|
||||
<Modal handleCancel={handleCancel} handleSubmit={handleSubmit}>
|
||||
<P>
|
||||
Are you sure you want to delete carrier{" "}
|
||||
@@ -49,24 +85,49 @@ export const DeleteCarrier = ({
|
||||
</P>
|
||||
</Modal>
|
||||
)}
|
||||
{hasLength(phoneNumbers) && (
|
||||
{hasBlockingDependencies && (
|
||||
<ModalClose handleClose={handleCancel}>
|
||||
<P>
|
||||
In order to delete the carrier it cannot be in use by any{" "}
|
||||
<span>Phone Numbers ({phoneNumbers.length})</span>.
|
||||
{hasLength(phoneNumbers) && (
|
||||
<span>Phone Numbers ({phoneNumbers.length})</span>
|
||||
)}
|
||||
{hasLength(phoneNumbers) && hasLength(lcrs) && " or "}
|
||||
{hasLength(lcrs) && (
|
||||
<span>Outbound call Routings ({lcrs.length})</span>
|
||||
)}
|
||||
.
|
||||
</P>
|
||||
<ul className="m">
|
||||
<li>
|
||||
<strong>Phone Numbers:</strong>
|
||||
</li>
|
||||
{phoneNumbers.map((phone) => {
|
||||
return (
|
||||
<li className="txt--teal" key={phone.phone_number_sid}>
|
||||
{formatPhoneNumber(phone.number)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{hasLength(phoneNumbers) && (
|
||||
<ul className="m">
|
||||
<li>
|
||||
<strong>Phone Numbers:</strong>
|
||||
</li>
|
||||
{phoneNumbers.map((phone) => {
|
||||
return (
|
||||
<li className="txt--teal" key={phone.phone_number_sid}>
|
||||
{formatPhoneNumber(phone.number)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{hasLength(lcrs) && (
|
||||
<ul className="m">
|
||||
<li>
|
||||
<strong>Outbound Call Routing:</strong>
|
||||
</li>
|
||||
{lcrs.map((lcr) => {
|
||||
return (
|
||||
<li className="txt--teal" key={lcr.lcr_sid}>
|
||||
{lcr.name || "Default route"}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</ModalClose>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -3,15 +3,17 @@ import { H1 } from "@jambonz/ui-kit";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { useApiData } from "src/api";
|
||||
import { toastError, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import { CarrierForm } from "./form";
|
||||
|
||||
import { Carrier, SipGateway, SmppGateway } from "src/api/types";
|
||||
import { useScopedRedirect } from "src/utils/use-scoped-redirect";
|
||||
import { ROUTE_INTERNAL_CARRIERS } from "src/router/routes";
|
||||
import { Scope } from "src/store/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const EditCarrier = () => {
|
||||
const { toastError } = useToast();
|
||||
const params = useParams();
|
||||
const user = useSelectState("user");
|
||||
const [data, refetch, error] = useApiData<Carrier>(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,16 @@
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Button, H1, Icon, M } from "@jambonz/ui-kit";
|
||||
import { Button, ButtonGroup, H1, Icon, M, MS } from "@jambonz/ui-kit";
|
||||
import {
|
||||
deleteCarrier,
|
||||
deleteSipGateway,
|
||||
deleteSmppGateway,
|
||||
getFetch,
|
||||
getSPVoipCarriers,
|
||||
useApiData,
|
||||
useServiceProviderData,
|
||||
} from "src/api";
|
||||
import { toastSuccess, toastError, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import { ROUTE_INTERNAL_CARRIERS } from "src/router/routes";
|
||||
import {
|
||||
AccountFilter,
|
||||
@@ -17,20 +18,18 @@ import {
|
||||
Section,
|
||||
Spinner,
|
||||
SearchFilter,
|
||||
Pagination,
|
||||
SelectFilter,
|
||||
} from "src/components";
|
||||
import { ScopedAccess } from "src/components/scoped-access";
|
||||
import { Gateways } from "./gateways";
|
||||
import {
|
||||
isUserAccountScope,
|
||||
hasLength,
|
||||
hasValue,
|
||||
useFilteredResults,
|
||||
} from "src/utils";
|
||||
import { isUserAccountScope, hasLength, hasValue } from "src/utils";
|
||||
import {
|
||||
API_SIP_GATEWAY,
|
||||
API_SMPP_GATEWAY,
|
||||
CARRIER_REG_OK,
|
||||
ENABLE_HOSTED_SYSTEM,
|
||||
PER_PAGE_SELECTION,
|
||||
USER_ACCOUNT,
|
||||
} from "src/api/constants";
|
||||
import { DeleteCarrier } from "./delete";
|
||||
@@ -44,37 +43,62 @@ import type {
|
||||
} from "src/api/types";
|
||||
import { Scope } from "src/store/types";
|
||||
import { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const Carriers = () => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const user = useSelectState("user");
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [apiUrl, setApiUrl] = useState("");
|
||||
const [carrier, setCarrier] = useState<Carrier | null>(null);
|
||||
const [carriers, refetch] = useApiData<Carrier[]>(apiUrl);
|
||||
const [carriers, setCarriers] = useState<Carrier[] | null>(null);
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const carriersFiltered = useMemo(() => {
|
||||
setAccountSid(getAccountFilter());
|
||||
if (user?.account_sid && user?.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
const [carriersTotal, setCarriersTotal] = useState(0);
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [perPageFilter, setPerPageFilter] = useState("25");
|
||||
const [maxPageNumber, setMaxPageNumber] = useState(1);
|
||||
|
||||
// Add a ref to track previous values
|
||||
const prevValuesRef = useRef({
|
||||
serviceProviderId: "",
|
||||
accountSid: "",
|
||||
filter: "",
|
||||
pageNumber: 1,
|
||||
perPageFilter: "25",
|
||||
});
|
||||
|
||||
const fetchCarriers = (resetPage = false) => {
|
||||
if (!currentServiceProvider) return;
|
||||
|
||||
setCarriers(null);
|
||||
|
||||
// Calculate the correct page to use
|
||||
const currentPage = resetPage ? 1 : pageNumber;
|
||||
|
||||
// If we're resetting the page, also update the state
|
||||
if (resetPage && pageNumber !== 1) {
|
||||
setPageNumber(1);
|
||||
}
|
||||
|
||||
return carriers
|
||||
? carriers.filter((carrier) =>
|
||||
accountSid
|
||||
? carrier.account_sid === accountSid
|
||||
: carrier.account_sid === null,
|
||||
)
|
||||
: [];
|
||||
}, [accountSid, carrier, carriers]);
|
||||
|
||||
const filteredCarriers = useFilteredResults<Carrier>(
|
||||
filter,
|
||||
carriersFiltered,
|
||||
);
|
||||
getSPVoipCarriers(currentServiceProvider.service_provider_sid, {
|
||||
page: currentPage,
|
||||
page_size: Number(perPageFilter),
|
||||
...(filter && { name: filter }),
|
||||
...(accountSid && { account_sid: accountSid }),
|
||||
})
|
||||
.then(({ json }) => {
|
||||
setCarriers(json.data);
|
||||
setCarriersTotal(json.total);
|
||||
setMaxPageNumber(Math.ceil(json.total / Number(perPageFilter)));
|
||||
})
|
||||
.catch((error) => {
|
||||
setCarriers([]);
|
||||
toastError(error.msg);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (carrier) {
|
||||
@@ -113,7 +137,7 @@ export const Carriers = () => {
|
||||
);
|
||||
});
|
||||
setCarrier(null);
|
||||
refetch();
|
||||
fetchCarriers(false);
|
||||
toastSuccess(
|
||||
<>
|
||||
Deleted Carrier <strong>{carrier.name}</strong>
|
||||
@@ -126,14 +150,45 @@ export const Carriers = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Initial account setup
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (currentServiceProvider) {
|
||||
setApiUrl(
|
||||
`ServiceProviders/${currentServiceProvider.service_provider_sid}/VoipCarriers`,
|
||||
);
|
||||
if (user?.account_sid && user?.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
} else {
|
||||
setAccountSid(getAccountFilter());
|
||||
}
|
||||
}, [user, currentServiceProvider, accountSid]);
|
||||
setLocation();
|
||||
}, [user, accounts]);
|
||||
|
||||
// Combined effect for all data fetching
|
||||
useEffect(() => {
|
||||
if (!currentServiceProvider) return;
|
||||
|
||||
const prevValues = prevValuesRef.current;
|
||||
const currentSPId = currentServiceProvider.service_provider_sid;
|
||||
|
||||
// Determine if we should reset pagination
|
||||
const isFilterOrProviderChange =
|
||||
prevValues.serviceProviderId !== currentSPId ||
|
||||
prevValues.accountSid !== accountSid ||
|
||||
prevValues.filter !== filter;
|
||||
|
||||
const isPageSizeChange =
|
||||
prevValues.perPageFilter !== perPageFilter &&
|
||||
prevValues.perPageFilter !== "25"; // Skip initial render
|
||||
|
||||
// Update ref for next comparison
|
||||
prevValuesRef.current = {
|
||||
serviceProviderId: currentSPId,
|
||||
accountSid,
|
||||
filter,
|
||||
pageNumber,
|
||||
perPageFilter,
|
||||
};
|
||||
|
||||
// Fetch data with page reset if filters changed
|
||||
fetchCarriers(isFilterOrProviderChange || isPageSizeChange);
|
||||
}, [currentServiceProvider, accountSid, filter, pageNumber, perPageFilter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -159,6 +214,7 @@ export const Carriers = () => {
|
||||
<SearchFilter
|
||||
placeholder="Filter carriers"
|
||||
filter={[filter, setFilter]}
|
||||
delay={1000}
|
||||
/>
|
||||
<ScopedAccess user={user} scope={Scope.service_provider}>
|
||||
<AccountFilter
|
||||
@@ -169,12 +225,12 @@ export const Carriers = () => {
|
||||
/>
|
||||
</ScopedAccess>
|
||||
</section>
|
||||
<Section {...(hasLength(filteredCarriers) && { slim: true })}>
|
||||
<Section {...(hasLength(carriers) && { slim: true })}>
|
||||
<div className="list">
|
||||
{!hasValue(carriers) && hasLength(accounts) ? (
|
||||
<Spinner />
|
||||
) : hasLength(filteredCarriers) ? (
|
||||
filteredCarriers.map((carrier) => (
|
||||
) : hasLength(carriers) ? (
|
||||
carriers.map((carrier) => (
|
||||
<div className="item" key={carrier.voip_carrier_sid}>
|
||||
<div className="item__info">
|
||||
<div className="item__title">
|
||||
@@ -274,6 +330,26 @@ export const Carriers = () => {
|
||||
Add carrier
|
||||
</Button>
|
||||
</Section>
|
||||
<footer>
|
||||
<ButtonGroup>
|
||||
<MS>
|
||||
Total: {carriersTotal} record
|
||||
{carriersTotal === 1 ? "" : "s"}
|
||||
</MS>
|
||||
{hasLength(carriers) && (
|
||||
<Pagination
|
||||
pageNumber={pageNumber}
|
||||
setPageNumber={setPageNumber}
|
||||
maxPageNumber={maxPageNumber}
|
||||
/>
|
||||
)}
|
||||
<SelectFilter
|
||||
id="page_filter"
|
||||
filter={[perPageFilter, setPerPageFilter]}
|
||||
options={PER_PAGE_SELECTION}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</footer>
|
||||
{carrier && (
|
||||
<DeleteCarrier
|
||||
carrier={carrier}
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
getPcap,
|
||||
getServiceProviderPcap,
|
||||
} from "src/api";
|
||||
import { toastError } from "src/store";
|
||||
|
||||
import type { DownloadedBlob } from "src/api/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type PcapButtonProps = {
|
||||
accountSid: string;
|
||||
@@ -21,6 +21,7 @@ export const PcapButton = ({
|
||||
serviceProviderSid,
|
||||
sipCallId,
|
||||
}: PcapButtonProps) => {
|
||||
const { toastError } = useToast();
|
||||
const [pcap, setPcap] = useState<DownloadedBlob>();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -3,11 +3,12 @@ import React, { useEffect } from "react";
|
||||
import { useNavigate, 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";
|
||||
import { ROUTE_INTERNAL_CLIENTS } from "src/router/routes";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const ClientsEdit = () => {
|
||||
const { toastError } = useToast();
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [data, refetch, error] = useApiData<Client>(
|
||||
|
||||
@@ -13,16 +13,18 @@ import { Section, Tooltip } 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 { useSelectState } from "src/store";
|
||||
import ClientsDelete from "./delete";
|
||||
import { hasValue } from "src/utils";
|
||||
import { IMessage } from "src/store/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type ClientsFormProps = {
|
||||
client?: UseApiDataMap<Client>;
|
||||
};
|
||||
|
||||
export const ClientsForm = ({ client }: ClientsFormProps) => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const user = useSelectState("user");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -12,13 +12,16 @@ import {
|
||||
Spinner,
|
||||
} from "src/components";
|
||||
import { ROUTE_INTERNAL_CLIENTS } from "src/router/routes";
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import { 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";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
import { getAccountFilter } from "src/store/localStore";
|
||||
|
||||
export const Clients = () => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const user = useSelectState("user");
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
@@ -32,6 +35,7 @@ export const Clients = () => {
|
||||
const [client, setClient] = useState<Client | null>();
|
||||
|
||||
const tmpFilteredClients = useMemo(() => {
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
if (user?.account_sid && user?.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
return clients;
|
||||
|
||||
@@ -4,8 +4,8 @@ import Card from "./card";
|
||||
import { hasLength } from "src/utils";
|
||||
import update from "immutability-helper";
|
||||
import { deleteLcrRoute } from "src/api";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
import { SelectorOption } from "src/components/forms/selector";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type ContainerProps = {
|
||||
lcrRoute: [LcrRoute[], React.Dispatch<React.SetStateAction<LcrRoute[]>>];
|
||||
@@ -16,6 +16,7 @@ export const Container = ({
|
||||
lcrRoute: [lcrRoutes, setLcrRoutes],
|
||||
carrierSelectorOptions,
|
||||
}: ContainerProps) => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const moveCard = (dragIndex: number, hoverIndex: number) => {
|
||||
setLcrRoutes((prevCards) =>
|
||||
update(prevCards, {
|
||||
|
||||
@@ -2,12 +2,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Button, ButtonGroup, Icon, MS, MXS } from "@jambonz/ui-kit";
|
||||
import { Icons, Section } from "src/components";
|
||||
import {
|
||||
toastError,
|
||||
toastSuccess,
|
||||
useDispatch,
|
||||
useSelectState,
|
||||
} from "src/store";
|
||||
import { useDispatch, useSelectState } from "src/store";
|
||||
import { MSG_REQUIRED_FIELDS } from "src/constants";
|
||||
import { setLocation } from "src/store/localStore";
|
||||
import { AccountSelect, Message, Selector } from "src/components/forms";
|
||||
@@ -35,6 +30,7 @@ import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import Container from "./container";
|
||||
import { hasValue } from "src/utils";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type LcrFormProps = {
|
||||
lcrDataMap?: UseApiDataMap<Lcr>;
|
||||
@@ -56,6 +52,7 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
|
||||
],
|
||||
};
|
||||
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -70,9 +67,6 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [lcrRoutes, setLcrRoutes] = useState<LcrRoute[]>([LCR_ROUTE_TEMPLATE]);
|
||||
const [previousLcrRoutes, setPreviousLcrRoutes] = useState<LcrRoute[]>([
|
||||
LCR_ROUTE_TEMPLATE,
|
||||
]);
|
||||
const [previouseLcr, setPreviousLcr] = useState<Lcr | null>();
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [lcrForDelete, setLcrForDelete] = useState<Lcr | null>();
|
||||
@@ -85,7 +79,7 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
|
||||
setLocation();
|
||||
if (currentServiceProvider) {
|
||||
setApiUrl(
|
||||
`ServiceProviders/${currentServiceProvider.service_provider_sid}/VoipCarriers`,
|
||||
`ServiceProviders/${currentServiceProvider.service_provider_sid}/VoipCarriers${accountSid ? `?account_sid=${accountSid}` : ""}`,
|
||||
);
|
||||
}
|
||||
}, [user, currentServiceProvider, accountSid]);
|
||||
@@ -95,16 +89,8 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
|
||||
setAccountSid(user?.account_sid);
|
||||
}
|
||||
|
||||
const carriersFiltered = carriers
|
||||
? carriers.filter((carrier) =>
|
||||
accountSid
|
||||
? carrier.account_sid === accountSid
|
||||
: carrier.account_sid === null,
|
||||
)
|
||||
: [];
|
||||
|
||||
const ret = carriersFiltered
|
||||
? carriersFiltered.map((c: Carrier, i) => {
|
||||
const ret = carriers
|
||||
? carriers.map((c: Carrier, i) => {
|
||||
if (i === 0) {
|
||||
setDefaultCarrier(c.voip_carrier_sid);
|
||||
}
|
||||
@@ -126,45 +112,47 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
|
||||
return ret;
|
||||
}, [accountSid, carriers]);
|
||||
|
||||
if (lcrDataMap && lcrDataMap.data && lcrDataMap.data !== previouseLcr) {
|
||||
setLcrName(lcrDataMap.data.name || "");
|
||||
setIsActive(lcrDataMap.data.is_active);
|
||||
setPreviousLcr(lcrDataMap.data);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (lcrDataMap && lcrDataMap.data && lcrDataMap.data !== previouseLcr) {
|
||||
setLcrName(lcrDataMap.data.name || "");
|
||||
setIsActive(lcrDataMap.data.is_active);
|
||||
setPreviousLcr(lcrDataMap.data);
|
||||
if (lcrDataMap.data.account_sid) {
|
||||
setAccountSid(lcrDataMap.data.account_sid);
|
||||
}
|
||||
}
|
||||
}, [lcrDataMap?.data, previouseLcr]);
|
||||
|
||||
useMemo(() => {
|
||||
let default_lcr_route_sid = "";
|
||||
if (
|
||||
lcrRouteDataMap &&
|
||||
lcrRouteDataMap.data &&
|
||||
lcrRouteDataMap.data !== previousLcrRoutes
|
||||
) {
|
||||
setPreviousLcrRoutes(lcrRouteDataMap.data);
|
||||
// Find default carrier
|
||||
lcrRouteDataMap.data.forEach((lr) => {
|
||||
lr.lcr_carrier_set_entries?.forEach((entry) => {
|
||||
if (
|
||||
entry.lcr_carrier_set_entry_sid ===
|
||||
lcrDataMap?.data?.default_carrier_set_entry_sid
|
||||
) {
|
||||
// Only process when both lcrDataMap and lcrRouteDataMap are available
|
||||
if (lcrRouteDataMap && lcrRouteDataMap.data && lcrDataMap?.data) {
|
||||
const defaultCarrierSetEntrySid =
|
||||
lcrDataMap.data.default_carrier_set_entry_sid;
|
||||
|
||||
// Find and store default route information
|
||||
lcrRouteDataMap.data.forEach((route) => {
|
||||
route.lcr_carrier_set_entries?.forEach((entry) => {
|
||||
if (entry.lcr_carrier_set_entry_sid === defaultCarrierSetEntrySid) {
|
||||
setDefaultLcrCarrier(entry.voip_carrier_sid || defaultCarrier);
|
||||
setDefaultLcrCarrierSetEntrySid(
|
||||
entry.lcr_carrier_set_entry_sid || null,
|
||||
);
|
||||
default_lcr_route_sid = entry.lcr_route_sid || "";
|
||||
setDefaultLcrRoute(lr);
|
||||
setDefaultLcrRoute(route);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (lcrRouteDataMap && lcrRouteDataMap.data)
|
||||
setLcrRoutes(
|
||||
lcrRouteDataMap.data.filter(
|
||||
(route) => route.lcr_route_sid !== default_lcr_route_sid,
|
||||
),
|
||||
);
|
||||
}, [lcrRouteDataMap?.data]);
|
||||
// Filter out routes that contain the default carrier set entry
|
||||
const filteredRoutes = lcrRouteDataMap.data.filter((route) => {
|
||||
return !route.lcr_carrier_set_entries?.some(
|
||||
(entry) =>
|
||||
entry.lcr_carrier_set_entry_sid === defaultCarrierSetEntrySid,
|
||||
);
|
||||
});
|
||||
|
||||
setLcrRoutes(filteredRoutes);
|
||||
}
|
||||
}, [lcrRouteDataMap?.data, lcrDataMap?.data]);
|
||||
|
||||
const addLcrRoutes = () => {
|
||||
const newLcrRoute = LCR_ROUTE_TEMPLATE;
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "src/components";
|
||||
import { ScopedAccess } from "src/components/scoped-access";
|
||||
import { ROUTE_INTERNAL_LEST_COST_ROUTING } from "src/router/routes";
|
||||
import { toastSuccess, toastError, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
// import { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
import { Scope } from "src/store/types";
|
||||
import {
|
||||
@@ -25,8 +25,10 @@ import {
|
||||
} from "src/utils";
|
||||
import { USER_ACCOUNT } from "src/api/constants";
|
||||
import DeleteLcr from "./delete";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const Lcrs = () => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const user = useSelectState("user");
|
||||
useScopedRedirect(
|
||||
Scope.admin,
|
||||
|
||||
@@ -3,12 +3,13 @@ import { H1 } from "@jambonz/ui-kit";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { useApiData } from "src/api";
|
||||
import { toastError } from "src/store";
|
||||
import { MsTeamsTenantForm } from "./form";
|
||||
|
||||
import type { MSTeamsTenant } from "src/api/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const EditMsTeamsTenant = () => {
|
||||
const { toastError } = useToast();
|
||||
const params = useParams();
|
||||
const [data, refetch, error] = useApiData<MSTeamsTenant>(
|
||||
`MicrosoftTeamsTenants/${params.ms_teams_tenant_sid}`,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
ApplicationSelect,
|
||||
} from "src/components/forms";
|
||||
import { MSG_REQUIRED_FIELDS } from "src/constants";
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import {
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
ROUTE_INTERNAL_MS_TEAMS_TENANTS,
|
||||
@@ -28,6 +28,7 @@ import type {
|
||||
MSTeamsTenant,
|
||||
UseApiDataMap,
|
||||
} from "src/api/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type MsTeamsTenantFormProps = {
|
||||
msTeamsTenant?: UseApiDataMap<MSTeamsTenant>;
|
||||
@@ -36,6 +37,7 @@ type MsTeamsTenantFormProps = {
|
||||
export const MsTeamsTenantForm = ({
|
||||
msTeamsTenant,
|
||||
}: MsTeamsTenantFormProps) => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
withAccessControl,
|
||||
useFilteredResults,
|
||||
} from "src/utils";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
import {
|
||||
Icons,
|
||||
Section,
|
||||
@@ -29,8 +28,10 @@ import { DeleteMsTeamsTenant } from "./delete";
|
||||
|
||||
import type { Account, MSTeamsTenant, Application } from "src/api/types";
|
||||
import type { ACLGetIMessage } from "src/utils/with-access-control";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const MSTeamsTenants = () => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const [msTeamsTenant, setMsTeamsTenant] = useState<MSTeamsTenant | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -3,12 +3,13 @@ import { H1 } from "@jambonz/ui-kit";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { useApiData } from "src/api";
|
||||
import { toastError } from "src/store";
|
||||
import { PhoneNumberForm } from "./form";
|
||||
|
||||
import type { PhoneNumber } from "src/api/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const EditPhoneNumber = () => {
|
||||
const { toastError } = useToast();
|
||||
const params = useParams();
|
||||
const [data, refetch, error] = useApiData<PhoneNumber>(
|
||||
`PhoneNumbers/${params.phone_number_sid}`,
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
ROUTE_INTERNAL_CARRIERS,
|
||||
ROUTE_INTERNAL_PHONE_NUMBERS,
|
||||
} from "src/router/routes";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
import { hasLength, useRedirect } from "src/utils";
|
||||
|
||||
import type {
|
||||
@@ -31,12 +30,14 @@ import type {
|
||||
UseApiDataMap,
|
||||
} from "src/api/types";
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type PhoneNumberFormProps = {
|
||||
phoneNumber?: UseApiDataMap<PhoneNumber>;
|
||||
};
|
||||
|
||||
export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [applications] = useServiceProviderData<Application[]>("Applications");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { Button, ButtonGroup, H1, Icon, MS } from "@jambonz/ui-kit";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
putPhoneNumber,
|
||||
useServiceProviderData,
|
||||
} from "src/api";
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import {
|
||||
Icons,
|
||||
Section,
|
||||
@@ -16,28 +16,28 @@ import {
|
||||
ApplicationFilter,
|
||||
SearchFilter,
|
||||
AccountFilter,
|
||||
Pagination,
|
||||
SelectFilter,
|
||||
} from "src/components";
|
||||
import {
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
ROUTE_INTERNAL_CARRIERS,
|
||||
ROUTE_INTERNAL_PHONE_NUMBERS,
|
||||
} from "src/router/routes";
|
||||
import {
|
||||
hasLength,
|
||||
hasValue,
|
||||
formatPhoneNumber,
|
||||
useFilteredResults,
|
||||
} from "src/utils";
|
||||
import { hasLength, hasValue, formatPhoneNumber } from "src/utils";
|
||||
import { DeletePhoneNumber } from "./delete";
|
||||
|
||||
import type { Account, PhoneNumber, Carrier, Application } from "src/api/types";
|
||||
import { ENABLE_PHONE_NUMBER_LAZY_LOAD, USER_ACCOUNT } from "src/api/constants";
|
||||
import { PER_PAGE_SELECTION, USER_ACCOUNT } from "src/api/constants";
|
||||
import { ScopedAccess } from "src/components/scoped-access";
|
||||
import { Scope } from "src/store/types";
|
||||
import { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const PhoneNumbers = () => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const user = useSelectState("user");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [applications] = useServiceProviderData<Application[]>("Applications");
|
||||
const [carriers] = useServiceProviderData<Carrier[]>("VoipCarriers");
|
||||
@@ -51,35 +51,51 @@ export const PhoneNumbers = () => {
|
||||
const [applyMassEdit, setApplyMassEdit] = useState(false);
|
||||
const [filter, setFilter] = useState("");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [phoneNumbersTotal, setphoneNumbersTotal] = useState(0);
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [perPageFilter, setPerPageFilter] = useState("25");
|
||||
const [maxPageNumber, setMaxPageNumber] = useState(1);
|
||||
|
||||
const phoneNumbersFiltered = useMemo(() => {
|
||||
setAccountSid(getAccountFilter());
|
||||
return phoneNumbers
|
||||
? phoneNumbers.filter(
|
||||
(phn) => !accountSid || phn.account_sid === accountSid,
|
||||
)
|
||||
: [];
|
||||
}, [phoneNumbers, ...[!ENABLE_PHONE_NUMBER_LAZY_LOAD && accountSid]]);
|
||||
// Add ref to track previous values
|
||||
const prevValuesRef = useRef({
|
||||
serviceProviderId: "",
|
||||
accountSid: "",
|
||||
filter: "",
|
||||
pageNumber: 1,
|
||||
perPageFilter: "25",
|
||||
});
|
||||
|
||||
const filteredPhoneNumbers = !ENABLE_PHONE_NUMBER_LAZY_LOAD
|
||||
? useFilteredResults<PhoneNumber>(filter, phoneNumbersFiltered)
|
||||
: phoneNumbersFiltered;
|
||||
const fetchPhoneNumbers = (resetPage = false) => {
|
||||
setPhoneNumbers(null);
|
||||
|
||||
const refetch = () => {
|
||||
getPhoneNumbers(
|
||||
!ENABLE_PHONE_NUMBER_LAZY_LOAD
|
||||
? {}
|
||||
: {
|
||||
...(accountSid && { account_sid: accountSid }),
|
||||
...(filter && { filter }),
|
||||
},
|
||||
)
|
||||
// Calculate the correct page to use
|
||||
const currentPage = resetPage ? 1 : pageNumber;
|
||||
|
||||
// If we're resetting the page, also update the state
|
||||
if (resetPage && pageNumber !== 1) {
|
||||
setPageNumber(1);
|
||||
}
|
||||
|
||||
const accSid = accountSid || getAccountFilter() || "";
|
||||
|
||||
getPhoneNumbers({
|
||||
page: currentPage,
|
||||
page_size: Number(perPageFilter),
|
||||
...(accSid && { account_sid: accSid }),
|
||||
...(filter && { filter }),
|
||||
...(currentServiceProvider?.service_provider_sid && {
|
||||
service_provider_sid: currentServiceProvider.service_provider_sid,
|
||||
}),
|
||||
})
|
||||
.then(({ json }) => {
|
||||
if (json) {
|
||||
setPhoneNumbers(json);
|
||||
setPhoneNumbers(json.data);
|
||||
setphoneNumbersTotal(json.total);
|
||||
setMaxPageNumber(Math.ceil(json.total / Number(perPageFilter)));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setPhoneNumbers([]);
|
||||
toastError(error.msg);
|
||||
});
|
||||
};
|
||||
@@ -95,9 +111,11 @@ export const PhoneNumbers = () => {
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
refetch();
|
||||
fetchPhoneNumbers(false);
|
||||
setApplicationSid("");
|
||||
setApplyMassEdit(false);
|
||||
setSelectAll(false);
|
||||
setSelectedPhoneNumbers([]);
|
||||
toastSuccess("Number routing updated successfully");
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -111,7 +129,7 @@ export const PhoneNumbers = () => {
|
||||
if (phoneNumber) {
|
||||
deletePhoneNumber(phoneNumber.phone_number_sid)
|
||||
.then(() => {
|
||||
refetch();
|
||||
fetchPhoneNumbers(false);
|
||||
setPhoneNumber(null);
|
||||
toastSuccess(
|
||||
<>
|
||||
@@ -125,21 +143,43 @@ export const PhoneNumbers = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Initial account setup
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (user?.account_sid && user.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
} else {
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
}
|
||||
setLocation();
|
||||
}, [user]);
|
||||
|
||||
// Combined effect for all data fetching
|
||||
useEffect(() => {
|
||||
if (ENABLE_PHONE_NUMBER_LAZY_LOAD) {
|
||||
setPhoneNumbers([]);
|
||||
return;
|
||||
}
|
||||
const prevValues = prevValuesRef.current;
|
||||
const currentSPId = currentServiceProvider?.service_provider_sid;
|
||||
|
||||
refetch();
|
||||
}, []);
|
||||
// Detect changes that require page reset
|
||||
const isFilterOrProviderChange =
|
||||
prevValues.serviceProviderId !== currentSPId ||
|
||||
prevValues.accountSid !== accountSid ||
|
||||
prevValues.filter !== filter;
|
||||
|
||||
const isPageSizeChange =
|
||||
prevValues.perPageFilter !== perPageFilter &&
|
||||
prevValues.perPageFilter !== "25"; // Skip initial render
|
||||
|
||||
// Update ref for next comparison
|
||||
prevValuesRef.current = {
|
||||
serviceProviderId: currentSPId || "",
|
||||
accountSid,
|
||||
filter,
|
||||
pageNumber,
|
||||
perPageFilter,
|
||||
};
|
||||
|
||||
// Fetch data with appropriate reset parameter
|
||||
fetchPhoneNumbers(isFilterOrProviderChange || isPageSizeChange);
|
||||
}, [currentServiceProvider, accountSid, filter, pageNumber, perPageFilter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -160,6 +200,7 @@ export const PhoneNumbers = () => {
|
||||
<SearchFilter
|
||||
placeholder="Filter phone numbers"
|
||||
filter={[filter, setFilter]}
|
||||
delay={1000}
|
||||
/>
|
||||
<ScopedAccess user={user} scope={Scope.service_provider}>
|
||||
<AccountFilter
|
||||
@@ -168,25 +209,12 @@ export const PhoneNumbers = () => {
|
||||
defaultOption
|
||||
/>
|
||||
</ScopedAccess>
|
||||
{ENABLE_PHONE_NUMBER_LAZY_LOAD && (
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
small
|
||||
onClick={() => {
|
||||
setPhoneNumbers(null);
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</section>
|
||||
<Section {...(hasLength(filteredPhoneNumbers) && { slim: true })}>
|
||||
<Section {...(hasLength(phoneNumbers) && { slim: true })}>
|
||||
<div className="list">
|
||||
{!hasValue(phoneNumbers) ? (
|
||||
<Spinner />
|
||||
) : hasLength(filteredPhoneNumbers) ? (
|
||||
) : hasLength(phoneNumbers) ? (
|
||||
<>
|
||||
<div className="item item--actions">
|
||||
{accountSid ? (
|
||||
@@ -200,7 +228,7 @@ export const PhoneNumbers = () => {
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectAll(true);
|
||||
setSelectedPhoneNumbers(filteredPhoneNumbers);
|
||||
setSelectedPhoneNumbers(phoneNumbers);
|
||||
} else {
|
||||
setSelectAll(false);
|
||||
setSelectedPhoneNumbers([]);
|
||||
@@ -224,10 +252,8 @@ export const PhoneNumbers = () => {
|
||||
<Button
|
||||
small
|
||||
onClick={() => {
|
||||
handleMassEdit();
|
||||
setSelectAll(false);
|
||||
setApplyMassEdit(true);
|
||||
setSelectedPhoneNumbers([]);
|
||||
handleMassEdit();
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
@@ -249,7 +275,7 @@ export const PhoneNumbers = () => {
|
||||
</MS>
|
||||
)}
|
||||
</div>
|
||||
{filteredPhoneNumbers.map((phoneNumber) => {
|
||||
{phoneNumbers.map((phoneNumber) => {
|
||||
return (
|
||||
<div className="item" key={phoneNumber.phone_number_sid}>
|
||||
<div className="item__info">
|
||||
@@ -385,6 +411,26 @@ export const PhoneNumbers = () => {
|
||||
</Button>
|
||||
)}
|
||||
</Section>
|
||||
<footer>
|
||||
<ButtonGroup>
|
||||
<MS>
|
||||
Total: {phoneNumbersTotal} record
|
||||
{phoneNumbersTotal === 1 ? "" : "s"}
|
||||
</MS>
|
||||
{hasLength(phoneNumbers) && (
|
||||
<Pagination
|
||||
pageNumber={pageNumber}
|
||||
setPageNumber={setPageNumber}
|
||||
maxPageNumber={maxPageNumber}
|
||||
/>
|
||||
)}
|
||||
<SelectFilter
|
||||
id="page_filter"
|
||||
filter={[perPageFilter, setPerPageFilter]}
|
||||
options={PER_PAGE_SELECTION}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</footer>
|
||||
{phoneNumber && (
|
||||
<DeletePhoneNumber
|
||||
phoneNumber={phoneNumber}
|
||||
|
||||
@@ -3,9 +3,9 @@ import React, { useEffect, useState } from "react";
|
||||
import { getRecentCallLog } from "src/api";
|
||||
import { RecentCall } from "src/api/types";
|
||||
import { Icons, Spinner } from "src/components";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
import { hasValue } from "src/utils";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
dayjs.extend(utc);
|
||||
|
||||
type CallSystemLogsProps = {
|
||||
@@ -29,6 +29,7 @@ const formatLog = (log: string): string => {
|
||||
};
|
||||
|
||||
export default function CallSystemLogs({ call }: CallSystemLogsProps) {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const [logs, setLogs] = useState<string[] | null>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
PER_PAGE_SELECTION,
|
||||
USER_ACCOUNT,
|
||||
} from "src/api/constants";
|
||||
import { toastError, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import {
|
||||
Section,
|
||||
AccountFilter,
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
getQueryFilter,
|
||||
setLocation,
|
||||
} from "src/store/localStore";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
const directionSelection = [
|
||||
{ name: "either", value: "io" },
|
||||
@@ -42,6 +43,7 @@ const statusSelection = [
|
||||
];
|
||||
|
||||
export const RecentCalls = () => {
|
||||
const { toastError } = useToast();
|
||||
const user = useSelectState("user");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
@@ -87,10 +89,10 @@ export const RecentCalls = () => {
|
||||
};
|
||||
|
||||
useMemo(() => {
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
if (!accountSid && user?.account_sid) setAccountSid(user?.account_sid);
|
||||
if (getQueryFilter()) {
|
||||
const [date, direction, status] = getQueryFilter().split("/");
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
if (!accountSid && user?.account_sid) setAccountSid(user?.account_sid);
|
||||
setDateFilter(date);
|
||||
setDirectionFilter(direction);
|
||||
setStatusFilter(status);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
.barGroup {
|
||||
border-radius: ui-vars.$px01;
|
||||
@include mixins.code();
|
||||
text-align: left;
|
||||
padding: ui-vars.$px03;
|
||||
color: ui-vars.$pink;
|
||||
@@ -13,6 +12,7 @@
|
||||
margin-top: ui-vars.$px02;
|
||||
overflow-x: auto;
|
||||
overflow-y: scroll;
|
||||
@include mixins.code();
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding: 15px;
|
||||
@@ -72,7 +72,6 @@
|
||||
|
||||
.spanDetailsWrapper {
|
||||
border-radius: ui-vars.$px01;
|
||||
@include mixins.code();
|
||||
text-align: left;
|
||||
padding: ui-vars.$px01;
|
||||
background-color: ui-vars.$white;
|
||||
@@ -82,6 +81,7 @@
|
||||
max-width: ui-vars.$width-tablet-2;
|
||||
max-height: 500px;
|
||||
overflow-y: scroll;
|
||||
@include mixins.code();
|
||||
|
||||
&__detailsWrapper {
|
||||
height: 100%;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { getPcap } from "src/api";
|
||||
import { toastError } from "src/store";
|
||||
|
||||
import type { DownloadedBlob, RecentCall } from "src/api/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type PcapButtonProps = {
|
||||
call: RecentCall;
|
||||
};
|
||||
|
||||
export const PcapButton = ({ call }: PcapButtonProps) => {
|
||||
const { toastError } = useToast();
|
||||
const [pcap, setPcap] = useState<DownloadedBlob | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -22,13 +22,14 @@ import {
|
||||
getSpansByNameRegex,
|
||||
getSpansFromJaegerRoot,
|
||||
} from "./utils";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type PlayerProps = {
|
||||
call: RecentCall;
|
||||
};
|
||||
|
||||
export const Player = ({ call }: PlayerProps) => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const { recording_url, call_sid } = call;
|
||||
const url =
|
||||
recording_url && recording_url.startsWith("http://")
|
||||
@@ -160,6 +161,7 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
|
||||
const drawSttRegionForSpan = (
|
||||
s: JaegerSpan,
|
||||
allSpans: JaegerSpan[],
|
||||
startPoint: JaegerSpan,
|
||||
channel = 0,
|
||||
) => {
|
||||
@@ -174,7 +176,36 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
const end =
|
||||
(s.endTimeUnixNano - startPoint.startTimeUnixNano) / 1_000_000_000;
|
||||
|
||||
const endSpeechTime = getSilenceStartTime(start, end, channel);
|
||||
const verbHookSpans = getSpansByNameRegex(allSpans, /verb:hook/);
|
||||
const verbHookSpan = verbHookSpans.find(
|
||||
(v) => v.parentSpanId === s.spanId,
|
||||
);
|
||||
|
||||
let verbHookDurantion = 0;
|
||||
let latency = 0;
|
||||
if (verbHookSpan) {
|
||||
verbHookDurantion =
|
||||
(verbHookSpan.endTimeUnixNano - verbHookSpan.startTimeUnixNano) /
|
||||
1_000_000_000;
|
||||
}
|
||||
|
||||
const [sttLatencyMs] = getSpanAttributeByName(
|
||||
s.attributes,
|
||||
"stt.latency_ms",
|
||||
);
|
||||
let endSpeechTime = 0;
|
||||
if (!sttLatencyMs) {
|
||||
endSpeechTime = getSilenceStartTime(start, end, channel);
|
||||
latency = Number(
|
||||
(end - endSpeechTime - verbHookDurantion).toFixed(2),
|
||||
);
|
||||
} else {
|
||||
endSpeechTime =
|
||||
end -
|
||||
Number(sttLatencyMs.value.stringValue) / 1_000 -
|
||||
verbHookDurantion;
|
||||
latency = Number(sttLatencyMs.value.stringValue) / 1_000;
|
||||
}
|
||||
|
||||
const [sttResult] = getSpanAttributeByName(s.attributes, "stt.result");
|
||||
let att: WaveSurferSttResult;
|
||||
@@ -186,7 +217,7 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
transcript: data.alternatives[0].transcript,
|
||||
confidence: data.alternatives[0].confidence,
|
||||
language_code: data.language_code,
|
||||
...(endSpeechTime > 0 && { latency: end - endSpeechTime }),
|
||||
latency,
|
||||
};
|
||||
|
||||
const [sttResolve] = getSpanAttributeByName(
|
||||
@@ -205,7 +236,7 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
color: "rgba(255, 255, 0, 0.55)",
|
||||
drag: false,
|
||||
resize: false,
|
||||
content: `${(end - endSpeechTime).toFixed(2)}s`,
|
||||
content: `${latency}s`,
|
||||
});
|
||||
|
||||
changeRegionMouseStyle(latencyRegion, channel);
|
||||
@@ -352,7 +383,7 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
if (startPoint) {
|
||||
const gatherSpans = getSpansByNameRegex(spans, /:gather{/);
|
||||
gatherSpans.forEach((s) => {
|
||||
drawSttRegionForSpan(s, startPoint);
|
||||
drawSttRegionForSpan(s, spans, startPoint);
|
||||
});
|
||||
|
||||
// Trasscription
|
||||
@@ -362,6 +393,7 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
const channel = Number(cs.name.split(":")[1]);
|
||||
drawSttRegionForSpan(
|
||||
cs,
|
||||
spans,
|
||||
startPoint,
|
||||
channel > 0 ? channel - 1 : channel,
|
||||
);
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
SystemInformation,
|
||||
TtsCache,
|
||||
} from "src/api/types";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
import { Selector } from "src/components/forms";
|
||||
import { hasValue, isvalidIpv4OrCidr } from "src/utils";
|
||||
import {
|
||||
@@ -22,8 +21,10 @@ import {
|
||||
PASSWORD_MIN,
|
||||
} from "src/api/constants";
|
||||
import { Modal } from "src/components";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const AdminSettings = () => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const [passwordSettings, passwordSettingsFetcher] =
|
||||
useApiData<PasswordSettings>("PasswordSettings");
|
||||
const [systemInformation, systemInformationFetcher] =
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { P, Button, ButtonGroup } from "@jambonz/ui-kit";
|
||||
|
||||
import { useDispatch, toastSuccess, toastError } from "src/store";
|
||||
import { useDispatch } from "src/store";
|
||||
import { hasLength } from "src/utils";
|
||||
import {
|
||||
putServiceProvider,
|
||||
@@ -15,6 +15,7 @@ import { Checkzone, LocalLimits } from "src/components/forms";
|
||||
import { withSelectState } from "src/utils";
|
||||
import type { Limit, ServiceProvider } from "src/api/types";
|
||||
import { removeActiveSP } from "src/store/localStore";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export type ServiceProviderSettingsProps = {
|
||||
serviceProviders: ServiceProvider[];
|
||||
@@ -25,6 +26,7 @@ export const ServiceProviderSettings = ({
|
||||
serviceProviders,
|
||||
currentServiceProvider,
|
||||
}: ServiceProviderSettingsProps) => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const dispatch = useDispatch();
|
||||
const [limits, refetchLimits] = useServiceProviderData<Limit[]>("Limits");
|
||||
const [name, setName] = useState("");
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { H1 } from "@jambonz/ui-kit";
|
||||
|
||||
import { useApiData } from "src/api";
|
||||
import { toastError, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import { SpeechServiceForm } from "./form";
|
||||
|
||||
import type { SpeechCredential } from "src/api/types";
|
||||
@@ -11,6 +11,7 @@ import { useScopedRedirect } from "src/utils/use-scoped-redirect";
|
||||
import { Scope } from "src/store/types";
|
||||
import { ROUTE_INTERNAL_SPEECH } from "src/router/routes";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const EditSpeechService = () => {
|
||||
const params = useParams();
|
||||
@@ -18,6 +19,7 @@ export const EditSpeechService = () => {
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [url, setUrl] = useState("");
|
||||
const [data, refetch, error] = useApiData<SpeechCredential>(url);
|
||||
const { toastError } = useToast();
|
||||
|
||||
useScopedRedirect(
|
||||
Scope.account,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Checkzone,
|
||||
Message,
|
||||
} from "src/components/forms";
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import {
|
||||
deleteGoogleCustomVoice,
|
||||
getGoogleCustomVoices,
|
||||
@@ -52,6 +52,11 @@ import {
|
||||
VENDOR_CARTESIA,
|
||||
VENDOR_VOXIST,
|
||||
VENDOR_OPENAI,
|
||||
VENDOR_INWORLD,
|
||||
VENDOR_DEEPGRAM_FLUX,
|
||||
VENDOR_RESEMBLE,
|
||||
VENDOR_HOUNDIFY,
|
||||
VENDOR_GLADIA,
|
||||
} from "src/vendor";
|
||||
import { MSG_REQUIRED_FIELDS } from "src/constants";
|
||||
import {
|
||||
@@ -80,9 +85,13 @@ import type {
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
import {
|
||||
ADDITIONAL_SPEECH_VENDORS,
|
||||
ASSEMBLYAI_STT_VERSIONS,
|
||||
DEEPGRAM_STT_ENPOINT,
|
||||
DEFAULT_ASSEMBLYAI_STT_VERSION,
|
||||
DEFAULT_CARTESIA_OPTIONS,
|
||||
DEFAULT_ELEVENLABS_OPTIONS,
|
||||
DEFAULT_GOOGLE_CUSTOM_VOICE,
|
||||
DEFAULT_INWORLD_OPTIONS,
|
||||
DEFAULT_PLAYHT_OPTIONS,
|
||||
DEFAULT_RIMELABS_OPTIONS,
|
||||
DEFAULT_VERBIO_MODEL,
|
||||
@@ -91,14 +100,23 @@ import {
|
||||
GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
||||
VERBIO_STT_MODELS,
|
||||
} from "src/api/constants";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type SpeechServiceFormProps = {
|
||||
credential?: UseApiDataMap<SpeechCredential>;
|
||||
};
|
||||
|
||||
export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const user = useSelectState("user");
|
||||
|
||||
// ElevenLabs API URI options
|
||||
const ELEVENLABS_API_URI_OPTIONS = [
|
||||
{ name: "US", value: "api.elevenlabs.io" },
|
||||
{ name: "EU", value: "api.eu.residency.elevenlabs.io" },
|
||||
{ name: "IN", value: "api.in.residency.elevenlabs.io" },
|
||||
];
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const regions = useRegionVendors();
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
@@ -112,11 +130,13 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
);
|
||||
const [region, setRegion] = useState("");
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiUri, setApiUri] = useState("api.elevenlabs.io");
|
||||
const [userId, setUserId] = useState("");
|
||||
const [accessKeyId, setAccessKeyId] = useState("");
|
||||
const [secretAccessKey, setSecretAccessKey] = useState("");
|
||||
const [clientId, setClientId] = useState("");
|
||||
const [secretKey, setSecretKey] = useState("");
|
||||
const [clientKey, setClientKey] = useState("");
|
||||
const [clientSecret, setClientSecret] = useState("");
|
||||
const [googleServiceKey, setGoogleServiceKey] =
|
||||
useState<GoogleServiceKey | null>(null);
|
||||
@@ -127,6 +147,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
const [ttsModelId, setTtsModelId] = useState("");
|
||||
const [sttModelId, setSttModelId] = useState("");
|
||||
const [engineVersion, setEngineVersion] = useState(DEFAULT_VERBIO_MODEL);
|
||||
const [serviceVersion, setServiceVersion] = useState(
|
||||
DEFAULT_ASSEMBLYAI_STT_VERSION,
|
||||
);
|
||||
const [instanceId, setInstanceId] = useState("");
|
||||
const [initialCheckCustomTts, setInitialCheckCustomTts] = useState(false);
|
||||
const [initialCheckCustomStt, setInitialCheckCustomStt] = useState(false);
|
||||
@@ -196,6 +219,13 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
const [tmpPlayhtTtsUri, setTmpPlayhtTtsUri] = useState("");
|
||||
const [initialPlayhtOnpremCheck, setInitialPlayhtOnpremCheck] =
|
||||
useState(false);
|
||||
const [resembleTtsUri, setResembleTtsUri] = useState("");
|
||||
const [tmpResembleTtsUri, setTmpResembleTtsUri] = useState("");
|
||||
const [initialResembleOnpremCheck, setInitialResembleOnpremCheck] =
|
||||
useState(false);
|
||||
const [resembleTtsUseTls, setResembleTtsUseTls] = useState(false);
|
||||
const [tmpResembleTtsUseTls, setTmpResembleTtsUseTls] = useState(false);
|
||||
const [houndifyServerUri, setHoundifyServerUri] = useState("");
|
||||
const handleFile = (file: File) => {
|
||||
const handleError = () => {
|
||||
setGoogleServiceKey(null);
|
||||
@@ -231,6 +261,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
return DEFAULT_PLAYHT_OPTIONS;
|
||||
case VENDOR_RIMELABS:
|
||||
return DEFAULT_RIMELABS_OPTIONS;
|
||||
case VENDOR_INWORLD:
|
||||
return DEFAULT_INWORLD_OPTIONS;
|
||||
case VENDOR_CARTESIA:
|
||||
return DEFAULT_CARTESIA_OPTIONS;
|
||||
}
|
||||
@@ -247,6 +279,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
return "https://docs.play.ht/reference/api-generate-tts-audio-stream";
|
||||
case VENDOR_RIMELABS:
|
||||
return "https://rimelabs.mintlify.app/api-reference/endpoint/streaming-mp3#variable-parameters";
|
||||
case VENDOR_INWORLD:
|
||||
return "https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream";
|
||||
case VENDOR_CARTESIA:
|
||||
return "https://docs.cartesia.ai/api-reference/tts/bytes";
|
||||
}
|
||||
@@ -258,7 +292,20 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
switch (vendor) {
|
||||
case VENDOR_PLAYHT:
|
||||
return "Voice Engine";
|
||||
case VENDOR_DEEPGRAM:
|
||||
return "Model ID";
|
||||
case VENDOR_CARTESIA:
|
||||
return "TTS Model ID";
|
||||
default:
|
||||
return "Model";
|
||||
}
|
||||
};
|
||||
|
||||
const getSTTModelLabelByVendor = (vendor: Lowercase<Vendor>) => {
|
||||
switch (vendor) {
|
||||
case VENDOR_CARTESIA:
|
||||
return " STT Model ID";
|
||||
case VENDOR_DEEPGRAM:
|
||||
return "Model ID";
|
||||
default:
|
||||
return "Model";
|
||||
@@ -394,6 +441,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
}),
|
||||
...(vendor === VENDOR_CARTESIA && {
|
||||
model_id: ttsModelId || null,
|
||||
stt_model_id: sttModelId || null,
|
||||
options: options || null,
|
||||
}),
|
||||
...(vendor === VENDOR_CUSTOM && {
|
||||
@@ -411,16 +459,27 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
nuance_tts_uri: onPremNuanceTtsUrl || null,
|
||||
nuance_stt_uri: onPremNuanceSttUrl || null,
|
||||
}),
|
||||
...(vendor === VENDOR_HOUNDIFY && {
|
||||
client_id: clientId || null,
|
||||
client_key: clientKey || null,
|
||||
user_id: userId || null,
|
||||
houndify_server_uri: houndifyServerUri || null,
|
||||
}),
|
||||
...(vendor === VENDOR_COBALT && {
|
||||
cobalt_server_uri: cobaltServerUri || null,
|
||||
}),
|
||||
...((vendor === VENDOR_ELEVENLABS ||
|
||||
vendor === VENDOR_WHISPER ||
|
||||
vendor === VENDOR_INWORLD ||
|
||||
vendor === VENDOR_RIMELABS) && {
|
||||
model_id: ttsModelId || null,
|
||||
}),
|
||||
...(vendor === VENDOR_ELEVENLABS && {
|
||||
api_uri: apiUri || null,
|
||||
}),
|
||||
...((vendor === VENDOR_ELEVENLABS ||
|
||||
vendor === VENDOR_PLAYHT ||
|
||||
vendor === VENDOR_INWORLD ||
|
||||
vendor === VENDOR_RIMELABS) && {
|
||||
options: options || null,
|
||||
}),
|
||||
@@ -436,6 +495,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
deepgram_stt_uri: deepgramSttUri || null,
|
||||
deepgram_tts_uri: deepgramTtsUri || null,
|
||||
deepgram_stt_use_tls: deepgramSttUseTls ? 1 : 0,
|
||||
model_id: sttModelId || null,
|
||||
}),
|
||||
...(vendor === VENDOR_SPEECHMATICS && {
|
||||
speechmatics_stt_uri: speechmaticsEndpoint || null,
|
||||
@@ -443,9 +503,20 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
...(vendor === VENDOR_VERBIO && {
|
||||
engine_version: engineVersion,
|
||||
}),
|
||||
...(vendor === VENDOR_ASSEMBLYAI && {
|
||||
service_version: serviceVersion || null,
|
||||
}),
|
||||
...(vendor === VENDOR_PLAYHT && {
|
||||
playht_tts_uri: playhtTtsUri || null,
|
||||
}),
|
||||
...(vendor === VENDOR_RESEMBLE && {
|
||||
resemble_tts_uri: resembleTtsUri || null,
|
||||
resemble_tts_use_tls: resembleTtsUseTls ? 1 : 0,
|
||||
}),
|
||||
...(vendor === VENDOR_GLADIA && {
|
||||
api_key: apiKey || null,
|
||||
region: region || null,
|
||||
}),
|
||||
};
|
||||
|
||||
if (credential && credential.data) {
|
||||
@@ -492,9 +563,13 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
vendor === VENDOR_ELEVENLABS ||
|
||||
vendor === VENDOR_PLAYHT ||
|
||||
vendor === VENDOR_RIMELABS ||
|
||||
vendor === VENDOR_INWORLD ||
|
||||
vendor === VENDOR_WHISPER ||
|
||||
vendor === VENDOR_CARTESIA ||
|
||||
vendor === VENDOR_OPENAI
|
||||
vendor === VENDOR_OPENAI ||
|
||||
vendor === VENDOR_RESEMBLE ||
|
||||
vendor === VENDOR_DEEPGRAM_FLUX ||
|
||||
vendor === VENDOR_GLADIA
|
||||
? apiKey
|
||||
: null,
|
||||
}),
|
||||
@@ -561,8 +636,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
vendor === VENDOR_WHISPER ||
|
||||
vendor === VENDOR_PLAYHT ||
|
||||
vendor === VENDOR_RIMELABS ||
|
||||
vendor === VENDOR_INWORLD ||
|
||||
vendor === VENDOR_CARTESIA ||
|
||||
vendor === VENDOR_OPENAI
|
||||
vendor === VENDOR_OPENAI ||
|
||||
vendor === VENDOR_DEEPGRAM
|
||||
) {
|
||||
getSpeechSupportedLanguagesAndVoices(
|
||||
currentServiceProvider?.service_provider_sid,
|
||||
@@ -572,21 +649,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
).then(({ json }) => {
|
||||
if (json.models) {
|
||||
setTtsModels(json.models);
|
||||
if (
|
||||
json.models.length > 0 &&
|
||||
!json.models.find((m) => m.value === ttsModelId)
|
||||
) {
|
||||
setTtsModelId(json.models[0].value);
|
||||
}
|
||||
}
|
||||
if (json.sttModels) {
|
||||
setSttModels(json.sttModels);
|
||||
if (
|
||||
json.sttModels.length > 0 &&
|
||||
!json.sttModels.some((m) => m.value === sttModelId)
|
||||
) {
|
||||
setSttModelId(json.sttModels[0].value);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -594,6 +659,24 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
}
|
||||
}, [vendor]);
|
||||
|
||||
useEffect(() => {
|
||||
const modelId = credential?.data?.model_id || "";
|
||||
if (sttModels.length > 0 && !sttModels.some((m) => m.value === modelId)) {
|
||||
setSttModelId(sttModels[0].value);
|
||||
} else {
|
||||
setSttModelId(modelId);
|
||||
}
|
||||
}, [credential, sttModels]);
|
||||
|
||||
useEffect(() => {
|
||||
const modelId = credential?.data?.model_id || "";
|
||||
if (ttsModels.length > 0 && !ttsModels.some((m) => m.value === modelId)) {
|
||||
setTtsModelId(ttsModels[0].value);
|
||||
} else {
|
||||
setTtsModelId(modelId);
|
||||
}
|
||||
}, [credential, ttsModels]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (credential && credential.data) {
|
||||
@@ -640,6 +723,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
setApiKey(credential.data.api_key);
|
||||
}
|
||||
|
||||
if (credential.data.api_uri) {
|
||||
setApiUri(credential.data.api_uri);
|
||||
}
|
||||
|
||||
if (credential.data.region) {
|
||||
setRegion(credential.data.region);
|
||||
}
|
||||
@@ -651,6 +738,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
if (credential.data.client_id) {
|
||||
setClientId(credential.data.client_id);
|
||||
}
|
||||
if (credential.data.client_key) {
|
||||
setClientKey(credential.data.client_key);
|
||||
}
|
||||
|
||||
if (credential.data.secret) {
|
||||
setSecretKey(credential.data.secret);
|
||||
@@ -742,12 +832,20 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
if (credential.data.model_id) {
|
||||
setTtsModelId(credential.data.model_id);
|
||||
}
|
||||
if (credential.data.model_id && vendor === VENDOR_OPENAI) {
|
||||
if (
|
||||
credential.data.model_id &&
|
||||
(vendor === VENDOR_OPENAI || vendor === VENDOR_DEEPGRAM)
|
||||
) {
|
||||
setSttModelId(credential.data.model_id);
|
||||
} else if (credential.data.stt_model_id) {
|
||||
setSttModelId(credential.data.stt_model_id);
|
||||
}
|
||||
if (credential?.data?.playht_tts_uri) {
|
||||
setPlayhtTtsUri(credential.data.playht_tts_uri);
|
||||
}
|
||||
if (credential?.data?.resemble_tts_uri) {
|
||||
setResembleTtsUri(credential.data.resemble_tts_uri);
|
||||
}
|
||||
}
|
||||
if (credential?.data?.options) {
|
||||
setOptions(credential.data.options);
|
||||
@@ -762,9 +860,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
setUseCustomVoicesCheck(json.length > 0);
|
||||
});
|
||||
}
|
||||
if (credential?.data?.deepgram_stt_uri) {
|
||||
setDeepgramSttUri(credential.data.deepgram_stt_uri);
|
||||
}
|
||||
setDeepgramSttUri(credential?.data?.deepgram_stt_uri || "");
|
||||
if (credential?.data?.deepgram_tts_uri) {
|
||||
setDeepgramTtsUri(credential.data.deepgram_tts_uri);
|
||||
}
|
||||
@@ -773,12 +869,21 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
credential?.data?.deepgram_stt_use_tls > 0 ? true : false,
|
||||
);
|
||||
}
|
||||
setInitialDeepgramOnpremCheck(hasValue(credential?.data?.deepgram_stt_uri));
|
||||
setInitialDeepgramOnpremCheck(
|
||||
hasValue(credential?.data?.deepgram_stt_uri) &&
|
||||
!DEEPGRAM_STT_ENPOINT.map((e) => e.value).includes(
|
||||
credential?.data?.deepgram_stt_uri,
|
||||
),
|
||||
);
|
||||
|
||||
if (credential?.data?.user_id) {
|
||||
setUserId(credential.data.user_id);
|
||||
}
|
||||
|
||||
if (credential?.data?.houndify_server_uri) {
|
||||
setHoundifyServerUri(credential.data.houndify_server_uri);
|
||||
}
|
||||
|
||||
if (credential?.data?.voice_engine) {
|
||||
setTtsModelId(credential.data.voice_engine);
|
||||
}
|
||||
@@ -803,6 +908,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
if (credential?.data?.engine_version) {
|
||||
setEngineVersion(credential.data.engine_version);
|
||||
}
|
||||
if (credential?.data?.service_version) {
|
||||
setServiceVersion(credential.data.service_version);
|
||||
}
|
||||
|
||||
if (credential?.data?.speechmatics_stt_uri) {
|
||||
setInitialSpeechMaticsOnpremCheck(
|
||||
@@ -811,6 +919,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
setSpeechmaticsEndpoint(credential.data.speechmatics_stt_uri);
|
||||
}
|
||||
setInitialPlayhtOnpremCheck(hasValue(credential?.data?.playht_tts_uri));
|
||||
setInitialResembleOnpremCheck(hasValue(credential?.data?.resemble_tts_uri));
|
||||
if (credential?.data?.resemble_tts_use_tls) {
|
||||
setResembleTtsUseTls(
|
||||
credential?.data?.resemble_tts_use_tls > 0 ? true : false,
|
||||
);
|
||||
setTmpResembleTtsUseTls(
|
||||
credential?.data?.resemble_tts_use_tls > 0 ? true : false,
|
||||
);
|
||||
}
|
||||
}, [credential]);
|
||||
|
||||
const updateCustomVoices = (
|
||||
@@ -874,6 +991,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
setVendor(e.target.value as Lowercase<Vendor>);
|
||||
setRegion("");
|
||||
setApiKey("");
|
||||
setApiUri(
|
||||
e.target.value === VENDOR_ELEVENLABS ? "api.elevenlabs.io" : "",
|
||||
);
|
||||
setGoogleServiceKey(null);
|
||||
}}
|
||||
disabled={credential ? true : false}
|
||||
@@ -929,7 +1049,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
vendor !== VENDOR_COBALT &&
|
||||
vendor !== VENDOR_SONIOX &&
|
||||
vendor !== VENDOR_SPEECHMATICS &&
|
||||
vendor !== VENDOR_DEEPGRAM_FLUX &&
|
||||
vendor !== VENDOR_HOUNDIFY &&
|
||||
vendor !== VENDOR_OPENAI &&
|
||||
vendor !== VENDOR_GLADIA &&
|
||||
vendor != VENDOR_CUSTOM && (
|
||||
<label htmlFor="use_for_tts" className="chk">
|
||||
<input
|
||||
@@ -947,7 +1070,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
vendor !== VENDOR_WHISPER &&
|
||||
vendor !== VENDOR_PLAYHT &&
|
||||
vendor !== VENDOR_RIMELABS &&
|
||||
vendor !== VENDOR_CARTESIA &&
|
||||
vendor !== VENDOR_INWORLD &&
|
||||
vendor !== VENDOR_RESEMBLE &&
|
||||
vendor !== VENDOR_ELEVENLABS && (
|
||||
<label htmlFor="use_for_stt" className="chk">
|
||||
<input
|
||||
@@ -1327,6 +1451,56 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{vendor === VENDOR_HOUNDIFY && (
|
||||
<fieldset>
|
||||
<label htmlFor="houndify_client_id">
|
||||
Client ID
|
||||
{!onPremNuanceSttCheck && !onPremNuanceTtsCheck && <span>*</span>}
|
||||
</label>
|
||||
<input
|
||||
id="houndify_client_id"
|
||||
required={!onPremNuanceSttCheck && !onPremNuanceTtsCheck}
|
||||
type="text"
|
||||
name="houndify_client_id"
|
||||
placeholder="Client ID"
|
||||
value={clientId}
|
||||
onChange={(e) => setClientId(e.target.value)}
|
||||
disabled={credential ? true : false}
|
||||
/>
|
||||
<label htmlFor="houndify_secret">
|
||||
Client Key
|
||||
{!onPremNuanceSttCheck && !onPremNuanceTtsCheck && <span>*</span>}
|
||||
</label>
|
||||
<Passwd
|
||||
id="houndify_secret"
|
||||
required={!onPremNuanceSttCheck && !onPremNuanceTtsCheck}
|
||||
name="houndify_secret"
|
||||
placeholder="Client Key"
|
||||
value={clientKey ? getObscuredSecret(clientKey) : clientKey}
|
||||
onChange={(e) => setClientKey(e.target.value)}
|
||||
disabled={credential ? true : false}
|
||||
/>
|
||||
<label htmlFor="houndify_user_id">User ID</label>
|
||||
<input
|
||||
id="houndify_user_id"
|
||||
type="text"
|
||||
name="houndify_user_id"
|
||||
placeholder="User ID"
|
||||
value={userId}
|
||||
onChange={(e) => setUserId(e.target.value)}
|
||||
disabled={credential ? true : false}
|
||||
/>
|
||||
<label htmlFor="houndify_server_uri">Audio Endpoint</label>
|
||||
<input
|
||||
id="houndify_server_uri"
|
||||
type="text"
|
||||
name="houndify_server_uri"
|
||||
placeholder="Audio Endpoint (optional)"
|
||||
value={houndifyServerUri}
|
||||
onChange={(e) => setHoundifyServerUri(e.target.value)}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{vendor === VENDOR_NUANCE && (
|
||||
<>
|
||||
<fieldset>
|
||||
@@ -1483,6 +1657,22 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
</fieldset>
|
||||
</>
|
||||
)}
|
||||
{vendor === VENDOR_ASSEMBLYAI && (
|
||||
<fieldset>
|
||||
<label htmlFor={`${vendor}_tts_model_id`}>
|
||||
Service version<span>*</span>
|
||||
</label>
|
||||
<Selector
|
||||
id={"assemblyai_service_version"}
|
||||
name={"assemblyai_service_version"}
|
||||
value={serviceVersion}
|
||||
options={ASSEMBLYAI_STT_VERSIONS}
|
||||
onChange={(e) => {
|
||||
setServiceVersion(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{vendor === VENDOR_AWS && (
|
||||
<fieldset>
|
||||
<label htmlFor="vendor">
|
||||
@@ -1671,16 +1861,98 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
{vendor === VENDOR_RESEMBLE && (
|
||||
<fieldset>
|
||||
<Checkzone
|
||||
disabled={hasValue(credential)}
|
||||
hidden
|
||||
name="use_on-prem_resemble_container"
|
||||
label="Use on-prem Resemble container"
|
||||
initialCheck={initialResembleOnpremCheck}
|
||||
handleChecked={(e) => {
|
||||
setInitialResembleOnpremCheck(e.target.checked);
|
||||
if (e.target.checked) {
|
||||
if (tmpResembleTtsUri) {
|
||||
setResembleTtsUri(tmpResembleTtsUri);
|
||||
}
|
||||
if (tmpResembleTtsUseTls) {
|
||||
setResembleTtsUseTls(tmpResembleTtsUseTls);
|
||||
}
|
||||
} else {
|
||||
setTmpResembleTtsUri(resembleTtsUri);
|
||||
setResembleTtsUri("");
|
||||
setTmpResembleTtsUseTls(resembleTtsUseTls);
|
||||
setResembleTtsUseTls(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label htmlFor="resemble_uri_for_tts">
|
||||
TTS Container URI<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="resemble_uri_for_tts"
|
||||
required
|
||||
type="text"
|
||||
name="resemble_uri_for_tts"
|
||||
placeholder=""
|
||||
value={resembleTtsUri}
|
||||
onChange={(e) => setResembleTtsUri(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="resemble_stt_use_tls" className="chk">
|
||||
<input
|
||||
id="resemble_stt_use_tls"
|
||||
name="resemble_stt_use_tls"
|
||||
type="checkbox"
|
||||
onChange={(e) => setResembleTtsUseTls(e.target.checked)}
|
||||
defaultChecked={resembleTtsUseTls}
|
||||
/>
|
||||
<div>Use TLS</div>
|
||||
</label>
|
||||
</Checkzone>
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
{vendor === VENDOR_ELEVENLABS && (
|
||||
<fieldset>
|
||||
<label htmlFor="elevenlabs_api_uri">
|
||||
Data residency<span>*</span>
|
||||
</label>
|
||||
<Selector
|
||||
id="elevenlabs_api_uri"
|
||||
name="elevenlabs_api_uri"
|
||||
value={apiUri}
|
||||
options={ELEVENLABS_API_URI_OPTIONS}
|
||||
onChange={(e) => setApiUri(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<label htmlFor={`${vendor}_apikey`}>
|
||||
API key<span>*</span>
|
||||
</label>
|
||||
<Passwd
|
||||
id={`${vendor}_apikey`}
|
||||
required
|
||||
name={`${vendor}_apikey`}
|
||||
placeholder="API key"
|
||||
value={apiKey ? getObscuredSecret(apiKey) : apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
disabled={credential ? true : false}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
{(vendor === VENDOR_WELLSAID ||
|
||||
vendor === VENDOR_ASSEMBLYAI ||
|
||||
vendor === VENDOR_VOXIST ||
|
||||
vendor == VENDOR_ELEVENLABS ||
|
||||
vendor === VENDOR_WHISPER ||
|
||||
vendor === VENDOR_RIMELABS ||
|
||||
vendor === VENDOR_INWORLD ||
|
||||
vendor === VENDOR_SONIOX ||
|
||||
vendor === VENDOR_CARTESIA ||
|
||||
vendor === VENDOR_OPENAI ||
|
||||
vendor === VENDOR_SPEECHMATICS) && (
|
||||
vendor === VENDOR_DEEPGRAM_FLUX ||
|
||||
vendor === VENDOR_RESEMBLE ||
|
||||
vendor === VENDOR_SPEECHMATICS ||
|
||||
vendor === VENDOR_GLADIA) && (
|
||||
<fieldset>
|
||||
<label htmlFor={`${vendor}_apikey`}>
|
||||
API key<span>*</span>
|
||||
@@ -1696,11 +1968,12 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{(vendor == VENDOR_ELEVENLABS ||
|
||||
vendor == VENDOR_WHISPER ||
|
||||
vendor === VENDOR_CARTESIA ||
|
||||
{(vendor === VENDOR_ELEVENLABS ||
|
||||
vendor === VENDOR_WHISPER ||
|
||||
vendor === VENDOR_PLAYHT ||
|
||||
vendor == VENDOR_RIMELABS) &&
|
||||
vendor === VENDOR_RIMELABS ||
|
||||
vendor === VENDOR_INWORLD ||
|
||||
(ttsCheck && vendor === VENDOR_CARTESIA)) &&
|
||||
ttsModels.length > 0 && (
|
||||
<fieldset>
|
||||
<label htmlFor={`${vendor}_tts_model_id`}>
|
||||
@@ -1717,26 +1990,30 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{vendor == VENDOR_OPENAI && sttModels.length > 0 && (
|
||||
<fieldset>
|
||||
<label htmlFor={`${vendor}_stt_model_id`}>
|
||||
{getModelLabelByVendor(vendor)}
|
||||
</label>
|
||||
<Selector
|
||||
id={"stt_model_id"}
|
||||
name={"stt_model_id"}
|
||||
value={sttModelId}
|
||||
options={sttModels}
|
||||
onChange={(e) => {
|
||||
setSttModelId(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{(vendor == VENDOR_OPENAI ||
|
||||
vendor === VENDOR_DEEPGRAM ||
|
||||
(sttCheck && vendor === VENDOR_CARTESIA)) &&
|
||||
sttModels.length > 0 && (
|
||||
<fieldset>
|
||||
<label htmlFor={`${vendor}_stt_model_id`}>
|
||||
{getSTTModelLabelByVendor(vendor)}
|
||||
</label>
|
||||
<Selector
|
||||
id={"stt_model_id"}
|
||||
name={"stt_model_id"}
|
||||
value={sttModelId}
|
||||
options={sttModels}
|
||||
onChange={(e) => {
|
||||
setSttModelId(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{(vendor === VENDOR_ELEVENLABS ||
|
||||
vendor === VENDOR_PLAYHT ||
|
||||
vendor === VENDOR_CARTESIA ||
|
||||
vendor === VENDOR_RIMELABS) && (
|
||||
vendor === VENDOR_RIMELABS ||
|
||||
vendor === VENDOR_INWORLD) && (
|
||||
<fieldset>
|
||||
<Checkzone
|
||||
hidden
|
||||
@@ -1957,6 +2234,19 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
disabled={credential ? true : false}
|
||||
/>
|
||||
<label htmlFor={`${vendor}_deepgram_stt_enpoint`}>
|
||||
Deepgram STT Endpoint<span>*</span>
|
||||
</label>
|
||||
<Selector
|
||||
id={"deepgram_stt_enpoint"}
|
||||
name={"deepgram_stt_enpoint"}
|
||||
value={deepgramSttUri}
|
||||
options={DEEPGRAM_STT_ENPOINT}
|
||||
onChange={(e) => {
|
||||
setDeepgramSttUri(e.target.value);
|
||||
setDeepgramSttUseTls(hasValue(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</Checkzone>
|
||||
<Checkzone
|
||||
disabled={hasValue(credential)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Link } from "react-router-dom";
|
||||
|
||||
import { USER_ACCOUNT } from "src/api/constants";
|
||||
import { AccountFilter, Icons, Section, Spinner } from "src/components";
|
||||
import { useSelectState, toastError, toastSuccess } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import {
|
||||
deleteSpeechService,
|
||||
useServiceProviderData,
|
||||
@@ -26,8 +26,10 @@ import { ScopedAccess } from "src/components/scoped-access";
|
||||
import { Scope } from "src/store/types";
|
||||
import { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
import { VENDOR_CUSTOM } from "src/vendor";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const SpeechServices = () => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const user = useSelectState("user");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [apiUrl, setApiUrl] = useState("");
|
||||
|
||||
@@ -5,11 +5,12 @@ import { useParams } from "react-router-dom";
|
||||
import { UserForm } from "./form";
|
||||
import { useApiData } from "src/api";
|
||||
import { User } from "src/api/types";
|
||||
import { toastError } from "src/store";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const EditUser = () => {
|
||||
const params = useParams();
|
||||
const [data, refetch, error] = useApiData<User>(`Users/${params.user_sid}`);
|
||||
const { toastError } = useToast();
|
||||
|
||||
/** Handle error toast at top level... */
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { Button, ButtonGroup, MS } from "@jambonz/ui-kit";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
import {
|
||||
deleteUser,
|
||||
postFetch,
|
||||
@@ -38,12 +38,14 @@ import type {
|
||||
} from "src/api/types";
|
||||
import type { IMessage } from "src/store/types";
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type UserFormProps = {
|
||||
user?: UseApiDataMap<User>;
|
||||
};
|
||||
|
||||
export const UserForm = ({ user }: UserFormProps) => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const { signout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const currentUser = useSelectState("user");
|
||||
|
||||
@@ -8,9 +8,10 @@ import { useNavigate } from "react-router-dom";
|
||||
import { MSG_SOMETHING_WRONG } from "src/constants";
|
||||
|
||||
import { ROUTE_LOGIN } from "src/router/routes";
|
||||
import { toastSuccess } from "src/store";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const ForgotPassword = () => {
|
||||
const { toastSuccess } = useToast();
|
||||
const [message, setMessage] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react";
|
||||
import { Button, H1 } from "@jambonz/ui-kit";
|
||||
import { useLocation, Navigate, Link } from "react-router-dom";
|
||||
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
import { getToken, parseJwt, useAuth } from "src/router/auth";
|
||||
import {
|
||||
SESS_FLASH_MSG,
|
||||
@@ -26,8 +25,10 @@ import { v4 as uuid } from "uuid";
|
||||
import { setLocationBeforeOauth, setOauthState } from "src/store/localStore";
|
||||
import { getGithubOauthUrl, getGoogleOauthUrl } from "./utils";
|
||||
import { UserData } from "src/store/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const Login = () => {
|
||||
const { toastSuccess, toastError } = useToast();
|
||||
const state = uuid();
|
||||
setOauthState(state);
|
||||
setLocationBeforeOauth("/sign-in");
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
BASE_URL,
|
||||
} from "src/api/constants";
|
||||
import { Spinner } from "src/components";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
import { setToken } from "src/router/auth";
|
||||
import {
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
ROUTE_REGISTER,
|
||||
ROUTE_REGISTER_SUB_DOMAIN,
|
||||
} from "src/router/routes";
|
||||
import { toastError } from "src/store";
|
||||
import {
|
||||
getLocationBeforeOauth,
|
||||
getOauthState,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "src/store/localStore";
|
||||
|
||||
export const OauthCallback = () => {
|
||||
const { toastError } = useToast();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const code = queryParams.get("code");
|
||||
const newState = queryParams.get("state");
|
||||
|
||||
@@ -7,10 +7,11 @@ import { Passwd } from "src/components/forms";
|
||||
import { ROUTE_LOGIN, ROUTE_REGISTER_EMAIL_VERIFY } from "src/router/routes";
|
||||
import { generateActivationCode } from "./utils";
|
||||
import { setToken } from "src/router/auth";
|
||||
import { toastError } from "src/store";
|
||||
import { setRootDomain } from "src/store/localStore";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const RegisterEmail = () => {
|
||||
const { toastError } = useToast();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -2,15 +2,16 @@ import { Button, H1, MS } from "@jambonz/ui-kit";
|
||||
import React, { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { putActivationCode } from "src/api";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
import { getToken, parseJwt } from "src/router/auth";
|
||||
import {
|
||||
ROUTE_REGISTER_EMAIL,
|
||||
ROUTE_REGISTER_SUB_DOMAIN,
|
||||
} from "src/router/routes";
|
||||
import { toastError } from "src/store";
|
||||
import { UserData } from "src/store/types";
|
||||
|
||||
export const EmailVerify = () => {
|
||||
const { toastError } = useToast();
|
||||
const [code, setCode] = useState("");
|
||||
const userData: UserData = parseJwt(getToken());
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -3,11 +3,12 @@ import React, { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { postChangepassword, postSignIn } from "src/api";
|
||||
import { Message, Passwd } from "src/components/forms";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
import { setToken } from "src/router/auth";
|
||||
import { ROUTE_LOGIN } from "src/router/routes";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
|
||||
export const ResetPassword = () => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const params = useParams();
|
||||
const resetId = params.id;
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
|
||||
17
src/main.tsx
17
src/main.tsx
@@ -7,17 +7,20 @@ import { AuthProvider } from "./router/auth";
|
||||
import { Router } from "./router";
|
||||
|
||||
import "./styles/index.scss";
|
||||
import { ToastProvider } from "./components/toast/toast-provider";
|
||||
|
||||
const root: Element = document.getElementById("root")!;
|
||||
|
||||
createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<StateProvider>
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<Router />
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</StateProvider>
|
||||
<ToastProvider>
|
||||
<StateProvider>
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<Router />
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</StateProvider>
|
||||
</ToastProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -25,11 +25,12 @@ import {
|
||||
import type { UserLogin } from "src/api/types";
|
||||
import { ENABLE_HOSTED_SYSTEM, USER_ACCOUNT } from "src/api/constants";
|
||||
import type { UserData } from "src/store/types";
|
||||
import { toastError } from "src/store";
|
||||
import {
|
||||
clearLocalStorage,
|
||||
removeLocationBeforeOauth,
|
||||
removeOauthState,
|
||||
} from "src/store/localStore";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
interface SignIn {
|
||||
(username: string, password: string): Promise<UserLogin>;
|
||||
@@ -93,6 +94,7 @@ export const parseJwt = (token: string) => {
|
||||
* Provider hook that creates auth object and handles state
|
||||
*/
|
||||
export const useProvideAuth = (): AuthStateContext => {
|
||||
const { toastError } = useToast();
|
||||
let token = getToken();
|
||||
let userData: UserData;
|
||||
const navigate = useNavigate();
|
||||
@@ -163,7 +165,7 @@ export const useProvideAuth = (): AuthStateContext => {
|
||||
postLogout()
|
||||
.then((response) => {
|
||||
if (response.status === StatusCodes.NO_CONTENT) {
|
||||
localStorage.clear();
|
||||
clearLocalStorage();
|
||||
sessionStorage.clear();
|
||||
sessionStorage.setItem(SESS_FLASH_MSG, MSG_LOGGED_OUT);
|
||||
window.location.href = ROUTE_LOGIN;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useReducer, useContext } from "react";
|
||||
|
||||
import { TOAST_TIME } from "src/constants";
|
||||
import {
|
||||
genericAction,
|
||||
userAsyncAction,
|
||||
@@ -11,8 +10,6 @@ import {
|
||||
} from "./actions";
|
||||
|
||||
import type {
|
||||
IMessage,
|
||||
Toast,
|
||||
State,
|
||||
Action,
|
||||
MiddleWare,
|
||||
@@ -49,22 +46,12 @@ const reducer: React.Reducer<State, Action<keyof State>> = (state, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
let toastTimeout: number;
|
||||
|
||||
/** Async middlewares */
|
||||
/** Proxies dispatch to reducer */
|
||||
const middleware: MiddleWare = (dispatch) => {
|
||||
/** This generic implementation enforces global dispatch type-safety */
|
||||
return <Type extends keyof State>(action: Action<Type>) => {
|
||||
switch (action.type) {
|
||||
case "toast":
|
||||
if (toastTimeout) {
|
||||
clearTimeout(toastTimeout);
|
||||
}
|
||||
toastTimeout = setTimeout(() => {
|
||||
dispatch({ type: "toast" });
|
||||
}, TOAST_TIME);
|
||||
return dispatch(action);
|
||||
case "user":
|
||||
return userAsyncAction().then((payload) => {
|
||||
dispatch({ ...action, payload });
|
||||
@@ -106,28 +93,6 @@ export const useDispatch = (): GlobalDispatch => {
|
||||
return globalDispatch;
|
||||
};
|
||||
|
||||
/** Toast dispatch helpers to make component code less cumbersome */
|
||||
const toastDispatch = (payload: Toast) => {
|
||||
globalDispatch({
|
||||
type: "toast",
|
||||
payload,
|
||||
});
|
||||
};
|
||||
|
||||
export const toastError = (msg: IMessage) => {
|
||||
toastDispatch({
|
||||
type: "error",
|
||||
message: msg,
|
||||
});
|
||||
};
|
||||
|
||||
export const toastSuccess = (msg: IMessage) => {
|
||||
toastDispatch({
|
||||
type: "success",
|
||||
message: msg,
|
||||
});
|
||||
};
|
||||
|
||||
/** Wrapper hook for state context */
|
||||
export const useStateContext = () => {
|
||||
const { state } = useContext(StateContext);
|
||||
|
||||
@@ -123,7 +123,18 @@ export const checkLocation = () => {
|
||||
|
||||
if (currentLocation !== storedLocation) {
|
||||
localStorage.removeItem(storeQueryFilter);
|
||||
localStorage.removeItem(storeAccountFilter);
|
||||
// Keep storeAccountFilter in different location that user can search for same account
|
||||
// in different location
|
||||
// localStorage.removeItem(storeAccountFilter);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const clearLocalStorage = () => {
|
||||
const toKeep = [storeActiveSP, storeAccountFilter];
|
||||
Object.keys(localStorage).forEach((key) => {
|
||||
if (!toKeep.includes(key)) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,12 +7,12 @@ input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="password"] {
|
||||
@include ui-mixins.m();
|
||||
padding: ui-vars.$px01 ui-vars.$px02;
|
||||
border-radius: ui-vars.$px01;
|
||||
border: 2px solid ui-vars.$grey;
|
||||
background-color: ui-vars.$white;
|
||||
color: inherit;
|
||||
@include ui-mixins.m();
|
||||
|
||||
&:focus {
|
||||
border-color: ui-vars.$dark;
|
||||
@@ -110,11 +110,11 @@ fieldset {
|
||||
}
|
||||
|
||||
label {
|
||||
@include ui-mixins.m();
|
||||
@include ui-mixins.font-medium();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
@include ui-mixins.font-medium();
|
||||
@include ui-mixins.m();
|
||||
|
||||
span {
|
||||
color: ui-vars.$jambonz;
|
||||
@@ -218,7 +218,8 @@ fieldset {
|
||||
}
|
||||
}
|
||||
|
||||
.gateway {
|
||||
.gateway,
|
||||
.gateway-inbound {
|
||||
padding: ui-vars.$px02;
|
||||
border-radius: ui-vars.$px01;
|
||||
border: 2px solid ui-vars.$grey;
|
||||
@@ -284,6 +285,18 @@ fieldset {
|
||||
}
|
||||
}
|
||||
|
||||
.gateway-inbound {
|
||||
> div {
|
||||
&:nth-child(1) {
|
||||
grid-template-columns: [col] calc(70% - #{ui-vars.$px02 * 2}) [col] 30%;
|
||||
|
||||
@include mixins.small() {
|
||||
grid-template-columns: [col] 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lcr {
|
||||
@extend .gateway;
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
}
|
||||
|
||||
&__row {
|
||||
@include ui-mixins.m();
|
||||
display: grid;
|
||||
padding: ui-vars.$px03;
|
||||
align-items: center;
|
||||
@include ui-mixins.m();
|
||||
|
||||
@include mixins.small() {
|
||||
padding: ui-vars.$px02;
|
||||
|
||||
@@ -30,11 +30,11 @@
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include ui-mixins.p();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-gap: ui-vars.$px02;
|
||||
color: ui-vars.$jambonz;
|
||||
@include ui-mixins.p();
|
||||
|
||||
+ .item__meta {
|
||||
@include mixins.small() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@use "sass:color";
|
||||
@use "./forms";
|
||||
// @use "./cards";
|
||||
@use "./lists";
|
||||
@@ -68,7 +69,7 @@ details {
|
||||
&.ok {
|
||||
color: ui-vars.$teal;
|
||||
border: 2px solid ui-vars.$teal;
|
||||
background-color: mix(ui-vars.$white, ui-vars.$teal, 95%);
|
||||
background-color: color.mix(ui-vars.$white, ui-vars.$teal, 95%);
|
||||
}
|
||||
|
||||
&.not-tested {
|
||||
@@ -77,8 +78,8 @@ details {
|
||||
}
|
||||
|
||||
summary {
|
||||
@include ui-mixins.m();
|
||||
cursor: pointer;
|
||||
@include ui-mixins.m();
|
||||
|
||||
+ * {
|
||||
margin-top: ui-vars.$px02;
|
||||
@@ -160,7 +161,6 @@ details {
|
||||
|
||||
/** Used for recent-calls */
|
||||
.pre-grid {
|
||||
@include mixins.code();
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-row-gap: ui-vars.$px01;
|
||||
@@ -171,6 +171,7 @@ details {
|
||||
background-color: ui-vars.$dark;
|
||||
border-radius: ui-vars.$px01;
|
||||
margin-top: ui-vars.$px02;
|
||||
@include mixins.code();
|
||||
}
|
||||
|
||||
.pre-grid-white {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { toastError } from "src/store";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
import type { IMessage } from "src/store/types";
|
||||
|
||||
@@ -11,6 +10,7 @@ export const useRedirect = <Type>(
|
||||
message: IMessage,
|
||||
) => {
|
||||
const navigate = useNavigate();
|
||||
const { toastError } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (collection && !collection.length) {
|
||||
|
||||
@@ -7,8 +7,9 @@ import {
|
||||
SpeechCredential,
|
||||
User,
|
||||
} from "src/api/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
import { toastError, useSelectState } from "src/store";
|
||||
import { useSelectState } from "src/store";
|
||||
|
||||
import { IMessage, Scope, UserData } from "src/store/types";
|
||||
|
||||
@@ -21,6 +22,7 @@ export const useScopedRedirect = (
|
||||
) => {
|
||||
const navigate = useNavigate();
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const { toastError } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -47,5 +49,5 @@ export const useScopedRedirect = (
|
||||
|
||||
navigate(redirect);
|
||||
}
|
||||
}, [user, currentServiceProvider, data]);
|
||||
}, [user, currentServiceProvider, data, toastError]);
|
||||
};
|
||||
|
||||
@@ -2,11 +2,12 @@ import React, { useEffect } from "react";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { toastError, useSelectState, useAccessControl } from "src/store";
|
||||
import { useSelectState, useAccessControl } from "src/store";
|
||||
import { ROUTE_INTERNAL_SETTINGS } from "src/router/routes";
|
||||
|
||||
import type { ACL, IMessage } from "src/store/types";
|
||||
import type { ServiceProvider } from "src/api/types";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
type PassthroughProps = {
|
||||
[key: string]: unknown;
|
||||
@@ -22,6 +23,7 @@ export const withAccessControl = (
|
||||
) => {
|
||||
return function WithAccessControl(Component: React.ComponentType) {
|
||||
return function ComponentWithAccessControl(props: PassthroughProps) {
|
||||
const { toastError } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const hasPermission = useAccessControl(acl);
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
|
||||
28
src/vendor/index.tsx
vendored
28
src/vendor/index.tsx
vendored
@@ -12,6 +12,7 @@ export const VENDOR_MICROSOFT = "microsoft";
|
||||
export const VENDOR_WELLSAID = "wellsaid";
|
||||
export const VENDOR_NUANCE = "nuance";
|
||||
export const VENDOR_DEEPGRAM = "deepgram";
|
||||
export const VENDOR_DEEPGRAM_FLUX = "deepgramflux";
|
||||
export const VENDOR_IBM = "ibm";
|
||||
export const VENDOR_NVIDIA = "nvidia";
|
||||
export const VENDOR_SONIOX = "soniox";
|
||||
@@ -24,9 +25,13 @@ export const VENDOR_VOXIST = "voxist";
|
||||
export const VENDOR_WHISPER = "whisper";
|
||||
export const VENDOR_PLAYHT = "playht";
|
||||
export const VENDOR_RIMELABS = "rimelabs";
|
||||
export const VENDOR_INWORLD = "inworld";
|
||||
export const VENDOR_VERBIO = "verbio";
|
||||
export const VENDOR_CARTESIA = "cartesia";
|
||||
export const VENDOR_OPENAI = "openai";
|
||||
export const VENDOR_RESEMBLE = "resemble";
|
||||
export const VENDOR_HOUNDIFY = "houndify";
|
||||
export const VENDOR_GLADIA = "gladia";
|
||||
|
||||
export const vendors: VendorOptions[] = [
|
||||
{
|
||||
@@ -41,6 +46,10 @@ export const vendors: VendorOptions[] = [
|
||||
name: "Deepgram",
|
||||
value: VENDOR_DEEPGRAM,
|
||||
},
|
||||
{
|
||||
name: "Deepgram Flux",
|
||||
value: VENDOR_DEEPGRAM_FLUX,
|
||||
},
|
||||
{
|
||||
name: "IBM",
|
||||
value: VENDOR_IBM,
|
||||
@@ -101,6 +110,10 @@ export const vendors: VendorOptions[] = [
|
||||
name: "RimeLabs",
|
||||
value: VENDOR_RIMELABS,
|
||||
},
|
||||
{
|
||||
name: "Inworld",
|
||||
value: VENDOR_INWORLD,
|
||||
},
|
||||
{
|
||||
name: "Verbio",
|
||||
value: VENDOR_VERBIO,
|
||||
@@ -113,6 +126,18 @@ export const vendors: VendorOptions[] = [
|
||||
name: "OpenAI",
|
||||
value: VENDOR_OPENAI,
|
||||
},
|
||||
{
|
||||
name: "Resemble",
|
||||
value: VENDOR_RESEMBLE,
|
||||
},
|
||||
{
|
||||
name: "SoundHound",
|
||||
value: VENDOR_HOUNDIFY,
|
||||
},
|
||||
{
|
||||
name: "Gladia",
|
||||
value: VENDOR_GLADIA,
|
||||
},
|
||||
].sort((a, b) => a.name.localeCompare(b.name)) as VendorOptions[];
|
||||
|
||||
export const AWS_CREDENTIAL_ACCESS_KEY = "access_key";
|
||||
@@ -145,12 +170,14 @@ export const useRegionVendors = () => {
|
||||
import("./regions/ms-azure-regions"),
|
||||
import("./regions/ibm-regions"),
|
||||
import("./regions/speechmatics-regions"),
|
||||
import("./regions/gladia-regions"),
|
||||
]).then(
|
||||
([
|
||||
{ default: awsRegions },
|
||||
{ default: msRegions },
|
||||
{ default: ibmRegions },
|
||||
{ default: speechmaticsRegions },
|
||||
{ default: gladiaRegions },
|
||||
]) => {
|
||||
if (!ignore) {
|
||||
setRegions({
|
||||
@@ -158,6 +185,7 @@ export const useRegionVendors = () => {
|
||||
microsoft: msRegions,
|
||||
ibm: ibmRegions,
|
||||
speechmatics: speechmaticsRegions,
|
||||
gladia: gladiaRegions,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
14
src/vendor/regions/gladia-regions.ts
vendored
Normal file
14
src/vendor/regions/gladia-regions.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Region } from "../types";
|
||||
|
||||
export const regions: Region[] = [
|
||||
{
|
||||
name: "US West",
|
||||
value: "us-west",
|
||||
},
|
||||
{
|
||||
name: "EU West",
|
||||
value: "eu-west",
|
||||
},
|
||||
];
|
||||
|
||||
export default regions;
|
||||
15
src/vendor/types.ts
vendored
15
src/vendor/types.ts
vendored
@@ -5,6 +5,7 @@ export type Vendor =
|
||||
| "WellSaid"
|
||||
| "Nuance"
|
||||
| "Deepgram"
|
||||
| "DeepgramFlux"
|
||||
| "IBM"
|
||||
| "Nvidia"
|
||||
| "Soniox"
|
||||
@@ -17,9 +18,13 @@ export type Vendor =
|
||||
| "whisper"
|
||||
| "playht"
|
||||
| "rimelabs"
|
||||
| "inworld"
|
||||
| "verbio"
|
||||
| "openai"
|
||||
| "Cartesia";
|
||||
| "Cartesia"
|
||||
| "Resemble"
|
||||
| "Houndify"
|
||||
| "gladia";
|
||||
|
||||
export interface VendorOptions {
|
||||
name: Vendor;
|
||||
@@ -31,6 +36,11 @@ export interface LabelOptions {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface JambonzResourceOptions {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
name: string;
|
||||
value: string;
|
||||
@@ -76,6 +86,7 @@ export interface RegionVendors {
|
||||
microsoft: Region[];
|
||||
ibm: Region[];
|
||||
speechmatics: Region[];
|
||||
gladia: Region[];
|
||||
}
|
||||
|
||||
export interface TtsModels {
|
||||
@@ -96,6 +107,7 @@ export interface RecognizerVendors {
|
||||
speechmatics: Language[];
|
||||
cobalt: Language[];
|
||||
assemblyai: Language[];
|
||||
deepgramflux: Language[];
|
||||
}
|
||||
|
||||
export interface SynthesisVendors {
|
||||
@@ -112,6 +124,7 @@ export interface SynthesisVendors {
|
||||
playht: VoiceLanguage[];
|
||||
cartesia: VoiceLanguage[];
|
||||
rimelabs: VoiceLanguage[];
|
||||
inworld: VoiceLanguage[];
|
||||
}
|
||||
|
||||
export interface MSRawSpeech {
|
||||
|
||||
@@ -15,6 +15,15 @@ export default defineConfig(() => {
|
||||
src: path.resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
|
||||
// Configure Sass to use the modern API
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern-compiler",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return config;
|
||||
|
||||
Reference in New Issue
Block a user