| @@ -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 = [ | |||
| @@ -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> | |||
| </> | |||
| @@ -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 | |||
| @@ -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', | |||
| @@ -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...', | |||
| @@ -93,6 +93,7 @@ const translation = { | |||
| switchLabel: '作成されるアプリのコピー', | |||
| removeOriginal: '元のアプリを削除する', | |||
| switchStart: '切り替えを開始する', | |||
| openInExplore: '"探索" で開く', | |||
| typeSelector: { | |||
| all: 'すべてのタイプ', | |||
| chatbot: 'チャットボット', | |||
| @@ -32,6 +32,7 @@ const translation = { | |||
| restore: '復元', | |||
| runApp: 'アプリを実行', | |||
| batchRunApp: 'バッチでアプリを実行', | |||
| openInExplore: '"探索" で開く', | |||
| accessAPIReference: 'APIリファレンスにアクセス', | |||
| embedIntoSite: 'サイトに埋め込む', | |||
| addTitle: 'タイトルを追加...', | |||
| @@ -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) => { | |||