mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 05:56:31 +08:00
Revert "refactor!: migrate commonLayout to SSR prefetch with TanStack Query hydration"
This reverts commit 2833965815.
This commit is contained in:
parent
e2913d9ee1
commit
f1c15e0a17
@ -1,39 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { AppInitializer } from '@/app/components/app-initializer'
|
||||
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||
import GotoAnything from '@/app/components/goto-anything'
|
||||
import Header from '@/app/components/header'
|
||||
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
||||
import ReadmePanel from '@/app/components/plugins/readme-panel'
|
||||
import { AppContextProvider } from '@/context/app-context'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||
import { ModalContextProvider } from '@/context/modal-context'
|
||||
import { ProviderContextProvider } from '@/context/provider-context'
|
||||
import PartnerStack from '../components/billing/partner-stack'
|
||||
|
||||
export const CommonLayoutClient = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
<AmplitudeProvider />
|
||||
<AppInitializer>
|
||||
<AppContextProvider>
|
||||
<EventEmitterContextProvider>
|
||||
<ProviderContextProvider>
|
||||
<ModalContextProvider>
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
{children}
|
||||
<PartnerStack />
|
||||
<ReadmePanel />
|
||||
<GotoAnything />
|
||||
</ModalContextProvider>
|
||||
</ProviderContextProvider>
|
||||
</EventEmitterContextProvider>
|
||||
</AppContextProvider>
|
||||
</AppInitializer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,46 +1,45 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
import { AppInitializer } from '@/app/components/app-initializer'
|
||||
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
import Zendesk from '@/app/components/base/zendesk'
|
||||
import { getQueryClientServer } from '@/context/query-client-server'
|
||||
import { serverFetchWithAuth } from '@/utils/ssr-fetch'
|
||||
import { CommonLayoutClient } from './layout-client'
|
||||
|
||||
const IS_DEV = process.env.NODE_ENV === 'development'
|
||||
|
||||
async function fetchUserProfileForSSR() {
|
||||
const { data: profile, headers } = await serverFetchWithAuth('/account/profile')
|
||||
return {
|
||||
profile,
|
||||
meta: {
|
||||
currentVersion: headers.get('x-version'),
|
||||
currentEnv: IS_DEV ? 'DEVELOPMENT' : headers.get('x-env'),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function CommonLayout({ children }: { children: ReactNode }) {
|
||||
const queryClient = getQueryClientServer()
|
||||
|
||||
await Promise.all([
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['common', 'user-profile'],
|
||||
queryFn: fetchUserProfileForSSR,
|
||||
}),
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['common', 'current-workspace'],
|
||||
queryFn: async () => {
|
||||
const { data } = await serverFetchWithAuth('/workspaces/current', 'POST', {})
|
||||
return data
|
||||
},
|
||||
}),
|
||||
])
|
||||
import GotoAnything from '@/app/components/goto-anything'
|
||||
import Header from '@/app/components/header'
|
||||
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
||||
import ReadmePanel from '@/app/components/plugins/readme-panel'
|
||||
import { AppContextProvider } from '@/context/app-context'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||
import { ModalContextProvider } from '@/context/modal-context'
|
||||
import { ProviderContextProvider } from '@/context/provider-context'
|
||||
import PartnerStack from '../components/billing/partner-stack'
|
||||
import Splash from '../components/splash'
|
||||
|
||||
const Layout = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<>
|
||||
<GA gaType={GaType.admin} />
|
||||
<CommonLayoutClient>{children}</CommonLayoutClient>
|
||||
<Zendesk />
|
||||
</HydrationBoundary>
|
||||
<AmplitudeProvider />
|
||||
<AppInitializer>
|
||||
<AppContextProvider>
|
||||
<EventEmitterContextProvider>
|
||||
<ProviderContextProvider>
|
||||
<ModalContextProvider>
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
{children}
|
||||
<PartnerStack />
|
||||
<ReadmePanel />
|
||||
<GotoAnything />
|
||||
<Splash />
|
||||
</ModalContextProvider>
|
||||
</ProviderContextProvider>
|
||||
</EventEmitterContextProvider>
|
||||
</AppContextProvider>
|
||||
<Zendesk />
|
||||
</AppInitializer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Layout
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import '@/app/components/base/loading/style.css'
|
||||
|
||||
export default function CommonLayoutLoading() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="spin-animation">
|
||||
<g clipPath="url(#clip0_324_2488)">
|
||||
<path d="M15 0H10C9.44772 0 9 0.447715 9 1V6C9 6.55228 9.44772 7 10 7H15C15.5523 7 16 6.55228 16 6V1C16 0.447715 15.5523 0 15 0Z" fill="#1C64F2" />
|
||||
<path opacity="0.5" d="M15 9H10C9.44772 9 9 9.44772 9 10V15C9 15.5523 9.44772 16 10 16H15C15.5523 16 16 15.5523 16 15V10C16 9.44772 15.5523 9 15 9Z" fill="#1C64F2" />
|
||||
<path opacity="0.1" d="M6 9H1C0.447715 9 0 9.44772 0 10V15C0 15.5523 0.447715 16 1 16H6C6.55228 16 7 15.5523 7 15V10C7 9.44772 6.55228 9 6 9Z" fill="#1C64F2" />
|
||||
<path opacity="0.2" d="M6 0H1C0.447715 0 0 0.447715 0 1V6C0 6.55228 0.447715 7 1 7H6C6.55228 7 7 6.55228 7 6V1C7 0.447715 6.55228 0 6 0Z" fill="#1C64F2" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_324_2488">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -2,15 +2,15 @@
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import Cookies from 'js-cookie'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { parseAsString, useQueryState } from 'nuqs'
|
||||
import { useEffect, useReducer, useRef } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
} from '@/app/education-apply/constants'
|
||||
import { useSetupStatusQuery } from '@/context/global-public-context'
|
||||
import { sendGAEvent } from '@/utils/gtag'
|
||||
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
|
||||
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
|
||||
import { trackEvent } from './base/amplitude'
|
||||
|
||||
@ -23,68 +23,80 @@ export const AppInitializer = ({
|
||||
}: AppInitializerProps) => {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [init, markInit] = useReducer(() => true, false)
|
||||
const { data: setupStatus } = useSetupStatusQuery()
|
||||
// Tokens are now stored in cookies, no need to check localStorage
|
||||
const pathname = usePathname()
|
||||
const [init, setInit] = useState(false)
|
||||
const [oauthNewUser, setOauthNewUser] = useQueryState(
|
||||
'oauth_new_user',
|
||||
parseAsString.withOptions({ history: 'replace' }),
|
||||
)
|
||||
const oauthTrackedRef = useRef(false)
|
||||
|
||||
const isSetupFinished = useCallback(async () => {
|
||||
try {
|
||||
const setUpStatus = await fetchSetupStatusWithCache()
|
||||
return setUpStatus.step === 'finished'
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (oauthNewUser !== 'true' || oauthTrackedRef.current)
|
||||
return
|
||||
oauthTrackedRef.current = true
|
||||
(async () => {
|
||||
const action = searchParams.get('action')
|
||||
|
||||
if (oauthNewUser === 'true') {
|
||||
let utmInfo = null
|
||||
const utmInfoStr = Cookies.get('utm_info')
|
||||
if (utmInfoStr) {
|
||||
try {
|
||||
utmInfo = JSON.parse(utmInfoStr)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to parse utm_info cookie:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Track registration event with UTM params
|
||||
trackEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
|
||||
method: 'oauth',
|
||||
...utmInfo,
|
||||
})
|
||||
|
||||
sendGAEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
|
||||
method: 'oauth',
|
||||
...utmInfo,
|
||||
})
|
||||
|
||||
// Clean up: remove utm_info cookie and URL params
|
||||
Cookies.remove('utm_info')
|
||||
setOauthNewUser(null)
|
||||
}
|
||||
|
||||
if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
||||
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
|
||||
|
||||
let utmInfo = null
|
||||
const utmInfoStr = Cookies.get('utm_info')
|
||||
if (utmInfoStr) {
|
||||
try {
|
||||
utmInfo = JSON.parse(utmInfoStr)
|
||||
const isFinished = await isSetupFinished()
|
||||
if (!isFinished) {
|
||||
router.replace('/install')
|
||||
return
|
||||
}
|
||||
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
if (redirectUrl) {
|
||||
location.replace(redirectUrl)
|
||||
return
|
||||
}
|
||||
|
||||
setInit(true)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to parse utm_info cookie:', e)
|
||||
catch {
|
||||
router.replace('/signin')
|
||||
}
|
||||
}
|
||||
|
||||
trackEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
|
||||
method: 'oauth',
|
||||
...utmInfo,
|
||||
})
|
||||
|
||||
sendGAEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
|
||||
method: 'oauth',
|
||||
...utmInfo,
|
||||
})
|
||||
|
||||
Cookies.remove('utm_info')
|
||||
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect -- setOauthNewUser is from nuqs useQueryState, not useState
|
||||
setOauthNewUser(null)
|
||||
}, [oauthNewUser, setOauthNewUser])
|
||||
|
||||
useEffect(() => {
|
||||
const action = searchParams.get('action')
|
||||
if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
||||
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
|
||||
}, [searchParams])
|
||||
|
||||
useEffect(() => {
|
||||
if (!setupStatus)
|
||||
return
|
||||
|
||||
if (setupStatus.step !== 'finished') {
|
||||
router.replace('/install')
|
||||
return
|
||||
}
|
||||
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
if (redirectUrl) {
|
||||
location.replace(redirectUrl)
|
||||
return
|
||||
}
|
||||
|
||||
markInit()
|
||||
}, [setupStatus, router, searchParams])
|
||||
})()
|
||||
}, [isSetupFinished, router, pathname, searchParams, oauthNewUser, setOauthNewUser])
|
||||
|
||||
return init ? children : null
|
||||
}
|
||||
|
||||
@ -23,8 +23,8 @@ const usePSInfo = () => {
|
||||
setTrue: setBind,
|
||||
}] = useBoolean(false)
|
||||
const { mutateAsync } = useBindPartnerStackInfo()
|
||||
|
||||
const getDomain = () => globalThis.location?.hostname.replace('cloud', '') ?? ''
|
||||
// Save to top domain. cloud.dify.ai => .dify.ai
|
||||
const domain = globalThis.location.hostname.replace('cloud', '')
|
||||
|
||||
const saveOrUpdate = useCallback(() => {
|
||||
if (!psPartnerKey || !psClickId)
|
||||
@ -37,7 +37,7 @@ const usePSInfo = () => {
|
||||
}), {
|
||||
expires: PARTNER_STACK_CONFIG.saveCookieDays,
|
||||
path: '/',
|
||||
domain: getDomain(),
|
||||
domain,
|
||||
})
|
||||
}, [psPartnerKey, psClickId, isPSChanged])
|
||||
|
||||
@ -56,7 +56,7 @@ const usePSInfo = () => {
|
||||
shouldRemoveCookie = true
|
||||
}
|
||||
if (shouldRemoveCookie)
|
||||
Cookies.remove(PARTNER_STACK_CONFIG.cookieName, { path: '/', domain: getDomain() })
|
||||
Cookies.remove(PARTNER_STACK_CONFIG.cookieName, { path: '/', domain })
|
||||
setBind()
|
||||
}
|
||||
}, [psPartnerKey, psClickId, mutateAsync, hasBind, setBind])
|
||||
|
||||
21
web/app/components/splash.tsx
Normal file
21
web/app/components/splash.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useIsLogin } from '@/service/use-common'
|
||||
import Loading from './base/loading'
|
||||
|
||||
const Splash: FC<PropsWithChildren> = () => {
|
||||
// would auto redirect to signin page if not logged in
|
||||
const { isLoading, data: loginData } = useIsLogin()
|
||||
const isLoggedIn = loginData?.logged_in
|
||||
|
||||
if (isLoading || !isLoggedIn) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[9999999] flex h-full items-center justify-center bg-background-body">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
export default React.memo(Splash)
|
||||
@ -1,16 +1,13 @@
|
||||
import type { Viewport } from 'next'
|
||||
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
|
||||
import { Provider as JotaiProvider } from 'jotai'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { Instrument_Serif } from 'next/font/google'
|
||||
import { NuqsAdapter } from 'nuqs/adapters/next/app'
|
||||
import GlobalPublicStoreProvider from '@/context/global-public-context'
|
||||
import { TanstackQueryInitializer } from '@/context/query-client'
|
||||
import { getQueryClientServer } from '@/context/query-client-server'
|
||||
import { getLocaleOnServer } from '@/i18n-config/server'
|
||||
import { DatasetAttr } from '@/types/feature'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { serverFetch } from '@/utils/ssr-fetch'
|
||||
import { ToastProvider } from './components/base/toast'
|
||||
import BrowserInitializer from './components/browser-initializer'
|
||||
import { ReactScanLoader } from './components/devtools/react-scan/loader'
|
||||
@ -42,18 +39,6 @@ const LocaleLayout = async ({
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const locale = await getLocaleOnServer()
|
||||
const queryClient = getQueryClientServer()
|
||||
|
||||
await Promise.all([
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['systemFeatures'],
|
||||
queryFn: () => serverFetch('/system-features'),
|
||||
}),
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['setupStatus'],
|
||||
queryFn: () => serverFetch('/setup'),
|
||||
}),
|
||||
])
|
||||
|
||||
const datasetMap: Record<DatasetAttr, string | undefined> = {
|
||||
[DatasetAttr.DATA_API_PREFIX]: process.env.NEXT_PUBLIC_API_PREFIX,
|
||||
@ -123,15 +108,13 @@ const LocaleLayout = async ({
|
||||
<BrowserInitializer>
|
||||
<SentryInitializer>
|
||||
<TanstackQueryInitializer>
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<I18nServerProvider>
|
||||
<ToastProvider>
|
||||
<GlobalPublicStoreProvider>
|
||||
{children}
|
||||
</GlobalPublicStoreProvider>
|
||||
</ToastProvider>
|
||||
</I18nServerProvider>
|
||||
</HydrationBoundary>
|
||||
<I18nServerProvider>
|
||||
<ToastProvider>
|
||||
<GlobalPublicStoreProvider>
|
||||
{children}
|
||||
</GlobalPublicStoreProvider>
|
||||
</ToastProvider>
|
||||
</I18nServerProvider>
|
||||
</TanstackQueryInitializer>
|
||||
</SentryInitializer>
|
||||
</BrowserInitializer>
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import type { SystemFeatures } from '@/types/feature'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect } from 'react'
|
||||
import { create } from 'zustand'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { consoleClient } from '@/service/client'
|
||||
@ -23,7 +22,10 @@ const systemFeaturesQueryKey = ['systemFeatures'] as const
|
||||
const setupStatusQueryKey = ['setupStatus'] as const
|
||||
|
||||
async function fetchSystemFeatures() {
|
||||
return consoleClient.systemFeatures()
|
||||
const data = await consoleClient.systemFeatures()
|
||||
const { setSystemFeatures } = useGlobalPublicStore.getState()
|
||||
setSystemFeatures({ ...defaultSystemFeatures, ...data })
|
||||
return data
|
||||
}
|
||||
|
||||
export function useSystemFeaturesQuery() {
|
||||
@ -49,15 +51,12 @@ export function useSetupStatusQuery() {
|
||||
const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { data, isPending } = useSystemFeaturesQuery()
|
||||
useSetupStatusQuery()
|
||||
// Fetch systemFeatures and setupStatus in parallel to reduce waterfall.
|
||||
// setupStatus is prefetched here and cached in localStorage for AppInitializer.
|
||||
const { isPending } = useSystemFeaturesQuery()
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const { setSystemFeatures } = useGlobalPublicStore.getState()
|
||||
setSystemFeatures({ ...defaultSystemFeatures, ...data })
|
||||
}
|
||||
}, [data])
|
||||
// Prefetch setupStatus for AppInitializer (result not needed here)
|
||||
useSetupStatusQuery()
|
||||
|
||||
if (isPending)
|
||||
return <div className="flex h-screen w-screen items-center justify-center"><Loading /></div>
|
||||
|
||||
@ -92,6 +92,8 @@ export const useUserProfile = () => {
|
||||
},
|
||||
}
|
||||
},
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
import { cookies } from 'next/headers'
|
||||
import { cache } from 'react'
|
||||
|
||||
const SSR_API_PREFIX = process.env.NEXT_PUBLIC_API_PREFIX || 'http://localhost:5001/console/api'
|
||||
|
||||
export const getAuthHeaders = cache(async () => {
|
||||
const cookieStore = await cookies()
|
||||
const cookieHeader = cookieStore.getAll().map(c => `${c.name}=${c.value}`).join('; ')
|
||||
const csrfToken = cookieStore.get('csrf_token')?.value
|
||||
|| cookieStore.get('__Host-csrf_token')?.value
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': cookieHeader,
|
||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}),
|
||||
}
|
||||
})
|
||||
|
||||
type ServerFetchResult<T> = {
|
||||
data: T
|
||||
headers: Headers
|
||||
}
|
||||
|
||||
export async function serverFetchWithAuth<T>(
|
||||
path: string,
|
||||
method = 'GET',
|
||||
body?: unknown,
|
||||
): Promise<ServerFetchResult<T>> {
|
||||
const headers = await getAuthHeaders()
|
||||
const res = await fetch(`${SSR_API_PREFIX}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
if (!res.ok)
|
||||
throw new Error(`${res.status}`)
|
||||
|
||||
const data: T = await res.json()
|
||||
return { data, headers: res.headers }
|
||||
}
|
||||
|
||||
export async function serverFetch<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${SSR_API_PREFIX}${path}`, { cache: 'no-store' })
|
||||
if (!res.ok)
|
||||
throw new Error(`${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user