diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx index 2f2b841..0ba13e6 100644 --- a/src/components/ui/carousel.tsx +++ b/src/components/ui/carousel.tsx @@ -56,14 +56,30 @@ function Carousel({ }, plugins ) - const [canScrollPrev, setCanScrollPrev] = React.useState(false) - const [canScrollNext, setCanScrollNext] = React.useState(false) - const onSelect = React.useCallback((api: CarouselApi) => { - if (!api) return - setCanScrollPrev(api.canScrollPrev()) - setCanScrollNext(api.canScrollNext()) - }, []) + const subscribe = React.useCallback( + (callback: () => void) => { + if (!api) return () => {} + api.on("reInit", callback) + api.on("select", callback) + return () => { + api.off("reInit", callback) + api.off("select", callback) + } + }, + [api] + ) + + const canScrollPrev = React.useSyncExternalStore( + subscribe, + () => api?.canScrollPrev() ?? false, + () => false + ) + const canScrollNext = React.useSyncExternalStore( + subscribe, + () => api?.canScrollNext() ?? false, + () => false + ) const scrollPrev = React.useCallback(() => { api?.scrollPrev() @@ -91,17 +107,6 @@ function Carousel({ setApi(api) }, [api, setApi]) - React.useEffect(() => { - if (!api) return - onSelect(api) - api.on("reInit", onSelect) - api.on("select", onSelect) - - return () => { - api?.off("select", onSelect) - } - }, [api, onSelect]) - return ( { + const [lastValue, setLastValue] = React.useState(value) + if (value !== lastValue) { + setLastValue(value) if (value) { setSelected(value) } - }, [value]) - - useEffect(() => { - /** If `onSearch` is provided, do not trigger options updated. */ - if (!arrayOptions || onSearch) { - return - } - - const newOption = transToGroupOption(arrayOptions || [], groupBy) + } - if (JSON.stringify(newOption) !== JSON.stringify(options)) { - setOptions(newOption) - } - }, [arrayDefaultOptions, arrayOptions, groupBy, onSearch, options]) + const incomingOptions = !arrayOptions || onSearch + ? null + : transToGroupOption(arrayOptions, groupBy) + const incomingOptionsKey = incomingOptions ? JSON.stringify(incomingOptions) : null + const [lastOptionsKey, setLastOptionsKey] = React.useState(null) + if (incomingOptions && incomingOptionsKey !== lastOptionsKey) { + setLastOptionsKey(incomingOptionsKey) + setOptions(incomingOptions) + } useEffect(() => { /** sync search */ @@ -414,7 +413,7 @@ const MultipleSelector = ({ const selectables = React.useMemo(() => removePickedOption(options, selected), [options, selected]) /** Avoid Creatable Selector freezing or lagging when paste a long string. */ - const commandFilter = React.useCallback(() => { + const commandFilter = () => { if (commandProps?.filter) { return commandProps.filter } @@ -425,9 +424,8 @@ const MultipleSelector = ({ } } - // Using default filter in `cmdk`. We don‘t have to provide it. return undefined - }, [creatable, commandProps?.filter]) + } return ( (undefined) +function subscribe(callback: () => void) { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + mql.addEventListener("change", callback) + return () => mql.removeEventListener("change", callback) +} + +function getSnapshot() { + return window.innerWidth < MOBILE_BREAKPOINT +} - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - } - mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) - }, []) +function getServerSnapshot() { + return false +} - return !!isMobile +export function useIsMobile() { + return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) } diff --git a/src/layouts/panel/sidebar/cluster/index.tsx b/src/layouts/panel/sidebar/cluster/index.tsx index 19bc881..cdb32e5 100644 --- a/src/layouts/panel/sidebar/cluster/index.tsx +++ b/src/layouts/panel/sidebar/cluster/index.tsx @@ -30,7 +30,7 @@ import {CLUSTER_ICON_LIST} from "./icon-list"; import ClusterAddDialog from "./add-dialog.tsx"; import SidebarClusterStatus from "./status.tsx"; import SidebarClusterLoader from "./loader.tsx"; -import clusterStore, {useClusterActions} from "@/store/cluster-store.ts"; +import {useClusterActions, useClusterId} from "@/store/cluster-store.ts"; import {useRouter} from "@/routes/hooks"; import SidebarClusterSettings from "@/layouts/panel/sidebar/cluster/settings.tsx"; @@ -45,23 +45,23 @@ export function SidebarClusterSwitcher() { const {t} = useTranslation(); const {isMobile} = useSidebar() const {clusters, setClusters, loading} = useClusters(); - const [activeCluster, setActiveCluster] = React.useState(null); const [clickedCluster, setClickedCluster] = React.useState(null); const [open, setOpen] = React.useState(false); const [dialogMode, setDialogMode] = React.useState("add"); const {setClusterId} = useClusterActions(); + const clusterId = useClusterId(); const {replace} = useRouter(); + const activeCluster: Cluster | null = clusters && clusters.length > 0 + ? (clusters.find(c => c.id === clusterId) ?? clusters[0]) + : null; + React.useEffect(() => { if (clusters && clusters.length > 0) { - const selectedClusterId = clusterStore.getState().clusterId; - const match = selectedClusterId ? clusters.find(c => c.id === selectedClusterId) : null; - if (match) { - setActiveCluster(match); - return; + const stored = clusterId ? clusters.find(c => c.id === clusterId) : null; + if (!stored) { + setClusterId(clusters[0].id); } - setActiveCluster(clusters[0]); - setClusterId(clusters[0].id); return; } if (!loading && (!clusters || clusters.length === 0)) { @@ -74,9 +74,8 @@ export function SidebarClusterSwitcher() { window.location.reload(); })(); } - }, [clusters, loading, replace, setClusters, setClusterId]); + }, [clusters, clusterId, loading, replace, setClusters, setClusterId]); const updateCluster = (cluster: Cluster) => { - setActiveCluster(cluster); setClusterId(cluster.id); replace("/overview/"); }; diff --git a/src/pages/authentication/login/index.tsx b/src/pages/authentication/login/index.tsx index c942b51..e7ad721 100644 --- a/src/pages/authentication/login/index.tsx +++ b/src/pages/authentication/login/index.tsx @@ -50,13 +50,12 @@ export default function LoginForm() { const [otp, setOtp] = useState("") const [recoveryOpen, setRecoveryOpen] = useState(false) const [recoveryCode, setRecoveryCode] = useState("") - const [rememberMe, setRememberMe] = useState(false) + const [rememberMe, setRememberMe] = useState(() => !!localStorage.getItem("remembered_email")) useEffect(() => { const rememberedEmail = localStorage.getItem("remembered_email") if (rememberedEmail) { form.setValue("email", rememberedEmail) - setRememberMe(true) setTimeout(() => setFocus("password"), 0) } else { setTimeout(() => setFocus("email"), 0) @@ -89,7 +88,7 @@ export default function LoginForm() { } } - const handleOtpSubmit = useCallback(async () => { + const handleOtpSubmit = useCallback(async (code: string) => { const { email, password } = getValues() if (!email || !password) return @@ -98,7 +97,7 @@ export default function LoginForm() { const response = await login({ email, password, - multi_factor_code: otp, + multi_factor_code: code, }) setOtp("") @@ -110,9 +109,9 @@ export default function LoginForm() { } finally { setLoading(false) } - }, [getValues, login, navigate, otp, t]) + }, [getValues, login, navigate, t]) - const handleRecoverySubmit = useCallback(async () => { + const handleRecoverySubmit = useCallback(async (code: string) => { const { email, password } = getValues() if (!email || !password) return @@ -121,7 +120,7 @@ export default function LoginForm() { const response = await login({ email, password, - multi_factor_code: recoveryCode, + multi_factor_code: code, }) if (response.success) { @@ -132,15 +131,17 @@ export default function LoginForm() { } finally { setLoading(false) } - }, [getValues, login, navigate, recoveryCode, t]) + }, [getValues, login, navigate, t]) - useEffect(() => { - if (otp.length === 6 && !loading) handleOtpSubmit() - }, [otp, loading, handleOtpSubmit]) + const handleOtpChange = (value: string) => { + setOtp(value) + if (value.length === 6 && !loading) handleOtpSubmit(value) + } - useEffect(() => { - if (recoveryCode.length === 16 && !loading) handleRecoverySubmit() - }, [recoveryCode, loading, handleRecoverySubmit]) + const handleRecoveryChange = (value: string) => { + setRecoveryCode(value) + if (value.length === 16 && !loading) handleRecoverySubmit(value) + } if (token.authentication_token) { return @@ -248,7 +249,7 @@ export default function LoginForm() {
- + {[0, 1, 2].map((i) => ( @@ -263,7 +264,7 @@ export default function LoginForm() {
- @@ -284,7 +285,7 @@ export default function LoginForm() { {[0, 4, 8, 12].map((start) => ( @@ -297,7 +298,7 @@ export default function LoginForm() {