From 9789bd02d8eaf4939083f10790fc73ffae44d921 Mon Sep 17 00:00:00 2001 From: lyzno1 <92089059+lyzno1@users.noreply.github.com> Date: Fri, 29 Aug 2025 11:57:08 +0800 Subject: [PATCH] feat: implement trigger card component with auto-refresh (#24743) --- api/fields/workflow_trigger_fields.py | 2 +- .../[appId]/overview/card-view.tsx | 7 + .../components/app/overview/trigger-card.tsx | 195 ++++++++++++++++++ .../components/tools/mcp/mcp-service-card.tsx | 2 +- .../workflow-header/app-publisher-trigger.tsx | 5 +- web/i18n/en-US/app-overview.ts | 8 + web/i18n/ja-JP/app-overview.ts | 8 + web/i18n/zh-Hans/app-overview.ts | 8 + web/service/use-tools.ts | 49 +++++ 9 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 web/app/components/app/overview/trigger-card.tsx diff --git a/api/fields/workflow_trigger_fields.py b/api/fields/workflow_trigger_fields.py index 665d4589ff..c6d254320f 100644 --- a/api/fields/workflow_trigger_fields.py +++ b/api/fields/workflow_trigger_fields.py @@ -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 = { diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index e58e79918f..e66dde269b 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -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 = ({ 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 = ({ appId, isInPanel, className }) => { appInfo={appDetail} /> )} + {showTriggerCard && ( + + )} ) } diff --git a/web/app/components/app/overview/trigger-card.tsx b/web/app/components/app/overview/trigger-card.tsx new file mode 100644 index 0000000000..63ec2567b1 --- /dev/null +++ b/web/app/components/app/overview/trigger-card.tsx @@ -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 +} + +const getTriggerIcon = (trigger: AppTrigger) => { + const { trigger_type, icon, status } = trigger + + // Status dot styling based on trigger status + const getStatusDot = () => { + if (status === 'enabled') { + return ( +
+ ) + } + else { + return ( +
+ ) + } + } + + 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 ( +
+ + {getStatusDot()} +
+ ) + case 'trigger-schedule': + return ( +
+ + {getStatusDot()} +
+ ) + case 'trigger-plugin': + return ( +
+ {icon ? ( +
+ ) : ( + + )} + {getStatusDot()} +
+ ) + default: + return ( +
+ + {getStatusDot()} +
+ ) + } +} + +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 ( +
+
+
+
+
+
+
+ ) + } + + return ( +
+
+
+
+
+
+ +
+
+
+ {triggerCount > 0 + ? t('appOverview.overview.triggerInfo.triggersAdded', { count: triggerCount }) + : t('appOverview.overview.triggerInfo.noTriggerAdded') + } +
+
+
+
+
+ + {triggerCount > 0 && ( +
+ {triggers.map((trigger, index) => ( +
+
+
+ {getTriggerIcon(trigger)} +
+ {trigger.title} +
+
+
+ +
+ {trigger.status === 'enabled' + ? t('appOverview.overview.status.running') + : t('appOverview.overview.status.disable')} +
+
+ onToggleTrigger(trigger, enabled)} + disabled={!isCurrentWorkspaceEditor} + /> +
+ {index < triggers.length - 1 && ( + + )} +
+ ))} +
+ )} + + {triggerCount === 0 && ( +
+
+ {t('appOverview.overview.triggerInfo.triggerStatusDescription')}{' '} + + {t('appOverview.overview.triggerInfo.learnAboutTriggers')} + +
+
+ )} +
+
+ ) +} + +export default TriggerCard diff --git a/web/app/components/tools/mcp/mcp-service-card.tsx b/web/app/components/tools/mcp/mcp-service-card.tsx index f9ad9f7e48..7d8d7aa1b3 100644 --- a/web/app/components/tools/mcp/mcp-service-card.tsx +++ b/web/app/components/tools/mcp/mcp-service-card.tsx @@ -142,7 +142,7 @@ function MCPServiceCard({
-
+
diff --git a/web/app/components/workflow-app/components/workflow-header/app-publisher-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/app-publisher-trigger.tsx index 395a840f9e..69a3772496 100644 --- a/web/app/components/workflow-app/components/workflow-header/app-publisher-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/app-publisher-trigger.tsx @@ -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) diff --git a/web/i18n/en-US/app-overview.ts b/web/i18n/en-US/app-overview.ts index 078f1a1aea..e2041f533c 100644 --- a/web/i18n/en-US/app-overview.ts +++ b/web/i18n/en-US/app-overview.ts @@ -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', diff --git a/web/i18n/ja-JP/app-overview.ts b/web/i18n/ja-JP/app-overview.ts index 3de41b2049..320dfc9625 100644 --- a/web/i18n/ja-JP/app-overview.ts +++ b/web/i18n/ja-JP/app-overview.ts @@ -122,6 +122,14 @@ const translation = { accessibleAddress: 'サービス API エンドポイント', doc: 'API リファレンス', }, + triggerInfo: { + title: 'トリガー', + explanation: 'ワークフロートリガー管理', + triggersAdded: '{{count}} 個のトリガーが追加されました', + noTriggerAdded: 'トリガーが追加されていません', + triggerStatusDescription: 'トリガーノードの状態がここに表示されます。', + learnAboutTriggers: 'トリガーについて学ぶ', + }, status: { running: '稼働中', disable: '無効', diff --git a/web/i18n/zh-Hans/app-overview.ts b/web/i18n/zh-Hans/app-overview.ts index 16d455bc27..dcc3b994f7 100644 --- a/web/i18n/zh-Hans/app-overview.ts +++ b/web/i18n/zh-Hans/app-overview.ts @@ -122,6 +122,14 @@ const translation = { accessibleAddress: 'API 访问凭据', doc: '查阅 API 文档', }, + triggerInfo: { + title: '触发器', + explanation: '工作流触发器管理', + triggersAdded: '已添加 {{count}} 个触发器', + noTriggerAdded: '未添加触发器', + triggerStatusDescription: '触发器节点状态显示在这里。', + learnAboutTriggers: '了解触发器', + }, status: { running: '运行中', disable: '已停用', diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts index 4db6039ed4..ab094da290 100644 --- a/web/service/use-tools.ts +++ b/web/service/use-tools.ts @@ -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(`/apps/${appId}/trigger-enable`, { + body: { + trigger_id: triggerId, + enable_trigger: enableTrigger, + }, + }) + }, + }) +}