From e65d9b9db6fc435302f1cdaa4fcde4b2c7272a55 Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Wed, 28 May 2025 21:03:39 +0700 Subject: [PATCH] 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 --- src/components/clipboard/index.tsx | 3 +- src/components/require-auth.cy.tsx | 13 ++- src/components/require-auth.tsx | 3 +- src/components/toast/toast-provider.tsx | 96 +++++++++++++++++++ src/containers/internal/api-keys/index.tsx | 3 +- src/containers/internal/navi/index.tsx | 9 +- .../internal/views/accounts/edit.tsx | 4 +- .../internal/views/accounts/form.tsx | 39 +++++++- .../internal/views/accounts/index.tsx | 4 +- .../views/accounts/manage-payment-form.tsx | 4 +- .../views/accounts/subscription-form.tsx | 3 +- .../internal/views/alerts/index.tsx | 4 +- .../internal/views/applications/edit.tsx | 4 +- .../internal/views/applications/form.tsx | 4 +- .../internal/views/applications/index.tsx | 4 +- .../views/applications/speech-selection.tsx | 4 +- .../internal/views/carriers/edit.tsx | 4 +- .../internal/views/carriers/form.tsx | 4 +- .../internal/views/carriers/index.tsx | 4 +- .../internal/views/carriers/pcap.tsx | 3 +- .../internal/views/clients/edit.tsx | 3 +- .../internal/views/clients/form.tsx | 4 +- .../internal/views/clients/index.tsx | 4 +- .../views/least-cost-routing/container.tsx | 3 +- .../views/least-cost-routing/form.tsx | 9 +- .../views/least-cost-routing/index.tsx | 4 +- .../internal/views/ms-teams-tenants/edit.tsx | 3 +- .../internal/views/ms-teams-tenants/form.tsx | 4 +- .../internal/views/ms-teams-tenants/index.tsx | 3 +- .../internal/views/phone-numbers/edit.tsx | 3 +- .../internal/views/phone-numbers/form.tsx | 3 +- .../internal/views/phone-numbers/index.tsx | 4 +- .../views/recent-calls/call-system-logs.tsx | 3 +- .../internal/views/recent-calls/index.tsx | 4 +- .../internal/views/recent-calls/pcap.tsx | 3 +- .../internal/views/recent-calls/player.tsx | 3 +- .../views/settings/admin-settings.tsx | 3 +- .../settings/service-provider-settings.tsx | 4 +- .../internal/views/speech-services/edit.tsx | 4 +- .../internal/views/speech-services/form.tsx | 4 +- .../internal/views/speech-services/index.tsx | 4 +- src/containers/internal/views/users/edit.tsx | 3 +- src/containers/internal/views/users/form.tsx | 4 +- src/containers/login/forgot-password.tsx | 3 +- src/containers/login/login.tsx | 3 +- src/containers/login/oauth-callback.tsx | 3 +- src/containers/login/register-email.tsx | 3 +- .../login/register-verify-email.tsx | 3 +- src/containers/login/reset-password.tsx | 3 +- src/main.tsx | 17 ++-- src/router/auth.tsx | 3 +- src/store/index.tsx | 35 ------- src/utils/use-redirect.ts | 4 +- src/utils/use-scoped-redirect.ts | 6 +- src/utils/with-access-control.tsx | 4 +- 55 files changed, 276 insertions(+), 113 deletions(-) create mode 100644 src/components/toast/toast-provider.tsx diff --git a/src/components/clipboard/index.tsx b/src/components/clipboard/index.tsx index 3e752c8..92537e8 100644 --- a/src/components/clipboard/index.tsx +++ b/src/components/clipboard/index.tsx @@ -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) diff --git a/src/components/require-auth.cy.tsx b/src/components/require-auth.cy.tsx index f8b497f..8213801 100644 --- a/src/components/require-auth.cy.tsx +++ b/src/components/require-auth.cy.tsx @@ -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 ( - -
-

Protected Route

-
-
+ + +
+

Protected Route

+
+
+
); }; diff --git a/src/components/require-auth.tsx b/src/components/require-auth.tsx index 852e6cd..86a8243 100644 --- a/src/components/require-auth.tsx +++ b/src/components/require-auth.tsx @@ -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(); diff --git a/src/components/toast/toast-provider.tsx b/src/components/toast/toast-provider.tsx new file mode 100644 index 0000000..be9602c --- /dev/null +++ b/src/components/toast/toast-provider.tsx @@ -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(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(null); + const timeoutRef = useRef(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 ( + + {children} + {toast && } + + ); +}; + +export const useToast = () => { + const context = useContext(ToastContext); + + if (context === undefined) { + throw new Error("useToast must be used within a ToastProvider"); + } + + return context; +}; diff --git a/src/containers/internal/api-keys/index.tsx b/src/containers/internal/api-keys/index.tsx index ce8a10b..c320a09 100644 --- a/src/containers/internal/api-keys/index.tsx +++ b/src/containers/internal/api-keys/index.tsx @@ -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(path); const [deleteKey, setDeleteKey] = useState(null); const [addedKey, setAddedKey] = useState(null); diff --git a/src/containers/internal/navi/index.tsx b/src/containers/internal/navi/index.tsx index 829fe53..e558a27 100644 --- a/src/containers/internal/navi/index.tsx +++ b/src/containers/internal/navi/index.tsx @@ -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"); diff --git a/src/containers/internal/views/accounts/edit.tsx b/src/containers/internal/views/accounts/edit.tsx index 8f684de..305b19f 100644 --- a/src/containers/internal/views/accounts/edit.tsx +++ b/src/containers/internal/views/accounts/edit.tsx @@ -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( diff --git a/src/containers/internal/views/accounts/form.tsx b/src/containers/internal/views/accounts/form.tsx index 528ef8b..acb4c9f 100644 --- a/src/containers/internal/views/accounts/form.tsx +++ b/src/containers/internal/views/accounts/form.tsx @@ -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(""); + } }} /> @@ -1122,6 +1142,17 @@ export const AccountForm = ({ setTmpEndpoint(e.target.value); }} /> + + { + setBucketRegion(e.target.value); + setTmpBucketRegion(e.target.value); + }} + /> )}