Skip to content

feat(ui): add DestinationFormItemV2 — AntD-free alert destination form - #29539

Merged
chirag-madlani merged 2 commits into
mainfrom
destination-form-item-migration
Jun 29, 2026
Merged

feat(ui): add DestinationFormItemV2 — AntD-free alert destination form#29539
chirag-madlani merged 2 commits into
mainfrom
destination-form-item-migration

Conversation

@chirag-madlani

@chirag-madlani chirag-madlani commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Describe your changes:

I worked on creating DestinationFormItemV2, a fully AntD-free replacement for the existing DestinationFormItem component family, because the alerts destination form still depended on Ant Design while the rest of the UI is migrating to @openmetadata/ui-core-components.

This PR adds 7 new files — DestinationFormItemV2, DestinationSelectItemV2, DestinationConfigField, and TeamAndUserSelectItemV2 — built on react-hook-form + react-aria-components from @openmetadata/ui-core-components, plus 3 test files covering 35 unit tests across all new components.

Type of change:

  • New feature

High-level design:

The new component family mirrors the structure of the existing DestinationFormItem family but replaces all Ant Design primitives (Select, Input, Form, Button, Switch, etc.) with their @openmetadata/ui-core-components equivalents.

Form state is managed entirely via react-hook-form (useFieldArray, useWatch, Controller, useFormContext) — no local state for field values. DestinationConfigField handles the per-destination-type config fields (Slack endpoint, Email receivers, Teams webhook, etc.) using Controller-wrapped Input/PasswordInput/Select components. Validation errors are displayed via standalone <p> elements rather than the errorMessage prop (which react-aria explicitly omits from TextFieldProps/SelectProps). The existing DestinationFormItem is untouched; DestinationFormItemV2 is a parallel addition ready to swap in.

Tests:

Use cases covered

  • Adding and removing destination rows
  • Selecting internal vs external destination categories
  • Warning messages for Owners/Followers/Admins/Teams/Users destinations
  • Notify-downstream toggle and downstream-depth input visibility
  • Destination status alert rendering (success/failure/loading)
  • Team/user search with debounce: open dropdown, search, select, remove badge
  • "No data found" state when search returns empty
  • Test-destination button: enabled/disabled state, API call with filtered/formatted destinations, error toast on failure
  • View-mode hides add/test/remove controls

Unit tests

  • I added unit tests for the new/changed logic.
  • Files added:
    • DestinationFormItemV2/DestinationFormItemV2.test.tsx (13 tests)
    • DestinationFormItemV2/DestinationSelectItemV2/DestinationSelectItemV2.test.tsx (14 tests)
    • DestinationFormItemV2/TeamAndUserSelectItemV2/TeamAndUserSelectItemV2.test.tsx (8 tests)

Backend integration tests

  • Not applicable (no backend API changes).

Ingestion integration tests

  • Not applicable (no ingestion changes).

Playwright (UI) tests

  • Not applicable (no UI changes to existing flows; new component is not yet wired into pages).

Manual testing performed

Not performed — component is not yet wired into any page. Unit tests cover the component behaviour end-to-end.

UI screen recording / screenshots:

Note: It's not actaully used just capture screenshot from temporary usage

12_owners_selected 11_slack_selected 03_destination_row_added 02_destination_form_v2_empty

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • My PR is linked to a GitHub issue via Fixes #<issue-number> above.
  • I have commented on my code, particularly in hard-to-understand areas.
  • I have added tests (unit / integration / Playwright as applicable) and listed them above.

Greptile Summary

This PR adds DestinationFormItemV2, a parallel AntD-free family of alert-destination form components built on react-hook-form and @openmetadata/ui-core-components, with 35 unit tests covering the full interaction surface.

  • DestinationFormItemV2 manages a useFieldArray of destination rows, drives the "test destinations" API call with filtered external config, and delegates each row to DestinationSelectItemV2.
  • DestinationSelectItemV2 handles category/type selection, inline config via DestinationConfigField, and the notify-downstream toggle; TeamAndUserSelectItemV2 is a hand-rolled async combobox with debounced search and badge selection for Teams/Users destinations.
  • The existing DestinationFormItem is untouched; this component is a parallel addition not yet wired into any page.

Confidence Score: 4/5

Safe to merge as a non-wired parallel addition; the debounce cleanup gap in TeamAndUserSelectItemV2 should be fixed before the component is connected to a page.

The component family is not yet wired into any page, so the identified issues carry no immediate user-facing risk. The missing debounce cancel on unmount is a real defect that will cause stale state updates once the component is rendered in production flows — it should be addressed before the wire-up PR. The other findings (dead defaultValue prop, missing email validation, hardcoded placeholder string, raw string comparison for status) are quality concerns that don't block correctness today but will need cleanup before this replaces the legacy form.

TeamAndUserSelectItemV2.tsx (missing debounce cleanup) and DestinationConfigField.tsx (email validation, i18n placeholder) deserve a second look before the component is connected to a page.

Important Files Changed

Filename Overview
openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx Main container component using useFieldArray/useFormContext; logic is solid but defaultValue on the controlled timeout inputs is ignored at runtime.
openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationSelectItemV2/DestinationSelectItemV2.tsx Destination row component; destination type / category / subscription-type wiring is correct; status alert variant uses a raw string 'Success' instead of the generated Status enum.
openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationSelectItemV2/DestinationConfigField/DestinationConfigField.tsx Per-destination config fields; hardcoded OAuth2 token URL placeholder violates i18n policy, and EmailTagInput adds receivers without validating email format.
openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/TeamAndUserSelectItemV2/TeamAndUserSelectItemV2.tsx Custom team/user combobox with debounced search; missing debouncedSearch.cancel() cleanup on unmount can cause state updates after the component is gone.
openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.test.tsx 13 unit tests covering add/remove rows, button enable/disable logic, API call filtering, and error toast; coverage is thorough for the component's surface area.
openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationSelectItemV2/DestinationSelectItemV2.test.tsx 14 tests covering internal/external selection, warning messages, downstream toggle, status alerts, and combobox interaction; well-structured.
openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/TeamAndUserSelectItemV2/TeamAndUserSelectItemV2.test.tsx 8 tests covering dropdown open/close, debounced search, option selection, badge removal, and empty state; fake timers used correctly.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[DestinationFormItemV2] -->|useFieldArray 'destinations'| B[DestinationSelectItemV2 × N]
    A -->|testAlertDestination API| C[Test Destinations Button]
    A -->|useWatch 'resources'| D{Source selected?}
    D -->|No| E[Add/Test buttons disabled]
    D -->|Yes| F[Add/Test buttons enabled]

    B -->|Select.ComboBox| G{Destination Type}
    G -->|External: Slack / MSTeams / GChat / Webhook| H[DestinationConfigField — endpoint + auth accordion]
    G -->|External: Email| I[DestinationConfigField — EmailTagInput]
    G -->|Internal: Teams / Users| J[DestinationConfigField → TeamAndUserSelectItemV2]
    G -->|Internal: Owners / Followers / Admins| K[DestinationConfigField — config flag only]
    G -->|Internal: any| L[Subscription Type Select + Warning Alert]

    J -->|debounced onSearch| M[(Search API)]
    M --> N[Checkbox dropdown with BadgeWithButton tags]

    B -->|notifyDownstream Toggle| O{notifyDownstream?}
    O -->|true| P[downstreamDepth Input]
    O -->|false| Q[hidden]

    C -->|getFormattedDestinations + filter empty config| R[testAlertDestination]
    R -->|results| S[destinationsWithStatus → Alert per row]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[DestinationFormItemV2] -->|useFieldArray 'destinations'| B[DestinationSelectItemV2 × N]
    A -->|testAlertDestination API| C[Test Destinations Button]
    A -->|useWatch 'resources'| D{Source selected?}
    D -->|No| E[Add/Test buttons disabled]
    D -->|Yes| F[Add/Test buttons enabled]

    B -->|Select.ComboBox| G{Destination Type}
    G -->|External: Slack / MSTeams / GChat / Webhook| H[DestinationConfigField — endpoint + auth accordion]
    G -->|External: Email| I[DestinationConfigField — EmailTagInput]
    G -->|Internal: Teams / Users| J[DestinationConfigField → TeamAndUserSelectItemV2]
    G -->|Internal: Owners / Followers / Admins| K[DestinationConfigField — config flag only]
    G -->|Internal: any| L[Subscription Type Select + Warning Alert]

    J -->|debounced onSearch| M[(Search API)]
    M --> N[Checkbox dropdown with BadgeWithButton tags]

    B -->|notifyDownstream Toggle| O{notifyDownstream?}
    O -->|true| P[downstreamDepth Input]
    O -->|false| Q[hidden]

    C -->|getFormattedDestinations + filter empty config| R[testAlertDestination]
    R -->|results| S[destinationsWithStatus → Alert per row]
Loading

Reviews (1): Last reviewed commit: "test(ui): add unit tests for Destination..." | Re-trigger Greptile

Greptile also left 5 inline comments on this PR.

Context used:

  • Context used - CLAUDE.md (source)

chirag-madlani and others added 2 commits June 26, 2026 20:24
New component family under DestinationFormItemV2/ that replicates all
DestinationFormItem behaviour (webhook, email, teams/users, owners/followers/admins,
notify-downstream, connection/read timeout, destination status testing) using
@openmetadata/ui-core-components + react-hook-form, with no Ant Design
dependencies. Lives alongside the existing component until parent pages migrate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
35 tests across 3 suites covering DestinationFormItemV2, DestinationSelectItemV2,
and TeamAndUserSelectItemV2. Uses react-hook-form FormProvider wrapper (no Ant
Design form context) with lightweight ui-core-components mocks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chirag-madlani
chirag-madlani requested a review from a team as a code owner June 26, 2026 17:53
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added safe to test Add this label to run secure Github workflows on PRs UI UI specific issues labels Jun 26, 2026
Comment on lines +153 to +165
<Input
data-testid="connection-timeout-input"
defaultValue="10"
inputDataTestId="connection-timeout-input-field"
placeholder={`${t('label.connection-timeout')} (${t(
'label.second-plural'
)})`}
ref={field.ref}
type="number"
value={field.value !== undefined ? String(field.value) : ''}
onBlur={field.onBlur}
onChange={(val) => field.onChange(val)}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: defaultValue ignored on controlled inputs — defaults never applied

The connection-timeout (defaultValue="10"), read-timeout (defaultValue={String(DEFAULT_READ_TIMEOUT)}) and downstream-depth (defaultValue="1") inputs are rendered as controlled components — they receive value={field.value !== undefined ? String(field.value) : ''}. In React, when both value and defaultValue are supplied, value wins and defaultValue is ignored. Because field.value starts as undefined, the inputs render empty and, more importantly, the form state is never initialized with the intended defaults (10s connection timeout, DEFAULT_READ_TIMEOUT, depth 1). Unless the user manually types a value, these fields submit as undefined. The original AntD form set these via form initial values, so this is a behavior regression.

Suggested fix: initialize the defaults in react-hook-form (e.g. via defaultValues / useEffect setValue when undefined) instead of relying on the defaultValue prop, or fall back to the default inside the value expression (e.g. value={field.value ?? '10'}) and push that default into form state.

Was this helpful? React with 👍 / 👎

Comment on lines +305 to +319
rules={{
required: t('message.field-text-is-required', {
fieldText: t('label.downstream-depth'),
}),
validate: (value) => {
const numVal = Number(value);

return !isEmpty(String(value)) && numVal <= 0
? t('message.value-must-be-greater-than', {
field: t('label.downstream-depth'),
minimum: 0,
})
: true;
},
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: downstreamDepth validation accepts non-numeric / non-integer input

The validate rule computes numVal = Number(value) and only fails when !isEmpty(String(value)) && numVal <= 0. For a non-numeric entry like "abc", Number("abc") is NaN, and NaN <= 0 is false, so the rule returns true (valid). Decimal values such as 1.5 are also accepted. The original implementation required an integer greater than 0, so the V2 component silently weakens validation and can let invalid depth values through to the API.

Suggested fix: explicitly reject NaN and non-integers, e.g. if (isEmpty(String(value))) return true; const n = Number(value); return Number.isInteger(n) && n > 0 ? true : t(...).

Was this helpful? React with 👍 / 👎

Comment on lines +61 to +64
const debouncedSearch = useMemo(
() => debounce(handleSearch, 500),
[handleSearch]
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Bug: Debounced search not cancelled on unmount

debouncedSearch (lodash debounce, 500ms) is invoked from a useEffect on searchText/entityType changes, but it is never cancelled. If the component unmounts (e.g. the dropdown destination is removed) while a debounced call is pending, the trailing invocation runs handleSearch, which calls setIsLoadingOptions/setOptions after unmount, producing a React state-update-after-unmount warning and a wasted network request.

Suggested fix: return a cleanup that cancels the debounce, e.g. useEffect(() => { debouncedSearch(searchText); return () => debouncedSearch.cancel(); }, [searchText, entityType, debouncedSearch]);.

Was this helpful? React with 👍 / 👎

Comment on lines +91 to +92
destination.category === 'External' &&
externalDestinationTypes.includes(destination.type ?? '')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Quality: Hardcoded 'External' literal instead of SubscriptionCategory.External

isExternalDestinationSelected and the test-destination filter compare against the string literal 'External' while the SubscriptionCategory enum is already imported and used elsewhere in the same component family. SubscriptionCategory.External resolves to "External", so the code works today, but the literal is fragile (a future enum value change would silently break the comparison) and inconsistent with the rest of the file.

Suggested fix: replace destination.category === 'External' and d.category === 'External' with === SubscriptionCategory.External.

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 4 findings

Adds the AntD-free DestinationFormItemV2 component family, but the implementation contains several issues including ignored default values for controlled inputs and missing debounced search cleanup.

⚠️ Bug: defaultValue ignored on controlled inputs — defaults never applied

📄 openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:153-165 📄 openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:186-200 📄 openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationSelectItemV2/DestinationSelectItemV2.tsx:284-297

The connection-timeout (defaultValue="10"), read-timeout (defaultValue={String(DEFAULT_READ_TIMEOUT)}) and downstream-depth (defaultValue="1") inputs are rendered as controlled components — they receive value={field.value !== undefined ? String(field.value) : ''}. In React, when both value and defaultValue are supplied, value wins and defaultValue is ignored. Because field.value starts as undefined, the inputs render empty and, more importantly, the form state is never initialized with the intended defaults (10s connection timeout, DEFAULT_READ_TIMEOUT, depth 1). Unless the user manually types a value, these fields submit as undefined. The original AntD form set these via form initial values, so this is a behavior regression.

Suggested fix: initialize the defaults in react-hook-form (e.g. via defaultValues / useEffect setValue when undefined) instead of relying on the defaultValue prop, or fall back to the default inside the value expression (e.g. value={field.value ?? '10'}) and push that default into form state.

💡 Edge Case: downstreamDepth validation accepts non-numeric / non-integer input

📄 openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationSelectItemV2/DestinationSelectItemV2.tsx:305-319

The validate rule computes numVal = Number(value) and only fails when !isEmpty(String(value)) && numVal <= 0. For a non-numeric entry like "abc", Number("abc") is NaN, and NaN <= 0 is false, so the rule returns true (valid). Decimal values such as 1.5 are also accepted. The original implementation required an integer greater than 0, so the V2 component silently weakens validation and can let invalid depth values through to the API.

Suggested fix: explicitly reject NaN and non-integers, e.g. if (isEmpty(String(value))) return true; const n = Number(value); return Number.isInteger(n) && n > 0 ? true : t(...).

💡 Bug: Debounced search not cancelled on unmount

📄 openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/TeamAndUserSelectItemV2/TeamAndUserSelectItemV2.tsx:61-64 📄 openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/TeamAndUserSelectItemV2/TeamAndUserSelectItemV2.tsx:91-93

debouncedSearch (lodash debounce, 500ms) is invoked from a useEffect on searchText/entityType changes, but it is never cancelled. If the component unmounts (e.g. the dropdown destination is removed) while a debounced call is pending, the trailing invocation runs handleSearch, which calls setIsLoadingOptions/setOptions after unmount, producing a React state-update-after-unmount warning and a wasted network request.

Suggested fix: return a cleanup that cancels the debounce, e.g. useEffect(() => { debouncedSearch(searchText); return () => debouncedSearch.cancel(); }, [searchText, entityType, debouncedSearch]);.

💡 Quality: Hardcoded 'External' literal instead of SubscriptionCategory.External

📄 openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:91-92 📄 openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:111

isExternalDestinationSelected and the test-destination filter compare against the string literal 'External' while the SubscriptionCategory enum is already imported and used elsewhere in the same component family. SubscriptionCategory.External resolves to "External", so the code works today, but the literal is fragile (a future enum value change would silently break the comparison) and inconsistent with the rest of the file.

Suggested fix: replace destination.category === 'External' and d.category === 'External' with === SubscriptionCategory.External.

🤖 Prompt for agents
Code Review: Adds the AntD-free `DestinationFormItemV2` component family, but the implementation contains several issues including ignored default values for controlled inputs and missing debounced search cleanup.

1. ⚠️ Bug: defaultValue ignored on controlled inputs — defaults never applied
   Files: openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:153-165, openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:186-200, openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationSelectItemV2/DestinationSelectItemV2.tsx:284-297

   The connection-timeout (`defaultValue="10"`), read-timeout (`defaultValue={String(DEFAULT_READ_TIMEOUT)}`) and downstream-depth (`defaultValue="1"`) inputs are rendered as controlled components — they receive `value={field.value !== undefined ? String(field.value) : ''}`. In React, when both `value` and `defaultValue` are supplied, `value` wins and `defaultValue` is ignored. Because `field.value` starts as `undefined`, the inputs render empty and, more importantly, the form state is never initialized with the intended defaults (10s connection timeout, DEFAULT_READ_TIMEOUT, depth 1). Unless the user manually types a value, these fields submit as `undefined`. The original AntD form set these via form initial values, so this is a behavior regression.
   
   Suggested fix: initialize the defaults in react-hook-form (e.g. via `defaultValues` / `useEffect` setValue when undefined) instead of relying on the `defaultValue` prop, or fall back to the default inside the `value` expression (e.g. `value={field.value ?? '10'}`) and push that default into form state.

2. 💡 Edge Case: downstreamDepth validation accepts non-numeric / non-integer input
   Files: openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationSelectItemV2/DestinationSelectItemV2.tsx:305-319

   The `validate` rule computes `numVal = Number(value)` and only fails when `!isEmpty(String(value)) && numVal <= 0`. For a non-numeric entry like `"abc"`, `Number("abc")` is `NaN`, and `NaN <= 0` is `false`, so the rule returns `true` (valid). Decimal values such as `1.5` are also accepted. The original implementation required an integer greater than 0, so the V2 component silently weakens validation and can let invalid depth values through to the API.
   
   Suggested fix: explicitly reject `NaN` and non-integers, e.g. `if (isEmpty(String(value))) return true; const n = Number(value); return Number.isInteger(n) && n > 0 ? true : t(...)`.

3. 💡 Bug: Debounced search not cancelled on unmount
   Files: openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/TeamAndUserSelectItemV2/TeamAndUserSelectItemV2.tsx:61-64, openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/TeamAndUserSelectItemV2/TeamAndUserSelectItemV2.tsx:91-93

   `debouncedSearch` (lodash `debounce`, 500ms) is invoked from a `useEffect` on `searchText`/`entityType` changes, but it is never cancelled. If the component unmounts (e.g. the dropdown destination is removed) while a debounced call is pending, the trailing invocation runs `handleSearch`, which calls `setIsLoadingOptions`/`setOptions` after unmount, producing a React state-update-after-unmount warning and a wasted network request.
   
   Suggested fix: return a cleanup that cancels the debounce, e.g. `useEffect(() => { debouncedSearch(searchText); return () => debouncedSearch.cancel(); }, [searchText, entityType, debouncedSearch]);`.

4. 💡 Quality: Hardcoded 'External' literal instead of SubscriptionCategory.External
   Files: openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:91-92, openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItemV2/DestinationFormItemV2.component.tsx:111

   `isExternalDestinationSelected` and the test-destination filter compare against the string literal `'External'` while the `SubscriptionCategory` enum is already imported and used elsewhere in the same component family. `SubscriptionCategory.External` resolves to `"External"`, so the code works today, but the literal is fragile (a future enum value change would silently break the comparison) and inconsistent with the rest of the file.
   
   Suggested fix: replace `destination.category === 'External'` and `d.category === 'External'` with `=== SubscriptionCategory.External`.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Comment on lines +61 to +64
const debouncedSearch = useMemo(
() => debounce(handleSearch, 500),
[handleSearch]
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing debounce cancellation on unmount

debouncedSearch is created via useMemo but never cancelled when the component unmounts. A pending 500 ms search will still fire after unmount — calling setIsLoadingOptions and setOptions on a stale closure. Additionally, when handleSearch changes (because the onSearch prop is replaced), useMemo silently creates a new debounce instance while the old one's pending timer continues running. Add a useEffect that calls debouncedSearch.cancel() in its cleanup to prevent stale state updates.

data-testid={`token-url-input-${fieldName}`}
inputDataTestId={`token-url-input-field-${fieldName}`}
label={`${t('label.token-url')}:`}
placeholder="https://auth.example.com/oauth/token"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded URL string literal violates i18n policy

The token URL placeholder is a raw string literal rather than a translation key. Per the project's CLAUDE.md requirement, all user-visible text must go through useTranslation. This placeholder should map to a locale key (e.g., placeholder.oauth2-token-url or similar).

Suggested change
placeholder="https://auth.example.com/oauth/token"
placeholder={t('placeholder.oauth2-token-url')}

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +153 to +166
<Input
data-testid="connection-timeout-input"
defaultValue="10"
inputDataTestId="connection-timeout-input-field"
placeholder={`${t('label.connection-timeout')} (${t(
'label.second-plural'
)})`}
ref={field.ref}
type="number"
value={field.value !== undefined ? String(field.value) : ''}
onBlur={field.onBlur}
onChange={(val) => field.onChange(val)}
/>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 defaultValue is silently ignored on a controlled Input

Both defaultValue and value are passed to the timeout Input components. React ignores defaultValue when value is present (controlled mode), so the field will render empty whenever the parent form is not initialized with an explicit timeout value. The same issue applies to the readTimeout field (with DEFAULT_READ_TIMEOUT). Either initialize the form with { timeout: '10', readTimeout: String(DEFAULT_READ_TIMEOUT) } in the parent's defaultValues, or drop defaultValue here to avoid the misleading prop that has no runtime effect.

Comment on lines +110 to +111
const statusAlertVariant =
destinationStatusDetails?.status === 'Success' ? 'success' : 'error';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Magic string comparison should use the Status enum

destinationStatusDetails?.status === 'Success' compares against a raw string literal. The generated Status enum is already imported by the test file, and Status.Success = "Success". Using the enum makes this resilient to future value changes in the generated schema.

Suggested change
const statusAlertVariant =
destinationStatusDetails?.status === 'Success' ? 'success' : 'error';
const statusAlertVariant =
destinationStatusDetails?.status === Status.Success ? 'success' : 'error';

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +78 to +90
const addEmail = useCallback(() => {
const trimmed = inputValue.trim();
if (!trimmed || receivers.includes(trimmed)) {
setInputValue('');

return;
}
setValue(`destinations.${fieldName}.config.receivers`, [
...receivers,
trimmed,
]);
setInputValue('');
}, [inputValue, receivers, fieldName, setValue]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No email format validation before adding a receiver

addEmail prevents exact duplicates but allows any arbitrary string (e.g., "not-an-email") to be added to the receivers list. A simple regex or the browser's type="email" constraint should guard against malformed addresses before they are stored in form state.

@github-actions

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 63%
63.43% (70855/111691) 45.94% (40781/88767) 47.84% (12422/25961)

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (15 flaky)

✅ 4460 passed · ❌ 0 failed · 🟡 15 flaky · ⏭️ 38 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 323 0 2 4
🟡 Shard 2 821 0 8 9
🟡 Shard 3 828 0 1 7
🟡 Shard 4 830 0 1 10
✅ Shard 5 873 0 0 0
🟡 Shard 6 785 0 3 8
🟡 15 flaky test(s) (passed on retry)
  • Pages/AuditLogs.spec.ts › should create audit log entry when glossary is updated (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › a fully denied user sees neither asset type when browsing (shard 1, 2 retries)
  • Features/BulkEditEntity.spec.ts › Glossary (shard 2, 1 retry)
  • Features/BulkEditOperationBadges.spec.ts › Database service bulk edit search filters rows and clear restores them (shard 2, 1 retry)
  • Features/BulkImport.spec.ts › Database service (shard 2, 1 retry)
  • Features/BulkImport.spec.ts › Database Schema (shard 2, 1 retry)
  • Features/ContextCenter.spec.ts › clicking a memory row opens the view-only modal (shard 2, 1 retry)
  • Features/DataQuality/ColumnLevelTests.spec.ts › Column Values Length To Be Between (shard 2, 1 retry)
  • Features/DataQuality/ColumnLevelTests.spec.ts › Column Values Missing Count To Be Equal (shard 2, 1 retry)
  • Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts › Admin: Complete export-import-validate flow (shard 2, 1 retry)
  • Features/Tasks/TaskNavigation.spec.ts › navigating to /table/TASK-XXXXX should show 404 (invalid URL pattern) (shard 3, 2 retries)
  • Pages/DescriptionVisibility.spec.ts › Glossary Term truncates long description and end of text is not visible before expand (shard 4, 1 retry)
  • Pages/GlossaryImportExport.spec.ts › Glossary Bulk Import Export (shard 6, 1 retry)
  • Pages/InputOutputPorts.spec.ts › Output ports section collapse/expand (shard 6, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify lineage schema filter selection (shard 6, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

@chirag-madlani

Copy link
Copy Markdown
Collaborator Author

UI Screenshots — DestinationFormItemV2

Captured against the Add Notification page with the component temporarily wired in.

State 1: Initial (empty) state

Component renders with timeout inputs and disabled Add/Test Destination buttons when no resource is selected, enabled when resource is set.

State 2: Destination row added — category dropdown open

The Select.ComboBox from @openmetadata/ui-core-components shows INTERNAL categories (Admins, Assignees, Followers, Mentions, Owners) and EXTERNAL destinations (Slack, Email, MSTeams, etc.) with category grouping.

State 3: Slack selected (external destination)

  • Webhook URL input appears inline next to the category selector
  • Advanced Configuration accordion expands for additional Slack settings
  • Notify Downstream toggle shows
  • Test Destinations button becomes enabled

State 4: Owners selected (internal destination)

  • Secondary "Select Destination" dropdown appears for choosing Activity Feed / Email / etc.
  • Test Destinations button remains disabled (internal destinations cannot be tested)

Screenshots available locally at /tmp/pr_screenshots/ — drag them into the PR description to embed.

@chirag-madlani chirag-madlani added the skip-pr-checks Bypass PR metadata validation check label Jun 27, 2026
@chirag-madlani
chirag-madlani merged commit 4a7f349 into main Jun 29, 2026
95 of 99 checks passed
@chirag-madlani
chirag-madlani deleted the destination-form-item-migration branch June 29, 2026 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Add this label to run secure Github workflows on PRs skip-pr-checks Bypass PR metadata validation check UI UI specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants