Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,25 @@
isAuthenticated?: boolean;
isApplicationLoading?: boolean;
isAuthenticating?: boolean;
applicationsLoaded?: boolean;
currentUser?: Record<string, unknown>;
}) => {
mockUseApplicationStore.mockImplementation(() => ({
const state = {
currentUser: {},
isAuthenticated: true,
isApplicationLoading: false,
isAuthenticating: false,
applicationsLoaded: true,
...overrides,
}));
};

// Honour the selector so individual fields (notably `applicationsLoaded`,
// which gates route registration) can be varied independently.
mockUseApplicationStore.mockImplementation((selector?: unknown) =>
typeof selector === 'function'
? (selector as (s: typeof state) => unknown)(state)
: state
);
};

const ModeRoutesMock: ComponentType = () => (
Expand Down Expand Up @@ -122,9 +132,9 @@
renderRouter();

expect(
await screen.findByTestId('default-authenticated-routes')

Check warning on line 135 in openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.test.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

Define a constant instead of duplicating this literal 7 times
).toBeInTheDocument();
expect(screen.queryByTestId('custom-mode-routes')).not.toBeInTheDocument();

Check warning on line 137 in openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.test.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

Define a constant instead of duplicating this literal 5 times
});

it('wraps the rendered routes in AuthenticatedApp for an authenticated user', async () => {
Expand Down Expand Up @@ -162,6 +172,40 @@
expect(screen.queryByTestId('custom-mode-routes')).not.toBeInTheDocument();
});

it('holds a loader instead of the default routes while a non-default mode is still registering', async () => {
// `applications` not loaded yet — the owning plugin registers its routes in
// an effect gated on that, so the registry is legitimately empty here.
setAuthState({ isAuthenticated: true, applicationsLoaded: false });
writeAppMode('ai');

renderRouter();

expect(await screen.findByTestId('full-screen-loader')).toBeInTheDocument();
// Rendering the default routes here would mount their `path="*"`
// catch-all, which navigates to /404 and destroys the requested URL.
expect(
screen.queryByTestId('default-authenticated-routes')
).not.toBeInTheDocument();
});

it('renders the mode routes once the owning plugin registers them after applications load', async () => {
setAuthState({ isAuthenticated: true, applicationsLoaded: false });
writeAppMode('ai');

renderRouter();

expect(await screen.findByTestId('full-screen-loader')).toBeInTheDocument();

act(() => {
useAppRoutesRegistry.getState().registerRoutes('ai', ModeRoutesMock);
});

expect(await screen.findByTestId('custom-mode-routes')).toBeInTheDocument();
expect(
screen.queryByTestId('default-authenticated-routes')
).not.toBeInTheDocument();
});

it('swaps to the registered mode component when the AppMode changes mid-session', async () => {
setAuthState({ isAuthenticated: true });
act(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { isEmpty } from 'lodash';
import { lazy } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { useShallow } from 'zustand/react/shallow';
import { DEFAULT_APP_MODE } from '../../constants/appMode.constants';
import { APP_ROUTER_ROUTES } from '../../constants/router.constants';
import { useApplicationStore } from '../../hooks/useApplicationStore';
import { useAppMode } from '../../hooks/useAppMode';
Expand Down Expand Up @@ -82,7 +83,18 @@ const AppRouter = () => {
const appMode = useAppMode();
const ModeRoutes = useAppRoutesRegistry((state) => state.routes[appMode]);

useResolvedAppMode();
const isRegistrySettled = useResolvedAppMode();

// A non-default mode's routes are registered by the plugin that owns the
// mode, in an effect that waits for `applications` to load. Until then the
// registry looks empty, and falling through to `AuthenticatedRoutes` mounts
// its `path="*"` catch-all, which navigates to /404 and destroys the
// requested URL before the real routes ever mount — a deep link or a reload
// on any mode-specific route lands on Not Found, non-deterministically.
// `registrySettled` is what distinguishes that startup window from a mode
// whose plugin is genuinely uninstalled, where falling back is correct.
const isModeRoutesPending =
appMode !== DEFAULT_APP_MODE && !ModeRoutes && !isRegistrySettled;

/**
* isApplicationLoading is true when the application is loading in AuthProvider
Expand All @@ -99,9 +111,17 @@ const AppRouter = () => {
if (isAuthenticated) {
const AuthenticatedRoutesComponent = ModeRoutes ?? AuthenticatedRoutes;

// The loader has to sit INSIDE AuthenticatedApp: `ApplicationsProvider`
// lives there, and it is what flips `applicationsLoaded` and so unblocks
// registration. Short-circuiting above this point would deadlock — the
// routes could never register, so the loader would never clear.
return (
<AuthenticatedApp>
<AuthenticatedRoutesComponent />
{isModeRoutesPending ? (
<Loader fullScreen />
) : (
<AuthenticatedRoutesComponent />
)}
</AuthenticatedApp>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
}}
owners={value?.assignee ? [value.assignee] : []}
placeHolder={t('label.no-entity', {
entity: t('label.assignee'),

Check warning on line 114 in openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManagerTable.component.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

Define a constant instead of duplicating this literal 3 times
})}
tooltipText={t('label.edit-entity', {
entity: t('label.assignee'),
Expand Down Expand Up @@ -142,14 +142,14 @@
() => (
<div className="tw:p-4">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton className="tw:mb-2" height={40} key={i} width="100%" />

Check warning on line 145 in openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManagerTable.component.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

Do not use Array index in keys
))}
</div>
),
[]
);

const renderRow = (record: TestCaseResolutionStatus) => {

Check warning on line 152 in openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManagerTable.component.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

{"message":"Function has a complexity of 11 which is greater than 10 authorized.","cost":1,"secondaryLocations":[{"line":152,"column":55,"endLine":152,"endColumn":57,"message":"+1"},{"line":155,"column":30,"endLine":155,"endColumn":32,"message":"+1"},{"line":164,"column":31,"endLine":164,"endColumn":33,"message":"+1"},{"line":176,"column":38,"endLine":176,"endColumn":40,"message":"+1"},{"line":181,"column":24,"endLine":181,"endColumn":26,"message":"+1"},{"line":186,"column":46,"endLine":186,"endColumn":48,"message":"+1"},{"line":194,"column":40,"endLine":194,"endColumn":42,"message":"+1"},{"line":202,"column":31,"endLine":202,"endColumn":32,"message":"+1"},{"line":208,"column":52,"endLine":208,"endColumn":54,"message":"+1"},{"line":214,"column":31,"endLine":214,"endColumn":32,"message":"+1"},{"line":219,"column":52,"endLine":219,"endColumn":54,"message":"+1"}]}
const ref = record.testCaseReference;
const tableFqn = getPartialNameFromTableFQN(
ref?.fullyQualifiedName ?? '',
Expand All @@ -162,9 +162,14 @@

return (
<Table.Row id={record.id ?? ''} key={record.id}>
<Table.Cell className="tw:w-72 tw:min-w-56">
{/* The table lays out `auto`, so `wrap-break-word` would leave a long
test case name as one unbreakable min-content word and stretch the
column to fit it. `wrap-anywhere` shrinks that contribution; the
floor matches the declared width so auto-layout cannot then
collapse the column and wrap every name onto three lines. */}
<Table.Cell className="tw:w-72 tw:min-w-72">
<Link
className="tw:m-0 tw:wrap-break-word"
className="tw:m-0 tw:wrap-anywhere"
data-testid={`test-case-${ref?.name}`}
state={{ breadcrumbData }}
to={observabilityRouterClassBase.getTestCaseDetailPagePath(
Expand Down Expand Up @@ -217,7 +222,12 @@
/>
)}
</Table.Cell>
<Table.Cell>
{/* 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 render nowrap+ellipsis already, so once every
row is unassigned nothing holds the column open and it collapses,
stacking the placeholder one letter per line. */}
<Table.Cell className="tw:whitespace-nowrap">
{testCaseResolutionStatusDetailsRender(
record.testCaseResolutionStatusDetails,
record
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,13 @@
* the resolver falls through to compute a fresh candidate.
*
* Consumers should invoke this hook exactly once, high in the tree
* (e.g. `AppRoot`). It has no return value — its effects are `writeAppMode`
* / `clearAppMode` calls.
* (e.g. `AppRoot`). Its work is done through effects — `writeAppMode` /
* `clearAppMode` calls. It returns the `registrySettled` flag described
* below so the router can tell "the owning plugin has not registered its
* routes yet" apart from "no plugin owns this mode", which are otherwise
* indistinguishable from an empty registry.
*/
export const useResolvedAppMode = (): void => {
export const useResolvedAppMode = (): boolean => {
const defaultPersonaId = useApplicationStore(
(state) => state.currentUser?.defaultPersona?.id
);
Expand Down Expand Up @@ -162,7 +165,7 @@
retry: false,
});

useEffect(() => {

Check warning on line 168 in openmetadata-ui/src/main/resources/ui/src/hooks/useResolvedAppMode.ts

View workflow job for this annotation

GitHub Actions / checkstyle

{"message":"Function has a complexity of 23 which is greater than 10 authorized.","cost":13,"secondaryLocations":[{"line":168,"column":15,"endLine":168,"endColumn":17,"message":"+1"},{"line":169,"column":4,"endLine":169,"endColumn":6,"message":"+1"},{"line":169,"column":25,"endLine":169,"endColumn":27,"message":"+1"},{"line":176,"column":4,"endLine":176,"endColumn":6,"message":"+1"},{"line":176,"column":26,"endLine":176,"endColumn":28,"message":"+1"},{"line":199,"column":48,"endLine":199,"endColumn":49,"message":"+1"},{"line":199,"column":14,"endLine":199,"endColumn":16,"message":"+1"},{"line":200,"column":4,"endLine":200,"endColumn":6,"message":"+1"},{"line":200,"column":16,"endLine":200,"endColumn":18,"message":"+1"},{"line":201,"column":6,"endLine":201,"endColumn":8,"message":"+1"},{"line":207,"column":4,"endLine":207,"endColumn":6,"message":"+1"},{"line":207,"column":21,"endLine":207,"endColumn":23,"message":"+1"},{"line":217,"column":30,"endLine":217,"endColumn":31,"message":"+1"},{"line":218,"column":4,"endLine":218,"endColumn":6,"message":"+1"},{"line":218,"column":33,"endLine":218,"endColumn":35,"message":"+1"},{"line":219,"column":6,"endLine":219,"endColumn":8,"message":"+1"},{"line":233,"column":6,"endLine":233,"endColumn":8,"message":"+1"},{"line":238,"column":46,"endLine":238,"endColumn":48,"message":"+1"},{"line":242,"column":26,"endLine":242,"endColumn":28,"message":"+1"},{"line":241,"column":20,"endLine":241,"endColumn":22,"message":"+1"},{"line":240,"column":28,"endLine":240,"endColumn":30,"message":"+1"},{"line":248,"column":4,"endLine":248,"endColumn":6,"message":"+1"},{"line":248,"column":39,"endLine":248,"endColumn":41,"message":"+1"}]}

Check warning on line 168 in openmetadata-ui/src/main/resources/ui/src/hooks/useResolvedAppMode.ts

View workflow job for this annotation

GitHub Actions / checkstyle

Refactor this function to reduce its Cognitive Complexity from 20 to the 15 allowed
if (!isAuthenticated || !currentUser?.name) {
return;
}
Expand Down Expand Up @@ -258,4 +261,6 @@
registeredRoutes,
registrySettled,
]);

return registrySettled;
};
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,19 @@
const DataQualityProvider = ({
children,
createActions,
isActive = true,
}: {
children: React.ReactNode;
createActions?: DataQualityContextInterface['createActions'];
/**
* Whether this page currently owns the URL. Filters here are derived from the
* query string, which is global — so a host that keeps the page mounted while
* routing elsewhere (AI mode caches visited routes) must pass `false`, or the
* backgrounded page re-derives its filters from whatever route now owns the
* query string and refetches with another page's params. Defaults to `true`
* for hosts that unmount the page on navigation.
*/
isActive?: boolean;
}) => {
const { tab: activeTab = DataQualityPageTabs.TEST_CASES } =
useRequiredParams<{
Expand Down Expand Up @@ -152,12 +162,19 @@
};

useEffect(() => {
// Backgrounded: hold the last loaded summary rather than refetching against
// a query string that now belongs to another route. Re-running on
// re-activation is intentional — it revalidates against the real filters.
if (!isActive) {
return;
}

if (getPrioritizedViewPermission(testCasePermission, Operation.ViewBasic)) {
fetchTestSummary(filterParams);
} else {
setIsTestCaseSummaryLoading(false);
}
}, [filterKey]);
}, [filterKey, isActive]);

Check warning on line 177 in openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityProvider.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

React Hook useEffect has missing dependencies: 'filterParams' and 'testCasePermission'. Either include them or remove the dependency array

return (
<DataQualityContext.Provider value={dataQualityContextValue}>
Expand Down
Loading