diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index a83c29fa..f7b15bea 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -2373,22 +2373,13 @@ export default { triggers: { kicker: 'Triggers', title: 'Workflow Triggers', - desc: 'Connect cron schedules and channel events to your published workflows. pattern_version increments on every cron expression change so multi-node deployments coordinate correctly.', + desc: 'Connect cron schedules, channel events and webhooks to your published workflows — the workflow runs automatically when the condition is met.', newTrigger: '+ New Trigger', - columns: { - name: 'Name', - pattern: 'Pattern', - target: 'Target Workflow', - rate: 'Rate', - fires: 'Fires', - patternVersion: 'pattern_v', - lastFired: 'Last fired', - state: 'State', - actions: 'Actions', - }, + emptyDesc: 'Triggers run a workflow automatically when a specific event happens — no manual start needed.', + neverFired: 'Never fired', + firedSummary: 'Fired {count}× · last {time}', targetWorkflowSelect: 'Pick a published workflow', targetWorkflowEmpty: 'No published workflows yet.', - lastDispatchedAt: 'Last attempt', patternTypeLabels: { cron: 'Cron schedule', channel_message: 'Channel message', @@ -2440,10 +2431,9 @@ export default { errSubstringRequired: 'Substring cannot be empty or the trigger never fires', }, unnamed: '(unnamed)', - rateUnit: '{count}/min', enabled: 'enabled', disabled: 'disabled', - empty: 'No triggers configured.', + empty: 'No triggers yet', actions: { edit: 'Edit', delete: 'Delete', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 4ca1f6e6..9785285c 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -2385,22 +2385,13 @@ export default { triggers: { kicker: '触发器', title: '工作流触发器', - desc: '把 cron 调度和渠道事件接到已发布的工作流上。每次修改 cron 表达式都会让 pattern_version 自增,多节点部署据此协调。', + desc: '把定时调度、渠道事件和 Webhook 接到已发布的工作流上——条件满足时,工作流自动运行。', newTrigger: '+ 新建触发器', - columns: { - name: '名称', - pattern: '模式', - target: '目标工作流', - rate: '速率', - fires: '触发次数', - patternVersion: 'pattern_v', - lastFired: '上次触发', - state: '状态', - actions: '操作', - }, + emptyDesc: '触发器让工作流在特定事件发生时自动运行,无需手动启动。', + neverFired: '尚未触发', + firedSummary: '已触发 {count} 次 · 最近 {time}', targetWorkflowSelect: '选择已发布的工作流', targetWorkflowEmpty: '尚无已发布的工作流。', - lastDispatchedAt: '最近一次尝试', patternTypeLabels: { cron: '定时任务', channel_message: '渠道消息', @@ -2452,10 +2443,9 @@ export default { errSubstringRequired: '子串不能为空,否则触发器不会激活', }, unnamed: '(未命名)', - rateUnit: '{count}/分钟', enabled: '已启用', disabled: '已停用', - empty: '暂无触发器配置。', + empty: '还没有触发器', actions: { edit: '编辑', delete: '删除', diff --git a/mateclaw-ui/src/views/Triggers.vue b/mateclaw-ui/src/views/Triggers.vue index a0592e6b..f6e012b5 100644 --- a/mateclaw-ui/src/views/Triggers.vue +++ b/mateclaw-ui/src/views/Triggers.vue @@ -11,60 +11,77 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ t('triggers.columns.name') }}{{ t('triggers.columns.pattern') }}{{ t('triggers.columns.target') }}{{ t('triggers.columns.rate') }}{{ t('triggers.columns.fires') }}{{ t('triggers.columns.patternVersion') }}{{ t('triggers.columns.lastFired') }}{{ t('triggers.columns.state') }}{{ t('triggers.columns.actions') }}
-
{{ row.name || t('triggers.unnamed') }}
-
+
+ +

{{ t('triggers.empty') }}

+

{{ t('triggers.emptyDesc') }}

+ +
+ +
+
+ +
+
+ {{ row.name || t('triggers.unnamed') }} + ⚠ {{ truncateError(row.lastError) }} -
-
- {{ patternTypeLabel(row.patternType) }} -
{{ patternSummary(row) }}
-
{{ formatTarget(row) }}{{ t('triggers.rateUnit', { count: row.rateLimitPerMin }) }}{{ row.fireCount }} / {{ row.maxFires }}{{ row.patternVersion }} -
{{ formatTime(row.lastFiredAt) }}
-
- {{ formatTime(row.lastDispatchedAt) }} -
-
- - - - -
{{ t('triggers.empty') }}
+ + +
+ + {{ patternTypeLabel(row.patternType) }} + + + {{ targetName(row) }} +
+
+ {{ patternSummary(row) }} + + {{ activityLine(row) }} +
+ +
+ + + +
+ +
@@ -258,17 +275,30 @@ async function reload() { } } -function formatTarget(row: TriggerSummary) { +function targetName(row: TriggerSummary): string { if (row.targetType === 'workflow') { - const wf = workflows.value.find((w) => w.id === row.targetId) - if (wf?.name) return `${wf.name} (#${row.targetId})` + const wf = workflows.value.find((w) => String(w.id) === String(row.targetId)) + if (wf?.name) return wf.name } - return `${row.targetType}#${row.targetId}` + return `${row.targetType} #${row.targetId}` } -function formatTime(iso?: string) { +// Compact "MM-DD HH:mm" for the activity subline — the full timestamp would +// crowd the card without telling the operator anything they scan for. +function shortTime(iso?: string): string { if (!iso) return '-' - return iso.replace('T', ' ').slice(0, 19) + return iso.slice(5, 16).replace('T', ' ') +} + +// One muted line summarizing operational state: how many times it has fired +// and when. Replaces the former fireCount / lastFired / pattern_version +// columns — those scalars never warranted a column each. +function activityLine(row: TriggerSummary): string { + if (!row.lastFiredAt) return t('triggers.neverFired') + return t('triggers.firedSummary', { + count: row.fireCount, + time: shortTime(row.lastFiredAt), + }) } function patternTypeLabel(pt: string): string { @@ -409,59 +439,77 @@ watch(workspaceId, reload) flex-direction: column; gap: 16px; } -.triggers-table { - width: 100%; - border-collapse: collapse; - padding: 0; - overflow: hidden; -} -.triggers-table th, -.triggers-table td { - padding: 10px 12px; - border-bottom: 1px solid var(--mc-border, rgba(0, 0, 0, 0.06)); - text-align: left; - font-size: 13px; - vertical-align: top; -} -.triggers-table th { - font-weight: 600; - background: var(--mc-bg-sunken, rgba(0, 0, 0, 0.04)); -} -.pattern-detail { - font-family: 'JetBrains Mono', Consolas, monospace; - font-size: 11px; - opacity: 0.7; - margin-top: 2px; -} -.pattern-summary { - font-family: 'JetBrains Mono', Consolas, monospace; - font-size: 11px; - opacity: 0.7; - margin-top: 3px; +.mc-page-header .btn-primary { white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 240px; +} +/* ── Trigger list — one card per rule, reads as "when X → run Y" ── */ +.trigger-list { + display: flex; + flex-direction: column; + gap: 10px; +} +.trigger-card { + display: flex; + align-items: center; + gap: 14px; + padding: 13px 18px; + transition: opacity 0.15s ease, border-color 0.15s ease; +} +.trigger-card.is-disabled { + opacity: 0.6; +} +.trigger-status-dot { + flex: 0 0 auto; + width: 8px; + height: 8px; + border-radius: 50%; +} +.trigger-status-dot.is-on { + background: #22c55e; + box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.16); +} +.trigger-status-dot.is-off { + background: var(--mc-text-tertiary, #b0a89e); +} +.trigger-main { + flex: 1 1 auto; + min-width: 0; + display: flex; + flex-direction: column; + gap: 5px; +} +.trigger-headline { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; } .trigger-name { - font-weight: 500; -} -.trigger-last-error { - font-size: 11px; - color: var(--mc-danger, #c0392b); - margin-top: 2px; + font-weight: 600; + font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - max-width: 240px; +} +.trigger-error-chip { + flex: 0 0 auto; + font-size: 11px; + color: var(--mc-danger, #c0392b); + background: var(--mc-danger-bg, rgba(192, 57, 43, 0.1)); + padding: 1px 8px; + border-radius: 999px; cursor: help; } -.trigger-attempt-time { - font-size: 10px; - opacity: 0.55; - margin-top: 1px; +/* Line 2 — the rule at its purest: "[type] → [workflow]". Both ends are + short, so this never needs to truncate; detail lives on the meta line. */ +.trigger-rule { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; } .trigger-type-pill { + flex: 0 0 auto; display: inline-block; padding: 2px 8px; font-size: 11px; @@ -473,20 +521,132 @@ watch(workspaceId, reload) letter-spacing: 0.02em; } :lang(zh-CN) .trigger-type-pill { text-transform: none; } -.empty-row { - text-align: center; - opacity: 0.6; - padding: 24px; +.trigger-rule-arrow { + flex: 0 0 auto; + color: var(--mc-text-tertiary, #b0a89e); + font-size: 13px; } -.toggle { +.trigger-target { + flex: 0 1 auto; + font-size: 13px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +/* Line 3 — muted detail: pattern summary + activity. Wraps instead of + truncating so neither the schedule nor the fire stats get amputated. */ +.trigger-meta { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 2px 7px; + font-size: 11.5px; + color: var(--mc-text-tertiary, #9a938b); +} +.trigger-meta-summary { + font-family: 'JetBrains Mono', Consolas, monospace; + font-size: 11px; +} +.trigger-meta-sep { + opacity: 0.5; +} +.trigger-controls { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 8px; +} +/* Toggle switch — enabled state is user-controlled, so it earns a switch */ +.trigger-toggle { + flex: 0 0 auto; + position: relative; + width: 38px; + height: 22px; + padding: 0; + border: none; + border-radius: 999px; + background: var(--mc-bg-sunken, rgba(0, 0, 0, 0.16)); + cursor: pointer; + transition: background 0.18s ease; +} +.trigger-toggle.is-on { + background: #22c55e; +} +.trigger-toggle-knob { + position: absolute; + top: 2px; + left: 2px; + width: 18px; + height: 18px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + transition: transform 0.18s ease; +} +.trigger-toggle.is-on .trigger-toggle-knob { + transform: translateX(16px); +} +.row-btn { display: inline-flex; align-items: center; - gap: 6px; - font-size: 12px; + justify-content: center; + width: 30px; + height: 30px; + padding: 0; + border: 1px solid var(--mc-border, rgba(0, 0, 0, 0.12)); + border-radius: 8px; + background: var(--mc-bg-elevated, transparent); + color: var(--mc-text-secondary, inherit); + cursor: pointer; + transition: background 0.14s ease, color 0.14s ease, border-color 0.14s ease; } -.actions { +.row-btn svg { + width: 15px; + height: 15px; +} +.row-btn:hover { + background: var(--mc-bg-sunken, rgba(0, 0, 0, 0.05)); + color: var(--mc-text-primary, inherit); +} +.row-btn.danger:hover { + background: var(--mc-danger-bg, rgba(192, 57, 43, 0.1)); + border-color: var(--mc-danger, #c0392b); + color: var(--mc-danger, #c0392b); +} +/* Empty state — a brand-new page should invite, not show a blank table */ +.trigger-empty { display: flex; - gap: 6px; + flex-direction: column; + align-items: center; + text-align: center; + gap: 10px; + padding: 56px 24px; +} +.trigger-empty-icon { + display: flex; + align-items: center; + justify-content: center; + width: 52px; + height: 52px; + border-radius: 14px; + background: var(--mc-bg-muted, rgba(0, 0, 0, 0.06)); + color: var(--mc-text-tertiary, #9a938b); +} +.trigger-empty-icon svg { + width: 26px; + height: 26px; +} +.trigger-empty-title { + margin: 4px 0 0; + font-size: 15px; + font-weight: 600; +} +.trigger-empty-desc { + margin: 0 0 8px; + font-size: 13px; + color: var(--mc-text-secondary, inherit); + max-width: 380px; } .trigger-modal-overlay { position: fixed; @@ -624,21 +784,11 @@ button:disabled { cursor: not-allowed; } -/* Mobile / tablet: the trigger table has 9 columns and breaks on - phones. Wrap it in a horizontal-scroll container, collapse the - form to one column, and stack the page header. */ -@media (max-width: 900px) { - .triggers-table { - display: block; - overflow-x: auto; - white-space: nowrap; - } - .triggers-table thead, - .triggers-table tbody, - .triggers-table tr { - display: table; - width: 100%; - table-layout: auto; +/* Mobile / tablet: let the rule line wrap instead of truncating, collapse + the form to one column, and stack the page header. */ +@media (max-width: 720px) { + .trigger-rule { + flex-wrap: wrap; } .form-grid { grid-template-columns: 1fr;