mirror of https://github.com/langgenius/dify.git
Merge remote-tracking branch 'origin/feat/trigger' into feat/trigger
This commit is contained in:
commit
d101a83be8
|
|
@ -196,7 +196,7 @@ class AsyncWorkflowConfig(BaseSettings):
|
|||
"to avoid this, workflow can be suspended if needed, to achieve"
|
||||
"this, a time-based checker is required, every granularity seconds, "
|
||||
"the checker will check the workflow queue and suspend the workflow",
|
||||
default=1,
|
||||
default=60,
|
||||
ge=1,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from core.workflow.graph_engine.layers.base import GraphEngineLayer
|
||||
from core.workflow.graph_events.base import GraphEngineEvent
|
||||
from core.workflow.graph_events.graph import GraphRunPausedEvent
|
||||
|
||||
|
||||
class SuspendLayer(GraphEngineLayer):
|
||||
|
|
@ -9,7 +10,11 @@ class SuspendLayer(GraphEngineLayer):
|
|||
pass
|
||||
|
||||
def on_event(self, event: GraphEngineEvent):
|
||||
pass
|
||||
"""
|
||||
Handle the paused event, stash runtime state into storage and wait for resume.
|
||||
"""
|
||||
if isinstance(event, GraphRunPausedEvent):
|
||||
pass
|
||||
|
||||
def on_graph_end(self, error: Exception | None):
|
||||
""" """
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from typing import ClassVar
|
|||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler # type: ignore
|
||||
|
||||
from configs import dify_config
|
||||
from core.workflow.graph_engine.entities.commands import CommandType, GraphEngineCommand
|
||||
from core.workflow.graph_engine.layers.base import GraphEngineLayer
|
||||
from core.workflow.graph_events.base import GraphEngineEvent
|
||||
|
|
@ -13,7 +12,7 @@ from services.workflow.scheduler import CFSPlanScheduler, SchedulerCommand
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TimesliceLayer(GraphEngineLayer):
|
||||
class TimeSliceLayer(GraphEngineLayer):
|
||||
"""
|
||||
CFS plan scheduler to control the timeslice of the workflow.
|
||||
"""
|
||||
|
|
@ -25,13 +24,43 @@ class TimesliceLayer(GraphEngineLayer):
|
|||
CFS plan scheduler allows to control the timeslice of the workflow.
|
||||
"""
|
||||
|
||||
if not TimesliceLayer.scheduler.running:
|
||||
TimesliceLayer.scheduler.start()
|
||||
if not TimeSliceLayer.scheduler.running:
|
||||
TimeSliceLayer.scheduler.start()
|
||||
|
||||
super().__init__()
|
||||
self.cfs_plan_scheduler = cfs_plan_scheduler
|
||||
self.stopped = False
|
||||
|
||||
def _checker_job(self, schedule_id: str):
|
||||
"""
|
||||
Check if the workflow need to be suspended.
|
||||
"""
|
||||
try:
|
||||
if self.stopped:
|
||||
self.scheduler.remove_job(schedule_id)
|
||||
return
|
||||
|
||||
if self.cfs_plan_scheduler.can_schedule() == SchedulerCommand.RESOURCE_LIMIT_REACHED:
|
||||
# remove the job
|
||||
self.scheduler.remove_job(schedule_id)
|
||||
|
||||
if not self.command_channel:
|
||||
logger.exception("No command channel to stop the workflow")
|
||||
return
|
||||
|
||||
# send command to pause the workflow
|
||||
self.command_channel.send_command(
|
||||
GraphEngineCommand(
|
||||
command_type=CommandType.PAUSE,
|
||||
payload={
|
||||
"reason": SchedulerCommand.RESOURCE_LIMIT_REACHED,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logger.exception("scheduler error during check if the workflow need to be suspended")
|
||||
|
||||
def on_graph_start(self):
|
||||
"""
|
||||
Start timer to check if the workflow need to be suspended.
|
||||
|
|
@ -39,39 +68,11 @@ class TimesliceLayer(GraphEngineLayer):
|
|||
|
||||
schedule_id = uuid.uuid4().hex
|
||||
|
||||
def runner():
|
||||
"""
|
||||
Whenever the workflow is running, keep checking if we need to suspend it.
|
||||
Otherwise, return directly.
|
||||
"""
|
||||
try:
|
||||
if self.stopped:
|
||||
self.scheduler.remove_job(schedule_id)
|
||||
return
|
||||
|
||||
if self.cfs_plan_scheduler.can_schedule() == SchedulerCommand.RESOURCE_LIMIT_REACHED:
|
||||
# remove the job
|
||||
self.scheduler.remove_job(schedule_id)
|
||||
|
||||
if not self.command_channel:
|
||||
logger.exception("No command channel to stop the workflow")
|
||||
return
|
||||
|
||||
# send command to pause the workflow
|
||||
self.command_channel.send_command(
|
||||
GraphEngineCommand(
|
||||
command_type=CommandType.PAUSE,
|
||||
payload={
|
||||
"reason": SchedulerCommand.RESOURCE_LIMIT_REACHED,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logger.exception("scheduler error during check if the workflow need to be suspended")
|
||||
|
||||
self.scheduler.add_job(
|
||||
runner, "interval", seconds=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY, id=schedule_id
|
||||
lambda: self._checker_job(schedule_id),
|
||||
"interval",
|
||||
seconds=self.cfs_plan_scheduler.plan.granularity,
|
||||
id=schedule_id,
|
||||
)
|
||||
|
||||
def on_event(self, event: GraphEngineEvent):
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from core.workflow.graph_events.graph import GraphRunFailedEvent, GraphRunPaused
|
|||
from models.engine import db
|
||||
from models.enums import WorkflowTriggerStatus
|
||||
from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository
|
||||
from tasks.workflow_cfs_scheduler.cfs_scheduler import TriggerWorkflowCFSPlanEntity
|
||||
from tasks.workflow_cfs_scheduler.cfs_scheduler import AsyncWorkflowCFSPlanEntity
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ class TriggerPostLayer(GraphEngineLayer):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
cfs_plan_scheduler_entity: TriggerWorkflowCFSPlanEntity,
|
||||
cfs_plan_scheduler_entity: AsyncWorkflowCFSPlanEntity,
|
||||
start_time: datetime,
|
||||
trigger_log_id: str,
|
||||
):
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ from celery import shared_task
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from configs import dify_config
|
||||
from core.app.apps.workflow.app_generator import WorkflowAppGenerator
|
||||
from core.app.engine_layers.timeslice_layer import TimesliceLayer
|
||||
from core.app.engine_layers.timeslice_layer import TimeSliceLayer
|
||||
from core.app.engine_layers.trigger_post_layer import TriggerPostLayer
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from extensions.ext_database import db
|
||||
|
|
@ -29,7 +30,7 @@ from services.workflow.entities import (
|
|||
WorkflowScheduleCFSPlanEntity,
|
||||
WorkflowTaskData,
|
||||
)
|
||||
from tasks.workflow_cfs_scheduler.cfs_scheduler import TriggerCFSPlanScheduler, TriggerWorkflowCFSPlanEntity
|
||||
from tasks.workflow_cfs_scheduler.cfs_scheduler import AsyncWorkflowCFSPlanEntity, AsyncWorkflowCFSPlanScheduler
|
||||
from tasks.workflow_cfs_scheduler.entities import AsyncWorkflowQueue
|
||||
|
||||
|
||||
|
|
@ -37,12 +38,14 @@ from tasks.workflow_cfs_scheduler.entities import AsyncWorkflowQueue
|
|||
def execute_workflow_professional(task_data_dict: dict[str, Any]):
|
||||
"""Execute workflow for professional tier with highest priority"""
|
||||
task_data = WorkflowTaskData.model_validate(task_data_dict)
|
||||
cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity(
|
||||
queue=AsyncWorkflowQueue.PROFESSIONAL_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice
|
||||
cfs_plan_scheduler_entity = AsyncWorkflowCFSPlanEntity(
|
||||
queue=AsyncWorkflowQueue.PROFESSIONAL_QUEUE,
|
||||
schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice,
|
||||
granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY,
|
||||
)
|
||||
_execute_workflow_common(
|
||||
task_data,
|
||||
TriggerCFSPlanScheduler(plan=cfs_plan_scheduler_entity),
|
||||
AsyncWorkflowCFSPlanScheduler(plan=cfs_plan_scheduler_entity),
|
||||
cfs_plan_scheduler_entity,
|
||||
)
|
||||
|
||||
|
|
@ -51,12 +54,14 @@ def execute_workflow_professional(task_data_dict: dict[str, Any]):
|
|||
def execute_workflow_team(task_data_dict: dict[str, Any]):
|
||||
"""Execute workflow for team tier"""
|
||||
task_data = WorkflowTaskData.model_validate(task_data_dict)
|
||||
cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity(
|
||||
queue=AsyncWorkflowQueue.TEAM_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice
|
||||
cfs_plan_scheduler_entity = AsyncWorkflowCFSPlanEntity(
|
||||
queue=AsyncWorkflowQueue.TEAM_QUEUE,
|
||||
schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice,
|
||||
granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY,
|
||||
)
|
||||
_execute_workflow_common(
|
||||
task_data,
|
||||
TriggerCFSPlanScheduler(plan=cfs_plan_scheduler_entity),
|
||||
AsyncWorkflowCFSPlanScheduler(plan=cfs_plan_scheduler_entity),
|
||||
cfs_plan_scheduler_entity,
|
||||
)
|
||||
|
||||
|
|
@ -65,20 +70,22 @@ def execute_workflow_team(task_data_dict: dict[str, Any]):
|
|||
def execute_workflow_sandbox(task_data_dict: dict[str, Any]):
|
||||
"""Execute workflow for free tier with lower retry limit"""
|
||||
task_data = WorkflowTaskData.model_validate(task_data_dict)
|
||||
cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity(
|
||||
queue=AsyncWorkflowQueue.SANDBOX_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice
|
||||
cfs_plan_scheduler_entity = AsyncWorkflowCFSPlanEntity(
|
||||
queue=AsyncWorkflowQueue.SANDBOX_QUEUE,
|
||||
schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice,
|
||||
granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY,
|
||||
)
|
||||
_execute_workflow_common(
|
||||
task_data,
|
||||
TriggerCFSPlanScheduler(plan=cfs_plan_scheduler_entity),
|
||||
AsyncWorkflowCFSPlanScheduler(plan=cfs_plan_scheduler_entity),
|
||||
cfs_plan_scheduler_entity,
|
||||
)
|
||||
|
||||
|
||||
def _execute_workflow_common(
|
||||
task_data: WorkflowTaskData,
|
||||
cfs_plan_scheduler: TriggerCFSPlanScheduler,
|
||||
cfs_plan_scheduler_entity: TriggerWorkflowCFSPlanEntity,
|
||||
cfs_plan_scheduler: AsyncWorkflowCFSPlanScheduler,
|
||||
cfs_plan_scheduler_entity: AsyncWorkflowCFSPlanEntity,
|
||||
):
|
||||
"""Execute workflow with common logic and trigger log updates."""
|
||||
|
||||
|
|
@ -140,7 +147,7 @@ def _execute_workflow_common(
|
|||
triggered_from=trigger_data.trigger_type,
|
||||
root_node_id=trigger_data.root_node_id,
|
||||
layers=[
|
||||
TimesliceLayer(cfs_plan_scheduler),
|
||||
TimeSliceLayer(cfs_plan_scheduler),
|
||||
TriggerPostLayer(cfs_plan_scheduler_entity, start_time, trigger_log.id),
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from services.workflow.scheduler import CFSPlanScheduler, SchedulerCommand
|
|||
from tasks.workflow_cfs_scheduler.entities import AsyncWorkflowQueue
|
||||
|
||||
|
||||
class TriggerWorkflowCFSPlanEntity(WorkflowScheduleCFSPlanEntity):
|
||||
class AsyncWorkflowCFSPlanEntity(WorkflowScheduleCFSPlanEntity):
|
||||
"""
|
||||
Trigger workflow CFS plan entity.
|
||||
"""
|
||||
|
|
@ -11,23 +11,22 @@ class TriggerWorkflowCFSPlanEntity(WorkflowScheduleCFSPlanEntity):
|
|||
queue: AsyncWorkflowQueue
|
||||
|
||||
|
||||
class TriggerCFSPlanScheduler(CFSPlanScheduler):
|
||||
class AsyncWorkflowCFSPlanScheduler(CFSPlanScheduler):
|
||||
"""
|
||||
Trigger workflow CFS plan scheduler.
|
||||
"""
|
||||
|
||||
plan: AsyncWorkflowCFSPlanEntity
|
||||
|
||||
def can_schedule(self) -> SchedulerCommand:
|
||||
"""
|
||||
Check if the workflow can be scheduled.
|
||||
"""
|
||||
assert isinstance(self.plan, TriggerWorkflowCFSPlanEntity)
|
||||
|
||||
if self.plan.queue in [AsyncWorkflowQueue.PROFESSIONAL_QUEUE, AsyncWorkflowQueue.TEAM_QUEUE]:
|
||||
"""
|
||||
permitted all paid users to schedule the workflow
|
||||
permitted all paid users to schedule the workflow any time
|
||||
"""
|
||||
return SchedulerCommand.NONE
|
||||
|
||||
# FIXME: avoid the sandbox user's workflow at a running state for ever
|
||||
|
||||
return SchedulerCommand.NONE
|
||||
return SchedulerCommand.RESOURCE_LIMIT_REACHED
|
||||
|
|
|
|||
|
|
@ -25,10 +25,12 @@ import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
|
|||
import { useGetLanguage } from '@/context/i18n'
|
||||
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import PluginList, { type ListProps } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import type { Plugin } from '../../plugins/types'
|
||||
import { PluginCategoryEnum } from '../../plugins/types'
|
||||
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import RAGToolSuggestions from './rag-tool-suggestions'
|
||||
import FeaturedTools from './featured-tools'
|
||||
import Link from 'next/link'
|
||||
|
||||
type AllToolsProps = {
|
||||
|
|
@ -47,6 +49,10 @@ type AllToolsProps = {
|
|||
canChooseMCPTool?: boolean
|
||||
onTagsChange?: Dispatch<SetStateAction<string[]>>
|
||||
isInRAGPipeline?: boolean
|
||||
featuredPlugins?: Plugin[]
|
||||
featuredLoading?: boolean
|
||||
showFeatured?: boolean
|
||||
onFeaturedInstallSuccess?: () => Promise<void> | void
|
||||
}
|
||||
|
||||
const DEFAULT_TAGS: AllToolsProps['tags'] = []
|
||||
|
|
@ -67,6 +73,10 @@ const AllTools = ({
|
|||
canChooseMCPTool,
|
||||
onTagsChange,
|
||||
isInRAGPipeline = false,
|
||||
featuredPlugins = [],
|
||||
featuredLoading = false,
|
||||
showFeatured = false,
|
||||
onFeaturedInstallSuccess,
|
||||
}: AllToolsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
|
|
@ -80,6 +90,16 @@ const AllTools = ({
|
|||
const isMatchingKeywords = (text: string, keywords: string) => {
|
||||
return text.toLowerCase().includes(keywords.toLowerCase())
|
||||
}
|
||||
const allProviders = useMemo(() => [...buildInTools, ...customTools, ...workflowTools, ...mcpTools], [buildInTools, customTools, workflowTools, mcpTools])
|
||||
const providerMap = useMemo(() => {
|
||||
const map = new Map<string, ToolWithProvider>()
|
||||
allProviders.forEach((provider) => {
|
||||
const key = provider.plugin_id || provider.id
|
||||
if (key)
|
||||
map.set(key, provider)
|
||||
})
|
||||
return map
|
||||
}, [allProviders])
|
||||
const tools = useMemo(() => {
|
||||
let mergedTools: ToolWithProvider[] = []
|
||||
if (activeTab === ToolTypeEnum.All)
|
||||
|
|
@ -136,6 +156,7 @@ const AllTools = ({
|
|||
} = useMarketplacePlugins()
|
||||
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
||||
useEffect(() => {
|
||||
if (!enable_marketplace) return
|
||||
if (hasFilter) {
|
||||
|
|
@ -155,6 +176,12 @@ const AllTools = ({
|
|||
const hasToolsContent = tools.length > 0
|
||||
const hasPluginContent = enable_marketplace && notInstalledPlugins.length > 0
|
||||
const shouldShowEmptyState = hasFilter && !hasToolsContent && !hasPluginContent
|
||||
const shouldShowFeatured = showFeatured
|
||||
&& enable_marketplace
|
||||
&& !isInRAGPipeline
|
||||
&& activeTab === ToolTypeEnum.All
|
||||
&& !hasFilter
|
||||
&& (featuredLoading || featuredPlugins.length > 0)
|
||||
|
||||
return (
|
||||
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
|
||||
|
|
@ -193,6 +220,19 @@ const AllTools = ({
|
|||
onTagsChange={onTagsChange}
|
||||
/>
|
||||
)}
|
||||
{shouldShowFeatured && (
|
||||
<FeaturedTools
|
||||
plugins={featuredPlugins}
|
||||
providerMap={providerMap}
|
||||
onSelect={onSelect}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
isLoading={featuredLoading}
|
||||
onInstallSuccess={async () => {
|
||||
await onFeaturedInstallSuccess?.()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Tools
|
||||
className={toolContentClassName}
|
||||
tools={tools}
|
||||
|
|
|
|||
|
|
@ -92,10 +92,10 @@ const DataSources = ({
|
|||
}, [searchText, enable_marketplace])
|
||||
|
||||
return (
|
||||
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
|
||||
<div className={cn('w-[400px] min-w-0 max-w-full', className)}>
|
||||
<div
|
||||
ref={wrapElemRef}
|
||||
className='max-h-[464px] overflow-y-auto'
|
||||
className='max-h-[464px] overflow-y-auto overflow-x-hidden'
|
||||
onScroll={pluginRef.current?.handleScroll}
|
||||
>
|
||||
<Tools
|
||||
|
|
|
|||
|
|
@ -0,0 +1,231 @@
|
|||
'use client'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum, type ToolWithProvider } from '../types'
|
||||
import type { ToolDefaultValue, ToolValue } from './types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import Button from '@/app/components/base/button'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine, RiLoader2Line, RiMoreLine } from '@remixicon/react'
|
||||
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Link from 'next/link'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import { ToolTypeEnum } from './types'
|
||||
import { ViewType } from './view-type-select'
|
||||
import Tools from './tools'
|
||||
|
||||
const MAX_RECOMMENDED_COUNT = 15
|
||||
const INITIAL_VISIBLE_COUNT = 5
|
||||
|
||||
type FeaturedToolsProps = {
|
||||
plugins: Plugin[]
|
||||
providerMap: Map<string, ToolWithProvider>
|
||||
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
isLoading?: boolean
|
||||
onInstallSuccess?: () => void
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'workflow_tools_featured_collapsed'
|
||||
|
||||
const FeaturedTools = ({
|
||||
plugins,
|
||||
providerMap,
|
||||
onSelect,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
isLoading = false,
|
||||
onInstallSuccess,
|
||||
}: FeaturedToolsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
|
||||
const [installingIdentifier, setInstallingIdentifier] = useState<string | null>(null)
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(false)
|
||||
|
||||
const installMutation = useInstallPackageFromMarketPlace({
|
||||
onSuccess: () => {
|
||||
onInstallSuccess?.()
|
||||
},
|
||||
onSettled: () => {
|
||||
setInstallingIdentifier(null)
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined')
|
||||
return
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
if (stored !== null)
|
||||
setIsCollapsed(stored === 'true')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined')
|
||||
return
|
||||
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
|
||||
}, [isCollapsed])
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleCount(INITIAL_VISIBLE_COUNT)
|
||||
}, [plugins])
|
||||
|
||||
const visiblePlugins = useMemo(
|
||||
() => plugins.slice(0, Math.min(MAX_RECOMMENDED_COUNT, visibleCount)),
|
||||
[plugins, visibleCount],
|
||||
)
|
||||
|
||||
const installedProviders = useMemo(
|
||||
() =>
|
||||
visiblePlugins
|
||||
.map(plugin => providerMap.get(plugin.plugin_id))
|
||||
.filter((provider): provider is ToolWithProvider => Boolean(provider)),
|
||||
[visiblePlugins, providerMap],
|
||||
)
|
||||
|
||||
const uninstalledPlugins = useMemo(
|
||||
() => visiblePlugins.filter(plugin => !providerMap.has(plugin.plugin_id)),
|
||||
[visiblePlugins, providerMap],
|
||||
)
|
||||
|
||||
const showMore = visibleCount < Math.min(MAX_RECOMMENDED_COUNT, plugins.length)
|
||||
const isMutating = installMutation.isPending
|
||||
const showEmptyState = !isLoading && visiblePlugins.length === 0
|
||||
|
||||
return (
|
||||
<div className='px-3 pb-3 pt-2'>
|
||||
<button
|
||||
type='button'
|
||||
className='flex w-full items-center justify-between rounded-md px-0 py-1 text-left text-text-tertiary'
|
||||
onClick={() => setIsCollapsed(prev => !prev)}
|
||||
>
|
||||
<span className='system-xs-medium'>{t('workflow.tabs.featuredTools')}</span>
|
||||
{isCollapsed ? <RiArrowRightSLine className='size-3.5' /> : <RiArrowDownSLine className='size-3.5' />}
|
||||
</button>
|
||||
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
{isLoading && (
|
||||
<div className='py-3'>
|
||||
<Loading type='app' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEmptyState && (
|
||||
<p className='system-xs-regular py-2 text-text-tertiary'>
|
||||
<Link className='text-text-accent' href={getMarketplaceUrl('', { category: 'tool' })} target='_blank' rel='noopener noreferrer'>
|
||||
{t('workflow.tabs.noFeaturedPlugins')}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!showEmptyState && !isLoading && (
|
||||
<>
|
||||
{installedProviders.length > 0 && (
|
||||
<Tools
|
||||
className='p-0'
|
||||
tools={installedProviders}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple
|
||||
toolType={ToolTypeEnum.All}
|
||||
viewType={ViewType.flat}
|
||||
hasSearchText={false}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
)}
|
||||
|
||||
{uninstalledPlugins.length > 0 && (
|
||||
<div className='mt-1 flex flex-col gap-1'>
|
||||
{uninstalledPlugins.map(plugin => (
|
||||
<FeaturedToolUninstalledItem
|
||||
key={plugin.plugin_id}
|
||||
plugin={plugin}
|
||||
language={language}
|
||||
installing={isMutating && installingIdentifier === plugin.latest_package_identifier}
|
||||
onInstall={() => {
|
||||
if (isMutating)
|
||||
return
|
||||
setInstallingIdentifier(plugin.latest_package_identifier)
|
||||
installMutation.mutate(plugin.latest_package_identifier)
|
||||
}}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isLoading && visiblePlugins.length > 0 && showMore && (
|
||||
<div
|
||||
className='mt-1 flex cursor-pointer items-center gap-x-2 py-1 pl-3 pr-2 text-text-tertiary hover:text-text-secondary'
|
||||
onClick={() => {
|
||||
setVisibleCount(count => Math.min(count + INITIAL_VISIBLE_COUNT, MAX_RECOMMENDED_COUNT, plugins.length))
|
||||
}}
|
||||
>
|
||||
<div className='px-1'>
|
||||
<RiMoreLine className='size-4' />
|
||||
</div>
|
||||
<div className='system-xs-regular'>
|
||||
{t('common.operation.more')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type FeaturedToolUninstalledItemProps = {
|
||||
plugin: Plugin
|
||||
language: string
|
||||
installing: boolean
|
||||
onInstall: () => void
|
||||
t: (key: string, options?: Record<string, any>) => string
|
||||
}
|
||||
|
||||
function FeaturedToolUninstalledItem({
|
||||
plugin,
|
||||
language,
|
||||
installing,
|
||||
onInstall,
|
||||
t,
|
||||
}: FeaturedToolUninstalledItemProps) {
|
||||
const label = plugin.label?.[language] || plugin.name
|
||||
const description = typeof plugin.brief === 'object' ? plugin.brief[language] : plugin.brief
|
||||
const installCountLabel = t('plugin.install', { num: plugin.install_count?.toLocaleString() ?? 0 })
|
||||
|
||||
return (
|
||||
<div className='group flex items-center justify-between rounded-lg px-3 py-2 hover:bg-state-base-hover'>
|
||||
<div className='flex min-w-0 items-center gap-3'>
|
||||
<BlockIcon type={BlockEnum.Tool} toolIcon={plugin.icon} />
|
||||
<div className='min-w-0'>
|
||||
<div className='system-sm-medium truncate text-text-primary'>{label}</div>
|
||||
{description && (
|
||||
<div className='system-xs-regular truncate text-text-tertiary'>{description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='ml-3 flex items-center'>
|
||||
<span className='system-xs-regular text-text-tertiary group-hover:hidden'>{installCountLabel}</span>
|
||||
<Button
|
||||
size='small'
|
||||
variant='secondary'
|
||||
className='hidden items-center gap-1 group-hover:flex'
|
||||
disabled={installing}
|
||||
onClick={onInstall}
|
||||
>
|
||||
{installing ? t('workflow.nodes.agent.pluginInstaller.installing') : t('workflow.nodes.agent.pluginInstaller.install')}
|
||||
{installing && <RiLoader2Line className='size-3 animate-spin' />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeaturedTools
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Dispatch, FC, SetStateAction } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools'
|
||||
import type {
|
||||
BlockEnum,
|
||||
NodeDefault,
|
||||
|
|
@ -13,6 +13,8 @@ import AllStartBlocks from './all-start-blocks'
|
|||
import AllTools from './all-tools'
|
||||
import DataSources from './data-sources'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
export type TabsProps = {
|
||||
activeTab: TabsEnum
|
||||
|
|
@ -53,6 +55,13 @@ const Tabs: FC<TabsProps> = ({
|
|||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const invalidateBuiltInTools = useInvalidateAllBuiltInTools()
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const inRAGPipeline = dataSources.length > 0
|
||||
const {
|
||||
plugins: featuredPlugins = [],
|
||||
isLoading: isFeaturedLoading,
|
||||
} = useFeaturedToolsRecommendations(enable_marketplace && !inRAGPipeline)
|
||||
|
||||
return (
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
|
|
@ -127,7 +136,13 @@ const Tabs: FC<TabsProps> = ({
|
|||
mcpTools={mcpTools || []}
|
||||
canChooseMCPTool
|
||||
onTagsChange={onTagsChange}
|
||||
isInRAGPipeline={dataSources.length > 0}
|
||||
isInRAGPipeline={inRAGPipeline}
|
||||
featuredPlugins={featuredPlugins}
|
||||
featuredLoading={isFeaturedLoading}
|
||||
showFeatured={enable_marketplace && !inRAGPipeline}
|
||||
onFeaturedInstallSuccess={async () => {
|
||||
invalidateBuiltInTools()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import {
|
|||
} from '@/service/tools'
|
||||
import type { CustomCollectionBackend } from '@/app/components/tools/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools, useInvalidateAllCustomTools } from '@/service/use-tools'
|
||||
import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -61,11 +63,18 @@ const ToolPicker: FC<Props> = ({
|
|||
const [searchText, setSearchText] = useState('')
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const invalidateCustomTools = useInvalidateAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const invalidateBuiltInTools = useInvalidateAllBuiltInTools()
|
||||
|
||||
const {
|
||||
plugins: featuredPlugins = [],
|
||||
isLoading: isFeaturedLoading,
|
||||
} = useFeaturedToolsRecommendations(enable_marketplace)
|
||||
|
||||
const { builtinToolList, customToolList, workflowToolList } = useMemo(() => {
|
||||
if (scope === 'plugins') {
|
||||
|
|
@ -179,6 +188,12 @@ const ToolPicker: FC<Props> = ({
|
|||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
onTagsChange={setTags}
|
||||
featuredPlugins={featuredPlugins}
|
||||
featuredLoading={isFeaturedLoading}
|
||||
showFeatured={scope === 'all' && enable_marketplace}
|
||||
onFeaturedInstallSuccess={async () => {
|
||||
invalidateBuiltInTools()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'Benutzereingabefeld',
|
||||
helpLink: 'Hilfelink',
|
||||
helpLink: 'Hilfe',
|
||||
about: 'Über',
|
||||
createdBy: 'Erstellt von ',
|
||||
nextStep: 'Nächster Schritt',
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ const translation = {
|
|||
emailSupport: 'Email Support',
|
||||
workspace: 'Workspace',
|
||||
createWorkspace: 'Create Workspace',
|
||||
helpCenter: 'Docs',
|
||||
helpCenter: 'View Docs',
|
||||
support: 'Support',
|
||||
compliance: 'Compliance',
|
||||
communityFeedback: 'Feedback',
|
||||
|
|
|
|||
|
|
@ -277,6 +277,13 @@ const translation = {
|
|||
'addAll': 'Add all',
|
||||
'sources': 'Sources',
|
||||
'searchDataSource': 'Search Data Source',
|
||||
'featuredTools': 'Featured',
|
||||
'showMoreFeatured': 'Show more',
|
||||
'installed': 'Installed',
|
||||
'pluginByAuthor': 'By {{author}}',
|
||||
'usePlugin': 'Select tool',
|
||||
'hideActions': 'Hide tools',
|
||||
'noFeaturedPlugins': 'Discover more tools in Marketplace',
|
||||
},
|
||||
blocks: {
|
||||
'start': 'User Input',
|
||||
|
|
@ -366,7 +373,7 @@ const translation = {
|
|||
panel: {
|
||||
userInputField: 'User Input Field',
|
||||
changeBlock: 'Change Node',
|
||||
helpLink: 'Help Link',
|
||||
helpLink: 'View Docs',
|
||||
about: 'About',
|
||||
createdBy: 'Created By ',
|
||||
nextStep: 'Next Step',
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'Campo de entrada del usuario',
|
||||
helpLink: 'Enlace de ayuda',
|
||||
helpLink: 'Ayuda',
|
||||
about: 'Acerca de',
|
||||
createdBy: 'Creado por ',
|
||||
nextStep: 'Siguiente paso',
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'فیلد ورودی کاربر',
|
||||
helpLink: 'لینک کمک',
|
||||
helpLink: 'راهنما',
|
||||
about: 'درباره',
|
||||
createdBy: 'ساخته شده توسط',
|
||||
nextStep: 'مرحله بعدی',
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'Champ de saisie de l\'utilisateur',
|
||||
helpLink: 'Lien d\'aide',
|
||||
helpLink: 'Aide',
|
||||
about: 'À propos',
|
||||
createdBy: 'Créé par',
|
||||
nextStep: 'Étape suivante',
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'उपयोगकर्ता इनपुट फ़ील्ड',
|
||||
helpLink: 'सहायता लिंक',
|
||||
helpLink: 'सहायता',
|
||||
about: 'के बारे में',
|
||||
createdBy: 'द्वारा बनाया गया ',
|
||||
nextStep: 'अगला कदम',
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ const translation = {
|
|||
userInputField: 'Bidang Input Pengguna',
|
||||
checklistResolved: 'Semua masalah terselesaikan',
|
||||
createdBy: 'Dibuat oleh',
|
||||
helpLink: 'Tautan Bantuan',
|
||||
helpLink: 'Docs',
|
||||
changeBlock: 'Ubah Node',
|
||||
runThisStep: 'Jalankan langkah ini',
|
||||
maximize: 'Maksimalkan Kanvas',
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'Campo di Input Utente',
|
||||
helpLink: 'Link di Aiuto',
|
||||
helpLink: 'Aiuto',
|
||||
about: 'Informazioni',
|
||||
createdBy: 'Creato da ',
|
||||
nextStep: 'Prossimo Passo',
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ const translation = {
|
|||
emailSupport: 'サポート',
|
||||
workspace: 'ワークスペース',
|
||||
createWorkspace: 'ワークスペースを作成',
|
||||
helpCenter: 'ヘルプ',
|
||||
helpCenter: 'ドキュメントを見る',
|
||||
support: 'サポート',
|
||||
compliance: 'コンプライアンス',
|
||||
communityFeedback: 'フィードバック',
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ const translation = {
|
|||
panel: {
|
||||
userInputField: 'ユーザー入力欄',
|
||||
changeBlock: 'ノード変更',
|
||||
helpLink: 'ヘルプリンク',
|
||||
helpLink: 'ドキュメントを見る',
|
||||
about: '詳細',
|
||||
createdBy: '作成者',
|
||||
nextStep: '次のステップ',
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: '사용자 입력 필드',
|
||||
helpLink: '도움말 링크',
|
||||
helpLink: '도움말 센터',
|
||||
about: '정보',
|
||||
createdBy: '작성자 ',
|
||||
nextStep: '다음 단계',
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'Pole wprowadzania użytkownika',
|
||||
helpLink: 'Link do pomocy',
|
||||
helpLink: 'Pomoc',
|
||||
about: 'O',
|
||||
createdBy: 'Stworzone przez ',
|
||||
nextStep: 'Następny krok',
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'Campo de entrada do usuário',
|
||||
helpLink: 'Link de ajuda',
|
||||
helpLink: 'Ajuda',
|
||||
about: 'Sobre',
|
||||
createdBy: 'Criado por ',
|
||||
nextStep: 'Próximo passo',
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'Câmp de introducere utilizator',
|
||||
helpLink: 'Link de ajutor',
|
||||
helpLink: 'Ajutor',
|
||||
about: 'Despre',
|
||||
createdBy: 'Creat de ',
|
||||
nextStep: 'Pasul următor',
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'Поле ввода пользователя',
|
||||
helpLink: 'Ссылка на справку',
|
||||
helpLink: 'Помощь',
|
||||
about: 'О программе',
|
||||
createdBy: 'Создано ',
|
||||
nextStep: 'Следующий шаг',
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ const translation = {
|
|||
addNextStep: 'Dodajte naslednji korak v ta delovni potek',
|
||||
checklistTip: 'Prepričajte se, da so vse težave rešene, preden objavite.',
|
||||
selectNextStep: 'Izberi naslednji korak',
|
||||
helpLink: 'Pomočna povezava',
|
||||
helpLink: 'Pomoč',
|
||||
checklist: 'Kontrolni seznam',
|
||||
checklistResolved: 'Vse težave so rešene',
|
||||
createdBy: 'Ustvarjeno z',
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'ฟิลด์ป้อนข้อมูลของผู้ใช้',
|
||||
helpLink: 'ลิงค์ช่วยเหลือ',
|
||||
helpLink: 'วิธีใช้',
|
||||
about: 'ประมาณ',
|
||||
createdBy: 'สร้างโดย',
|
||||
nextStep: 'ขั้นตอนถัดไป',
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'Kullanıcı Giriş Alanı',
|
||||
helpLink: 'Yardım Linki',
|
||||
helpLink: 'Yardım',
|
||||
about: 'Hakkında',
|
||||
createdBy: 'Oluşturan: ',
|
||||
nextStep: 'Sonraki Adım',
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'Поле введення користувача',
|
||||
helpLink: 'Посилання на допомогу',
|
||||
helpLink: 'Довідковий центр',
|
||||
about: 'Про',
|
||||
createdBy: 'Створено ',
|
||||
nextStep: 'Наступний крок',
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const translation = {
|
|||
},
|
||||
panel: {
|
||||
userInputField: 'Trường đầu vào của người dùng',
|
||||
helpLink: 'Liên kết trợ giúp',
|
||||
helpLink: 'Trung tâm trợ giúp',
|
||||
about: 'Giới thiệu',
|
||||
createdBy: 'Tạo bởi ',
|
||||
nextStep: 'Bước tiếp theo',
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ const translation = {
|
|||
emailSupport: '邮件支持',
|
||||
workspace: '工作空间',
|
||||
createWorkspace: '创建工作空间',
|
||||
helpCenter: '帮助文档',
|
||||
helpCenter: '查看帮助文档',
|
||||
support: '支持',
|
||||
compliance: '合规',
|
||||
communityFeedback: '用户反馈',
|
||||
|
|
|
|||
|
|
@ -263,6 +263,13 @@ const translation = {
|
|||
'sources': '数据源',
|
||||
'searchDataSource': '搜索数据源',
|
||||
'start': '开始',
|
||||
'featuredTools': '精选推荐',
|
||||
'showMoreFeatured': '查看更多',
|
||||
'installed': '已安装',
|
||||
'pluginByAuthor': '来自 {{author}}',
|
||||
'usePlugin': '选择工具',
|
||||
'hideActions': '收起工具',
|
||||
'noFeaturedPlugins': '前往插件市场查看更多工具',
|
||||
},
|
||||
blocks: {
|
||||
'start': '用户输入',
|
||||
|
|
@ -352,7 +359,7 @@ const translation = {
|
|||
panel: {
|
||||
userInputField: '用户输入字段',
|
||||
changeBlock: '更改节点',
|
||||
helpLink: '帮助链接',
|
||||
helpLink: '查看帮助文档',
|
||||
about: '关于',
|
||||
createdBy: '作者',
|
||||
nextStep: '下一步',
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ const translation = {
|
|||
emailSupport: '電子郵件支援',
|
||||
workspace: '工作空間',
|
||||
createWorkspace: '建立工作空間',
|
||||
helpCenter: '幫助文件',
|
||||
helpCenter: '查看幫助文件',
|
||||
communityFeedback: '使用者反饋',
|
||||
roadmap: '路線圖',
|
||||
community: '社群',
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ const translation = {
|
|||
panel: {
|
||||
userInputField: '用戶輸入字段',
|
||||
changeBlock: '更改節點',
|
||||
helpLink: '幫助鏈接',
|
||||
helpLink: '查看幫助文件',
|
||||
about: '關於',
|
||||
createdBy: '作者',
|
||||
nextStep: '下一步',
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import useReferenceSetting from '@/app/components/plugins/plugin-page/use-refere
|
|||
import { uninstallPlugin } from '@/service/plugins'
|
||||
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
|
||||
|
||||
const NAME_SPACE = 'plugins'
|
||||
|
||||
|
|
@ -66,6 +67,50 @@ export const useCheckInstalled = ({
|
|||
})
|
||||
}
|
||||
|
||||
const useRecommendedMarketplacePluginsKey = [NAME_SPACE, 'recommendedMarketplacePlugins']
|
||||
export const useRecommendedMarketplacePlugins = ({
|
||||
category = PluginCategoryEnum.tool,
|
||||
enabled = true,
|
||||
limit = 15,
|
||||
}: {
|
||||
category?: string
|
||||
enabled?: boolean
|
||||
limit?: number
|
||||
} = {}) => {
|
||||
return useQuery<Plugin[]>({
|
||||
queryKey: [...useRecommendedMarketplacePluginsKey, category, limit],
|
||||
queryFn: async () => {
|
||||
const response = await postMarketplace<{ data: { plugins: Plugin[] } }>(
|
||||
'/collections/__recommended-plugins-overall/plugins',
|
||||
{
|
||||
body: {
|
||||
category,
|
||||
limit,
|
||||
},
|
||||
},
|
||||
)
|
||||
return response.data.plugins.map(plugin => getFormattedPlugin(plugin))
|
||||
},
|
||||
enabled,
|
||||
staleTime: 60 * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
export const useFeaturedToolsRecommendations = (enabled: boolean, limit = 15) => {
|
||||
const {
|
||||
data: plugins = [],
|
||||
isLoading,
|
||||
} = useRecommendedMarketplacePlugins({
|
||||
enabled,
|
||||
limit,
|
||||
})
|
||||
|
||||
return {
|
||||
plugins,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
export const useInstalledPluginList = (disable?: boolean, pageSize = 100) => {
|
||||
const fetchPlugins = async ({ pageParam = 1 }) => {
|
||||
const response = await get<InstalledPluginListWithTotalResponse>(
|
||||
|
|
|
|||
Loading…
Reference in New Issue