dify/web/app/components/app-sidebar/app-detail-section.tsx
Stephen Zhou a84c2d36a3
style: format with vp fmt (#38803)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-12 15:57:46 +00:00

204 lines
6.8 KiB
TypeScript

'use client'
import type { ComponentProps } from 'react'
import type { NavIcon } from './nav-link'
import { cn } from '@langgenius/dify-ui/cn'
import {
RiDashboard2Fill,
RiDashboard2Line,
RiFileList3Fill,
RiFileList3Line,
RiLock2Fill,
RiLock2Line,
RiTerminalBoxFill,
RiTerminalBoxLine,
RiTerminalWindowFill,
RiTerminalWindowLine,
} from '@remixicon/react'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { Fragment, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useStore } from '@/app/components/app/store'
import Divider from '@/app/components/base/divider'
import Annotations from '@/app/components/base/icons/src/vender/Annotations'
import { userProfileIdAtom } from '@/context/account-state'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { usePathname } from '@/next/navigation'
import { AppModeEnum } from '@/types/app'
import { getAppACLCapabilities } from '@/utils/permission'
import { AppInfoView } from './app-info'
import { useAppInfoActions } from './app-info/use-app-info-actions'
import NavLink from './nav-link'
type AppDetailNavItem = {
name: string
href: string
icon: NavIcon
selectedIcon: NavIcon
}
const AnnotationNavIcon = ({ className, ...props }: ComponentProps<typeof Annotations>) => (
<Annotations {...props} className={cn(className, 'size-4')} />
)
AnnotationNavIcon.displayName = 'Annotations'
const isLogsNavItem = (item: AppDetailNavItem) => item.href.endsWith('/logs')
const isAnnotationsNavItem = (item: AppDetailNavItem) => item.href.endsWith('/annotations')
const renderNavDivider = (key: string, expand: boolean) => (
<div key={key} className={cn(expand ? 'px-3 py-0.5' : 'px-1 py-0.5')}>
<Divider
type="horizontal"
bgStyle={expand ? 'gradient' : 'solid'}
className={cn(
'my-0 h-px',
expand
? 'bg-linear-to-r from-divider-subtle to-background-gradient-mask-transparent'
: 'bg-divider-subtle',
)}
/>
</div>
)
type AppDetailSectionProps = {
expand?: boolean
}
const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
const { t } = useTranslation()
const pathname = usePathname()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const isRbacEnabled = systemFeatures.rbac_enabled
const appDetail = useStore((state) => state.appDetail)
const appInfoActions = useAppInfoActions({
resetKey: appDetail?.id,
})
const navigation = useMemo<AppDetailNavItem[]>(() => {
if (!appDetail) return []
const appId = appDetail.id
const isWorkflowApp =
appDetail.mode === AppModeEnum.WORKFLOW || appDetail.mode === AppModeEnum.ADVANCED_CHAT
const supportsAnnotations =
appDetail.mode !== AppModeEnum.WORKFLOW && appDetail.mode !== AppModeEnum.COMPLETION
const appACLCapabilities = getAppACLCapabilities(appDetail.permission_keys, {
currentUserId,
resourceMaintainer: appDetail.maintainer,
workspacePermissionKeys,
isRbacEnabled,
})
return [
...(appACLCapabilities.canAccessLayout
? [
{
name: t(($) => $['appMenus.promptEng'], { ns: 'common' }),
href: `/app/${appId}/${isWorkflowApp ? 'workflow' : 'configuration'}`,
icon: RiTerminalWindowLine,
selectedIcon: RiTerminalWindowFill,
},
]
: []),
{
name: t(($) => $['appMenus.apiAccess'], { ns: 'common' }),
href: `/app/${appId}/develop`,
icon: RiTerminalBoxLine,
selectedIcon: RiTerminalBoxFill,
},
...(appACLCapabilities.canAccessLogAndAnnotation
? [
{
name: t(($) => $['appMenus.logs'], { ns: 'common' }),
href: `/app/${appId}/logs`,
icon: RiFileList3Line,
selectedIcon: RiFileList3Fill,
},
]
: []),
...(appACLCapabilities.canAccessLogAndAnnotation && supportsAnnotations
? [
{
name: t(($) => $['appMenus.annotations'], { ns: 'common' }),
href: `/app/${appId}/annotations`,
icon: AnnotationNavIcon,
selectedIcon: AnnotationNavIcon,
},
]
: []),
...(appACLCapabilities.canMonitor
? [
{
name: t(($) => $['appMenus.overview'], { ns: 'common' }),
href: `/app/${appId}/overview`,
icon: RiDashboard2Line,
selectedIcon: RiDashboard2Fill,
},
]
: []),
...(appACLCapabilities.canAccessConfig
? [
{
name: t(($) => $['settings.resourceAccess'], { ns: 'common' }),
href: `/app/${appId}/access-config`,
icon: RiLock2Line,
selectedIcon: RiLock2Fill,
},
]
: []),
]
}, [appDetail, t, currentUserId, workspacePermissionKeys, isRbacEnabled])
if (!appDetail) return null
const hasLogsNavigation = navigation.some(isLogsNavItem)
const hasAnnotationsNavigation = navigation.some(isAnnotationsNavItem)
return (
<div className={cn('flex min-h-0 flex-1 flex-col', expand ? 'px-2 pb-2' : 'pb-2')}>
{!expand && (
<div className="flex w-full shrink-0 justify-center px-3.5 pt-0.5 pb-[3px]">
<Divider
type="horizontal"
bgStyle="solid"
className="my-0 h-px w-[27px] bg-divider-subtle"
/>
</div>
)}
<div className="px-1 py-2">
<AppInfoView expand={expand} actions={appInfoActions} />
</div>
<nav className={cn('flex flex-col gap-y-0.5 py-1', expand ? 'px-1' : 'px-3')}>
{navigation.map((item) => {
const shouldRenderDividerBefore =
isLogsNavItem(item) || (!hasLogsNavigation && isAnnotationsNavItem(item))
const shouldRenderDividerAfter = hasAnnotationsNavigation
? isAnnotationsNavItem(item)
: isLogsNavItem(item)
return (
<Fragment key={item.href}>
{shouldRenderDividerBefore && renderNavDivider(`${item.href}-before`, expand)}
<NavLink
mode={expand ? 'expand' : 'collapse'}
iconMap={{ selected: item.selectedIcon, normal: item.icon }}
name={item.name}
href={item.href}
pathname={pathname}
/>
{shouldRenderDividerAfter && renderNavDivider(`${item.href}-after`, expand)}
</Fragment>
)
})}
</nav>
</div>
)
}
export default AppDetailSection