From 5d2fbf5215806b5afae40704069865d2bd67b336 Mon Sep 17 00:00:00 2001 From: zhsama Date: Mon, 17 Nov 2025 16:23:04 +0800 Subject: [PATCH 01/85] Perf/mutual node UI (#28282) --- .../[appId]/overview/card-view.tsx | 102 ++++++++++++------ web/app/components/app/overview/app-card.tsx | 58 +++++++--- .../components/tools/mcp/mcp-service-card.tsx | 21 +++- web/i18n/en-US/app-overview.ts | 3 + web/i18n/ja-JP/app-overview.ts | 3 + web/i18n/zh-Hans/app-overview.ts | 3 + 6 files changed, 139 insertions(+), 51 deletions(-) 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 57f3ef6881..fb431c5ac8 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import AppCard from '@/app/components/app/overview/app-card' @@ -24,6 +24,7 @@ import { useStore as useAppStore } from '@/app/components/app/store' import { useAppWorkflow } from '@/service/use-workflow' import type { BlockEnum } from '@/app/components/workflow/types' import { isTriggerNode } from '@/app/components/workflow/types' +import { useDocLink } from '@/context/i18n' export type ICardViewProps = { appId: string @@ -33,6 +34,7 @@ export type ICardViewProps = { const CardView: FC = ({ appId, isInPanel, className }) => { const { t } = useTranslation() + const docLink = useDocLink() const { notify } = useContext(ToastContext) const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) @@ -53,6 +55,35 @@ const CardView: FC = ({ appId, isInPanel, className }) => { }) }, [isWorkflowApp, currentWorkflow]) const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false + const disableAppCards = !shouldRenderAppCards + + const triggerDocUrl = docLink('/guides/workflow/node/start') + const buildTriggerModeMessage = useCallback((featureName: string) => ( +
+
+ {t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })} +
+
{ + event.stopPropagation() + window.open(triggerDocUrl, '_blank') + }} + > + {t('appOverview.overview.appInfo.enableTooltip.learnMore')} +
+
+ ), [t, triggerDocUrl]) + + const disableWebAppTooltip = disableAppCards + ? buildTriggerModeMessage(t('appOverview.overview.appInfo.title')) + : null + const disableApiTooltip = disableAppCards + ? buildTriggerModeMessage(t('appOverview.overview.apiInfo.title')) + : null + const disableMcpTooltip = disableAppCards + ? buildTriggerModeMessage(t('tools.mcp.server.title')) + : null const updateAppDetail = async () => { try { @@ -124,39 +155,48 @@ const CardView: FC = ({ appId, isInPanel, className }) => { if (!appDetail) return - return ( -
- { - shouldRenderAppCards && ( - <> - - - {showMCPCard && ( - - )} - - ) - } - {showTriggerCard && ( - + + + {showMCPCard && ( + )} + + ) + + const triggerCardNode = showTriggerCard ? ( + + ) : null + + return ( +
+ {disableAppCards && triggerCardNode} + {appCards} + {!disableAppCards && triggerCardNode}
) } diff --git a/web/app/components/app/overview/app-card.tsx b/web/app/components/app/overview/app-card.tsx index dcb6ae6b4d..a0f5780b71 100644 --- a/web/app/components/app/overview/app-card.tsx +++ b/web/app/components/app/overview/app-card.tsx @@ -51,6 +51,8 @@ export type IAppCardProps = { isInPanel?: boolean cardType?: 'api' | 'webapp' customBgColor?: string + triggerModeDisabled?: boolean // true when Trigger Node mode needs UI locked to avoid conflicting actions + triggerModeMessage?: React.ReactNode // contextual copy explaining why the card is disabled in trigger mode onChangeStatus: (val: boolean) => Promise onSaveSiteConfig?: (params: ConfigParams) => Promise onGenerateCode?: () => Promise @@ -61,6 +63,8 @@ function AppCard({ isInPanel, cardType = 'webapp', customBgColor, + triggerModeDisabled = false, + triggerModeMessage = '', onChangeStatus, onSaveSiteConfig, onGenerateCode, @@ -111,7 +115,7 @@ function AppCard({ const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start) const missingStartNode = isWorkflowApp && !hasStartNode const hasInsufficientPermissions = isApp ? !isCurrentWorkspaceEditor : !isCurrentWorkspaceManager - const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode + const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode || triggerModeDisabled const runningStatus = (appUnpublished || missingStartNode) ? false : (isApp ? appInfo.enable_site : appInfo.enable_api) const isMinimalState = appUnpublished || missingStartNode const { app_base_url, access_token } = appInfo.site ?? {} @@ -189,7 +193,20 @@ function AppCard({ className={ `${isInPanel ? 'border-l-[0.5px] border-t' : 'border-[0.5px] shadow-xs'} w-full max-w-full rounded-xl border-effects-highlight ${className ?? ''} ${isMinimalState ? 'h-12' : ''}`} > -
+
+ {triggerModeDisabled && ( + triggerModeMessage + ? ( + + + + ) + : + )}
-
- {t('appOverview.overview.appInfo.enableTooltip.description')} -
-
window.open(docLink('/guides/workflow/node/user-input'), '_blank')} - > - {t('appOverview.overview.appInfo.enableTooltip.learnMore')} -
- + toggleDisabled ? ( + triggerModeDisabled && triggerModeMessage + ? triggerModeMessage + : (appUnpublished || missingStartNode) ? ( + <> +
+ {t('appOverview.overview.appInfo.enableTooltip.description')} +
+
window.open(docLink('/guides/workflow/node/user-input'), '_blank')} + > + {t('appOverview.overview.appInfo.enableTooltip.learnMore')} +
+ + ) + : '' ) : '' } position="right" @@ -329,9 +351,11 @@ function AppCard({ {!isApp && } {OPERATIONS_MAP[cardType].map((op) => { const disabled - = op.opName === t('appOverview.overview.appInfo.settings.entry') - ? false - : !runningStatus + = triggerModeDisabled + ? true + : op.opName === t('appOverview.overview.appInfo.settings.entry') + ? false + : !runningStatus return (
- ) - } - + ) + } + + {showStartNodeLimitHint && ( +
+

+ {t('workflow.publishLimit.startNodeTitlePrefix')} + {t('workflow.publishLimit.startNodeTitleSuffix')} +

+

+ {t('workflow.publishLimit.startNodeDesc')} +

+ +
+ )} + ) }
diff --git a/web/app/components/billing/config.ts b/web/app/components/billing/config.ts index c0a21c1ebf..f343f4b487 100644 --- a/web/app/components/billing/config.ts +++ b/web/app/components/billing/config.ts @@ -90,4 +90,8 @@ export const defaultPlan = { apiRateLimit: ALL_PLANS.sandbox.apiRateLimit, triggerEvents: ALL_PLANS.sandbox.triggerEvents, }, + reset: { + apiRateLimit: null, + triggerEvents: null, + }, } diff --git a/web/app/components/billing/plan/index.tsx b/web/app/components/billing/plan/index.tsx index 4b68fcfb15..b695302965 100644 --- a/web/app/components/billing/plan/index.tsx +++ b/web/app/components/billing/plan/index.tsx @@ -6,15 +6,16 @@ import { useRouter } from 'next/navigation' import { RiBook2Line, RiFileEditLine, - RiFlashlightLine, RiGraduationCapLine, RiGroupLine, - RiSpeedLine, } from '@remixicon/react' import { Plan, SelfHostedPlan } from '../type' +import { NUM_INFINITE } from '../config' +import { getDaysUntilEndOfMonth } from '@/utils/time' import VectorSpaceInfo from '../usage-info/vector-space-info' import AppsInfo from '../usage-info/apps-info' import UpgradeBtn from '../upgrade-btn' +import { ApiAggregate, TriggerAll } from '@/app/components/base/icons/src/vender/workflow' import { useProviderContext } from '@/context/provider-context' import { useAppContext } from '@/context/app-context' import Button from '@/app/components/base/button' @@ -44,9 +45,20 @@ const PlanComp: FC = ({ const { usage, total, + reset, } = plan - const perMonthUnit = ` ${t('billing.usagePage.perMonth')}` - const triggerEventUnit = plan.type === Plan.sandbox ? undefined : perMonthUnit + const triggerEventsResetInDays = type === Plan.professional && total.triggerEvents !== NUM_INFINITE + ? reset.triggerEvents ?? undefined + : undefined + const apiRateLimitResetInDays = (() => { + if (total.apiRateLimit === NUM_INFINITE) + return undefined + if (typeof reset.apiRateLimit === 'number') + return reset.apiRateLimit + if (type === Plan.sandbox) + return getDaysUntilEndOfMonth() + return undefined + })() const [showModal, setShowModal] = React.useState(false) const { mutateAsync } = useEducationVerify() @@ -79,7 +91,6 @@ const PlanComp: FC = ({
{t(`billing.plans.${type}.name`)}
-
{t('billing.currentPlan')}
{t(`billing.plans.${type}.for`)}
@@ -124,18 +135,20 @@ const PlanComp: FC = ({ total={total.annotatedResponse} />
diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/list/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/list/index.tsx index 0b35ee7e97..7674affc15 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/list/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/list/index.tsx @@ -46,16 +46,10 @@ const List = ({ label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })} tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')} /> - + + - + ) => { + const [visible, setVisible] = useState(args.show ?? true) + useEffect(() => { + setVisible(args.show ?? true) + }, [args.show]) + const handleHide = () => setVisible(false) + return ( + +
+ + +
+
+ ) +} + +const meta = { + title: 'Billing/TriggerEventsLimitModal', + component: TriggerEventsLimitModal, + parameters: { + layout: 'centered', + }, + args: { + show: true, + usage: 120, + total: 120, + resetInDays: 5, + planType: Plan.professional, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Professional: Story = { + args: { + onDismiss: () => { /* noop */ }, + onUpgrade: () => { /* noop */ }, + }, + render: args =>