Skip to content

fix(ui): stop deep links 404ing on cold boot and fix incident table column sizing - #31301

Merged
shah-harshit merged 4 commits into
open-metadata:mainfrom
ShaileshParmar11:fix/ai-mode-router-cold-boot-and-incident-table-layout
Aug 12, 2026
Merged

fix(ui): stop deep links 404ing on cold boot and fix incident table column sizing#31301
shah-harshit merged 4 commits into
open-metadata:mainfrom
ShaileshParmar11:fix/ai-mode-router-cold-boot-and-incident-table-layout

Conversation

@ShaileshParmar11

@ShaileshParmar11 ShaileshParmar11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Problem

Three defects found while reviewing the AI-mode Observability pages. All three are in shared UI — the last two fix Classic mode too.

1. Deep links and reloads land on /404

AppRouter resolves routes as ModeRoutes ?? AuthenticatedRoutes. A non-default app mode's routes are registered by the plugin that owns the mode, in an effect gated on applications loading — so for the first renders the registry is legitimately empty. Falling through to the default routes in that window mounts their path="*" catch-all:

<Route element={<Navigate to={ROUTES.NOT_FOUND} />} path="*" />

which navigates to /404 and destroys the requested URL before the real routes ever mount. It is a race, so it reproduces intermittently — roughly half of hard loads locally.

2. A long test case name stretches the Incident Manager's name column

The cell declares w-72, but the table lays out auto and overflow-wrap: break-word does not reduce a word's min-content contribution. The column grew to the full width of the longest name — 900px on a 1033px container.

3. The Assignee column collapses when every row is unassigned

antd's .ant-typography sets word-break: break-word, which drops the "No Assignee" placeholder's min-content contribution to a single character. Owner names already render nowrap + ellipsis, so once every row is unassigned (e.g. filtering to New) nothing holds the column open and the placeholder stacks one letter per line.

Solution

Routing. useResolvedAppMode already computes a registrySettled flag for exactly this window — it deliberately refuses to clear a valid session before the flag flips. It now returns that flag, and AppRouter holds a loader while a non-default mode is active but unregistered. Once settled and still unregistered (plugin genuinely uninstalled) it falls back to the default routes exactly as before.

The loader sits inside AuthenticatedApp deliberately: ApplicationsProvider lives there and is what flips applicationsLoaded. Short-circuiting above it deadlocks — the routes could never register, so the loader would never clear. There is a comment at the call site to stop that being "simplified" later.

Table. wrap-anywhere on the name link (it does shrink min-content) with the floor pinned to the same 18rem the cell declares, so auto-layout cannot then collapse the column. whitespace-nowrap on the assignee cell, matching the Last Updated cell in the same file.

Measurements

before after
Name column, 116-char name 900px 288px
Assignee column, all unassigned 103px (placeholder 15×108px) 166px (72×18px)

Tests

Two new AppRouter cases covering the registration window. Both were confirmed failing with the router change reverted (2 failed, 5 passed) and passing with it, so they are real regression coverage.

  • yarn jest src/components/AppRouter src/hooks/useResolvedAppMode src/hooks/useAppMode — 11 suites, 112 passed
  • yarn jest src/components/IncidentManager/IncidentManagerTable.component.test.tsx — 10 passed
  • prettier + eslint clean on all changed files

Type of change

  • Bug fix

🤖 Generated with Claude Code


Summary by Gitar

  • DataQualityProvider enhancements:
    • Added isActive prop to freeze URL tracking and avoid background refetches against shared query parameters in cached routes
  • Router and incident table bug fixes:
    • Prevented deep links from 404ing on cold boot by holding a loader until non-default app mode routes are settled
    • Fixed incident table column sizing by updating cell width constraints and wrapping behaviors for name and assignee columns

This will update automatically on new commits.

…olumn sizing

Three defects surfaced while reviewing the AI-mode Observability pages. All
three live in shared UI, so Classic mode benefits from the last two as well.

**Deep links / reloads landing on /404**

`AppRouter` resolved routes as `ModeRoutes ?? AuthenticatedRoutes`. A non-default
app mode registers its routes from the plugin that owns the mode, in an effect
gated on `applications` loading, so for the first few renders the registry is
legitimately empty. Falling through to the default routes in that window mounts
their `path="*"` catch-all, which navigates to /404 and destroys the requested
URL before the real routes ever mount — non-deterministically, depending on
which request settles first.

`useResolvedAppMode` already computes a `registrySettled` flag for exactly this
window (it deliberately refuses to clear a valid session before it flips). It
now returns that flag, and `AppRouter` holds a loader while a non-default mode
is active but unregistered. Once settled and still unregistered — the plugin is
genuinely uninstalled — it falls back to the default routes as before.

The loader sits INSIDE `AuthenticatedApp` on purpose: `ApplicationsProvider`
lives there and is what flips `applicationsLoaded`, so short-circuiting above it
would deadlock.

**Incident table: a long test case name stretched the name column**

The cell declares `w-72`, but the table lays out `auto` and
`overflow-wrap: break-word` does not shrink a word's min-content contribution —
so the column grew to fit the longest name (~900px on a 1033px container).
`wrap-anywhere` does shrink it; the floor is pinned to the same 18rem so
auto-layout cannot then collapse the column and wrap every name onto three
lines.

**Incident table: the Assignee column collapsed when every row was unassigned**

antd's `.ant-typography` sets `word-break: break-word`, which drops the "No
Assignee" placeholder's min-content contribution to a single character. Owner
names already render nowrap + ellipsis, so once every row is unassigned nothing
holds the column open and it collapsed until the placeholder stacked one letter
per line. The cell is now `whitespace-nowrap`, matching the Last Updated cell.

Tests: two new AppRouter cases covering the registration window; both fail
without the router change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added safe to test Add this label to run secure Github workflows on PRs UI UI specific issues labels Aug 10, 2026
The provider derives its filters from the query string, which is global. A host
that keeps the page mounted while routing elsewhere — AI mode caches visited
routes — leaves a backgrounded Data Quality page re-deriving its filters from
whatever route now owns the URL. Its filter keys (`testPlatforms`, `tags`,
`serviceName`, …) overlap with the Test Library's, so applying a Test Library
filter made the hidden page refetch with another page's values, flip its
loading flag, and remount the whole dashboard — 26 stray requests per filter
click, plus an error toast from the generic catch when one of them failed.

`isActive` defaults to true, so hosts that unmount the page on navigation are
unaffected. Re-running the effect on re-activation is intentional: it
revalidates against the page's real filters when it comes back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added safe to test Add this label to run secure Github workflows on PRs and removed safe to test Add this label to run secure Github workflows on PRs labels Aug 10, 2026
@ShaileshParmar11

Copy link
Copy Markdown
Contributor Author

Added a fourth commit: fix(ui): let a host freeze DataQualityProvider's URL tracking.

Problem. DataQualityProvider derives its filters from the query string, which is global:

const filterParams = omit(QueryString.parse(location.search), ['currentPage','pageSize','searchValue'])
useEffect(() => { fetchTestSummary(filterParams) }, [filterKey])

Its filter keys — testPlatforms, tags, serviceName, dataQualityDimension, testCaseStatus, testCaseType — overlap with the Test Library's. A host that keeps the page mounted while routing elsewhere (AI mode caches visited routes) therefore leaves a backgrounded Data Quality page re-deriving its filters from whatever route now owns the URL.

Captured off the wire — the hidden Data Quality page issuing an ES query built from the Test Library's filter:

{"query":{"bool":{"must":[{"terms":{"testPlatforms":"GreatExpectations"}}, 

(Note terms with a string where an array is expected — malformed enough to 400 on a stricter index, which is what surfaces the toast from the generic catch { showErrorToast(error) }.)

That call also flips isTestCaseSummaryLoading, which unmounts and remounts the dashboard — measured at 26 stray requests per filter click.

Change. An optional isActive prop (default true, so hosts that unmount on navigation are unaffected) short-circuits the effect while the page is backgrounded. Re-running on re-activation is intentional — it revalidates against the page's real filters.

Measured on the same repro after wiring it up downstream: 28 requests → 1, no remount, and navigating back still revalidates.

@ShaileshParmar11
ShaileshParmar11 marked this pull request as ready for review August 10, 2026 15:53
@ShaileshParmar11
ShaileshParmar11 requested a review from a team as a code owner August 10, 2026 15: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 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 66%
66.36% (78843/118810) 50.37% (47653/94598) 51.62% (14367/27827)

@github-actions github-actions Bot added safe to test Add this label to run secure Github workflows on PRs and removed safe to test Add this label to run secure Github workflows on PRs labels Aug 12, 2026
@shah-harshit shah-harshit added the skip-pr-checks Bypass PR metadata validation check label Aug 12, 2026
@github-actions github-actions Bot added safe to test Add this label to run secure Github workflows on PRs and removed safe to test Add this label to run secure Github workflows on PRs labels Aug 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ UI Checkstyle passed — lint findings in changed files

🔍 ESLint findings in this PR's files — 0 error(s), 8 warning(s)

Errors block the build. Warnings do not yet — they are rules whose backlog is still
being worked down, listed so this PR does not add to it. See docs/ui-code-quality-gate.md.

0 error(s), 8 warning(s) across 4 changed file(s).

Count Rule
3 sonarjs/no-duplicate-string
2 sonarjs/cyclomatic-complexity
1 react/no-array-index-key
1 sonarjs/cognitive-complexity
1 react-hooks/exhaustive-deps
All findings
Location Rule Message
🟡 src/components/AppRouter/AppRouter.test.tsx:135:33 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 7 times.
🟡 src/components/AppRouter/AppRouter.test.tsx:137:33 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 5 times.
🟡 src/components/IncidentManager/IncidentManagerTable.component.tsx:114:23 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 3 times.
🟡 src/components/IncidentManager/IncidentManagerTable.component.tsx:145:58 react/no-array-index-key Do not use Array index in keys
🟡 src/components/IncidentManager/IncidentManagerTable.component.tsx:152:56 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 11 which is greater than 10 authorized.","cost":1,"secondaryLocations":[{"line":152,"column":55,"endLine":152,"endColum
🟡 src/hooks/useResolvedAppMode.ts:168:16 sonarjs/cognitive-complexity Refactor this function to reduce its Cognitive Complexity from 20 to the 15 allowed.
🟡 src/hooks/useResolvedAppMode.ts:168:16 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 23 which is greater than 10 authorized.","cost":13,"secondaryLocations":[{"line":168,"column":15,"endLine":168,"endColu
🟡 src/pages/DataQuality/DataQualityProvider.tsx:177:6 react-hooks/exhaustive-deps React Hook useEffect has missing dependencies: 'filterParams' and 'testCasePermission'. Either include them or remove the dependency array.

Fix locally (fast - only checks files changed in this branch):

make ui-checkstyle-changed

@sonarqubecloud

Copy link
Copy Markdown

@shah-harshit
shah-harshit added this pull request to the merge queue Aug 12, 2026
Merged via the queue into open-metadata:main with commit af573dd Aug 12, 2026
83 of 150 checks passed
shah-harshit added a commit that referenced this pull request Aug 12, 2026
…olumn sizing (#31301)

* fix(ui): stop deep links 404ing on cold boot and fix incident table column sizing

Three defects surfaced while reviewing the AI-mode Observability pages. All
three live in shared UI, so Classic mode benefits from the last two as well.

**Deep links / reloads landing on /404**

`AppRouter` resolved routes as `ModeRoutes ?? AuthenticatedRoutes`. A non-default
app mode registers its routes from the plugin that owns the mode, in an effect
gated on `applications` loading, so for the first few renders the registry is
legitimately empty. Falling through to the default routes in that window mounts
their `path="*"` catch-all, which navigates to /404 and destroys the requested
URL before the real routes ever mount — non-deterministically, depending on
which request settles first.

`useResolvedAppMode` already computes a `registrySettled` flag for exactly this
window (it deliberately refuses to clear a valid session before it flips). It
now returns that flag, and `AppRouter` holds a loader while a non-default mode
is active but unregistered. Once settled and still unregistered — the plugin is
genuinely uninstalled — it falls back to the default routes as before.

The loader sits INSIDE `AuthenticatedApp` on purpose: `ApplicationsProvider`
lives there and is what flips `applicationsLoaded`, so short-circuiting above it
would deadlock.

**Incident table: a long test case name stretched the name column**

The cell declares `w-72`, but the table lays out `auto` and
`overflow-wrap: break-word` does not shrink a word's min-content contribution —
so the column grew to fit the longest name (~900px on a 1033px container).
`wrap-anywhere` does shrink it; the floor is pinned to the same 18rem so
auto-layout cannot then collapse the column and wrap every name onto three
lines.

**Incident table: the Assignee column collapsed when every row was unassigned**

antd's `.ant-typography` sets `word-break: break-word`, which drops the "No
Assignee" placeholder's min-content contribution to a single character. Owner
names already render nowrap + ellipsis, so once every row is unassigned nothing
holds the column open and it collapsed until the placeholder stacked one letter
per line. The cell is now `whitespace-nowrap`, matching the Last Updated cell.

Tests: two new AppRouter cases covering the registration window; both fail
without the router change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ui): let a host freeze DataQualityProvider's URL tracking

The provider derives its filters from the query string, which is global. A host
that keeps the page mounted while routing elsewhere — AI mode caches visited
routes — leaves a backgrounded Data Quality page re-deriving its filters from
whatever route now owns the URL. Its filter keys (`testPlatforms`, `tags`,
`serviceName`, …) overlap with the Test Library's, so applying a Test Library
filter made the hidden page refetch with another page's values, flip its
loading flag, and remount the whole dashboard — 26 stray requests per filter
click, plus an error toast from the generic catch when one of them failed.

`isActive` defaults to true, so hosts that unmount the page on navigation are
unaffected. Re-running the effect on re-activation is intentional: it
revalidates against the page's real filters when it comes back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Harshit Shah <harshit.shah@getcollate.io>
@gitar-bot

gitar-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 0 resolved / 1 findings

Fixes cold-boot 404s on deep links and corrects incident table column sizing with robust wrapping and layout constraints. Consider adding getAppDefaultMode() to the effect's dependency array to avoid stale reads.

💡 Bug: getAppDefaultMode() read in effect but not a dependency

📄 openmetadata-ui/src/main/resources/ui/src/hooks/useResolvedAppMode.ts:259-263

The effect reads getAppDefaultMode() (a module-level in-memory cache set during AuthProvider bootstrap) but it is not in the dependency array. If the tenant default resolves to a non-default mode and setAppDefaultMode runs after this effect's last execution, the resolver won't re-run to pick it up and the user falls through to DEFAULT_APP_MODE until another dep changes. In practice bootstrap ordering likely makes this benign, but if you observe the tenant default being ignored intermittently, consider surfacing the default mode as a reactive value (state/store) so it participates in the dep array rather than reading a mutable module cache imperatively.

🤖 Prompt for agents
Code Review: Fixes cold-boot 404s on deep links and corrects incident table column sizing with robust wrapping and layout constraints. Consider adding `getAppDefaultMode()` to the effect's dependency array to avoid stale reads.

1. 💡 Bug: getAppDefaultMode() read in effect but not a dependency
   Files: openmetadata-ui/src/main/resources/ui/src/hooks/useResolvedAppMode.ts:259-263

   The effect reads `getAppDefaultMode()` (a module-level in-memory cache set during AuthProvider bootstrap) but it is not in the dependency array. If the tenant default resolves to a non-default mode and `setAppDefaultMode` runs after this effect's last execution, the resolver won't re-run to pick it up and the user falls through to `DEFAULT_APP_MODE` until another dep changes. In practice bootstrap ordering likely makes this benign, but if you observe the tenant default being ignored intermittently, consider surfacing the default mode as a reactive value (state/store) so it participates in the dep array rather than reading a mutable module cache imperatively.

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 👍 / 👎 | Powered by Gitar — free for open source

k-anshul pushed a commit to k-anshul/OpenMetadata that referenced this pull request Aug 12, 2026
…olumn sizing (open-metadata#31301)

* fix(ui): stop deep links 404ing on cold boot and fix incident table column sizing

Three defects surfaced while reviewing the AI-mode Observability pages. All
three live in shared UI, so Classic mode benefits from the last two as well.

**Deep links / reloads landing on /404**

`AppRouter` resolved routes as `ModeRoutes ?? AuthenticatedRoutes`. A non-default
app mode registers its routes from the plugin that owns the mode, in an effect
gated on `applications` loading, so for the first few renders the registry is
legitimately empty. Falling through to the default routes in that window mounts
their `path="*"` catch-all, which navigates to /404 and destroys the requested
URL before the real routes ever mount — non-deterministically, depending on
which request settles first.

`useResolvedAppMode` already computes a `registrySettled` flag for exactly this
window (it deliberately refuses to clear a valid session before it flips). It
now returns that flag, and `AppRouter` holds a loader while a non-default mode
is active but unregistered. Once settled and still unregistered — the plugin is
genuinely uninstalled — it falls back to the default routes as before.

The loader sits INSIDE `AuthenticatedApp` on purpose: `ApplicationsProvider`
lives there and is what flips `applicationsLoaded`, so short-circuiting above it
would deadlock.

**Incident table: a long test case name stretched the name column**

The cell declares `w-72`, but the table lays out `auto` and
`overflow-wrap: break-word` does not shrink a word's min-content contribution —
so the column grew to fit the longest name (~900px on a 1033px container).
`wrap-anywhere` does shrink it; the floor is pinned to the same 18rem so
auto-layout cannot then collapse the column and wrap every name onto three
lines.

**Incident table: the Assignee column collapsed when every row was unassigned**

antd's `.ant-typography` sets `word-break: break-word`, which drops the "No
Assignee" placeholder's min-content contribution to a single character. Owner
names already render nowrap + ellipsis, so once every row is unassigned nothing
holds the column open and it collapsed until the placeholder stacked one letter
per line. The cell is now `whitespace-nowrap`, matching the Last Updated cell.

Tests: two new AppRouter cases covering the registration window; both fail
without the router change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ui): let a host freeze DataQualityProvider's URL tracking

The provider derives its filters from the query string, which is global. A host
that keeps the page mounted while routing elsewhere — AI mode caches visited
routes — leaves a backgrounded Data Quality page re-deriving its filters from
whatever route now owns the URL. Its filter keys (`testPlatforms`, `tags`,
`serviceName`, …) overlap with the Test Library's, so applying a Test Library
filter made the hidden page refetch with another page's values, flip its
loading flag, and remount the whole dashboard — 26 stray requests per filter
click, plus an error toast from the generic catch when one of them failed.

`isActive` defaults to true, so hosts that unmount the page on navigation are
unaffected. Re-running the effect on re-activation is intentional: it
revalidates against the page's real filters when it comes back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Harshit Shah <harshit.shah@getcollate.io>
@ShaileshParmar11
ShaileshParmar11 deleted the fix/ai-mode-router-cold-boot-and-incident-table-layout branch August 12, 2026 11:28
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.

3 participants