diff --git a/web/app/(commonLayout)/_layout-client.tsx b/web/app/(commonLayout)/_layout-client.tsx deleted file mode 100644 index 325621878a..0000000000 --- a/web/app/(commonLayout)/_layout-client.tsx +++ /dev/null @@ -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 = ({ 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(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() - const [langeniusVersionInfo, setLangeniusVersionInfo] = useState() - 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 - - 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 ( - - - -
-
- {children} -
-
-
-
- ) -} -export default React.memo(CommonLayout) diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx index 901fd95eae..c3aed76002 100644 --- a/web/app/(commonLayout)/layout.tsx +++ b/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 = ({ children }) => { +const Layout = ({ children }: { children: ReactNode }) => { return ( <> - + + +
+ {children} + + ) } @@ -16,4 +23,4 @@ export const metadata = { title: 'Dify', } -export default Layout \ No newline at end of file +export default Layout diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index b1dd6382cb..b5a1b8ca7d 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/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 (
@@ -107,7 +117,7 @@ export default function AppSelector({ userProfile, onLogout, langeniusVersionInf
-
onLogout()}> +
handleLogout()}>
diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index 562d3e63af..1390f6f8a8 100644 --- a/web/app/components/header/index.tsx +++ b/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 = ({ - 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 = ({ ) } - +
diff --git a/web/app/components/sentry-initor.tsx b/web/app/components/sentry-initor.tsx new file mode 100644 index 0000000000..6bf0abbec2 --- /dev/null +++ b/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 diff --git a/web/app/components/swr-initor.tsx b/web/app/components/swr-initor.tsx new file mode 100644 index 0000000000..bf85a85cd6 --- /dev/null +++ b/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 ( + + {children} + + ) +} + +export default SwrInitor diff --git a/web/app/layout.tsx b/web/app/layout.tsx index eea56e7aea..dee605b88b 100644 --- a/web/app/layout.tsx +++ b/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 ( - {/* @ts-expect-error Async Server Component */} - {children} + + {/* @ts-expect-error Async Server Component */} + {children} + ) diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index 90cfc5ec11..5b39d69a1f 100644 --- a/web/context/app-context.tsx +++ b/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 = (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, - useSelector: typeof useSelector, + pageContainerRef: React.RefObject + langeniusVersionInfo: LangGeniusVersionResponse + useSelector: typeof useSelector } const AppContext = createContext({ @@ -27,18 +30,59 @@ const AppContext = createContext({ }, 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 -}> +export function useSelector(selector: (value: AppContextValue) => T): T { + return useContextSelector(AppContext, selector) +} -export const AppContextProvider: FC = ({ value, children }) => ( - - {children} - -) +export type AppContextProviderProps = { + children: ReactNode +} + +export const AppContextProvider: FC = ({ children }) => { + const pageContainerRef = useRef(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() + const [langeniusVersionInfo, setLangeniusVersionInfo] = useState() + 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 + + return ( + +
+ {children} +
+
+ ) +} export const useAppContext = () => useContext(AppContext)