Pārlūkot izejas kodu

feat: add 'Open in Explore' link for each apps on studio (#11402)

kurokobo 4 mēneši atpakaļ
vecāks
revīzija
230fa3286b

+ 11 - 1
api/controllers/console/explore/installed_app.py

@@ -1,5 +1,6 @@
 from datetime import UTC, datetime
 
+from flask import request
 from flask_login import current_user
 from flask_restful import Resource, inputs, marshal_with, reqparse
 from sqlalchemy import and_
@@ -20,8 +21,17 @@ class InstalledAppsListApi(Resource):
     @account_initialization_required
     @marshal_with(installed_app_list_fields)
     def get(self):
+        app_id = request.args.get("app_id", default=None, type=str)
         current_tenant_id = current_user.current_tenant_id
-        installed_apps = db.session.query(InstalledApp).filter(InstalledApp.tenant_id == current_tenant_id).all()
+
+        if app_id:
+            installed_apps = (
+                db.session.query(InstalledApp)
+                .filter(and_(InstalledApp.tenant_id == current_tenant_id, InstalledApp.app_id == app_id))
+                .all()
+            )
+        else:
+            installed_apps = db.session.query(InstalledApp).filter(InstalledApp.tenant_id == current_tenant_id).all()
 
         current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
         installed_apps = [

+ 24 - 3
web/app/(commonLayout)/apps/AppCard.tsx

@@ -9,6 +9,7 @@ import s from './style.module.css'
 import cn from '@/utils/classnames'
 import type { App } from '@/types/app'
 import Confirm from '@/app/components/base/confirm'
+import Toast from '@/app/components/base/toast'
 import { ToastContext } from '@/app/components/base/toast'
 import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
 import DuplicateAppModal from '@/app/components/app/duplicate-modal'
@@ -31,6 +32,7 @@ import TagSelector from '@/app/components/base/tag-management/selector'
 import type { EnvironmentVariable } from '@/app/components/workflow/types'
 import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
 import { fetchWorkflowDraft } from '@/service/workflow'
+import { fetchInstalledAppList } from '@/service/explore'
 
 export type AppCardProps = {
   app: App
@@ -209,6 +211,21 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
       e.preventDefault()
       setShowConfirmDelete(true)
     }
+    const onClickInstalledApp = async (e: React.MouseEvent<HTMLButtonElement>) => {
+      e.stopPropagation()
+      props.onClick?.()
+      e.preventDefault()
+      try {
+        const { installed_apps }: any = await fetchInstalledAppList(app.id) || {}
+        if (installed_apps?.length > 0)
+          window.open(`/explore/installed/${installed_apps[0].id}`, '_blank')
+        else
+          throw new Error('No app found in Explore')
+      }
+      catch (e: any) {
+        Toast.notify({ type: 'error', message: `${e.message || e}` })
+      }
+    }
     return (
       <div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
         <button className={s.actionItem} onClick={onClickSettings}>
@@ -233,6 +250,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
           </>
         )}
         <Divider className="!my-1" />
+        <button className={s.actionItem} onClick={onClickInstalledApp}>
+          <span className={s.actionName}>{t('app.openInExplore')}</span>
+        </button>
+        <Divider className="!my-1" />
         <div
           className={cn(s.actionItem, s.deleteActionItem, 'group')}
           onClick={onClickDelete}
@@ -353,10 +374,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
                   }
                   popupClassName={
                     (app.mode === 'completion' || app.mode === 'chat')
-                      ? '!w-[238px] translate-x-[-110px]'
-                      : ''
+                      ? '!w-[256px] translate-x-[-224px]'
+                      : '!w-[160px] translate-x-[-128px]'
                   }
-                  className={'!w-[128px] h-fit !z-20'}
+                  className={'h-fit !z-20'}
                 />
               </div>
             </>

+ 25 - 1
web/app/components/app/app-publisher/index.tsx

@@ -5,7 +5,8 @@ import {
 } from 'react'
 import { useTranslation } from 'react-i18next'
 import dayjs from 'dayjs'
-import { RiArrowDownSLine } from '@remixicon/react'
+import { RiArrowDownSLine, RiPlanetLine } from '@remixicon/react'
+import Toast from '../../base/toast'
 import type { ModelAndParameter } from '../configuration/debug/types'
 import SuggestedAction from './suggested-action'
 import PublishWithMultipleModel from './publish-with-multiple-model'
@@ -15,6 +16,7 @@ import {
   PortalToFollowElemContent,
   PortalToFollowElemTrigger,
 } from '@/app/components/base/portal-to-follow-elem'
+import { fetchInstalledAppList } from '@/service/explore'
 import EmbeddedModal from '@/app/components/app/overview/embedded'
 import { useStore as useAppStore } from '@/app/components/app/store'
 import { useGetLanguage } from '@/context/i18n'
@@ -105,6 +107,19 @@ const AppPublisher = ({
       setPublished(false)
   }, [disabled, onToggle, open])
 
+  const handleOpenInExplore = useCallback(async () => {
+    try {
+      const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {}
+      if (installed_apps?.length > 0)
+        window.open(`/explore/installed/${installed_apps[0].id}`, '_blank')
+      else
+        throw new Error('No app found in Explore')
+    }
+    catch (e: any) {
+      Toast.notify({ type: 'error', message: `${e.message || e}` })
+    }
+  }, [appDetail?.id])
+
   const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
 
   return (
@@ -205,6 +220,15 @@ const AppPublisher = ({
                   {t('workflow.common.embedIntoSite')}
                 </SuggestedAction>
               )}
+            <SuggestedAction
+              onClick={() => {
+                handleOpenInExplore()
+              }}
+              disabled={!publishedAt}
+              icon={<RiPlanetLine className='w-4 h-4' />}
+            >
+              {t('workflow.common.openInExplore')}
+            </SuggestedAction>
             <SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
             {appDetail?.mode === 'workflow' && (
               <WorkflowToolConfigureButton

+ 1 - 0
web/i18n/en-US/app.ts

@@ -101,6 +101,7 @@ const translation = {
   switchLabel: 'The app copy to be created',
   removeOriginal: 'Delete the original app',
   switchStart: 'Start switch',
+  openInExplore: 'Open in Explore',
   typeSelector: {
     all: 'ALL Types',
     chatbot: 'Chatbot',

+ 1 - 0
web/i18n/en-US/workflow.ts

@@ -32,6 +32,7 @@ const translation = {
     restore: 'Restore',
     runApp: 'Run App',
     batchRunApp: 'Batch Run App',
+    openInExplore: 'Open in Explore',
     accessAPIReference: 'Access API Reference',
     embedIntoSite: 'Embed Into Site',
     addTitle: 'Add title...',

+ 1 - 0
web/i18n/ja-JP/app.ts

@@ -93,6 +93,7 @@ const translation = {
   switchLabel: '作成されるアプリのコピー',
   removeOriginal: '元のアプリを削除する',
   switchStart: '切り替えを開始する',
+  openInExplore: '"探索" で開く',
   typeSelector: {
     all: 'すべてのタイプ',
     chatbot: 'チャットボット',

+ 1 - 0
web/i18n/ja-JP/workflow.ts

@@ -32,6 +32,7 @@ const translation = {
     restore: '復元',
     runApp: 'アプリを実行',
     batchRunApp: 'バッチでアプリを実行',
+    openInExplore: '"探索" で開く',
     accessAPIReference: 'APIリファレンスにアクセス',
     embedIntoSite: 'サイトに埋め込む',
     addTitle: 'タイトルを追加...',

+ 2 - 2
web/service/explore.ts

@@ -12,8 +12,8 @@ export const fetchAppDetail = (id: string): Promise<any> => {
   return get(`/explore/apps/${id}`)
 }
 
-export const fetchInstalledAppList = () => {
-  return get('/installed-apps')
+export const fetchInstalledAppList = (app_id?: string | null) => {
+  return get(`/installed-apps${app_id ? `?app_id=${app_id}` : ''}`)
 }
 
 export const installApp = (id: string) => {