Skip to content

feat(ui): add Asset Health widget to the table detail page#29469

Open
siddhant1 wants to merge 7 commits into
mainfrom
figma-asset-health-untitled-rewrite
Open

feat(ui): add Asset Health widget to the table detail page#29469
siddhant1 wants to merge 7 commits into
mainfrom
figma-asset-health-untitled-rewrite

Conversation

@siddhant1

@siddhant1 siddhant1 commented Jun 25, 2026

Copy link
Copy Markdown
Member

What this adds

A new Asset Health widget in the right rail of the table detail page that gives a single, scannable summary of an asset's operational health across four categories:

  • Pipeline — latest metadata ingestion run state (healthy / running / queued / partial success / paused / failed)
  • Data Quality — test execution summary (passing, failed count, queued, aborted)
  • Data Observability — unresolved test-case incidents, escalating to error tone on high-severity anomalies
  • Contract — latest data-contract result (passing / running / queued / partial success / failed-to-run / breach)

Each row renders a status badge with a severity tone. When a category isn't configured yet, the row shows a setup CTA instead — Link pipeline, Add tests, Enable, or Create — that navigates to the right place to set it up. An overall header badge rolls the rows up into All clear / Running / Attention / Critical / Not set up.

How it's built

  • Registered as a customizable detail-page widget (KnowledgePanel.AssetHealth) via TableClassBase and TableTabsUtils, so it participates in the existing page-customization system.
  • Data fetching is isolated in useAssetHealth (React Query); the contract query reuses the shared ['contract', id, EntityType.TABLE] key.
  • All status→presentation logic is pure and lives in AssetHealthWidget.utils.ts, covered by unit tests in AssetHealthWidget.utils.test.ts.
  • UI uses @openmetadata/ui-core-components (Badge, FeaturedIcon, Skeleton) and Untitled icons; all strings are i18n keys synced across all locales.

Testing

  • yarn test AssetHealthWidget.utils.test.ts28/28 pass (pipeline / data-quality / observability / contract mappings + header roll-up + empty-subtitle edge cases)
  • tsc --noEmit clean on the touched files; eslint + prettier --check clean; yarn check-i18n reports locales in sync

🤖 Generated with Claude Code

Greptile Summary

This PR adds an Asset Health widget to the table detail page's right rail, surfacing pipeline, data quality, data observability, and contract health in a single scannable panel. The widget is registered as a customizable KnowledgePanel.AssetHealth widget, data is fetched via a dedicated useAssetHealth React Query hook, and all status-to-presentation mappings are pure functions with 28 unit tests.

  • New widget (AssetHealthWidget, AssetHealthRowItem, useAssetHealth) provides a four-row health summary with setup CTAs for unconfigured categories and an overall header badge rolling up all rows.
  • Registration in TableClassBase and TableTabsUtils wires the widget into the existing page-customization system with a default height of 3 grid units.
  • i18n keys added across all 17 locale files; Playwright E2E specs cover widget render, CTA visibility, and navigation flows against sample_data tables.

Confidence Score: 4/5

Safe to merge after addressing the badge-label mismatch in the data quality row.

In getDataQualityHealthRow, the success < total fallback branch sets the tone to Warning but never updates badgeLabel from its initial t('label.passing') value. Any table whose test summary has more total tests than successful ones — but no recorded failures, queued, or aborted tests — will render a warning-colored badge that reads "Passing." The rest of the widget logic, data fetching, registration, and tests are well-structured.

AssetHealthWidget.utils.ts — the success < total branch in getDataQualityHealthRow leaves an inconsistent badge label.

Important Files Changed

Filename Overview
openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetHealthWidget/AssetHealthWidget.utils.ts Core status-to-presentation mapping logic; contains a badge label bug where the success < total fallback branch leaves badgeLabel as "Passing" while setting a Warning tone.
openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetHealthWidget/useAssetHealth.ts React Query data-fetching hook; exposes isError (excluding contract 404s intentionally); pipeline query is scoped to service level which is correct for the use case.
openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetHealthWidget/AssetHealthWidget.component.tsx Main widget component; renders loading/error/success states, CTA navigation handler, and summary header badge.
openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetHealthWidget/AssetHealthWidget.utils.test.ts 28 unit tests covering all four health row functions and header roll-up; the success < total warning case only asserts tone, missing a badgeLabel assertion that would have caught the label bug.
openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/AssetHealthWidget.spec.ts Playwright E2E specs targeting sample-data tables; covers widget render, CTA visibility, and navigation flows.
openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts Adds ASSET_HEALTH to the widget-height map and TableWidgetKeys union; straightforward registration.
openmetadata-ui/src/main/resources/ui/src/utils/TableTabsUtils.tsx Registers AssetHealthWidget under the KnowledgePanel.AssetHealth widget key; lazy-loaded correctly.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    AHW[AssetHealthWidget] --> UAH[useAssetHealth hook]
    UAH --> PQ[pipelineQuery\ngetIngestionPipelines\nby serviceFqn]
    UAH --> SQ[summaryQuery\ngetTestCaseExecutionSummary\nby testSuiteId]
    UAH --> IQ[incidentsQuery\ngetListTestCaseIncidentStatus\nby tableFqn]
    UAH --> CQ[contractQuery\ngetContractByEntityId\nby tableId]
    PQ --> GPH[getPipelineHealthRow]
    SQ --> GDQ[getDataQualityHealthRow]
    IQ --> GDO[getDataObservabilityHealthRow]
    CQ --> GCH[getContractHealthRow]
    GPH --> ROWS[rows array]
    GDQ --> ROWS
    GDO --> ROWS
    GCH --> ROWS
    ROWS --> GAH[getAssetHealthHeader]
    GAH --> BADGE[Header Badge: All clear / Running / Attention / Critical / Not set up]
    ROWS --> RI[AssetHealthRowItem x4]
    RI --> CTA{Has CTA?}
    CTA -- yes --> BTN[BadgeWithIcon button navigate]
    CTA -- no --> SBADGE[Status Badge]
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
    AHW[AssetHealthWidget] --> UAH[useAssetHealth hook]
    UAH --> PQ[pipelineQuery\ngetIngestionPipelines\nby serviceFqn]
    UAH --> SQ[summaryQuery\ngetTestCaseExecutionSummary\nby testSuiteId]
    UAH --> IQ[incidentsQuery\ngetListTestCaseIncidentStatus\nby tableFqn]
    UAH --> CQ[contractQuery\ngetContractByEntityId\nby tableId]
    PQ --> GPH[getPipelineHealthRow]
    SQ --> GDQ[getDataQualityHealthRow]
    IQ --> GDO[getDataObservabilityHealthRow]
    CQ --> GCH[getContractHealthRow]
    GPH --> ROWS[rows array]
    GDQ --> ROWS
    GDO --> ROWS
    GCH --> ROWS
    ROWS --> GAH[getAssetHealthHeader]
    GAH --> BADGE[Header Badge: All clear / Running / Attention / Critical / Not set up]
    ROWS --> RI[AssetHealthRowItem x4]
    RI --> CTA{Has CTA?}
    CTA -- yes --> BTN[BadgeWithIcon button navigate]
    CTA -- no --> SBADGE[Status Badge]
Loading

Reviews (7): Last reviewed commit: "Merge branch 'main' into figma-asset-hea..." | Re-trigger Greptile

Surfaces an at-a-glance health summary in the table right rail covering
pipeline runs, data quality tests, data observability incidents, and data
contracts. Each row shows a status badge with a severity tone and renders a
setup CTA (link pipeline, add tests, enable observability, create contract)
when the category isn't configured yet. An overall header badge rolls the
rows up to All clear / Running / Attention / Critical / Not set up.

Wired in as a customizable detail-page widget (KnowledgePanel.AssetHealth)
via TableClassBase and TableTabsUtils, with React Query-backed data fetching
in useAssetHealth and pure mapping logic plus unit tests in the utils module.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@siddhant1 siddhant1 requested a review from a team as a code owner June 25, 2026 09:55
Copilot AI review requested due to automatic review settings June 25, 2026 09:55
@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

Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@siddhant1 siddhant1 added UI UI specific issues safe to test Add this label to run secure Github workflows on PRs skip-pr-checks Bypass PR metadata validation check labels Jun 25, 2026
siddhant1 and others added 2 commits June 25, 2026 15:34
Replaces the raw tw:bg-white palette class on the widget's loading and main
card surfaces with the semantic tw:bg-primary token, which remaps to the
dark surface color under .dark-mode. All other color classes in the widget
already use auto-inverting tokens from the color handbook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The card border and header divider used utility-gray-blue-100, which inverts
to gray-blue-900 (~#101323) in dark mode — nearly identical to the bg-primary
card surface (~#0c0e12) — so the card edge and divider disappeared. Switch to
the semantic border-secondary token, which stays subtle in light mode and is
visible in dark mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 25, 2026 10:11

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

… query errors)

- getAssetHealthHeader: a warning-only roll-up now uses the amber Warning tone
  instead of the red Error tone; Error is reserved for actual error rows.
- getDataQualityHealthRow: treat success < total (with no failed/queued/aborted
  bucket) as a Warning rather than green "Passing".
- useAssetHealth: expose isError (aggregating the pipeline, summary and incident
  queries; the contract query is excluded because its 404 is the expected
  "no contract" case) and render an error state so a failed fetch no longer shows
  a falsely healthy "No anomalies"/setup CTAs.
- Add header tone assertions and a success<total case to the unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +162 to +176
let tone = AssetHealthTone.Success;
let badgeLabel = t('label.passing');
if (failed > 0) {
tone = AssetHealthTone.Error;
badgeLabel = t('message.count-failed-test', { count: failed });
} else if (queued > 0) {
tone = AssetHealthTone.Info;
badgeLabel = t('label.queued');
} else if (aborted > 0) {
tone = AssetHealthTone.Warning;
badgeLabel = t('label.aborted');
} else if (success < total) {
tone = AssetHealthTone.Warning;
}

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: Warning-tone DQ row still shows 'Passing' badge label

In getDataQualityHealthRow, the new branch else if (success < total) { tone = AssetHealthTone.Warning; } (utils.ts:173-174) changes only the tone but leaves badgeLabel = t('label.passing') from line 163 unchanged. As a result, when some tests are neither failed, queued, nor aborted but success < total (e.g. total: 16, success: 14), the row renders a yellow Warning-tone badge that still reads "Passing". The badge text contradicts its tone. The added unit test only asserts row.tone and does not catch the mislabeled badge. Consider setting a more accurate label in this branch (e.g. a partial/incomplete label) so the badge text matches the Warning tone.

Use a distinct label for the partial-success Warning case so the badge text no longer reads 'Passing'.:

} else if (success < total) {
  tone = AssetHealthTone.Warning;
  badgeLabel = t('label.partial');
}
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

Comment on lines +103 to +108
// The contract query is excluded: getContractByEntityId returns a 404 for the
// common "no contract" case, which is already handled as the Create CTA row.
const isError =
pipelineQuery.isError || summaryQuery.isError || incidentsQuery.isError;

return { rows, isLoading, isError };

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: Single query failure hides all healthy Asset Health rows

isError is the OR of pipelineQuery.isError || summaryQuery.isError || incidentsQuery.isError (useAssetHealth.ts:105-106), and the component returns a full-widget error state when isError is true (AssetHealthWidget.component.tsx:99-110). This means a failure in any single category query (e.g. the incidents endpoint) replaces the entire widget with a generic error message, hiding the other three categories whose data loaded successfully. Since the four categories are independent, consider rendering the per-row data that did resolve and only showing an error/empty state for the category that failed, rather than collapsing the whole widget.

Was this helpful? React with 👍 / 👎

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 63%
63.16% (70968/112351) 46.04% (41061/89170) 47.46% (12662/26678)

…ata)

Drives the real app against demo sample_data tables (no seeded fixtures, no
response mocking): asserts the widget renders all four category rows, that an
unconfigured table (fact_sale) shows the Add tests / Enable / Create setup CTAs
and each navigates to its tab, and that a table with a real test suite
(dim_address) renders the data observability row as a status instead of a CTA.
Exhaustive per-tone/transient/loading/error states remain covered by the
AssetHealthWidget.utils unit tests, as noted in the spec header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 25, 2026 11:11

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🔴 Playwright Results — 30 failure(s), 83 flaky

✅ 4379 passed · ❌ 30 failed · 🟡 83 flaky · ⏭️ 37 skipped

Shard Passed Failed Flaky Skipped
🔴 Shard 1 419 2 9 16
🔴 Shard 2 770 27 12 8
🟡 Shard 3 803 0 7 7
🟡 Shard 4 803 0 9 5
🟡 Shard 5 820 0 37 0
🔴 Shard 6 764 1 9 1

Genuine Failures (failed on all attempts)

Flow/PlatformLineage.spec.ts › Verify Platform Lineage View (shard 1)
�[31mTest timeout of 60000ms exceeded.�[39m
Flow/SearchRBAC.spec.ts › a dashboard-scoped user sees dashboards but never tables (shard 1)
Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoHaveCount�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed

Locator:  getByTestId('search-container').locator('[data-testid="entity-header-display-name"]').filter({ hasText: 'pw table ea1290f0-b52f-4ea1-aa22-706eff8a63c4' })
Expected: �[32m0�[39m
Received: �[31m1�[39m
Timeout:  15000ms

Call log:
�[2m  - Expect "toHaveCount" with timeout 15000ms�[22m
�[2m  - waiting for getByTestId('search-container').locator('[data-testid="entity-header-display-name"]').filter({ hasText: 'pw table ea1290f0-b52f-4ea1-aa22-706eff8a63c4' })�[22m
�[2m    19 × locator resolved to 1 element�[22m
�[2m       - unexpected value "1"�[22m

Features/BulkEditEntity.spec.ts › Database Schema (shard 2)
Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoContainText�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed

Locator: locator('.rdg-cell[aria-selected="true"][aria-readonly="true"]')
Expected substring: �[32m"pw-table-1925b054-4ded-4840-943d-7ac6c94a8ad1"�[39m
Received string:    �[31m"PW.91dc5d6c%Shark5a07ad74"�[39m
Timeout: 15000ms

Call log:
�[2m  - Expect "toContainText" with timeout 15000ms�[22m
�[2m  - waiting for locator('.rdg-cell[aria-selected="true"][aria-readonly="true"]')�[22m
�[2m    19 × locator resolved to <div tabindex="0" role="gridcell" aria-colindex="2" aria-selected="true" aria-readonly="true" class="rdg-cell cj343x07-0-0-beta-47 rdg-cell-name rdg-cell-frozen csofj7r7-0-0-beta-47">PW.91dc5d6c%Shark5a07ad74</div>�[22m
�[2m       - unexpected value "PW.91dc5d6c%Shark5a07ad74"�[22m

Features/ContextCenterPermission.spec.ts › user with editAll permission can see restore action but not delete action on an archived document, and can restore it (shard 2)
Error: Document 1f641255-eb4e-4f76-b842-753c8dc5325e did not appear in archive API within 60000ms
Features/DataQuality/ColumnLevelTests.spec.ts › Column Values To Be Not Null (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Values To Be Between (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Values To Be Unique (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Values To Be In Set (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Values To Be Not In Set (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Values To Match Regex (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Values To Not Match Regex (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Value Max To Be Between (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Value Min To Be Between (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Value Mean To Be Between (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Value Median To Be Between (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Value StdDev To Be Between (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Values Sum To Be Between (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Values Length To Be Between (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Values Missing Count To Be Equal (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/ColumnLevelTests.spec.ts › Column Value To Be At Expected Location (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/TableLevelTests.spec.ts › Table Row Count To Be Between (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/TableLevelTests.spec.ts › Table Row Count To Equal (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/TableLevelTests.spec.ts › Table Column Count To Be Between (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/TableLevelTests.spec.ts › Table Column Count To Equal (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/TableLevelTests.spec.ts › Table Column Name To Exist (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/TableLevelTests.spec.ts › Table Column To Match Set (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/TableLevelTests.spec.ts › Table Difference (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/TableLevelTests.spec.ts › Custom SQL Query (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Features/DataQuality/TableLevelTests.spec.ts › Table Row Inserted Count To Be Between (shard 2)
Error: locator.click: Error: strict mode violation: getByText('Data Observability') resolved to 2 elements:
    1) <div class="d-flex justify-between gap-1">Data Observability</div> aka getByTestId('profiler').getByText('Data Observability')
    2) <span class="tw:truncate tw:text-xs tw:font-medium tw:text-primary">Data Observability</span> aka getByTestId('asset-health-row-dataObservability').getByText('Data Observability')

Call log:
�[2m  - waiting for getByText('Data Observability')�[22m

Pages/LogsViewer.spec.ts › Logs page shows breadcrumb, summary, and log viewer or empty state after opening from bundle suite pipeline tab (shard 6)
Error: Failed to trigger pipeline de6b35e2-1638-4db7-a995-c9dc727fc0a5: 400
🟡 83 flaky test(s) (passed on retry)
  • Features/DataAssetRulesEnabled.spec.ts › Verify the ApiEndpoint Entity Action items after rules is Enabled (shard 1, 1 retry)
  • Features/DataAssetRulesEnabled.spec.ts › Verify the Store Procedure Entity Action items after rules is Enabled (shard 1, 1 retry)
  • Features/DataAssetRulesDisabled.spec.ts › Verify the Directory entity item action after rules disabled (shard 1, 2 retries)
  • Features/DescriptionSuggestion.spec.ts › should add and accept a requested topic schema field description (shard 1, 1 retry)
  • Features/Glossary/GlossaryPagination.spec.ts › should filter by InReview status (shard 1, 1 retry)
  • Pages/Lineage/DataAssetLineage.spec.ts › verify create lineage for entity - Worksheet (shard 1, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab IS visible for supported type: metric (shard 1, 1 retry)
  • Pages/SearchSettings.spec.ts › Preview config reflects reverted n-gram weight after save (shard 1, 2 retries)
  • Flow/SearchRBAC.spec.ts › User without permission (shard 1, 1 retry)
  • Features/BulkEditEntity.spec.ts › Database (shard 2, 1 retry)
  • Features/BulkEditEntity.spec.ts › Glossary Term (Nested) (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/BulkImportWithDotInName.spec.ts › Import at database level with dot in service name (shard 2, 1 retry)
  • Features/Container.spec.ts › Container page should show Schema and Children count (shard 2, 1 retry)
  • Features/Container.spec.ts › Container page children pagination (shard 2, 1 retry)
  • Features/Container.spec.ts › expand / collapse should not appear after updating nested fields for container (shard 2, 1 retry)
  • Features/Container.spec.ts › Copy column link button should copy the column URL to clipboard (shard 2, 1 retry)
  • Features/ContextCenter.spec.ts › clicking a memory row opens the view-only modal (shard 2, 1 retry)
  • Features/ContextCenter.spec.ts › edit-memory button on the row opens the modal in edit mode (shard 2, 1 retry)
  • Features/Permissions/EntityPermissions.spec.ts › MlModel deny common operations permissions (shard 3, 1 retry)
  • Features/Permissions/EntityPermissions.spec.ts › File deny common operations permissions (shard 3, 1 retry)
  • Features/RestoreEntityInheritedFields.spec.ts › Validate restore with Inherited domain and data products assigned (shard 3, 1 retry)
  • Features/SearchExport.spec.ts › Export queues a background job and downloads from the jobs tray (shard 3, 1 retry)
  • Features/Table.spec.ts › should persist page size (shard 3, 1 retry)
  • Features/TestSuiteMultiPipeline.spec.ts › TestSuite multi pipeline support (shard 3, 1 retry)
  • Features/TestSuiteMultiPipeline.spec.ts › Edit the pipeline's test case (shard 3, 1 retry)
  • Flow/ExploreDiscovery.spec.ts › Should display domain and owner of deleted asset in suggestions when showDeleted is on (shard 4, 1 retry)
  • Flow/FrequentlyJoined.spec.ts › should display frequently joined table (shard 4, 1 retry)
  • ... and 53 more

📦 Download artifacts

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

Address design review on the Asset Health widget row: top-align the icon and the status badge/CTA to the text (items-start), bump the subtitle to 12px (text-xs), and cap the content column width (max-w-[220px]) so the long Contract description wraps before the right-aligned badge column instead of running under it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 2, 2026 06:21
@gitar-bot

gitar-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 2 resolved / 4 findings

Adds a new Asset Health widget to the table detail page with comprehensive test coverage, though the pipeline query lacks specific ordering and error handling is overly broad. Please address the potential for masking failing pipelines and ensure partial failure states correctly affect individual row visibility.

💡 Quality: Warning-tone DQ row still shows 'Passing' badge label

📄 openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetHealthWidget/AssetHealthWidget.utils.ts:162-176

In getDataQualityHealthRow, the new branch else if (success < total) { tone = AssetHealthTone.Warning; } (utils.ts:173-174) changes only the tone but leaves badgeLabel = t('label.passing') from line 163 unchanged. As a result, when some tests are neither failed, queued, nor aborted but success < total (e.g. total: 16, success: 14), the row renders a yellow Warning-tone badge that still reads "Passing". The badge text contradicts its tone. The added unit test only asserts row.tone and does not catch the mislabeled badge. Consider setting a more accurate label in this branch (e.g. a partial/incomplete label) so the badge text matches the Warning tone.

Use a distinct label for the partial-success Warning case so the badge text no longer reads 'Passing'.
} else if (success < total) {
  tone = AssetHealthTone.Warning;
  badgeLabel = t('label.partial');
}
💡 Edge Case: Single query failure hides all healthy Asset Health rows

📄 openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetHealthWidget/useAssetHealth.ts:103-108 📄 openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetHealthWidget/AssetHealthWidget.component.tsx:99-110

isError is the OR of pipelineQuery.isError || summaryQuery.isError || incidentsQuery.isError (useAssetHealth.ts:105-106), and the component returns a full-widget error state when isError is true (AssetHealthWidget.component.tsx:99-110). This means a failure in any single category query (e.g. the incidents endpoint) replaces the entire widget with a generic error message, hiding the other three categories whose data loaded successfully. Since the four categories are independent, consider rendering the per-row data that did resolve and only showing an error/empty state for the category that failed, rather than collapsing the whole widget.

✅ 2 resolved
Quality: Warning-only health rolls up to red Error tone in header

📄 openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetHealthWidget/AssetHealthWidget.utils.ts:303-304
In getAssetHealthHeader, the errorCount === 1 || hasWarning branch always sets tone: AssetHealthTone.Error (which maps to the red error badge color). This means a state where the only issue is a warning (e.g. a partial-success pipeline/contract, or aborted-only test runs) renders the header badge in the red error tone, while the individual row for that category correctly shows the amber warning tone. The header severity therefore visually over-escalates a pure-warning state. AssetHealthTone.Warning is never used for the header at all. Consider splitting this branch so a warning-only roll-up uses AssetHealthTone.Warning and only an actual error uses AssetHealthTone.Error (the existing unit test only asserts labelKey, not tone, so this slipped through).

Edge Case: Data quality shows green 'Passing' when not all tests succeeded

📄 openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetHealthWidget/AssetHealthWidget.utils.ts:162-176
getDataQualityHealthRow defaults to tone = Success / badgeLabel = 'label.passing' and only downgrades when failed, queued, or aborted are > 0. If a summary reports total greater than success but with zero failed/queued/aborted (e.g. tests in some other/unaccounted state, or success + failed + queued + aborted < total), the row renders a green 'Passing' badge with a subtitle like '14 / 16 tests', which is misleading. Consider treating success < total (after accounting for known buckets) as a non-success tone, or verifying the summary always partitions cleanly into the four handled buckets.

🤖 Prompt for agents
Code Review: Adds a new Asset Health widget to the table detail page with comprehensive test coverage, though the pipeline query lacks specific ordering and error handling is overly broad. Please address the potential for masking failing pipelines and ensure partial failure states correctly affect individual row visibility.

1. 💡 Quality: Warning-tone DQ row still shows 'Passing' badge label
   Files: openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetHealthWidget/AssetHealthWidget.utils.ts:162-176

   In `getDataQualityHealthRow`, the new branch `else if (success < total) { tone = AssetHealthTone.Warning; }` (utils.ts:173-174) changes only the tone but leaves `badgeLabel = t('label.passing')` from line 163 unchanged. As a result, when some tests are neither failed, queued, nor aborted but `success < total` (e.g. total: 16, success: 14), the row renders a yellow Warning-tone badge that still reads "Passing". The badge text contradicts its tone. The added unit test only asserts `row.tone` and does not catch the mislabeled badge. Consider setting a more accurate label in this branch (e.g. a partial/incomplete label) so the badge text matches the Warning tone.

   Fix (Use a distinct label for the partial-success Warning case so the badge text no longer reads 'Passing'.):
   } else if (success < total) {
     tone = AssetHealthTone.Warning;
     badgeLabel = t('label.partial');
   }

2. 💡 Edge Case: Single query failure hides all healthy Asset Health rows
   Files: openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetHealthWidget/useAssetHealth.ts:103-108, openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetHealthWidget/AssetHealthWidget.component.tsx:99-110

   `isError` is the OR of `pipelineQuery.isError || summaryQuery.isError || incidentsQuery.isError` (useAssetHealth.ts:105-106), and the component returns a full-widget error state when `isError` is true (AssetHealthWidget.component.tsx:99-110). This means a failure in any single category query (e.g. the incidents endpoint) replaces the entire widget with a generic error message, hiding the other three categories whose data loaded successfully. Since the four categories are independent, consider rendering the per-row data that did resolve and only showing an error/empty state for the category that failed, rather than collapsing the whole widget.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 4 comments.

Comment on lines +173 to +175
} else if (success < total) {
tone = AssetHealthTone.Warning;
}
Comment on lines +137 to +141
it('should be a warning tone when success is below total without a known failure bucket', () => {
const row = getDataQualityHealthRow(true, { total: 16, success: 14 });

expect(row.tone).toBe(AssetHealthTone.Warning);
});
Comment on lines +53 to +56
originEntityFQN: tableFqn,
}),
enabled: Boolean(testSuiteId),
});
Comment on lines +82 to +85
getDataObservabilityHealthRow(
Boolean(testSuiteId),
incidentsQuery.data?.data ?? []
),
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

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