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);
+ }}
+ />
>
)}