some S3 compatible storage systems have a region parameter (#524)

* some S3 compatible storage systems have a region parameter

* wip

* wip

* replace current toastMethod by new toastProvider

* wip

* fix failing testcase

* wip
This commit is contained in:
Hoan Luu Huu
2025-05-28 21:03:39 +07:00
committed by GitHub
parent 10818493bc
commit e65d9b9db6
55 changed files with 276 additions and 113 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -5,12 +5,7 @@ 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,
toastSuccess,
toastError,
} from "src/store";
import { useSelectState, useDispatch } from "src/store";
import {
getActiveSP,
removeAccountFilter,
@@ -26,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;
@@ -67,6 +63,7 @@ export const Navi = ({
handleMenu,
handleLogout,
}: NaviProps) => {
const { toastSuccess, toastError } = useToast();
const dispatch = useDispatch();
const navigate = useNavigate();
const user = useSelectState("user");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
@@ -57,12 +57,14 @@ import { hasLength, 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");

View File

@@ -21,7 +21,7 @@ import {
SelectFilter,
} from "src/components";
import { DeleteApplication } from "./delete";
import { toastError, toastSuccess, useSelectState } from "src/store";
import { useSelectState } from "src/store";
import { isUserAccountScope, hasLength, hasValue } from "src/utils";
import type { Application, Account } from "src/api/types";
@@ -29,8 +29,10 @@ import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
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("");

View File

@@ -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,
@@ -82,6 +83,7 @@ export const SpeechProviderSelection = ({
sttLabelOptions,
sttLabel: [recogLabel, setRecogLabel],
}: SpeechProviderSelectionProbs) => {
const { toastError } = useToast();
const user = useSelectState("user");
const [
synthesisSupportedLanguagesAndVoices,

View File

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

View File

@@ -41,7 +41,7 @@ import {
} from "src/components/forms";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import { ROUTE_INTERNAL_CARRIERS } from "src/router/routes";
import { toastError, toastSuccess, useSelectState } from "src/store";
import { useSelectState } from "src/store";
import {
checkSelectOptions,
getIpValidationType,
@@ -67,6 +67,7 @@ import {
} from "src/api/types";
import { setAccountFilter, setLocation } from "src/store/localStore";
import { RegisterStatus } from "./register-status";
import { useToast } from "src/components/toast/toast-provider";
type CarrierFormProps = {
carrier?: UseApiDataMap<Carrier>;
@@ -79,6 +80,7 @@ export const CarrierForm = ({
carrierSipGateways,
carrierSmppGateways,
}: CarrierFormProps) => {
const { toastSuccess, toastError } = useToast();
const navigate = useNavigate();
const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");

View File

@@ -10,7 +10,7 @@ import {
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,
@@ -43,8 +43,10 @@ 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");

View File

@@ -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(() => {

View File

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

View File

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

View File

@@ -12,14 +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");

View File

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

View File

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

View File

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

View File

@@ -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}`,

View File

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

View File

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

View File

@@ -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}`,

View File

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

View File

@@ -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,
@@ -32,8 +32,10 @@ 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");

View File

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

View File

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

View File

@@ -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(() => {

View File

@@ -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://")

View File

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

View File

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

View File

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

View File

@@ -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,
@@ -91,12 +91,14 @@ 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");
const currentServiceProvider = useSelectState("currentServiceProvider");

View File

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

View File

@@ -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(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,12 +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>;
@@ -94,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();

View File

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

View File

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

View File

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

View File

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