mirror of https://github.com/langgenius/dify.git
feat: implement trigger card component with auto-refresh (#24743)
This commit is contained in:
parent
d94e54923f
commit
9789bd02d8
|
|
@ -12,7 +12,7 @@ trigger_fields = {
|
|||
"updated_at": fields.DateTime(dt_format="iso8601"),
|
||||
}
|
||||
|
||||
triggers_list_fields = {"triggers": fields.List(fields.Nested(trigger_fields))}
|
||||
triggers_list_fields = {"data": fields.List(fields.Nested(trigger_fields))}
|
||||
|
||||
|
||||
webhook_trigger_fields = {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useContext } from 'use-context-selector'
|
|||
import AppCard from '@/app/components/app/overview/app-card'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
||||
import TriggerCard from '@/app/components/app/overview/trigger-card'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import {
|
||||
fetchAppDetail,
|
||||
|
|
@ -33,6 +34,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
|
||||
const showMCPCard = isInPanel
|
||||
const showTriggerCard = isInPanel && appDetail?.mode === 'workflow'
|
||||
|
||||
const updateAppDetail = async () => {
|
||||
try {
|
||||
|
|
@ -125,6 +127,11 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||
appInfo={appDetail}
|
||||
/>
|
||||
)}
|
||||
{showTriggerCard && (
|
||||
<TriggerCard
|
||||
appInfo={appDetail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import { Schedule, TriggerAll, WebhookLine } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import type { AppSSO } from '@/types/app'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import {
|
||||
type AppTrigger,
|
||||
useAppTriggers,
|
||||
useInvalidateAppTriggers,
|
||||
useUpdateTriggerStatus,
|
||||
} from '@/service/use-tools'
|
||||
|
||||
export type ITriggerCardProps = {
|
||||
appInfo: AppDetailResponse & Partial<AppSSO>
|
||||
}
|
||||
|
||||
const getTriggerIcon = (trigger: AppTrigger) => {
|
||||
const { trigger_type, icon, status } = trigger
|
||||
|
||||
// Status dot styling based on trigger status
|
||||
const getStatusDot = () => {
|
||||
if (status === 'enabled') {
|
||||
return (
|
||||
<div className="absolute -left-0.5 -top-0.5 h-1.5 w-1.5 rounded-sm border border-black/15 bg-green-500" />
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div className="absolute -left-0.5 -top-0.5 h-1.5 w-1.5 rounded-sm border border-components-badge-status-light-disabled-border-inner bg-components-badge-status-light-disabled-bg shadow-status-indicator-gray-shadow" />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const baseIconClasses = 'relative flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-white/2 shadow-xs'
|
||||
|
||||
switch (trigger_type) {
|
||||
case 'trigger-webhook':
|
||||
return (
|
||||
<div className={`${baseIconClasses} bg-util-colors-blue-blue-500 text-white`}>
|
||||
<WebhookLine className="h-4 w-4" />
|
||||
{getStatusDot()}
|
||||
</div>
|
||||
)
|
||||
case 'trigger-schedule':
|
||||
return (
|
||||
<div className={`${baseIconClasses} bg-util-colors-violet-violet-500 text-white`}>
|
||||
<Schedule className="h-4 w-4" />
|
||||
{getStatusDot()}
|
||||
</div>
|
||||
)
|
||||
case 'trigger-plugin':
|
||||
return (
|
||||
<div className={`${baseIconClasses} bg-util-colors-white-white-500`}>
|
||||
{icon ? (
|
||||
<div
|
||||
className="h-full w-full shrink-0 rounded-md bg-cover bg-center"
|
||||
style={{ backgroundImage: `url(${icon})` }}
|
||||
/>
|
||||
) : (
|
||||
<WebhookLine className="h-4 w-4 text-text-secondary" />
|
||||
)}
|
||||
{getStatusDot()}
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<div className={`${baseIconClasses} bg-util-colors-blue-blue-500 text-white`}>
|
||||
<WebhookLine className="h-4 w-4" />
|
||||
{getStatusDot()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function TriggerCard({ appInfo }: ITriggerCardProps) {
|
||||
const { t } = useTranslation()
|
||||
const appId = appInfo.id
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { data: triggersResponse, isLoading } = useAppTriggers(appId)
|
||||
const { mutateAsync: updateTriggerStatus } = useUpdateTriggerStatus()
|
||||
const invalidateAppTriggers = useInvalidateAppTriggers()
|
||||
|
||||
const triggers = triggersResponse?.data || []
|
||||
const triggerCount = triggers.length
|
||||
|
||||
const onToggleTrigger = async (trigger: AppTrigger, enabled: boolean) => {
|
||||
try {
|
||||
await updateTriggerStatus({
|
||||
appId,
|
||||
triggerId: trigger.id,
|
||||
enableTrigger: enabled,
|
||||
})
|
||||
invalidateAppTriggers(appId)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to update trigger status:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLearnMoreClick = () => {
|
||||
console.log('Learn about Triggers clicked')
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight">
|
||||
<div className="rounded-xl bg-background-default">
|
||||
<div className="flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3">
|
||||
<div className="h-6 w-full animate-pulse rounded bg-components-input-bg-normal"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight">
|
||||
<div className="rounded-xl bg-background-default">
|
||||
<div className="flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3">
|
||||
<div className="flex w-full items-center gap-3 self-stretch">
|
||||
<div className="flex grow items-center">
|
||||
<div className="mr-2 shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-purple-purple-500 p-1 shadow-md">
|
||||
<TriggerAll className="h-4 w-4 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<div className="group w-full">
|
||||
<div className="system-md-semibold min-w-0 overflow-hidden text-ellipsis break-normal text-text-secondary group-hover:text-text-primary">
|
||||
{triggerCount > 0
|
||||
? t('appOverview.overview.triggerInfo.triggersAdded', { count: triggerCount })
|
||||
: t('appOverview.overview.triggerInfo.noTriggerAdded')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{triggerCount > 0 && (
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
{triggers.map((trigger, index) => (
|
||||
<div key={trigger.id}>
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<div className="flex grow items-center gap-2">
|
||||
{getTriggerIcon(trigger)}
|
||||
<div className="system-sm-medium text-text-secondary">
|
||||
{trigger.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Indicator color={trigger.status === 'enabled' ? 'green' : 'yellow'} />
|
||||
<div className={`${trigger.status === 'enabled' ? 'text-text-success' : 'text-text-warning'} system-xs-semibold-uppercase`}>
|
||||
{trigger.status === 'enabled'
|
||||
? t('appOverview.overview.status.running')
|
||||
: t('appOverview.overview.status.disable')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
defaultValue={trigger.status === 'enabled'}
|
||||
onChange={enabled => onToggleTrigger(trigger, enabled)}
|
||||
disabled={!isCurrentWorkspaceEditor}
|
||||
/>
|
||||
</div>
|
||||
{index < triggers.length - 1 && (
|
||||
<Divider className="my-2" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{triggerCount === 0 && (
|
||||
<div className="p-3">
|
||||
<div className="system-xs-regular leading-4 text-text-tertiary">
|
||||
{t('appOverview.overview.triggerInfo.triggerStatusDescription')}{' '}
|
||||
<Link
|
||||
href="#"
|
||||
onClick={handleLearnMoreClick}
|
||||
className="text-text-accent hover:underline"
|
||||
>
|
||||
{t('appOverview.overview.triggerInfo.learnAboutTriggers')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TriggerCard
|
||||
|
|
@ -142,7 +142,7 @@ function MCPServiceCard({
|
|||
<div className='flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3'>
|
||||
<div className='flex w-full items-center gap-3 self-stretch'>
|
||||
<div className='flex grow items-center'>
|
||||
<div className='mr-2 shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-indigo-indigo-500 p-1 shadow-md'>
|
||||
<div className='mr-2 shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md'>
|
||||
<Mcp className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<div className="group w-full">
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
} from '@/app/components/workflow/types'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useInvalidateAppWorkflow, usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
import { useInvalidateAppTriggers } from '@/service/use-tools'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
|
@ -67,6 +68,7 @@ const AppPublisherTrigger = () => {
|
|||
const { notify } = useToastContext()
|
||||
|
||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory(appDetail!.id)
|
||||
const invalidateAppTriggers = useInvalidateAppTriggers()
|
||||
|
||||
const updateAppDetail = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -102,6 +104,7 @@ const AppPublisherTrigger = () => {
|
|||
notify({ type: 'success', message: t('common.api.actionSuccess') })
|
||||
updatePublishedWorkflow(appID!)
|
||||
updateAppDetail()
|
||||
invalidateAppTriggers(appID!)
|
||||
workflowStore.getState().setPublishedAt(res.created_at)
|
||||
resetWorkflowVersionHistory()
|
||||
}
|
||||
|
|
@ -109,7 +112,7 @@ const AppPublisherTrigger = () => {
|
|||
else {
|
||||
throw new Error('Checklist failed')
|
||||
}
|
||||
}, [needWarningNodes, handleCheckBeforePublish, publishWorkflow, notify, t, updatePublishedWorkflow, appID, updateAppDetail, workflowStore, resetWorkflowVersionHistory])
|
||||
}, [needWarningNodes, handleCheckBeforePublish, publishWorkflow, notify, t, updatePublishedWorkflow, appID, updateAppDetail, invalidateAppTriggers, workflowStore, resetWorkflowVersionHistory])
|
||||
|
||||
const onPublisherToggle = useCallback((state: boolean) => {
|
||||
if (state)
|
||||
|
|
|
|||
|
|
@ -122,6 +122,14 @@ const translation = {
|
|||
accessibleAddress: 'Service API Endpoint',
|
||||
doc: 'API Reference',
|
||||
},
|
||||
triggerInfo: {
|
||||
title: 'Triggers',
|
||||
explanation: 'Workflow trigger management',
|
||||
triggersAdded: '{{count}} Triggers added',
|
||||
noTriggerAdded: 'No trigger added',
|
||||
triggerStatusDescription: 'Trigger node status appears here.',
|
||||
learnAboutTriggers: 'Learn about Triggers',
|
||||
},
|
||||
status: {
|
||||
running: 'In Service',
|
||||
disable: 'Disabled',
|
||||
|
|
|
|||
|
|
@ -122,6 +122,14 @@ const translation = {
|
|||
accessibleAddress: 'サービス API エンドポイント',
|
||||
doc: 'API リファレンス',
|
||||
},
|
||||
triggerInfo: {
|
||||
title: 'トリガー',
|
||||
explanation: 'ワークフロートリガー管理',
|
||||
triggersAdded: '{{count}} 個のトリガーが追加されました',
|
||||
noTriggerAdded: 'トリガーが追加されていません',
|
||||
triggerStatusDescription: 'トリガーノードの状態がここに表示されます。',
|
||||
learnAboutTriggers: 'トリガーについて学ぶ',
|
||||
},
|
||||
status: {
|
||||
running: '稼働中',
|
||||
disable: '無効',
|
||||
|
|
|
|||
|
|
@ -122,6 +122,14 @@ const translation = {
|
|||
accessibleAddress: 'API 访问凭据',
|
||||
doc: '查阅 API 文档',
|
||||
},
|
||||
triggerInfo: {
|
||||
title: '触发器',
|
||||
explanation: '工作流触发器管理',
|
||||
triggersAdded: '已添加 {{count}} 个触发器',
|
||||
noTriggerAdded: '未添加触发器',
|
||||
triggerStatusDescription: '触发器节点状态显示在这里。',
|
||||
learnAboutTriggers: '了解触发器',
|
||||
},
|
||||
status: {
|
||||
running: '运行中',
|
||||
disable: '已停用',
|
||||
|
|
|
|||
|
|
@ -311,3 +311,52 @@ export const useRemoveProviderCredentials = ({
|
|||
onSuccess,
|
||||
})
|
||||
}
|
||||
|
||||
// App Triggers API hooks
|
||||
export type AppTrigger = {
|
||||
id: string
|
||||
trigger_type: 'trigger-webhook' | 'trigger-schedule' | 'trigger-plugin'
|
||||
title: string
|
||||
node_id: string
|
||||
provider_name: string
|
||||
icon: string
|
||||
status: 'enabled' | 'disabled' | 'unauthorized'
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export const useAppTriggers = (appId: string) => {
|
||||
return useQuery<{ data: AppTrigger[] }>({
|
||||
queryKey: [NAME_SPACE, 'app-triggers', appId],
|
||||
queryFn: () => get<{ data: AppTrigger[] }>(`/apps/${appId}/triggers`),
|
||||
enabled: !!appId,
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidateAppTriggers = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return (appId: string) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [NAME_SPACE, 'app-triggers', appId],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const useUpdateTriggerStatus = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'update-trigger-status'],
|
||||
mutationFn: (payload: {
|
||||
appId: string
|
||||
triggerId: string
|
||||
enableTrigger: boolean
|
||||
}) => {
|
||||
const { appId, triggerId, enableTrigger } = payload
|
||||
return post<AppTrigger>(`/apps/${appId}/trigger-enable`, {
|
||||
body: {
|
||||
trigger_id: triggerId,
|
||||
enable_trigger: enableTrigger,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue