Skip to content

Commit 161a88e

Browse files
committed
Merge branch 'main' of https://github.com/opendatahub-io/odh-dashboard into RHOAIENG-56269-inline-rag-fail-on-absent-vector-store-configmap
2 parents d131643 + 256c356 commit 161a88e

173 files changed

Lines changed: 4903 additions & 4442 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/@mf-types/@mf/modelRegistry/compiled-types/src/odh/extension-points/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './details';
55
export * from './detailsCard';
66
export * from './table';
77
export * from './admin';
8+
export * from './namespace-selector';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Extension } from '@openshift/dynamic-plugin-sdk';
2+
import type { ComponentCodeRef } from '@odh-dashboard/plugin-core';
3+
export type NamespaceSelectorFieldProps = {
4+
selectedNamespace: string;
5+
onSelect: (namespace: string) => void;
6+
hasAccess?: boolean | undefined;
7+
isLoading?: boolean;
8+
error?: Error | undefined;
9+
cannotCheck?: boolean;
10+
registryName?: string;
11+
};
12+
/**
13+
* Extension point for providing a custom namespace/project selector component.
14+
* This allows ODH to replace the upstream namespace selector with its own
15+
* project selector that uses the OpenShift Projects API (which supports
16+
* non-admin listing) and filters out system projects.
17+
*/
18+
export type NamespaceSelectorExtension = Extension<'model-registry.namespace/selector', {
19+
component: ComponentCodeRef<NamespaceSelectorFieldProps>;
20+
}>;
21+
export declare const isNamespaceSelectorExtension: (extension: Extension) => extension is NamespaceSelectorExtension;

frontend/src/app/AppRoutes.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import React from 'react';
22
import { Route, Routes } from 'react-router-dom';
3-
import { isRouteExtension } from '@odh-dashboard/plugin-core/extension-points';
3+
import {
4+
isRouteExtension,
5+
isTabRoutePageExtension,
6+
TabRoutePageExtension,
7+
} from '@odh-dashboard/plugin-core/extension-points';
48
import { LazyCodeRefComponent, useExtensions } from '@odh-dashboard/plugin-core';
9+
import TabRoutePage from '#~/app/navigation/TabRoutePage';
510
import { InvalidArgoDeploymentAlert } from '#~/concepts/pipelines/content/InvalidArgoDeploymentAlert';
611
import ApplicationsPage from '#~/pages/ApplicationsPage';
712
import UnauthorizedError from '#~/pages/UnauthorizedError';
@@ -18,6 +23,7 @@ const fallback = <ApplicationsPage title="" description="" loaded={false} empty
1823
const AppRoutes: React.FC = () => {
1924
const { isAllowed } = useUser();
2025
const routeExtensions = useExtensions(isRouteExtension);
26+
const tabRoutePageExtensions = useExtensions<TabRoutePageExtension>(isTabRoutePageExtension);
2127

2228
const dynamicRoutes = React.useMemo(
2329
() =>
@@ -37,6 +43,18 @@ const AppRoutes: React.FC = () => {
3743
[routeExtensions],
3844
);
3945

46+
const tabRoutePages = React.useMemo(
47+
() =>
48+
tabRoutePageExtensions.map((pageExtension) => (
49+
<Route
50+
key={pageExtension.uid}
51+
path={pageExtension.properties.path}
52+
element={<TabRoutePage extension={pageExtension} />}
53+
/>
54+
)),
55+
[tabRoutePageExtensions],
56+
);
57+
4058
if (!isAllowed) {
4159
return (
4260
<Routes>
@@ -50,6 +68,7 @@ const AppRoutes: React.FC = () => {
5068
<InvalidArgoDeploymentAlert />
5169
<Routes>
5270
{dynamicRoutes}
71+
{tabRoutePages}
5372
<Route path="/dependency-missing/:area" element={<DependencyMissingPage />} />
5473
<Route path="*" element={<NotFound />} />
5574
</Routes>

frontend/src/app/navigation/ExtensibleNav.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import React from 'react';
22
import { Nav, NavList } from '@patternfly/react-core';
3-
import { isNavExtension, NavExtension } from '@odh-dashboard/plugin-core/extension-points';
3+
import {
4+
isNavExtension,
5+
isTabRoutePageExtension,
6+
NavExtension,
7+
TabRoutePageExtension,
8+
} from '@odh-dashboard/plugin-core/extension-points';
49
import { useExtensions } from '@odh-dashboard/plugin-core';
510
import { NavItem } from './NavItem';
611
import { getTopLevelExtensions } from './utils';
@@ -9,15 +14,21 @@ type Props = {
914
label: string;
1015
};
1116

17+
type AnyNavExtension = NavExtension | TabRoutePageExtension;
18+
1219
export const ExtensibleNav: React.FC<Props> = ({ label }) => {
13-
const extensions = useExtensions<NavExtension>(isNavExtension);
14-
const topLevelExtensions = React.useMemo(() => getTopLevelExtensions(extensions), [extensions]);
20+
const navExtensions = useExtensions<NavExtension>(isNavExtension);
21+
const tabRouteExtensions = useExtensions<TabRoutePageExtension>(isTabRoutePageExtension);
22+
const topLevelExtensions = React.useMemo(
23+
() => getTopLevelExtensions<AnyNavExtension>([...navExtensions, ...tabRouteExtensions]),
24+
[navExtensions, tabRouteExtensions],
25+
);
1526

1627
return (
1728
<Nav aria-label={label}>
1829
<NavList>
1930
{topLevelExtensions.map((extension) => (
20-
<NavItem key={extension.uid} extension={extension} />
31+
<NavItem key={extension.properties.id} extension={extension} />
2132
))}
2233
</NavList>
2334
</Nav>

frontend/src/app/navigation/NavItem.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,25 @@ import * as React from 'react';
22
import {
33
StatusReport,
44
NavExtension,
5+
TabRoutePageExtension,
56
isNavSectionExtension,
7+
isTabRoutePageExtension,
68
} from '@odh-dashboard/plugin-core/extension-points';
79
import { NavItemHref } from './NavItemHref';
10+
import { NavItemTabRoute } from './NavItemTabRoute';
811
import { NavSection } from './NavSection';
912

1013
export type Props = {
11-
extension: NavExtension;
14+
extension: NavExtension | TabRoutePageExtension;
1215
onNotifyStatus?: (status: StatusReport | undefined) => void;
1316
};
1417

1518
export const NavItem: React.FC<Props> = ({ extension, onNotifyStatus }) => {
1619
if (isNavSectionExtension(extension)) {
1720
return <NavSection extension={extension} />;
1821
}
22+
if (isTabRoutePageExtension(extension)) {
23+
return <NavItemTabRoute extension={extension} />;
24+
}
1925
return <NavItemHref extension={extension} onNotifyStatus={onNotifyStatus} />;
2026
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as React from 'react';
2+
import { NavItem } from '@patternfly/react-core';
3+
import { Link, useMatch } from 'react-router-dom';
4+
import {
5+
isTabRouteTabExtension,
6+
TabRoutePageExtension,
7+
TabRouteTabExtension,
8+
} from '@odh-dashboard/plugin-core/extension-points';
9+
import { useExtensions } from '@odh-dashboard/plugin-core';
10+
import { NavItemTitle } from './NavItemTitle';
11+
import NavIcon from './NavIcon';
12+
13+
type Props = {
14+
extension: TabRoutePageExtension;
15+
};
16+
17+
export const NavItemTabRoute: React.FC<Props> = ({
18+
extension: {
19+
properties: { id, href, path, dataAttributes, title, iconRef },
20+
},
21+
}) => {
22+
const allTabExtensions = useExtensions<TabRouteTabExtension>(isTabRouteTabExtension);
23+
const tabs = React.useMemo(
24+
() => allTabExtensions.filter((tab) => tab.properties.pageId === id),
25+
[allTabExtensions, id],
26+
);
27+
const isMatch = !!useMatch(path);
28+
29+
// Don't render nav item if no tabs are registered
30+
if (tabs.length === 0) {
31+
return null;
32+
}
33+
34+
return (
35+
<NavItem isActive={isMatch}>
36+
<Link {...dataAttributes} to={href}>
37+
<NavItemTitle
38+
title={title}
39+
navIcon={iconRef ? <NavIcon componentRef={iconRef} /> : null}
40+
statusIcon={null}
41+
/>
42+
</Link>
43+
</NavItem>
44+
);
45+
};

frontend/src/app/navigation/NavSection.tsx

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import * as React from 'react';
22
import { matchPath, useLocation } from 'react-router-dom';
33
import { NavExpandable } from '@patternfly/react-core';
4-
import { LoadedExtension } from '@openshift/dynamic-plugin-sdk';
4+
import type { Extension, LoadedExtension } from '@openshift/dynamic-plugin-sdk';
55
import {
66
StatusReport,
77
NavSectionExtension,
88
NavExtension,
9+
TabRoutePageExtension,
10+
TabRouteTabExtension,
911
isHrefNavItemExtension,
1012
isNavSectionExtension,
1113
isNavExtension,
14+
isTabRoutePageExtension,
15+
isTabRouteTabExtension,
1216
} from '@odh-dashboard/plugin-core/extension-points';
1317
import { useExtensions } from '@odh-dashboard/plugin-core';
1418
import { useAccessReviewExtensions } from '#~/utilities/useAccessReviewExtensions';
@@ -19,6 +23,34 @@ import { NavItemTitle } from './NavItemTitle';
1923
import { compareNavItemGroups } from './utils';
2024
import NavIcon from './NavIcon';
2125

26+
type AnyNavExtension = NavExtension | TabRoutePageExtension;
27+
28+
/** Returns true if the extension is a leaf nav item (href or tab-route page). */
29+
const isLeafNavExtension = (e: Extension): boolean =>
30+
isHrefNavItemExtension(e) || isTabRoutePageExtension(e);
31+
32+
/** Gets the path/href for active state matching from a leaf extension. */
33+
const getLeafPath = (e: Extension): string | undefined => {
34+
if (isHrefNavItemExtension(e)) {
35+
return e.properties.path ?? e.properties.href;
36+
}
37+
if (isTabRoutePageExtension(e)) {
38+
return e.properties.path;
39+
}
40+
return undefined;
41+
};
42+
43+
/** Gets the access review from a leaf extension. */
44+
const getLeafAccessReview = (e: Extension) => {
45+
if (isHrefNavItemExtension(e)) {
46+
return e.properties.accessReview;
47+
}
48+
if (isTabRoutePageExtension(e)) {
49+
return e.properties.accessReview;
50+
}
51+
return undefined;
52+
};
53+
2254
type Props = {
2355
extension: NavSectionExtension;
2456
};
@@ -30,7 +62,21 @@ export const NavSection: React.FC<Props> = ({
3062
}) => {
3163
const [status, setStatus] = React.useState<Record<string, StatusReport | undefined>>({});
3264
const { pathname } = useLocation();
33-
const extensions = useExtensions(isNavExtension);
65+
const navOnlyExtensions = useExtensions(isNavExtension);
66+
const tabRoutePageExtensions = useExtensions<TabRoutePageExtension>(isTabRoutePageExtension);
67+
const tabRouteTabExtensions = useExtensions<TabRouteTabExtension>(isTabRouteTabExtension);
68+
// Only include tab-route pages that have at least one registered tab
69+
const tabRoutePagesWithTabs = React.useMemo(
70+
() =>
71+
tabRoutePageExtensions.filter((page) =>
72+
tabRouteTabExtensions.some((tab) => tab.properties.pageId === page.properties.id),
73+
),
74+
[tabRoutePageExtensions, tabRouteTabExtensions],
75+
);
76+
const extensions: LoadedExtension<AnyNavExtension>[] = React.useMemo(
77+
() => [...navOnlyExtensions, ...tabRoutePagesWithTabs],
78+
[navOnlyExtensions, tabRoutePagesWithTabs],
79+
);
3480

3581
const navExtensions = React.useMemo(
3682
() =>
@@ -45,9 +91,9 @@ export const NavSection: React.FC<Props> = ({
4591
navExtensions.forEach((child) => parentByIdMap.set(child.properties.id, id));
4692

4793
const collectDescendants = (
48-
roots: LoadedExtension<NavExtension>[],
49-
): LoadedExtension<NavExtension>[] => {
50-
return roots.flatMap((ext) => {
94+
roots: LoadedExtension<AnyNavExtension>[],
95+
): LoadedExtension<AnyNavExtension>[] =>
96+
roots.flatMap((ext) => {
5197
if (isNavSectionExtension(ext)) {
5298
const currentSectionId = ext.properties.id;
5399
const children = extensions.filter(
@@ -58,7 +104,6 @@ export const NavSection: React.FC<Props> = ({
58104
}
59105
return [ext];
60106
});
61-
};
62107

63108
return {
64109
descendantExtensions: collectDescendants(navExtensions),
@@ -68,11 +113,11 @@ export const NavSection: React.FC<Props> = ({
68113

69114
const [allowedDescendants, isAllowedLoaded] = useAccessReviewExtensions(
70115
descendantExtensions,
71-
(e) => (isHrefNavItemExtension(e) ? e.properties.accessReview : undefined),
116+
(e) => getLeafAccessReview(e),
72117
);
73118

74-
const { visibleHrefIds, visibleSectionIds } = React.useMemo(() => {
75-
const hrefIds = new Set<string>();
119+
const { visibleLeafIds, visibleSectionIds } = React.useMemo(() => {
120+
const leafIds = new Set<string>();
76121
const sectionIds = new Set<string>();
77122

78123
const addAncestorSections = (childId: string): void => {
@@ -84,38 +129,37 @@ export const NavSection: React.FC<Props> = ({
84129
};
85130

86131
allowedDescendants.forEach((ext) => {
87-
if (isHrefNavItemExtension(ext)) {
88-
const hrefId = ext.properties.id;
89-
hrefIds.add(hrefId);
90-
addAncestorSections(hrefId);
132+
if (isLeafNavExtension(ext)) {
133+
const leafId = ext.properties.id;
134+
leafIds.add(leafId);
135+
addAncestorSections(leafId);
91136
}
92137
});
93138

94-
return { visibleHrefIds: hrefIds, visibleSectionIds: sectionIds };
139+
return { visibleLeafIds: leafIds, visibleSectionIds: sectionIds };
95140
}, [allowedDescendants, parentById]);
96141

97142
const visibleChildren = React.useMemo(
98143
() =>
99144
navExtensions.filter((e) => {
100-
if (isHrefNavItemExtension(e)) {
101-
return visibleHrefIds.has(e.properties.id);
145+
if (isLeafNavExtension(e)) {
146+
return visibleLeafIds.has(e.properties.id);
102147
}
103148
if (isNavSectionExtension(e)) {
104149
return visibleSectionIds.has(e.properties.id);
105150
}
106151
return false;
107152
}),
108-
[navExtensions, visibleHrefIds, visibleSectionIds],
153+
[navExtensions, visibleLeafIds, visibleSectionIds],
109154
);
110155

111156
const isActive = React.useMemo(
112157
() =>
113158
isAllowedLoaded &&
114-
allowedDescendants.some(
115-
(e) =>
116-
isHrefNavItemExtension(e) &&
117-
!!matchPath(e.properties.path ?? e.properties.href, pathname),
118-
),
159+
allowedDescendants.some((e) => {
160+
const path = getLeafPath(e);
161+
return path ? !!matchPath(path, pathname) : false;
162+
}),
119163
[isAllowedLoaded, allowedDescendants, pathname],
120164
);
121165

0 commit comments

Comments
 (0)