From a5c59a02aed1c6d03dfbdfa106385eadc8a4a322 Mon Sep 17 00:00:00 2001 From: twwu Date: Mon, 18 May 2026 13:34:51 +0800 Subject: [PATCH] feat: enhance permission handling and UI updates across various components --- .../(appDetailLayout)/[appId]/layout-main.tsx | 22 ++- .../[appId]/overview/tracing/panel.tsx | 31 +-- web/app/components/apps/app-card.tsx | 181 +++++++++--------- web/app/components/apps/list.tsx | 29 +-- web/app/components/apps/new-app-card.tsx | 18 +- .../components/operations-dropdown.tsx | 5 + .../datasets/list/dataset-card/index.tsx | 2 +- .../datasets/list/dataset-card/operations.tsx | 12 +- web/app/components/datasets/list/datasets.tsx | 4 +- web/app/components/datasets/list/index.tsx | 23 ++- .../datasets/list/new-dataset-card/index.tsx | 10 +- .../datasets/list/new-dataset-card/option.tsx | 16 ++ web/app/components/tools/provider-list.tsx | 18 +- .../tag-management/components/tag-panel.tsx | 46 +++-- .../components/tag-selector.tsx | 5 + 15 files changed, 253 insertions(+), 169 deletions(-) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx index 4d032533b3..9dbf834bff 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx @@ -31,6 +31,7 @@ import useDocumentTitle from '@/hooks/use-document-title' import { usePathname, useRouter } from '@/next/navigation' import { fetchAppDetailDirect } from '@/service/apps' import { AppModeEnum } from '@/types/app' +import { hasPermission } from '@/utils/permission' import s from './style.module.css' type IAppDetailLayoutProps = { @@ -48,7 +49,7 @@ const AppDetailLayout: FC = (props) => { const pathname = usePathname() const media = useBreakpoints() const isMobile = media === MediaType.mobile - const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace, currentWorkspace } = useAppContext() + const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace, currentWorkspace, workspacePermissionKeys } = useAppContext() const appInfoActions = useAppInfoActions({ resetKey: appId }) const { appDetail, setAppDetail, setAppSidebarExpand } = useStore(useShallow(state => ({ appDetail: state.appDetail, @@ -64,6 +65,9 @@ const AppDetailLayout: FC = (props) => { selectedIcon: NavIcon }>>([]) + const canAccessMonitor = hasPermission(workspacePermissionKeys, 'app.monitor.access') + const canAccessLog = hasPermission(workspacePermissionKeys, 'app.log.access') + const getNavigationConfig = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: AppModeEnum) => { const navConfig = [ ...(isCurrentWorkspaceEditor @@ -81,7 +85,7 @@ const AppDetailLayout: FC = (props) => { icon: RiTerminalBoxLine, selectedIcon: RiTerminalBoxFill, }, - ...(isCurrentWorkspaceEditor + ...(canAccessLog ? [{ name: mode !== AppModeEnum.WORKFLOW ? t('appMenus.logAndAnn', { ns: 'common' }) @@ -92,12 +96,14 @@ const AppDetailLayout: FC = (props) => { }] : [] ), - { - name: t('appMenus.overview', { ns: 'common' }), - href: `/app/${appId}/overview`, - icon: RiDashboard2Line, - selectedIcon: RiDashboard2Fill, - }, + ...(canAccessMonitor + ? [{ + name: t('appMenus.overview', { ns: 'common' }), + href: `/app/${appId}/overview`, + icon: RiDashboard2Line, + selectedIcon: RiDashboard2Fill, + }] + : []), ...(isCurrentWorkspaceEditor ? [{ name: 'Access Config', diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx index 35fafb2a79..72d894c8e0 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx @@ -4,21 +4,29 @@ import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangS import type { TracingStatus } from '@/models/app' import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' -import { - RiArrowDownDoubleLine, - RiEqualizer2Line, -} from '@remixicon/react' import { useBoolean } from 'ahooks' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' -import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing' +import { + AliyunIcon, + ArizeIcon, + DatabricksIcon, + LangfuseIcon, + LangsmithIcon, + MlflowIcon, + OpikIcon, + PhoenixIcon, + TencentIcon, + WeaveIcon, +} from '@/app/components/base/icons/src/public/tracing' import Loading from '@/app/components/base/loading' import Indicator from '@/app/components/header/indicator' -import { useAppContext } from '@/context/app-context' +import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { usePathname } from '@/next/navigation' import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps' +import { hasPermission } from '@/utils/permission' import ConfigButton from './config-button' import TracingIcon from './tracing-icon' import { TracingProvider } from './type' @@ -30,8 +38,9 @@ const Panel: FC = () => { const pathname = usePathname() const matched = /\/app\/([^/]+)/.exec(pathname) const appId = (matched?.length && matched[1]) ? matched[1] : '' - const { isCurrentWorkspaceEditor } = useAppContext() - const readOnly = !isCurrentWorkspaceEditor + const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) + const canConfigTracing = hasPermission(workspacePermissionKeys, 'app.monitor.tracking_config') + const readOnly = !canConfigTracing const [isLoaded, { setTrue: setLoaded, @@ -253,11 +262,11 @@ const Panel: FC = () => {
{t(`${I18N_PREFIX}.title`, { ns: 'app' })}
- +
- +
@@ -297,7 +306,7 @@ const Panel: FC = () => { {InUseProviderIcon && }
- +
diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index 10f7a18111..68ca878cc5 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -37,7 +37,7 @@ import AppIcon from '@/app/components/base/app-icon' import Input from '@/app/components/base/input' import { UserAvatarList } from '@/app/components/base/user-avatar-list' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' -import { useAppContext } from '@/context/app-context' +import { useAppContext, useSelector } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { AppCardTags } from '@/features/tag-management/components/app-card-tags' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' @@ -53,6 +53,7 @@ import { fetchWorkflowDraft } from '@/service/workflow' import { AppModeEnum } from '@/types/app' import { getRedirection } from '@/utils/app-redirection' import { downloadBlob } from '@/utils/download' +import { hasPermission } from '@/utils/permission' import { formatTime } from '@/utils/time' import { basePath } from '@/utils/var' @@ -107,6 +108,8 @@ const AppCardOperationsMenu: React.FC = ({ const { t } = useTranslation() const openAsyncWindow = useAsyncWindowOpen() const { push } = useRouter() + const workspacePermissionKeys = useSelector(state => state.workspacePermissionKeys) + const canManageAccessConfig = hasPermission(workspacePermissionKeys, 'app.access_config') const handleMenuAction = useCallback((e: React.MouseEvent, action: () => void) => { e.stopPropagation() @@ -177,9 +180,11 @@ const AppCardOperationsMenu: React.FC = ({ )} - - Access Config - + {canManageAccessConfig && ( + + Access Config + + )} e.preventDefault() getRedirection(isCurrentWorkspaceEditor, app, push) }} - className="group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border border-solid border-components-card-border bg-components-card-bg shadow-sm transition-shadow duration-200 ease-in-out hover:shadow-lg" + className="group relative col-span-1 inline-flex h-40 cursor-pointer flex-col rounded-xl border border-solid border-components-card-border bg-components-card-bg shadow-sm transition-shadow duration-200 ease-in-out hover:shadow-lg" > -
+
{app.name}
-
+
{app.author_name}
ยท
{EditTimeText}
@@ -502,7 +507,7 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
-
+
{app.description}
-
+
+
{ + e.stopPropagation() + e.preventDefault() + }} + > +
+ +
+
{isCurrentWorkspaceEditor && ( - <> -
{ - e.stopPropagation() - e.preventDefault() - }} - > -
- -
-
-
-
- - { - e.stopPropagation() - e.preventDefault() - }} - > -
- {t('operation.more', { ns: 'common' })} - -
-
- - {systemFeatures.webapp_auth.enabled - ? ( - - ) - : ( - - )} - -
-
- +
+
+ + { + e.stopPropagation() + e.preventDefault() + }} + > +
+ {t('operation.more', { ns: 'common' })} + +
+
+ + {systemFeatures.webapp_auth.enabled + ? ( + + ) + : ( + + )} + +
+
)}
diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index e2e8e737fc..e330f2f2d1 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -18,6 +18,7 @@ import dynamic from '@/next/dynamic' import { consoleQuery } from '@/service/client' import { systemFeaturesQueryOptions } from '@/service/system-features' import { AppModeEnum } from '@/types/app' +import { hasPermission } from '@/utils/permission' import AppCard from './app-card' import { AppCardSkeleton } from './app-card-skeleton' import { APP_LIST_SEARCH_DEBOUNCE_MS } from './constants' @@ -43,7 +44,9 @@ const List: FC = ({ }) => { const { t } = useTranslation() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) - const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace } = useAppContext() + const { isLoadingCurrentWorkspace, workspacePermissionKeys } = useAppContext() + const canCreateApp = hasPermission(workspacePermissionKeys, 'app.create') + const canAccessAppList = hasPermission(workspacePermissionKeys, 'app_library.access') // eslint-disable-next-line react/use-state -- custom URL query hook, not React.useState const { @@ -68,7 +71,7 @@ const List: FC = ({ const { dragging } = useDSLDragDrop({ onDSLFileDropped: handleDSLFileDropped, containerRef, - enabled: isCurrentWorkspaceEditor, + enabled: canCreateApp, }) const appListQuery = useMemo(() => ({ @@ -101,7 +104,7 @@ const List: FC = ({ initialPageParam: 1, placeholderData: keepPreviousData, }), - enabled: !isCurrentWorkspaceDatasetOperator, + enabled: canAccessAppList, refetchInterval: systemFeatures.enable_collaboration_mode ? 10000 : false, }) @@ -113,12 +116,12 @@ const List: FC = ({ const anchorRef = useRef(null) const options = [ - { value: 'all', text: t('types.all', { ns: 'app' }), icon: }, - { value: AppModeEnum.WORKFLOW, text: t('types.workflow', { ns: 'app' }), icon: }, - { value: AppModeEnum.ADVANCED_CHAT, text: t('types.advanced', { ns: 'app' }), icon: }, - { value: AppModeEnum.CHAT, text: t('types.chatbot', { ns: 'app' }), icon: }, - { value: AppModeEnum.AGENT_CHAT, text: t('types.agent', { ns: 'app' }), icon: }, - { value: AppModeEnum.COMPLETION, text: t('types.completion', { ns: 'app' }), icon: }, + { value: 'all', text: t('types.all', { ns: 'app' }), icon: }, + { value: AppModeEnum.WORKFLOW, text: t('types.workflow', { ns: 'app' }), icon: }, + { value: AppModeEnum.ADVANCED_CHAT, text: t('types.advanced', { ns: 'app' }), icon: }, + { value: AppModeEnum.CHAT, text: t('types.chatbot', { ns: 'app' }), icon: }, + { value: AppModeEnum.AGENT_CHAT, text: t('types.agent', { ns: 'app' }), icon: }, + { value: AppModeEnum.COMPLETION, text: t('types.completion', { ns: 'app' }), icon: }, ] useEffect(() => { @@ -129,7 +132,7 @@ const List: FC = ({ }, [refetch]) useEffect(() => { - if (isCurrentWorkspaceDatasetOperator) + if (!canAccessAppList) return const hasMore = hasNextPage ?? true let observer: IntersectionObserver | undefined @@ -156,7 +159,7 @@ const List: FC = ({ observer.observe(anchorRef.current) } return () => observer?.disconnect() - }, [isLoading, isFetchingNextPage, fetchNextPage, error, hasNextPage, isCurrentWorkspaceDatasetOperator]) + }, [isLoading, isFetchingNextPage, fetchNextPage, error, hasNextPage, canAccessAppList]) const handleCreatedByMeChange = useCallback(() => { setIsCreatedByMe(!isCreatedByMe) @@ -225,7 +228,7 @@ const List: FC = ({ !hasAnyApp && 'overflow-hidden', )} > - {(isCurrentWorkspaceEditor || isLoadingCurrentWorkspace) && ( + {(canCreateApp || isLoadingCurrentWorkspace) && ( = ({ )}
- {isCurrentWorkspaceEditor && ( + {canCreateApp && (
ctx.controlHideCreateFromTemplatePanel) useEffect(() => { if (controlHideCreateFromTemplatePanel > 0) - // eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect setShowNewAppTemplateDialog(false) }, [controlHideCreateFromTemplatePanel]) @@ -68,27 +66,27 @@ const CreateAppCard = ({
-
{t('createApp', { ns: 'app' })}
- -
diff --git a/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx b/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx index d17bbb3f40..4f225b6535 100644 --- a/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx +++ b/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx @@ -6,6 +6,8 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import * as React from 'react' +import { useSelector as useAppContextWithSelector } from '@/context/app-context' +import { hasPermission } from '@/utils/permission' import Operations from '../operations' type OperationsDropdownProps = { @@ -26,6 +28,8 @@ const OperationsDropdown = ({ openAccessConfig, }: OperationsDropdownProps) => { const [open, setOpen] = React.useState(false) + const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) + const canManageAccessConfig = hasPermission(workspacePermissionKeys, 'dataset.access_config') return (
diff --git a/web/app/components/datasets/list/dataset-card/operations.tsx b/web/app/components/datasets/list/dataset-card/operations.tsx index 443e98b2ea..5d10cad659 100644 --- a/web/app/components/datasets/list/dataset-card/operations.tsx +++ b/web/app/components/datasets/list/dataset-card/operations.tsx @@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next' type OperationsProps = { showDelete: boolean showExportPipeline: boolean + showAccessConfig?: boolean openRenameModal: () => void handleExportPipeline: () => void detectIsUsedByApp: () => void @@ -18,6 +19,7 @@ type OperationsProps = { const Operations = ({ showDelete, showExportPipeline, + showAccessConfig = false, openRenameModal, handleExportPipeline, detectIsUsedByApp, @@ -58,10 +60,12 @@ const Operations = ({ {t('operations.exportPipeline', { ns: 'datasetPipeline' })} )} - - - Access Config - + {showAccessConfig && ( + + + Access Config + + )} {showDelete && ( <> diff --git a/web/app/components/datasets/list/datasets.tsx b/web/app/components/datasets/list/datasets.tsx index 1e5c268636..d2831a11b8 100644 --- a/web/app/components/datasets/list/datasets.tsx +++ b/web/app/components/datasets/list/datasets.tsx @@ -3,7 +3,6 @@ import { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' -import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { useDatasetList, useInvalidDatasetList } from '@/service/knowledge/use-dataset' import DatasetCard from './dataset-card' import NewDatasetCard from './new-dataset-card' @@ -22,7 +21,6 @@ const Datasets = ({ onOpenTagManagement = () => {}, }: Props) => { const { t } = useTranslation() - const isCurrentWorkspaceEditor = useAppContextWithSelector(state => state.isCurrentWorkspaceEditor) const { data: datasetList, fetchNextPage, @@ -60,7 +58,7 @@ const Datasets = ({ return ( <>