Browse Source

refact common layout (#490)

zxhlyh 1 year ago
parent
commit
39ea967b30

+ 0 - 111
web/app/(commonLayout)/_layout-client.tsx

@@ -1,111 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React, { useEffect, useRef, useState } from 'react'
-import { usePathname, useRouter, useSelectedLayoutSegments } from 'next/navigation'
-import useSWR, { SWRConfig } from 'swr'
-import * as Sentry from '@sentry/react'
-import Header from '../components/header'
-import { fetchAppList } from '@/service/apps'
-import { fetchDatasets } from '@/service/datasets'
-import { fetchLanggeniusVersion, fetchUserProfile, logout } from '@/service/common'
-import Loading from '@/app/components/base/loading'
-import { AppContextProvider } from '@/context/app-context'
-import DatasetsContext from '@/context/datasets-context'
-import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
-
-const isDevelopment = process.env.NODE_ENV === 'development'
-
-export type ICommonLayoutProps = {
-  children: React.ReactNode
-}
-
-const CommonLayout: FC<ICommonLayoutProps> = ({ children }) => {
-  useEffect(() => {
-    const SENTRY_DSN = document?.body?.getAttribute('data-public-sentry-dsn')
-    if (!isDevelopment && SENTRY_DSN) {
-      Sentry.init({
-        dsn: SENTRY_DSN,
-        integrations: [
-          new Sentry.BrowserTracing({
-          }),
-          new Sentry.Replay(),
-        ],
-        tracesSampleRate: 0.1,
-        replaysSessionSampleRate: 0.1,
-        replaysOnErrorSampleRate: 1.0,
-      })
-    }
-  }, [])
-  const router = useRouter()
-  const pathname = usePathname()
-  const segments = useSelectedLayoutSegments()
-  const pattern = pathname.replace(/.*\/app\//, '')
-  const [idOrMethod] = pattern.split('/')
-  const isNotDetailPage = idOrMethod === 'list'
-  const pageContainerRef = useRef<HTMLDivElement>(null)
-
-  const appId = isNotDetailPage ? '' : idOrMethod
-
-  const { data: appList, mutate: mutateApps } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
-  const { data: datasetList, mutate: mutateDatasets } = useSWR(segments[0] === 'datasets' ? { url: '/datasets', params: { page: 1 } } : null, fetchDatasets)
-  const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
-
-  const [userProfile, setUserProfile] = useState<UserProfileResponse>()
-  const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>()
-  const updateUserProfileAndVersion = async () => {
-    if (userProfileResponse && !userProfileResponse.bodyUsed) {
-      const result = await userProfileResponse.json()
-      setUserProfile(result)
-      const current_version = userProfileResponse.headers.get('x-version')
-      const current_env = userProfileResponse.headers.get('x-env')
-      const versionData = await fetchLanggeniusVersion({ url: '/version', params: { current_version } })
-      setLangeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env })
-    }
-  }
-  useEffect(() => {
-    updateUserProfileAndVersion()
-  }, [userProfileResponse])
-
-  if (!appList || !userProfile || !langeniusVersionInfo)
-    return <Loading type='app' />
-
-  const curAppId = segments[0] === 'app' && segments[2]
-  const currentDatasetId = segments[0] === 'datasets' && segments[2]
-  const currentDataset = datasetList?.data?.find(opt => opt.id === currentDatasetId)
-
-  // if (!isNotDetailPage && !curApp) {
-  //   alert('app not found') // TODO: use toast. Now can not get toast context here.
-  //   // notify({ type: 'error', message: 'App not found' })
-  //   router.push('/apps')
-  // }
-
-  const onLogout = async () => {
-    await logout({
-      url: '/logout',
-      params: {},
-    })
-    router.push('/signin')
-  }
-
-  return (
-    <SWRConfig value={{
-      shouldRetryOnError: false,
-    }}>
-      <AppContextProvider value={{ apps: appList.data, mutateApps, userProfile, mutateUserProfile, pageContainerRef }}>
-        <DatasetsContext.Provider value={{ datasets: datasetList?.data || [], mutateDatasets, currentDataset }}>
-          <div ref={pageContainerRef} className='relative flex flex-col h-full overflow-auto bg-gray-100'>
-            <Header
-              isBordered={['/apps', '/datasets'].includes(pathname)}
-              curAppId={curAppId || ''}
-              userProfile={userProfile}
-              onLogout={onLogout}
-              langeniusVersionInfo={langeniusVersionInfo}
-            />
-            {children}
-          </div>
-        </DatasetsContext.Provider>
-      </AppContextProvider>
-    </SWRConfig>
-  )
-}
-export default React.memo(CommonLayout)

+ 13 - 6
web/app/(commonLayout)/layout.tsx

@@ -1,13 +1,20 @@
-import React from "react";
-import type { FC } from 'react'
-import LayoutClient, { ICommonLayoutProps } from "./_layout-client";
+import React from 'react'
+import type { ReactNode } from 'react'
+import SwrInitor from '@/app/components/swr-initor'
+import { AppContextProvider } from '@/context/app-context'
 import GA, { GaType } from '@/app/components/base/ga'
+import Header from '@/app/components/header'
 
-const Layout: FC<ICommonLayoutProps> = ({ children }) => {
+const Layout = ({ children }: { children: ReactNode }) => {
   return (
     <>
       <GA gaType={GaType.admin} />
-      <LayoutClient children={children}></LayoutClient>
+      <SwrInitor>
+        <AppContextProvider>
+          <Header />
+          {children}
+        </AppContextProvider>
+      </SwrInitor>
     </>
   )
 }
@@ -16,4 +23,4 @@ export const metadata = {
   title: 'Dify',
 }
 
-export default Layout
+export default Layout

+ 13 - 3
web/app/components/header/account-dropdown/index.tsx

@@ -1,6 +1,7 @@
 'use client'
 import { useTranslation } from 'react-i18next'
 import { Fragment, useState } from 'react'
+import { useRouter } from 'next/navigation'
 import { useContext } from 'use-context-selector'
 import classNames from 'classnames'
 import Link from 'next/link'
@@ -13,24 +14,33 @@ import WorkplaceSelector from './workplace-selector'
 import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
 import I18n from '@/context/i18n'
 import Avatar from '@/app/components/base/avatar'
+import { logout } from '@/service/common'
 
 type IAppSelectorProps = {
   userProfile: UserProfileResponse
-  onLogout: () => void
   langeniusVersionInfo: LangGeniusVersionResponse
 }
 
-export default function AppSelector({ userProfile, onLogout, langeniusVersionInfo }: IAppSelectorProps) {
+export default function AppSelector({ userProfile, langeniusVersionInfo }: IAppSelectorProps) {
   const itemClassName = `
     flex items-center w-full h-10 px-3 text-gray-700 text-[14px]
     rounded-lg font-normal hover:bg-gray-100 cursor-pointer
   `
+  const router = useRouter()
   const [settingVisible, setSettingVisible] = useState(false)
   const [aboutVisible, setAboutVisible] = useState(false)
 
   const { locale } = useContext(I18n)
   const { t } = useTranslation()
 
+  const handleLogout = async () => {
+    await logout({
+      url: '/logout',
+      params: {},
+    })
+    router.push('/signin')
+  }
+
   return (
     <div className="">
       <Menu as="div" className="relative inline-block text-left">
@@ -107,7 +117,7 @@ export default function AppSelector({ userProfile, onLogout, langeniusVersionInf
               </Menu.Item>
             </div>
             <Menu.Item>
-              <div className='p-1' onClick={() => onLogout()}>
+              <div className='p-1' onClick={() => handleLogout()}>
                 <div
                   className='flex items-center justify-between h-12 px-3 rounded-lg cursor-pointer group hover:bg-gray-100'
                 >

+ 8 - 16
web/app/components/header/index.tsx

@@ -1,8 +1,7 @@
 'use client'
 import { useEffect, useState } from 'react'
-import type { FC } from 'react'
 import { useTranslation } from 'react-i18next'
-import { useSelectedLayoutSegment } from 'next/navigation'
+import { usePathname, useSelectedLayoutSegment } from 'next/navigation'
 import classNames from 'classnames'
 import { CommandLineIcon } from '@heroicons/react/24/solid'
 import Link from 'next/link'
@@ -10,19 +9,14 @@ import AccountDropdown from './account-dropdown'
 import AppNav from './app-nav'
 import DatasetNav from './dataset-nav'
 import s from './index.module.css'
-import type { GithubRepo, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
+import type { GithubRepo } from '@/models/common'
 import { WorkspaceProvider } from '@/context/workspace-context'
+import { useAppContext } from '@/context/app-context'
 import { Grid01 } from '@/app/components/base/icons/src/vender/line/layout'
 import { Grid01 as Grid01Solid } from '@/app/components/base/icons/src/vender/solid/layout'
 import { PuzzlePiece01 } from '@/app/components/base/icons/src/vender/line/development'
 import { PuzzlePiece01 as PuzzlePiece01Solid } from '@/app/components/base/icons/src/vender/solid/development'
 
-export type IHeaderProps = {
-  userProfile: UserProfileResponse
-  onLogout: () => void
-  langeniusVersionInfo: LangGeniusVersionResponse
-  isBordered: boolean
-}
 const navClassName = `
   flex items-center relative mr-3 px-3 h-8 rounded-xl
   font-medium text-sm
@@ -32,18 +26,16 @@ const headerEnvClassName: { [k: string]: string } = {
   DEVELOPMENT: 'bg-[#FEC84B] border-[#FDB022] text-[#93370D]',
   TESTING: 'bg-[#A5F0FC] border-[#67E3F9] text-[#164C63]',
 }
-const Header: FC<IHeaderProps> = ({
-  userProfile,
-  onLogout,
-  langeniusVersionInfo,
-  isBordered,
-}) => {
+const Header = () => {
   const { t } = useTranslation()
+  const pathname = usePathname()
+  const { userProfile, langeniusVersionInfo } = useAppContext()
   const showEnvTag = langeniusVersionInfo.current_env === 'TESTING' || langeniusVersionInfo.current_env === 'DEVELOPMENT'
   const selectedSegment = useSelectedLayoutSegment()
   const isPluginsComingSoon = selectedSegment === 'plugins-coming-soon'
   const isExplore = selectedSegment === 'explore'
   const [starCount, setStarCount] = useState(0)
+  const isBordered = ['/apps', '/datasets'].includes(pathname)
 
   useEffect(() => {
     globalThis.fetch('https://api.github.com/repos/langgenius/dify').then(res => res.json()).then((data: GithubRepo) => {
@@ -136,7 +128,7 @@ const Header: FC<IHeaderProps> = ({
             )
           }
           <WorkspaceProvider>
-            <AccountDropdown userProfile={userProfile} onLogout={onLogout} langeniusVersionInfo={langeniusVersionInfo} />
+            <AccountDropdown userProfile={userProfile} langeniusVersionInfo={langeniusVersionInfo} />
           </WorkspaceProvider>
         </div>
       </div>

+ 30 - 0
web/app/components/sentry-initor.tsx

@@ -0,0 +1,30 @@
+'use client'
+
+import { useEffect } from 'react'
+import * as Sentry from '@sentry/react'
+
+const isDevelopment = process.env.NODE_ENV === 'development'
+
+const SentryInit = ({
+  children,
+}: { children: React.ReactElement }) => {
+  useEffect(() => {
+    const SENTRY_DSN = document?.body?.getAttribute('data-public-sentry-dsn')
+    if (!isDevelopment && SENTRY_DSN) {
+      Sentry.init({
+        dsn: SENTRY_DSN,
+        integrations: [
+          new Sentry.BrowserTracing({
+          }),
+          new Sentry.Replay(),
+        ],
+        tracesSampleRate: 0.1,
+        replaysSessionSampleRate: 0.1,
+        replaysOnErrorSampleRate: 1.0,
+      })
+    }
+  }, [])
+  return children
+}
+
+export default SentryInit

+ 21 - 0
web/app/components/swr-initor.tsx

@@ -0,0 +1,21 @@
+'use client'
+
+import { SWRConfig } from 'swr'
+import type { ReactNode } from 'react'
+
+type SwrInitorProps = {
+  children: ReactNode
+}
+const SwrInitor = ({
+  children,
+}: SwrInitorProps) => {
+  return (
+    <SWRConfig value={{
+      shouldRetryOnError: false,
+    }}>
+      {children}
+    </SWRConfig>
+  )
+}
+
+export default SwrInitor

+ 6 - 2
web/app/layout.tsx

@@ -1,4 +1,5 @@
 import I18nServer from './components/i18n-server'
+import SentryInitor from './components/sentry-initor'
 import { getLocaleOnServer } from '@/i18n/server'
 
 import './styles/globals.css'
@@ -14,6 +15,7 @@ const LocaleLayout = ({
   children: React.ReactNode
 }) => {
   const locale = getLocaleOnServer()
+
   return (
     <html lang={locale ?? 'en'} className="h-full">
       <body
@@ -23,8 +25,10 @@ const LocaleLayout = ({
         data-public-edition={process.env.NEXT_PUBLIC_EDITION}
         data-public-sentry-dsn={process.env.NEXT_PUBLIC_SENTRY_DSN}
       >
-        {/* @ts-expect-error Async Server Component */}
-        <I18nServer locale={locale}>{children}</I18nServer>
+        <SentryInitor>
+          {/* @ts-expect-error Async Server Component */}
+          <I18nServer locale={locale}>{children}</I18nServer>
+        </SentryInitor>
       </body>
     </html>
   )

+ 59 - 15
web/context/app-context.tsx

@@ -1,20 +1,23 @@
 'use client'
 
+import { createRef, useEffect, useRef, useState } from 'react'
+import useSWR from 'swr'
 import { createContext, useContext, useContextSelector } from 'use-context-selector'
+import type { FC, ReactNode } from 'react'
+import { fetchAppList } from '@/service/apps'
+import Loading from '@/app/components/base/loading'
+import { fetchLanggeniusVersion, fetchUserProfile } from '@/service/common'
 import type { App } from '@/types/app'
-import type { UserProfileResponse } from '@/models/common'
-import { createRef, FC, PropsWithChildren } from 'react'
-
-export const useSelector = <T extends any>(selector: (value: AppContextValue) => T): T =>
-  useContextSelector(AppContext, selector);
+import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
 
 export type AppContextValue = {
   apps: App[]
   mutateApps: () => void
   userProfile: UserProfileResponse
   mutateUserProfile: () => void
-  pageContainerRef: React.RefObject<HTMLDivElement>,
-  useSelector: typeof useSelector,
+  pageContainerRef: React.RefObject<HTMLDivElement>
+  langeniusVersionInfo: LangGeniusVersionResponse
+  useSelector: typeof useSelector
 }
 
 const AppContext = createContext<AppContextValue>({
@@ -27,18 +30,59 @@ const AppContext = createContext<AppContextValue>({
   },
   mutateUserProfile: () => { },
   pageContainerRef: createRef(),
+  langeniusVersionInfo: {
+    current_env: '',
+    current_version: '',
+    latest_version: '',
+    release_date: '',
+    release_notes: '',
+    version: '',
+    can_auto_update: false,
+  },
   useSelector,
 })
 
-export type AppContextProviderProps = PropsWithChildren<{
-  value: Omit<AppContextValue, 'useSelector'>
-}>
+export function useSelector<T>(selector: (value: AppContextValue) => T): T {
+  return useContextSelector(AppContext, selector)
+}
+
+export type AppContextProviderProps = {
+  children: ReactNode
+}
+
+export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => {
+  const pageContainerRef = useRef<HTMLDivElement>(null)
+
+  const { data: appList, mutate: mutateApps } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
+  const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
+
+  const [userProfile, setUserProfile] = useState<UserProfileResponse>()
+  const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>()
+  const updateUserProfileAndVersion = async () => {
+    if (userProfileResponse && !userProfileResponse.bodyUsed) {
+      const result = await userProfileResponse.json()
+      setUserProfile(result)
+      const current_version = userProfileResponse.headers.get('x-version')
+      const current_env = userProfileResponse.headers.get('x-env')
+      const versionData = await fetchLanggeniusVersion({ url: '/version', params: { current_version } })
+      setLangeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env })
+    }
+  }
+  useEffect(() => {
+    updateUserProfileAndVersion()
+  }, [userProfileResponse])
 
-export const AppContextProvider: FC<AppContextProviderProps> = ({ value, children }) => (
-  <AppContext.Provider value={{ ...value, useSelector }}>
-    {children}
-  </AppContext.Provider>
-)
+  if (!appList || !userProfile || !langeniusVersionInfo)
+    return <Loading type='app' />
+
+  return (
+    <AppContext.Provider value={{ apps: appList.data, mutateApps, userProfile, mutateUserProfile, pageContainerRef, langeniusVersionInfo, useSelector }}>
+      <div ref={pageContainerRef} className='relative flex flex-col h-full overflow-auto bg-gray-100'>
+        {children}
+      </div>
+    </AppContext.Provider>
+  )
+}
 
 export const useAppContext = () => useContext(AppContext)