feat: implement trigger card component with auto-refresh (#24743)

This commit is contained in:
lyzno1 2025-08-29 11:57:08 +08:00 committed by GitHub
parent d94e54923f
commit 9789bd02d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 281 additions and 3 deletions

View File

@ -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 = {

View File

@ -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>
)
}

View File

@ -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

View File

@ -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">

View File

@ -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)

View File

@ -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',

View File

@ -122,6 +122,14 @@ const translation = {
accessibleAddress: 'サービス API エンドポイント',
doc: 'API リファレンス',
},
triggerInfo: {
title: 'トリガー',
explanation: 'ワークフロートリガー管理',
triggersAdded: '{{count}} 個のトリガーが追加されました',
noTriggerAdded: 'トリガーが追加されていません',
triggerStatusDescription: 'トリガーノードの状態がここに表示されます。',
learnAboutTriggers: 'トリガーについて学ぶ',
},
status: {
running: '稼働中',
disable: '無効',

View File

@ -122,6 +122,14 @@ const translation = {
accessibleAddress: 'API 访问凭据',
doc: '查阅 API 文档',
},
triggerInfo: {
title: '触发器',
explanation: '工作流触发器管理',
triggersAdded: '已添加 {{count}} 个触发器',
noTriggerAdded: '未添加触发器',
triggerStatusDescription: '触发器节点状态显示在这里。',
learnAboutTriggers: '了解触发器',
},
status: {
running: '运行中',
disable: '已停用',

View File

@ -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,
},
})
},
})
}