feat: optimize host onboarding - #2230
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe PR adds centralized onboarding actions and modals, expands progress tracking, imports a Linux host-monitoring pack, adds follow-up and test-alert flows, persists verified datasources, and adds localized onboarding and mute-method guidance. ChangesOnboarding progress and actions
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant OnboardingTracks
participant OnboardingActionsProvider
participant HostMonitorPackModal
participant SendTestAlertModal
participant OnboardingProgress
OnboardingTracks->>OnboardingActionsProvider: open host pack action
OnboardingActionsProvider->>HostMonitorPackModal: render import modal
HostMonitorPackModal->>OnboardingProgress: refresh after effective imports
HostMonitorPackModal->>SendTestAlertModal: continue to test alert
SendTestAlertModal->>OnboardingProgress: refresh after successful delivery
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (4)
src/pages/hosts/pages/List/CollectSetup/index.tsx (2)
160-164: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueRead the remembered IDs once with a lazy state initializer.
useRef(readVerifiedDatasourceIds())evaluates its argument on every render. The ref keeps only the first value, so each later render performs alocalStorage.getItemplusJSON.parseand discards the result. A lazyuseStateinitializer runs the read once.♻️ Proposed refactor
- const rememberedDatasourceIds = useRef(readVerifiedDatasourceIds()).current; + const [rememberedDatasourceIds] = useState(readVerifiedDatasourceIds);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/hosts/pages/List/CollectSetup/index.tsx` around lines 160 - 164, Replace the useRef(readVerifiedDatasourceIds()) initialization with a lazy useState initializer so readVerifiedDatasourceIds executes only once, while preserving the rememberedDatasourceIds value used by defaultFromRemembered.
196-207: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDeclare the effect dependencies or add the lint exemption.
The effect reads
arrivalDatasources,arrival.hitDatasourceNames, anddatasourceIds, but the dependency array lists onlyarrival.status. TheverifiedMarkedRefguard makes the single-run behavior intentional, so state the intent explicitly. The same file already uses this pattern at Line 291.♻️ Proposed change
writeVerifiedDatasourceIds(hitIds.length > 0 ? hitIds : datasourceIds); + // 只在 detected 的那一次写入,其余依赖有意省略 + // eslint-disable-next-line react-hooks/exhaustive-deps }, [arrival.status]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/hosts/pages/List/CollectSetup/index.tsx` around lines 196 - 207, Update the useEffect handling arrival verification to explicitly document its intentional single-run behavior, following the existing pattern around the later effect in the same file. Either add the referenced values to the dependency array while preserving the verifiedMarkedRef guard, or add the established lint exemption for intentional omitted dependencies; ensure arrivalDatasources, arrival.hitDatasourceNames, and datasourceIds are covered by the chosen approach.src/components/OnboardingProgress/detect.test.ts (1)
80-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake the storage-unavailable test exercise the catch branch.
The test relies on
delete globalThis.localStorageinafterEach. Under jsdom,localStorageis defined onwindow, so the delete may not remove it. The assertions then still pass because the key is absent, and thetry/catchindetect.tsis never exercised. Install a throwing stub to test the failure path directly.🧪 Proposed test change
it('reports false instead of throwing when storage is unavailable', () => { // 隐私模式 / 禁用存储:读写都不能把整轮进度探测带崩 + (globalThis as { localStorage?: unknown }).localStorage = { + getItem: () => { + throw new Error('storage disabled'); + }, + setItem: () => { + throw new Error('storage disabled'); + }, + }; expect(() => writeOnboardingMarker('testDelivered')).not.toThrow(); expect(readOnboardingMarker('testDelivered')).toBe(false); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/OnboardingProgress/detect.test.ts` around lines 80 - 84, Update the storage-unavailable test around writeOnboardingMarker and readOnboardingMarker to replace localStorage with a stub whose access methods throw, ensuring both operations enter the catch path directly. Preserve the existing no-throw and false-result assertions, and restore the original storage state through the test cleanup.src/components/OnboardingActions/SendTestAlert/index.tsx (1)
98-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse a native type check instead of
_.isString.Line 99 uses
_.isString(res?.dat). Replace it withtypeof res?.dat === 'string'.As per coding guidelines: "For type narrowing, prefer native checks such as Array.isArray, typeof x === 'number', and x == null over lodash type-guard functions."
♻️ Proposed fix
- (res) => ({ index, label, ok: true, detail: _.isString(res?.dat) ? res.dat : undefined } as SendResult), + (res) => ({ index, label, ok: true, detail: typeof res?.dat === 'string' ? res.dat : undefined } as SendResult),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/OnboardingActions/SendTestAlert/index.tsx` around lines 98 - 101, In the notifyRuleTest success handler within the SendResult mapping, replace the lodash _.isString(res?.dat) check with the native typeof res?.dat === 'string' type guard, preserving the existing detail fallback and error handling.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/OnboardingActions/HostMonitorPack/index.tsx`:
- Around line 306-310: Update the board-name rendering in the selectedBoards
display to use a locale-appropriate separator instead of the hardcoded Chinese
“、”. Reuse the component’s existing translation or locale mechanism so en_US,
ja_JP, and ru_RU display names naturally, while preserving the current
empty-list handling and ordering.
- Around line 161-176: Update the rule processing around selectedRules and
ruleBodies so buildAlertRuleImportBody failures are preserved as failed
ImportItemResult entries using the existing bad-template translation, rather
than returning null and being removed by _.compact. Keep successfully parsed
rules in the import request, then merge parse-failure results with the import
responses so results.rules contains one result for every selected rule.
In `@src/components/OnboardingActions/HostMonitorPack/transform.ts`:
- Line 30: Replace the Lodash array checks in buildBoardImportBody and
buildAlertRuleImportBody with the native Array.isArray(parsed) check, preserving
the existing parsed[0] versus parsed selection behavior.
In `@src/components/OnboardingActions/NextStepsCard/index.tsx`:
- Around line 109-141: Replace the clickable anchors in the pending-row map and
dismiss control with native button elements using type="button". Preserve their
existing onClick behavior, labels, styling, and Tooltip wrappers, following the
button pattern used by OnboardingTracks.tsx.
In `@src/components/OnboardingActions/SendTestAlert/index.tsx`:
- Around line 83-115: Update handleSend to ignore stale async results by
tracking component mount state and the rule/request identity captured when
sending begins. Before setResults, setSending, and related onboarding updates,
verify the component remains mounted and the captured rule is still current;
ensure rule selection cannot change while sending is active, following the
existing cancelled guard pattern in the component effect.
In `@src/components/OnboardingProgress/detect.ts`:
- Around line 46-52: Update isHostBoard to match HOST_PACK_TAG as a complete tag
token rather than using substring matching on board.tags. Since appendPackTag
joins tags with spaces, split or otherwise test the space-delimited tags for
exact equality, while preserving the existing name-hint fallback behavior.
In `@src/components/OnboardingProgress/useOnboardingProgress.ts`:
- Around line 192-223: Update refreshOnboardingProgress and the shared probe
flow so a refresh arriving while pendingProbe is active schedules a follow-up
probe after the current one completes, rather than reusing only the in-flight
result. Ensure the follow-up runs with the latest lastDetect state and publishes
its result, while preserving the direct local-marker path and existing request
deduplication for concurrent initial probes.
In `@src/pages/hosts/pages/List/List.tsx`:
- Around line 384-397: Add an explicit aria-label to both install and collect
action Buttons in the List component, using their existing translated entry
labels so the controls remain accessible when ACTION_LABEL_CLASS hides the
visible text. Keep the current Tooltip and click behavior unchanged.
- Line 235: Update the history replacement in the host list onboarding flow to
remove only the consumed onboarding query parameter while preserving all other
search parameters. Use the existing location search and query-string handling
around history.replace rather than clearing search entirely.
---
Nitpick comments:
In `@src/components/OnboardingActions/SendTestAlert/index.tsx`:
- Around line 98-101: In the notifyRuleTest success handler within the
SendResult mapping, replace the lodash _.isString(res?.dat) check with the
native typeof res?.dat === 'string' type guard, preserving the existing detail
fallback and error handling.
In `@src/components/OnboardingProgress/detect.test.ts`:
- Around line 80-84: Update the storage-unavailable test around
writeOnboardingMarker and readOnboardingMarker to replace localStorage with a
stub whose access methods throw, ensuring both operations enter the catch path
directly. Preserve the existing no-throw and false-result assertions, and
restore the original storage state through the test cleanup.
In `@src/pages/hosts/pages/List/CollectSetup/index.tsx`:
- Around line 160-164: Replace the useRef(readVerifiedDatasourceIds())
initialization with a lazy useState initializer so readVerifiedDatasourceIds
executes only once, while preserving the rememberedDatasourceIds value used by
defaultFromRemembered.
- Around line 196-207: Update the useEffect handling arrival verification to
explicitly document its intentional single-run behavior, following the existing
pattern around the later effect in the same file. Either add the referenced
values to the dependency array while preserving the verifiedMarkedRef guard, or
add the established lint exemption for intentional omitted dependencies; ensure
arrivalDatasources, arrival.hitDatasourceNames, and datasourceIds are covered by
the chosen approach.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ad36069f-5e74-4aab-adab-5e912b6c2828
📒 Files selected for processing (52)
src/App.tsxsrc/components/OnboardingActions/HostMonitorPack/index.tsxsrc/components/OnboardingActions/HostMonitorPack/resolvePack.test.tssrc/components/OnboardingActions/HostMonitorPack/resolvePack.tssrc/components/OnboardingActions/HostMonitorPack/services.tssrc/components/OnboardingActions/HostMonitorPack/transform.test.tssrc/components/OnboardingActions/HostMonitorPack/transform.tssrc/components/OnboardingActions/NextStepsCard/index.tsxsrc/components/OnboardingActions/NextStepsCard/visibility.test.tssrc/components/OnboardingActions/NextStepsCard/visibility.tssrc/components/OnboardingActions/SendTestAlert/index.tsxsrc/components/OnboardingActions/constants.tssrc/components/OnboardingActions/index.tsxsrc/components/OnboardingActions/locale/en_US.tssrc/components/OnboardingActions/locale/index.tssrc/components/OnboardingActions/locale/ja_JP.tssrc/components/OnboardingActions/locale/ru_RU.tssrc/components/OnboardingActions/locale/zh_CN.tssrc/components/OnboardingActions/locale/zh_HK.tssrc/components/OnboardingActions/types.tssrc/components/OnboardingActions/useOnboardingStepClick.tssrc/components/OnboardingProgress/OnboardingTracks.tsxsrc/components/OnboardingProgress/PopoverContent.tsxsrc/components/OnboardingProgress/detect.test.tssrc/components/OnboardingProgress/detect.tssrc/components/OnboardingProgress/index.tsxsrc/components/OnboardingProgress/tracks.tssrc/components/OnboardingProgress/useOnboardingProgress.tssrc/pages/event/EventNotifyRecords/services.tssrc/pages/hosts/constants.tssrc/pages/hosts/locale/en_US.tssrc/pages/hosts/locale/ja_JP.tssrc/pages/hosts/locale/ru_RU.tssrc/pages/hosts/locale/zh_CN.tssrc/pages/hosts/locale/zh_HK.tssrc/pages/hosts/pages/List/CollectSetup/index.tsxsrc/pages/hosts/pages/List/InstallCategraf/index.tsxsrc/pages/hosts/pages/List/List.tsxsrc/pages/landing/OnboardingChecklist.tsxsrc/pages/landing/locale/en_US.tssrc/pages/landing/locale/ja_JP.tssrc/pages/landing/locale/ru_RU.tssrc/pages/landing/locale/zh_CN.tssrc/pages/landing/locale/zh_HK.tssrc/pages/notificationRules/services.tssrc/pages/warning/shield/components/operateForm.tsxsrc/pages/warning/shield/locale/en_US.tssrc/pages/warning/shield/locale/ja_JP.tssrc/pages/warning/shield/locale/ru_RU.tssrc/pages/warning/shield/locale/zh_CN.tssrc/pages/warning/shield/locale/zh_HK.tssrc/utils/request.tsx
| <a onClick={() => setPreviewOpen(!previewOpen)}>{previewOpen ? t('common:btn.collapse') : t('pack.preview')}</a> | ||
| </div> | ||
| {/* 名字由后端按语言翻译过,直接展示实际选中的,不写死在文案里 */} | ||
| {!previewOpen && !_.isEmpty(selectedBoards) && <div className='text-soft'>{_.join(_.map(selectedBoards, 'name'), '、')}</div>} | ||
| {previewOpen && renderPreviewList(resolved?.boards ?? [], boardIds, setBoardIds)} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Board name list uses a hardcoded Chinese separator.
Line 309 joins board names with '、', the Chinese enumeration comma, regardless of the active locale. This component ships en_US, ja_JP, and ru_RU translations, so this list looks out of place for non-Chinese users.
🌐 Proposed fix
- const { t } = useTranslation(NS);
+ const { t, i18n } = useTranslation(NS);- {!previewOpen && !_.isEmpty(selectedBoards) && <div className='text-soft'>{_.join(_.map(selectedBoards, 'name'), '、')}</div>}
+ {!previewOpen && !_.isEmpty(selectedBoards) && (
+ <div className='text-soft'>{new Intl.ListFormat(i18n.language).format(_.map(selectedBoards, 'name'))}</div>
+ )}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/OnboardingActions/HostMonitorPack/index.tsx` around lines 306
- 310, Update the board-name rendering in the selectedBoards display to use a
locale-appropriate separator instead of the hardcoded Chinese “、”. Reuse the
component’s existing translation or locale mechanism so en_US, ja_JP, and ru_RU
display names naturally, while preserving the current empty-list handling and
ordering.
| */ | ||
| export function buildBoardImportBody(content: string): BoardImportBody { | ||
| const parsed = JSON.parse(content); | ||
| const board = _.isArray(parsed) ? parsed[0] : parsed; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Use native Array.isArray instead of _.isArray.
buildBoardImportBody (line 30) and buildAlertRuleImportBody (line 57) both call _.isArray(parsed) to narrow the parsed payload. Use the native check instead.
As per coding guidelines, "prefer native checks such as Array.isArray, typeof x === 'number', and x == null over equivalent Lodash predicates such as _.isArray, _.isNumber, and _.isNil."
♻️ Proposed fix
- const board = _.isArray(parsed) ? parsed[0] : parsed;
+ const board = Array.isArray(parsed) ? parsed[0] : parsed;- const rule = _.isArray(parsed) ? parsed[0] : parsed;
+ const rule = Array.isArray(parsed) ? parsed[0] : parsed;Also applies to: 57-57
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/OnboardingActions/HostMonitorPack/transform.ts` at line 30,
Replace the Lodash array checks in buildBoardImportBody and
buildAlertRuleImportBody with the native Array.isArray(parsed) check, preserving
the existing parsed[0] versus parsed selection behavior.
Source: Coding guidelines
| return ( | ||
| <div className='mb-3 flex flex-wrap items-center gap-x-3 gap-y-1 border-0 border-b border-dashed border-[var(--fc-border-color)] pb-3'> | ||
| <span className='font-bold'>{t('card.title')}</span> | ||
| <span className='text-soft'>{`${doneCount}/${required.length}`}</span> | ||
| {_.map(pending, (row) => ( | ||
| // 描述进 Tooltip:撑住原先「一项一行」的正是这段文案,摘掉它几项才能并排 | ||
| <Tooltip key={row.key} title={t(`card.rows.${row.key}.desc`)}> | ||
| <a onClick={() => runAction(row.onClick)}> | ||
| {t(`card.rows.${row.key}.title`)} | ||
| {row.optional && <span className='ml-1 text-[10px] text-soft'>{t('card.optional')}</span>} | ||
| </a> | ||
| </Tooltip> | ||
| ))} | ||
| {primary && ( | ||
| // ml-auto 把主按钮和关闭推到最右:中间的步骤链接再多也不会把它们挤成不对齐的一坨 | ||
| <Button size='small' type='primary' className='ml-auto' onClick={() => runAction(primary.onClick)}> | ||
| {t(`card.rows.${primary.key}.action`)} | ||
| </Button> | ||
| )} | ||
| <Tooltip title={t('card.dismiss')}> | ||
| <a | ||
| className='text-soft' | ||
| onClick={() => { | ||
| setDismissed(true); | ||
| persistDismissed(); | ||
| }} | ||
| > | ||
| <CloseOutlined /> | ||
| </a> | ||
| </Tooltip> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Make the inline row link and dismiss control keyboard-operable.
Line 116 renders <a onClick={() => runAction(row.onClick)}> without an href. Line 129 renders the dismiss control the same way. An anchor without href is not part of the keyboard tab order and is not exposed as a link to assistive technology. A keyboard user cannot reach or activate either control.
Use <button type='button'> for both controls, matching the pattern already used in OnboardingTracks.tsx for step buttons.
♿ Proposed fix using native buttons
- <Tooltip key={row.key} title={t(`card.rows.${row.key}.desc`)}>
- <a onClick={() => runAction(row.onClick)}>
- {t(`card.rows.${row.key}.title`)}
- {row.optional && <span className='ml-1 text-[10px] text-soft'>{t('card.optional')}</span>}
- </a>
- </Tooltip>
+ <Tooltip key={row.key} title={t(`card.rows.${row.key}.desc`)}>
+ <button type='button' className='border-0 bg-transparent p-0 text-inherit hover:underline' onClick={() => runAction(row.onClick)}>
+ {t(`card.rows.${row.key}.title`)}
+ {row.optional && <span className='ml-1 text-[10px] text-soft'>{t('card.optional')}</span>}
+ </button>
+ </Tooltip> <Tooltip title={t('card.dismiss')}>
- <a
+ <button
+ type='button'
className='text-soft'
onClick={() => {
setDismissed(true);
persistDismissed();
}}
>
<CloseOutlined />
- </a>
+ </button>
</Tooltip>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return ( | |
| <div className='mb-3 flex flex-wrap items-center gap-x-3 gap-y-1 border-0 border-b border-dashed border-[var(--fc-border-color)] pb-3'> | |
| <span className='font-bold'>{t('card.title')}</span> | |
| <span className='text-soft'>{`${doneCount}/${required.length}`}</span> | |
| {_.map(pending, (row) => ( | |
| // 描述进 Tooltip:撑住原先「一项一行」的正是这段文案,摘掉它几项才能并排 | |
| <Tooltip key={row.key} title={t(`card.rows.${row.key}.desc`)}> | |
| <a onClick={() => runAction(row.onClick)}> | |
| {t(`card.rows.${row.key}.title`)} | |
| {row.optional && <span className='ml-1 text-[10px] text-soft'>{t('card.optional')}</span>} | |
| </a> | |
| </Tooltip> | |
| ))} | |
| {primary && ( | |
| // ml-auto 把主按钮和关闭推到最右:中间的步骤链接再多也不会把它们挤成不对齐的一坨 | |
| <Button size='small' type='primary' className='ml-auto' onClick={() => runAction(primary.onClick)}> | |
| {t(`card.rows.${primary.key}.action`)} | |
| </Button> | |
| )} | |
| <Tooltip title={t('card.dismiss')}> | |
| <a | |
| className='text-soft' | |
| onClick={() => { | |
| setDismissed(true); | |
| persistDismissed(); | |
| }} | |
| > | |
| <CloseOutlined /> | |
| </a> | |
| </Tooltip> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className='mb-3 flex flex-wrap items-center gap-x-3 gap-y-1 border-0 border-b border-dashed border-[var(--fc-border-color)] pb-3'> | |
| <span className='font-bold'>{t('card.title')}</span> | |
| <span className='text-soft'>{`${doneCount}/${required.length}`}</span> | |
| {_.map(pending, (row) => ( | |
| // 描述进 Tooltip:撑住原先「一项一行」的正是这段文案,摘掉它几项才能并排 | |
| <Tooltip key={row.key} title={t(`card.rows.${row.key}.desc`)}> | |
| <button type='button' className='border-0 bg-transparent p-0 text-inherit hover:underline' onClick={() => runAction(row.onClick)}> | |
| {t(`card.rows.${row.key}.title`)} | |
| {row.optional && <span className='ml-1 text-[10px] text-soft'>{t('card.optional')}</span>} | |
| </button> | |
| </Tooltip> | |
| ))} | |
| {primary && ( | |
| // ml-auto 把主按钮和关闭推到最右:中间的步骤链接再多也不会把它们挤成不对齐的一坨 | |
| <Button size='small' type='primary' className='ml-auto' onClick={() => runAction(primary.onClick)}> | |
| {t(`card.rows.${primary.key}.action`)} | |
| </Button> | |
| )} | |
| <Tooltip title={t('card.dismiss')}> | |
| <button | |
| type='button' | |
| className='text-soft' | |
| onClick={() => { | |
| setDismissed(true); | |
| persistDismissed(); | |
| }} | |
| > | |
| <CloseOutlined /> | |
| </button> | |
| </Tooltip> | |
| </div> | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/OnboardingActions/NextStepsCard/index.tsx` around lines 109 -
141, Replace the clickable anchors in the pending-row map and dismiss control
with native button elements using type="button". Preserve their existing onClick
behavior, labels, styling, and Tooltip wrappers, following the button pattern
used by OnboardingTracks.tsx.
| const handleSend = () => { | ||
| if (_.isEmpty(configs)) return; | ||
| setSending(true); | ||
| setResults(undefined); | ||
|
|
||
| // 逐个 notify_config 发一次,这样用户能看到「钉钉成功、邮件失败」而不是一个笼统的结果。 | ||
| // time_ranges 直接用接口返回的原始值:列表接口给的就是后端要的 'HH:mm' 字符串, | ||
| // 只有表单态才会被 normalizeInitialValues 转成 moment。 | ||
| Promise.all( | ||
| _.map(configs, (config, index) => { | ||
| const label = getConfigLabel(config, index); | ||
| if (!config?.channel_id || config.channel_id <= 0) { | ||
| // 后端对 channel_id <= 0 直接 400,先在前端说清楚是"没选通知媒介" | ||
| return Promise.resolve<SendResult>({ index, label, ok: false, detail: t('test.no_channel') }); | ||
| } | ||
| return notifyRuleTest({ use_mock_event: true, notify_config: config }, { silence: true }).then( | ||
| (res) => ({ index, label, ok: true, detail: _.isString(res?.dat) ? res.dat : undefined } as SendResult), | ||
| (err) => ({ index, label, ok: false, detail: err?.message || t('test.unknown_error') } as SendResult), | ||
| ); | ||
| }), | ||
| ) | ||
| .then((list) => { | ||
| setResults(list); | ||
| if (_.some(list, { ok: true })) { | ||
| // 本地标记让这一步立刻点亮;换浏览器/其他用户由服务端送达探测兜住 | ||
| writeOnboardingMarker('testDelivered'); | ||
| refreshOnboardingProgress(['testDelivered']); | ||
| } | ||
| }) | ||
| .finally(() => { | ||
| setSending(false); | ||
| }); | ||
| }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Guard handleSend against updates after unmount or a rule change.
handleSend does not track mount state. If the user closes the modal (triggers onCancel, unmounting this component) while Promise.all is still pending, setResults and setSending in .then/.finally run after unmount. The effect at lines 60-78 in this same file already guards setRules/setRuleId/setLoading with a cancelled flag for this exact failure mode; handleSend needs the same guard.
The rule Select is also not disabled while sending is true. If the user switches ruleId before the pending send resolves, the stale result overwrites state for a rule that is no longer selected.
🔒 Proposed fix using a mount ref and a request-identity check
+ const mountedRef = React.useRef(true);
+ React.useEffect(
+ () => () => {
+ mountedRef.current = false;
+ },
+ [],
+ );
+
const handleSend = () => {
if (_.isEmpty(configs)) return;
setSending(true);
setResults(undefined);
+ const requestedRuleId = ruleId;
Promise.all(
_.map(configs, (config, index) => {
...
}),
)
.then((list) => {
+ if (!mountedRef.current || requestedRuleId !== ruleId) return;
setResults(list);
if (_.some(list, { ok: true })) {
writeOnboardingMarker('testDelivered');
refreshOnboardingProgress(['testDelivered']);
}
})
.finally(() => {
- setSending(false);
+ if (mountedRef.current) setSending(false);
});
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/OnboardingActions/SendTestAlert/index.tsx` around lines 83 -
115, Update handleSend to ignore stale async results by tracking component mount
state and the rule/request identity captured when sending begins. Before
setResults, setSending, and related onboarding updates, verify the component
remains mounted and the captured rule is still current; ensure rule selection
cannot change while sending is active, following the existing cancelled guard
pattern in the component effect.
| export function isHostBoard(board?: { name?: string; tags?: string } | null): boolean { | ||
| if (!board) return false; | ||
| if (board.tags && board.tags.includes(HOST_PACK_TAG)) return true; | ||
| if (!board.name) return false; | ||
| const lower = board.name.toLowerCase(); | ||
| return HOST_DASHBOARD_NAME_HINTS.some((hint) => lower.includes(hint)); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Match the pack tag as a whole token.
board.tags.includes(HOST_PACK_TAG) is a substring test. A user tag such as n9e-host-pack-old then counts as a pack board. appendPackTag in src/components/OnboardingActions/HostMonitorPack/transform.ts joins tags with spaces, so an exact token test is available and equally cheap.
🔍 Proposed fix
-export function isHostBoard(board?: { name?: string; tags?: string } | null): boolean {
- if (!board) return false;
- if (board.tags && board.tags.includes(HOST_PACK_TAG)) return true;
+export function isHostBoard(board?: { name?: string; tags?: string } | null): boolean {
+ if (!board) return false;
+ // tags 是空白分隔的自由字符串,按 token 比对,避免 n9e-host-pack-xxx 这类前缀误命中
+ if (board.tags && board.tags.split(/\s+/).includes(HOST_PACK_TAG)) return true;
if (!board.name) return false;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function isHostBoard(board?: { name?: string; tags?: string } | null): boolean { | |
| if (!board) return false; | |
| if (board.tags && board.tags.includes(HOST_PACK_TAG)) return true; | |
| if (!board.name) return false; | |
| const lower = board.name.toLowerCase(); | |
| return HOST_DASHBOARD_NAME_HINTS.some((hint) => lower.includes(hint)); | |
| } | |
| export function isHostBoard(board?: { name?: string; tags?: string } | null): boolean { | |
| if (!board) return false; | |
| // tags 是空白分隔的自由字符串,按 token 比对,避免 n9e-host-pack-xxx 这类前缀误命中 | |
| if (board.tags && board.tags.split(/\s+/).includes(HOST_PACK_TAG)) return true; | |
| if (!board.name) return false; | |
| const lower = board.name.toLowerCase(); | |
| return HOST_DASHBOARD_NAME_HINTS.some((hint) => lower.includes(hint)); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/OnboardingProgress/detect.ts` around lines 46 - 52, Update
isHostBoard to match HOST_PACK_TAG as a complete tag token rather than using
substring matching on board.tags. Since appendPackTag joins tags with spaces,
split or otherwise test the space-delimited tags for exact equality, while
preserving the existing name-hint fallback behavior.
| } else if (installMeta.collect) { | ||
| setCollectVisible(true); | ||
| } | ||
| history.replace({ pathname: location.pathname, search: '' }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Remove only the onboarding parameter instead of the whole query string.
history.replace({ pathname: location.pathname, search: '' }) discards every other query parameter on /targets. Any co-existing parameter, for example a deep link that also carries a filter, is lost after the wizard opens. Delete the single consumed key.
🔧 Proposed fix
- const onboarding = new URLSearchParams(location.search).get('onboarding');
+ const searchParams = new URLSearchParams(location.search);
+ const onboarding = searchParams.get('onboarding');
if (onboarding !== 'install' && onboarding !== 'collect') return;
@@
- history.replace({ pathname: location.pathname, search: '' });
+ searchParams.delete('onboarding');
+ const nextSearch = searchParams.toString();
+ history.replace({ pathname: location.pathname, search: nextSearch ? `?${nextSearch}` : '' });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| history.replace({ pathname: location.pathname, search: '' }); | |
| searchParams.delete('onboarding'); | |
| const nextSearch = searchParams.toString(); | |
| history.replace({ pathname: location.pathname, search: nextSearch ? `?${nextSearch}` : '' }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/hosts/pages/List/List.tsx` at line 235, Update the history
replacement in the host list onboarding flow to remove only the consumed
onboarding query parameter while preserving all other search parameters. Use the
existing location search and query-string handling around history.replace rather
than clearing search entirely.
| {!aiTaskMode && installMeta && ( | ||
| <Tooltip title={t('install.entry')}> | ||
| <Button type='primary' ghost icon={<DownloadOutlined />} onClick={() => setInstallVisible(true)}> | ||
| <span className={ACTION_LABEL_CLASS}>{t('install.entry')}</span> | ||
| </Button> | ||
| </Tooltip> | ||
| )} | ||
| {!aiTaskMode && installMeta?.collect && ( | ||
| <Tooltip title={t('collect.entry')}> | ||
| <Button type='primary' ghost icon={<AppstoreAddOutlined />} onClick={() => setCollectVisible(true)}> | ||
| <span className={ACTION_LABEL_CLASS}>{t('collect.entry')}</span> | ||
| </Button> | ||
| </Tooltip> | ||
| )} |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Give the collapsed action buttons an accessible name.
ACTION_LABEL_CLASS is hidden 2xl:inline-block. Below the 2xl breakpoint the label leaves the accessibility tree, so each button exposes only an icon. The antd Tooltip sets aria-describedby only while the tooltip is open, so screen readers announce an unnamed button. Add aria-label to both buttons.
♿ Proposed fix
{!aiTaskMode && installMeta && (
<Tooltip title={t('install.entry')}>
- <Button type='primary' ghost icon={<DownloadOutlined />} onClick={() => setInstallVisible(true)}>
+ <Button type='primary' ghost aria-label={t('install.entry')} icon={<DownloadOutlined />} onClick={() => setInstallVisible(true)}>
<span className={ACTION_LABEL_CLASS}>{t('install.entry')}</span>
</Button>
</Tooltip>
)}
{!aiTaskMode && installMeta?.collect && (
<Tooltip title={t('collect.entry')}>
- <Button type='primary' ghost icon={<AppstoreAddOutlined />} onClick={() => setCollectVisible(true)}>
+ <Button type='primary' ghost aria-label={t('collect.entry')} icon={<AppstoreAddOutlined />} onClick={() => setCollectVisible(true)}>
<span className={ACTION_LABEL_CLASS}>{t('collect.entry')}</span>
</Button>
</Tooltip>
)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {!aiTaskMode && installMeta && ( | |
| <Tooltip title={t('install.entry')}> | |
| <Button type='primary' ghost icon={<DownloadOutlined />} onClick={() => setInstallVisible(true)}> | |
| <span className={ACTION_LABEL_CLASS}>{t('install.entry')}</span> | |
| </Button> | |
| </Tooltip> | |
| )} | |
| {!aiTaskMode && installMeta?.collect && ( | |
| <Tooltip title={t('collect.entry')}> | |
| <Button type='primary' ghost icon={<AppstoreAddOutlined />} onClick={() => setCollectVisible(true)}> | |
| <span className={ACTION_LABEL_CLASS}>{t('collect.entry')}</span> | |
| </Button> | |
| </Tooltip> | |
| )} | |
| {!aiTaskMode && installMeta && ( | |
| <Tooltip title={t('install.entry')}> | |
| <Button type='primary' ghost aria-label={t('install.entry')} icon={<DownloadOutlined />} onClick={() => setInstallVisible(true)}> | |
| <span className={ACTION_LABEL_CLASS}>{t('install.entry')}</span> | |
| </Button> | |
| </Tooltip> | |
| )} | |
| {!aiTaskMode && installMeta?.collect && ( | |
| <Tooltip title={t('collect.entry')}> | |
| <Button type='primary' ghost aria-label={t('collect.entry')} icon={<AppstoreAddOutlined />} onClick={() => setCollectVisible(true)}> | |
| <span className={ACTION_LABEL_CLASS}>{t('collect.entry')}</span> | |
| </Button> | |
| </Tooltip> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/hosts/pages/List/List.tsx` around lines 384 - 397, Add an explicit
aria-label to both install and collect action Buttons in the List component,
using their existing translated entry labels so the controls remain accessible
when ACTION_LABEL_CLASS hides the visible text. Keep the current Tooltip and
click behavior unchanged.
- import alert rules without force: pre-check same-name rules in the target busi group and skip them (same policy as dashboards) instead of letting the backend upsert silently overwrite user-tuned rules, or duplicate them after a language switch changes template names - gate the delivered probe on this round's notification result instead of the previous round's cache, so the first probe of a session can light up "send a test alert" for deployments that already delivered - drop the one-shot ref guarding the ?onboarding deep link: clearing the query already prevents re-triggering, and the ref swallowed a second click on the same onboarding step within one mount - track bgid + loading for the existing-name pre-check and keep submit disabled until the pre-check matches the selected busi group
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/components/OnboardingActions/HostMonitorPack/index.tsx (1)
178-199: 🎯 Functional Correctness | 🟠 MajorRule parse failures still disappear from results (regression).
buildAlertRuleImportBodyfailures returnnullin thecatchblock._.compactdrops that entry fromruleBodiesbefore it reachesexistedRules,rulesToImport, orskippedResults. The failing rule never appears inresults.rules, unlike the board path, which keeps a'failed'entry witht('pack.bad_template')(Line 170).This is the same defect flagged in a prior review round and marked addressed. The current refactor, which adds the
bodyNamepartition logic, dropped the failure-tracking fix. Reintroduce it: collect parse failures separately and merge them back into the finalruleTaskresult.🐛 Proposed fix
+ const ruleParseFailures: ImportItemResult[] = []; const ruleBodies = _.compact( _.map(selectedRules, (payload) => { try { const body = buildAlertRuleImportBody(payload.content, { datasourceQueries: values.datasource_queries, notifyRuleIds }); // bodyName 才是后端落库与响应 map 的 key(payload.name 只是列表展示名,两者通常一致) return { name: payload.name, bodyName: body.name ?? payload.name, body }; } catch (e) { - return null; + ruleParseFailures.push({ name: payload.name, status: 'failed', detail: t('pack.bad_template') }); + return null; } }), ); // 与大盘同一口径:重名跳过、不覆盖。不能走后端 force 导入 —— 那是按 (group_id, name) // 的整行覆盖,会把用户改过阈值 / 主动停用 / 换过通知绑定的同名规则静默还原(见 services.ts) const [existedRules, rulesToImport] = _.partition(ruleBodies, (item) => existing.rules[item.bodyName]); const skippedResults = _.map(existedRules, (item) => ({ name: item.name, status: 'skipped' } as ImportItemResult)); const ruleTask: Promise<ImportItemResult[]> = _.isEmpty(rulesToImport) - ? Promise.resolve(skippedResults) + ? Promise.resolve<ImportItemResult[]>(skippedResults) : importAlertRules(targetBgid, _.map(rulesToImport, 'body')).then( (res) => [..._.map(rulesToImport, (item) => ({ name: item.name, status: res?.[item.bodyName] ? 'failed' : 'ok', detail: res?.[item.bodyName] } as ImportItemResult)), ...skippedResults], (err) => [..._.map(rulesToImport, (item) => ({ name: item.name, status: 'failed', detail: err?.message || t('pack.unknown_error') } as ImportItemResult)), ...skippedResults], - ); + ); + const ruleTaskWithParseFailures = ruleTask.then((items) => [...ruleParseFailures, ...items]);Replace the later
ruleTaskreference inPromise.all([Promise.all(boardTasks), ruleTask])withruleTaskWithParseFailures.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/OnboardingActions/HostMonitorPack/index.tsx` around lines 178 - 199, Update the rule-processing flow around buildAlertRuleImportBody to preserve parse failures as failed results instead of dropping them via _.compact. Collect failed payloads with the existing bad-template result shape and merge them into the final rule task, then use ruleTaskWithParseFailures in the Promise.all result assembly so each failed rule appears in results.rules alongside imported and skipped rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@src/components/OnboardingActions/HostMonitorPack/index.tsx`:
- Around line 178-199: Update the rule-processing flow around
buildAlertRuleImportBody to preserve parse failures as failed results instead of
dropping them via _.compact. Collect failed payloads with the existing
bad-template result shape and merge them into the final rule task, then use
ruleTaskWithParseFailures in the Promise.all result assembly so each failed rule
appears in results.rules alongside imported and skipped rules.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 02f79649-465b-42e4-adc2-decb0c4fb31c
📒 Files selected for processing (10)
src/components/OnboardingActions/HostMonitorPack/index.tsxsrc/components/OnboardingActions/HostMonitorPack/services.tssrc/components/OnboardingActions/HostMonitorPack/transform.tssrc/components/OnboardingActions/locale/en_US.tssrc/components/OnboardingActions/locale/ja_JP.tssrc/components/OnboardingActions/locale/ru_RU.tssrc/components/OnboardingActions/locale/zh_CN.tssrc/components/OnboardingActions/locale/zh_HK.tssrc/components/OnboardingProgress/useOnboardingProgress.tssrc/pages/hosts/pages/List/List.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
- src/components/OnboardingActions/locale/zh_HK.ts
- src/components/OnboardingActions/locale/ja_JP.ts
- src/components/OnboardingActions/locale/en_US.ts
- src/components/OnboardingActions/HostMonitorPack/transform.ts
- src/components/OnboardingActions/locale/ru_RU.ts
- src/components/OnboardingProgress/useOnboardingProgress.ts
Backend relaxed the check from delivered-only to any notification record (and renamed the endpoint), so the delivered-only query no longer exists. Rename getNotifyDelivered -> getNotifyUsed, detect field delivered -> notifyUsed. Local test-alert markers (testDelivered*) keep their names: they track the send-test-alert UX, not the server probe.
- the "bind notifications" step no longer lights up on rule existence alone: once enabled host-cate alert rules exist, at least one of them must be bound to a notification rule (the pack allows importing with notify_rule_ids empty, so all four steps could turn green while real alerts reached nobody); legacy-notify rules (notify_version != 1) count as bound since their notify config is embedded in the rule. detection reuses the same alert-rules request as hostAlert - after quick-creating a notification rule from the guided flow, show a pointer to batch-bind it when enabled host rules are still unbound -- the new rule is not attached to imported rules automatically, and without the hint the step would stay unlit with no explanation - refresh onboarding progress as soon as InstallCategraf detects the first reporting machine: the machine flag was only re-probed on route change, so the inline next-steps strip on the host list would not appear until the user navigated away and back
… errors - the backend treats any HTTP 200 as a successful send without parsing the body (http_common.go), while dingtalk/wecom bots report invalid tokens and unmatched security keywords exactly as 200 + non-zero errcode; such sends showed "Delivered" and stamped the testDelivered onboarding marker - add a conservative response interpreter that only recognizes the de-facto errcode convention (0 = ok): fields like `code` vary by channel (custom webhooks may use code:200 for success), so anything unrecognized still counts as sent instead of guessing - success rows now say "request sent" instead of "delivered", render the raw provider response alongside (errcode/errmsg is the user's only troubleshooting clue), and the hint stresses that only actual receipt proves the notification path works
- OnboardingActionModals was mounted unconditionally in the App-level catch-all, so its progress probe (five authenticated requests) also fired on anonymous routes and in plus builds; on shared dashboard / chart pages the 401 handler then kicked anonymous visitors to the login page, breaking sharing. The mount gate now lives inside the component: it renders nothing on anonymous paths (checked via useLocation so SPA navigations re-evaluate -- the module-level `anonymous` const in App.tsx does not) and when the layer is disabled (plus edition) - the host list no longer holds its own probe hook: the "has machines" gate moved into NextStepsCard's inline variant, and the strip is only mounted for !aiTaskMode && !IS_PLUS, since returning null cannot stop an already-mounted hook from probing - actions are now permission-gated in line with backend rt.perm (pack = /dashboards/add + /alert-rules/add, notify = /notification-rules/add, test = /notification-rules): openAction refuses unpermitted actions, checklist steps fall back to their `to` navigation, NextStepsCard hides unpermitted rows, and the pack modal hides its quick-create / send-test entries -- previously a read-only user got a modal that could only 403 on submit
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/components/OnboardingActions/SendTestAlert/interpretResponse.test.ts (1)
12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNarrow the provider-response fixtures with
as const.Apply
as constto these literal fixtures. This preserves their narrow literal types.Proposed change
- const dat = 'status_code:200, response:{"errcode":310000,"errmsg":"keywords not in content"}'; + const dat = 'status_code:200, response:{"errcode":310000,"errmsg":"keywords not in content"}' as const;As per coding guidelines, “Prefer
as constfor test data literals to preserve narrow literal types.”Also applies to: 17-17, 22-22, 28-28
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/OnboardingActions/SendTestAlert/interpretResponse.test.ts` at line 12, Update the provider-response fixture literals in interpretResponse.test.ts, including dat and the additional fixtures at the referenced locations, by applying as const so their narrow literal types are preserved. Do not change the fixture values or test behavior.Source: Coding guidelines
src/components/OnboardingActions/SendTestAlert/interpretResponse.ts (1)
1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse native checks for type narrowing.
Replace
_.isString(dat)withtypeof dat !== 'string'. Replace_.isNumber(errcode)withtypeof errcode === 'number'. Then remove the Lodash import.Proposed change
-import _ from 'lodash'; - export interface TestSendOutcome { @@ - if (!_.isString(dat) || dat === '' || dat === 'success') { + if (typeof dat !== 'string' || dat === '' || dat === 'success') { @@ - if (_.isNumber(errcode) && errcode !== 0) { + if (typeof errcode === 'number' && errcode !== 0) {As per coding guidelines, “For type narrowing in JavaScript and TypeScript code, prefer native checks … over equivalent Lodash predicates.”
Also applies to: 23-31
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/OnboardingActions/SendTestAlert/interpretResponse.ts` at line 1, In interpretResponse, replace the Lodash predicates used for dat and errcode type narrowing with native typeof checks, using typeof dat !== 'string' and typeof errcode === 'number' semantics. Remove the now-unused Lodash import while preserving the surrounding response handling.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/OnboardingActions/NextStepsCard/index.tsx`:
- Around line 77-83: Update the onboarding progress state used by the
notification row in NextStepsCard so it distinguishes “no host alerts” from a
failed or unknown host-alert probe. Expose and consume a separate
successful-probe or known-state flag from useOnboardingProgress, and require
that flag before allowing doneMap.notification to mark the row complete;
preserve the existing hostNotifyBound requirement when host alerts are present.
In `@src/pages/hosts/pages/List/InstallCategraf/index.tsx`:
- Around line 40-47: Update refreshOnboardingProgress and its
probeOnboardingShared flow so a machine detection refresh waits for any existing
in-flight probe to complete, then queues a follow-up probe instead of reusing
only the current promise. Preserve the one-time trigger guarded by
machineRefreshedRef and ensure the post-install probe publishes the latest
machine state.
---
Nitpick comments:
In `@src/components/OnboardingActions/SendTestAlert/interpretResponse.test.ts`:
- Line 12: Update the provider-response fixture literals in
interpretResponse.test.ts, including dat and the additional fixtures at the
referenced locations, by applying as const so their narrow literal types are
preserved. Do not change the fixture values or test behavior.
In `@src/components/OnboardingActions/SendTestAlert/interpretResponse.ts`:
- Line 1: In interpretResponse, replace the Lodash predicates used for dat and
errcode type narrowing with native typeof checks, using typeof dat !== 'string'
and typeof errcode === 'number' semantics. Remove the now-unused Lodash import
while preserving the surrounding response handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 05ecfb53-3ab5-494d-b2af-62b697c45a70
📒 Files selected for processing (19)
src/App.tsxsrc/components/OnboardingActions/HostMonitorPack/index.tsxsrc/components/OnboardingActions/NextStepsCard/index.tsxsrc/components/OnboardingActions/SendTestAlert/index.tsxsrc/components/OnboardingActions/SendTestAlert/interpretResponse.test.tssrc/components/OnboardingActions/SendTestAlert/interpretResponse.tssrc/components/OnboardingActions/constants.tssrc/components/OnboardingActions/index.tsxsrc/components/OnboardingActions/locale/en_US.tssrc/components/OnboardingActions/locale/ja_JP.tssrc/components/OnboardingActions/locale/ru_RU.tssrc/components/OnboardingActions/locale/zh_CN.tssrc/components/OnboardingActions/locale/zh_HK.tssrc/components/OnboardingActions/useOnboardingStepClick.tssrc/components/OnboardingProgress/detect.test.tssrc/components/OnboardingProgress/detect.tssrc/components/OnboardingProgress/useOnboardingProgress.tssrc/pages/hosts/pages/List/InstallCategraf/index.tsxsrc/pages/hosts/pages/List/List.tsx
🚧 Files skipped from review as they are similar to previous changes (10)
- src/components/OnboardingActions/locale/zh_HK.ts
- src/components/OnboardingActions/useOnboardingStepClick.ts
- src/pages/hosts/pages/List/List.tsx
- src/components/OnboardingActions/locale/en_US.ts
- src/components/OnboardingProgress/useOnboardingProgress.ts
- src/components/OnboardingActions/locale/zh_CN.ts
- src/components/OnboardingActions/locale/ja_JP.ts
- src/components/OnboardingActions/locale/ru_RU.ts
- src/components/OnboardingActions/SendTestAlert/index.tsx
- src/components/OnboardingActions/HostMonitorPack/index.tsx
| permittedActions.notify && { | ||
| key: 'notify' as RowKey, | ||
| // 只看「存在通知规则」不够:基础包允许 notify_rule_ids 留空导入,主机告警可能一条都没绑 | ||
| // 通知,真告警仍无人收到。已有启用中的主机告警时,额外要求至少一条真的绑定了通知; | ||
| // 主机告警还没导入时「绑定」无从谈起,维持「有通知规则即完成」的原口径。 | ||
| done: doneMap.notification && (!doneMap.hostAlert || doneMap.hostNotifyBound), | ||
| onClick: () => openAction('notify'), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Do not mark notification setup complete when host-alert detection fails.
!doneMap.hostAlert treats “no host alerts exist” and “the host-alert request failed” as the same state. The progress hook preserves false on a failed alert probe, so a successful notification-rule probe can mark this row complete and hide the binding action. (raw.githubusercontent.com)
Expose a separate successful-probe or known-state flag. Keep this row incomplete while host-alert state is unknown.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/OnboardingActions/NextStepsCard/index.tsx` around lines 77 -
83, Update the onboarding progress state used by the notification row in
NextStepsCard so it distinguishes “no host alerts” from a failed or unknown
host-alert probe. Expose and consume a separate successful-probe or known-state
flag from useOnboardingProgress, and require that flag before allowing
doneMap.notification to mark the row complete; preserve the existing
hostNotifyBound requirement when host alerts are present.
| // 检测到机器上报就立刻刷新引导进度:machine 只随路由变化重探,首台机器装完若不主动刷一次, | ||
| // 列表页工具栏的 inline 引导条要等用户切一次页面才会出现。ref 保证整个弹窗生命周期只触发一次 | ||
| const machineRefreshedRef = useRef(false); | ||
| useEffect(() => { | ||
| if (!detected || machineRefreshedRef.current) return; | ||
| machineRefreshedRef.current = true; | ||
| refreshOnboardingProgress(['machine']); | ||
| }, [detected]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -A45 -B8 'pendingProbe|probeOnboardingShared|refreshOnboardingProgress' \
src/components/OnboardingProgress/useOnboardingProgress.tsRepository: n9e/fe
Length of output: 6062
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="src/pages/hosts/pages/List/InstallCategraf/index.tsx"
printf '--- outline ---\n'
ast-grep outline "$file" --view condensed || true
printf '\n--- relevant imports and effect/hook area ---\n'
rg -n -A120 -B80 'useOnboardingProgress|refreshOnboardingProgress|machineRefreshedRef|onboarding|detect|detected' "$file"
printf '\n--- all installed categraf onboarding callsites ---\n'
rg -n -A30 -B15 'refreshOnboardingProgress\(\[.machine.\]\)|onboardingProgress|doneMap.machine|useOnboardingProgress' srcRepository: n9e/fe
Length of output: 235
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="src/pages/hosts/pages/List/InstallCategraf/index.tsx"
printf '%s\n' '--- outline ---'
ast-grep outline "$file" --view condensed || true
printf '%s\n' ''
printf '%s\n' '--- relevant imports and effect/hook area ---'
rg -n -A120 -B80 'useOnboardingProgress|refreshOnboardingProgress|machineRefreshedRef|onboarding|detect|detected' "$file"
printf '%s\n' ''
printf '%s\n' '--- all installed categraf onboarding callsites ---'
rg -n -A30 -B15 'refreshOnboardingProgress\(\[.machine.\]\)|onboardingProgress|doneMap.machine|useOnboardingProgress' srcRepository: n9e/fe
Length of output: 50362
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('src/components/OnboardingProgress/useOnboardingProgress.ts')
s = p.read_text()
checks = {
"lastDetect initializes machine false": "INIT_DETECT = {\nl machine: false," in s,
"pendingProbe is module-level null/empty": "let pendingProbe: Promise<DetectState> | null = null;" in s,
"probeOnboardingShared returns immediately if existing": "function probeOnboardingShared(): Promise<DetectState> {\\n if (!pendingProbe) {\\n pendingProbe = probeOnboarding()\\n .then((next) => {\\n publish(next);\\n return next;\\n })\\n .finally(() => {\\n pendingProbe = null;\\n });\\n }\\n return pendingProbe;\\n}" in s,
"refreshOnboardingProgress uses shared probe when keys include non-marker keys": " probeOnboardingShared().catch(() => undefined);" in s,
}
for name, ok in checks.items():
print(f"{name}: {ok}")
start = s.index("function probeOnboarding(): Promise<DetectState> {")
end = s.index("/** 共享同一轮探测:并发挂载只发一组请求,结果广播给所有实例 */")
probe = s[start:end]
print("probeOnboarding reads known.machine via local 'known':", "const known = lastDetect;" in probe and "known.machine" in probe)
PYRepository: n9e/fe
Length of output: 439
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '114,145p' src/components/OnboardingProgress/useOnboardingProgress.ts
sed -n '214,244p' src/components/OnboardingProgress/useOnboardingProgress.ts
sed -n '1,45p' src/components/OnboardingProgress/useOnboardingProgress.tsRepository: n9e/fe
Length of output: 3717
Schedule a post-install refresh when a probe is already running.
refreshOnboardingProgress(['machine']) only calls probeOnboardingShared(), and probeOnboardingShared() returns the existing in-flight promise instead of starting a new probe. If that earlier probe started with lastDetect.machine === false, the post-install detection can publish machine: false, so the inline onboarding card stays hidden until route refresh. Make refreshOnboardingProgress(['machine']) wait for the current probe, then queue a follow-up probe.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/hosts/pages/List/InstallCategraf/index.tsx` around lines 40 - 47,
Update refreshOnboardingProgress and its probeOnboardingShared flow so a machine
detection refresh waits for any existing in-flight probe to complete, then
queues a follow-up probe instead of reusing only the current promise. Preserve
the one-time trigger guarded by machineRefreshedRef and ensure the post-install
probe publishes the latest machine state.
- 12b2f0d's errcode interpreter only covers the dingtalk/wecom dialect. Feishu/Lark report business failures as HTTP 200 + non-zero StatusCode (see alert/sender/provider/feishucard_provider_test.go), so those sends still rendered a green check; worse, index.tsx then stamped the testDelivered marker, which readMarkers ORs into the done state forever without ever re-validating - rather than maintaining a per-channel error-code dialect table that new channels will always outrun, drop the guess entirely: interpretTestSendResponse becomes formatTestSendResponse, which only pretty-prints the provider body so errcode / StatusCode / code are readable whatever they happen to be called - SendResult.ok becomes SendResult.called, narrowed to "the request went out"; the green check becomes a neutral SendOutlined and the raw response renders in a <pre>. Hard failures (request never sent, no channel picked) keep the red icon - troubleshooting links used to appear only on !ok, which hid them for exactly the HTTP-200 business failures that need them most; they now sit under hard-failure rows and beside the "only actual receipt proves the notification path works" hint - the localStorage marker keeps its key and keeps being written: /notify-rule/test does not write notification_record, so the server side used probe can never light this step. Its meaning is now "you ran the send action", matching the step name, with no implied delivery claim
- the host list's business group tree offers preset filters that are not business groups: -2 (all machines) and 0 (ungrouped), plus the dashboard page's -1 (public). Clicking one sets businessGroup.id to that value and persists it through the businessGroupKey localStorage entry, since getDefaultBusiness deliberately skips the existence check for preset values - defaultBgid piped businessGroup.id through `??`, which only falls through on null/undefined, so those values became the import target: 0 left existing.bgid undefined and the "pre-check done and belongs to the current group" submit gate never closed (button permanently disabled), while -2/-1 actually posted the dashboards and alert rules to /busi-group/-2/..., failing the whole batch. Either way the business group select rendered a bare number, since -2/0 match no option - guard all three sources with the positivity check curBusiId already had: preset values are exactly the non-positive ones, and a positive id is always a real group (tree nodes are built from busiGroups, and getDefaultBusiness already validates and clears stale localStorage values), so no membership lookup is needed
探测的跳过条件收紧成 known.alert && known.hostAlert && known.hostNotifyBound 之后,全量告警规则列表从「每次页面加载最多拉一次」退化成「每次路由切换都拉一次」: hostAlert 要求存在启用中的 cate=host 规则,而内置库里的 host 规则一律以 disabled=1 出厂,这个合取式对多数部署永远闭合不了,doneCount === total 的会话短路同样永远不写入。 已完成项交给缓存、恒为假的项交给时间窗: - 新增 detectCache.ts:探测结论按 profile.id 隔离落 localStorage。localStorage 是 origin 级的,而这些结论探的是「当前用户可见业务组内是否存在」,不按用户隔离会让 换账号登录的人直接继承上一个人的完成态。 - 只写 true、绝不写 false:一次网络抖动探到的 false 若被固化,用户就再也点不亮那一步。 collectVerified / testDeliveredLocal 本身就是 localStorage 标记,notifyUsed 会随 服务端记录按保留期清理而回退,三者都不进缓存。 - 缓存记两个时间戳:at 每轮探测前移、只用于节流;establishedAt 只在完成项集合真的变化时 前移、只用于 TTL。合用一个会让活跃用户的有效期被无限顺延,TTL 等于永不生效,删光规则的 用户永久停在「已完成」。 - PROBE_MIN_INTERVAL 取 60s:九步里有八步在完成处显式调 refreshOnboardingProgress (不经过节流,点完即时变绿),只有 llm 与从常规页面创建的大盘/规则靠路由探测兜底。 - 同一次页面加载内换登录用户时归零并广播,不继承前一个人的完成态。
Summary by CodeRabbit