From 26f4f88ccc4e68f01a149fbb63b3e6911e7345c8 Mon Sep 17 00:00:00 2001 From: NFish Date: Wed, 2 Apr 2025 17:51:34 +0800 Subject: [PATCH] wip: show web app access control menu in app card and app detail --- web/app/(commonLayout)/apps/AppCard.tsx | 27 ++++++++- web/app/components/app-sidebar/app-info.tsx | 16 +++++ .../access-control-dialog.tsx | 59 +++++++++++++++++++ .../app/app-access-control/index.tsx | 31 ++++++++++ web/i18n/en-US/app.ts | 11 ++++ web/i18n/zh-Hans/app.ts | 11 ++++ 6 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 web/app/components/app/app-access-control/access-control-dialog.tsx create mode 100644 web/app/components/app/app-access-control/index.tsx diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index dabe75ee62..12ff4b5a58 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -4,7 +4,7 @@ import { useContext, useContextSelector } from 'use-context-selector' import { useRouter } from 'next/navigation' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { RiMoreFill } from '@remixicon/react' +import { RiGlobalLine, RiMoreFill } from '@remixicon/react' import s from './style.module.css' import cn from '@/utils/classnames' import type { App } from '@/types/app' @@ -31,6 +31,8 @@ import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm- import { fetchWorkflowDraft } from '@/service/workflow' import { fetchInstalledAppList } from '@/service/explore' import { AppTypeIcon } from '@/app/components/app/type-selector' +import Tooltip from '@/app/components/base/tooltip' +import AccessControl from '@/app/components/app/app-access-control' export type AppCardProps = { app: App @@ -53,6 +55,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { const [showDuplicateModal, setShowDuplicateModal] = useState(false) const [showSwitchModal, setShowSwitchModal] = useState(false) const [showConfirmDelete, setShowConfirmDelete] = useState(false) + const [showAccessControl, setShowAccessControl] = useState(true) const [secretEnvList, setSecretEnvList] = useState([]) const onConfirmDelete = useCallback(async () => { @@ -209,6 +212,12 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { e.preventDefault() setShowConfirmDelete(true) } + const onClickAccessControl = async (e: React.MouseEvent) => { + e.stopPropagation() + props.onClick?.() + e.preventDefault() + setShowAccessControl(true) + } const onClickInstalledApp = async (e: React.MouseEvent) => { e.stopPropagation() props.onClick?.() @@ -252,6 +261,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { {t('app.openInExplore')} + +
{ }} className='relative h-[160px] group col-span-1 bg-components-card-bg border-[1px] border-solid border-components-card-border rounded-xl shadow-sm inline-flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg' > -
+
{ {app.mode === 'completion' &&
{t('app.types.completion').toUpperCase()}
}
+
+ + + +
{ popupClassName={ (app.mode === 'completion' || app.mode === 'chat') ? '!w-[256px] translate-x-[-224px]' - : '!w-[160px] translate-x-[-128px]' + : '!w-[216px] translate-x-[-128px]' } className={'h-fit !z-20'} /> @@ -418,6 +436,9 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { onClose={() => setSecretEnvList([])} /> )} + {showAccessControl && ( + setShowAccessControl(false)} /> + )} ) } diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index 12f9c59cd1..5bfe209724 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -5,6 +5,7 @@ import { RiArrowDownSLine } from '@remixicon/react' import React, { useCallback, useState } from 'react' import AppIcon from '../base/app-icon' import SwitchAppModal from '../app/switch-app-modal' +import AccessControl from '../app/app-access-control' import s from './style.module.css' import cn from '@/utils/classnames' import { @@ -50,6 +51,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => { const [showSwitchTip, setShowSwitchTip] = useState('') const [showSwitchModal, setShowSwitchModal] = useState(false) const [showImportDSLModal, setShowImportDSLModal] = useState(false) + const [showAccessControl, setShowAccessControl] = useState(false) const [secretEnvList, setSecretEnvList] = useState([]) const mutateApps = useContextSelector( @@ -177,6 +179,13 @@ const AppInfo = ({ expand }: IAppInfoProps) => { setShowConfirmDelete(false) }, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, t]) + const handleClickAccessControl = useCallback(() => { + if (!appDetail) + return + setShowAccessControl(true) + setOpen(false) + }, [appDetail]) + const { isCurrentWorkspaceEditor } = useAppContext() if (!appDetail) @@ -374,6 +383,10 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
) } + +
+ {t('app.accessControl')} +
{ setOpen(false) @@ -466,6 +479,9 @@ const AppInfo = ({ expand }: IAppInfoProps) => { onClose={() => setSecretEnvList([])} /> )} + { + showAccessControl && + }
) diff --git a/web/app/components/app/app-access-control/access-control-dialog.tsx b/web/app/components/app/app-access-control/access-control-dialog.tsx new file mode 100644 index 0000000000..87d4fd4c33 --- /dev/null +++ b/web/app/components/app/app-access-control/access-control-dialog.tsx @@ -0,0 +1,59 @@ +import { Fragment, useCallback } from 'react' +import type { ReactNode } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { RiCloseLine } from '@remixicon/react' +import cn from '@/utils/classnames' + +type DialogProps = { + className?: string + children: ReactNode + show: boolean + onClose?: () => void +} + +const AccessControlDialog = ({ + className, + children, + show, + onClose, +}: DialogProps) => { + const close = useCallback(() => onClose?.(), [onClose]) + return ( + + + +
+ + +
+ + +
+ +
+ {children} +
+
+
+
+
+ ) +} + +export default AccessControlDialog diff --git a/web/app/components/app/app-access-control/index.tsx b/web/app/components/app/app-access-control/index.tsx new file mode 100644 index 0000000000..71015a0106 --- /dev/null +++ b/web/app/components/app/app-access-control/index.tsx @@ -0,0 +1,31 @@ +'use client' +import { Dialog } from '@headlessui/react' +import { useTranslation } from 'react-i18next' +import Button from '../../base/button' +import AccessControlDialog from './access-control-dialog' + +type AccessControlProps = { + onClose: () => void +} + +export default function AccessControl(props: AccessControlProps) { + const { t } = useTranslation() + return +
+
+ {t('app.accessControlDialog.title')} + {t('app.accessControlDialog.description')} +
+
+
+

{t('app.accessControlDialog.accessLabel')}

+
+ +
+
+ + +
+
+
+} diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index 343b01e9b5..e903c20925 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -174,6 +174,17 @@ const translation = { }, }, showMyCreatedAppsOnly: 'Created by me', + accessControl: 'Webapp Access Control', + accessControlDialog: { + title: 'Access Control', + description: 'Set webapp access permissions', + accessLabel: 'Who has access', + accessItems: { + anyone: 'Anyone with the link', + specific: 'Specific groups or members', + organization: 'Only members within the organization', + }, + }, } export default translation diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index be93a84195..45bd4a5006 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -175,6 +175,17 @@ const translation = { }, openInExplore: '在“探索”中打开', showMyCreatedAppsOnly: '我创建的', + accessControl: 'WebApp 访问控制', + accessControlDialog: { + title: 'WebApp 访问权限', + description: '设置 WebApp 的访问权限', + accessLabel: '配置谁有访问权限', + accessItems: { + anyone: '任何人', + specific: '特定组或用户', + organization: '组织内的任何人', + }, + }, } export default translation