Skip to content

Commit 3d990b9

Browse files
author
Daniel Duong
committed
Merge branch 'main' of https://github.com/opendatahub-io/odh-dashboard into feat/automl-create
2 parents 64fb41f + 46b45e6 commit 3d990b9

350 files changed

Lines changed: 17365 additions & 1880 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.

.claude/rules/react.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,66 @@ const MyContextProvider: React.FC<PropsWithChildren> = ({ children }) => {
188188

189189
**Always** wrap context values in `React.useMemo` to prevent unnecessary re-renders of consumers.
190190

191+
## Navigation
192+
193+
### Prefer `<Link>` over `useNavigate`
194+
195+
Use React Router's `<Link>` (or PatternFly's `component` prop pattern) for navigation instead of calling `navigate()` imperatively. Links are accessible by default (right-click, Cmd+click, screen readers), while `navigate()` breaks these expectations.
196+
197+
```tsx
198+
// Bad — imperative navigation in a click handler
199+
import { useNavigate } from 'react-router-dom';
200+
201+
const navigate = useNavigate();
202+
<Button onClick={() => navigate('/projects')}>View projects</Button>
203+
204+
// Good — declarative link
205+
import { Link } from 'react-router-dom';
206+
207+
<Button component={(props) => <Link {...props} to="/projects" />}>
208+
View projects
209+
</Button>
210+
```
211+
212+
For PatternFly empty states and other components that accept `href` props, prefer those over `handleClick` + `navigate`:
213+
214+
```tsx
215+
// Bad
216+
<ModelsEmptyState
217+
actionButtonText="Deploy a model"
218+
handleActionButtonClick={() => navigate(`/deployments/${ns}`)}
219+
/>
220+
221+
// Good
222+
<ModelsEmptyState
223+
actionButtonText="Deploy a model"
224+
actionButtonHref={`/deployments/${ns}`}
225+
/>
226+
```
227+
228+
### When `useNavigate` is acceptable
229+
230+
Reserve `useNavigate` for programmatic navigation that cannot be expressed as a static href:
231+
232+
- **Post-action redirects** — navigating after an async operation completes:
233+
234+
```tsx
235+
const navigate = useNavigate();
236+
237+
const handleSubmit = async () => {
238+
await api.createResource(data);
239+
navigate(`/resources/${data.name}`);
240+
};
241+
```
242+
243+
- **Cancel / go-back buttons** — returning to a known route:
244+
245+
```tsx
246+
const navigate = useNavigate();
247+
248+
<Button variant="link" onClick={() => navigate(redirectPath)}>Cancel</Button>
249+
```
250+
191251
## Performance
192252

193253
### Memoization
@@ -411,6 +471,10 @@ When reviewing React code, verify:
411471
- [ ] Primitive values preferred in dependency arrays
412472
- [ ] No barrel file imports — import from source modules directly
413473

474+
### Navigation
475+
- [ ] `<Link>` used instead of `navigate()` for user-initiated navigation
476+
- [ ] `useNavigate` only used for post-action programmatic redirects
477+
414478
### Accessibility
415479
- [ ] `aria-label` on icon buttons and non-text interactive elements
416480
- [ ] Semantic HTML elements used
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
description: Theming external libraries (Perses, etc.) with PatternFly tokens in ODH Dashboard
3+
globs: "packages/observability/**,packages/*/src/**/*theme*,packages/*/src/**/*Theme*"
4+
alwaysApply: false
5+
---
6+
7+
# Third-Party Library Theming — ODH Dashboard
8+
9+
Reference implementation: `packages/observability/src/perses/theme.ts`
10+
11+
For the MLflow federated UI theming approach (Emotion token translation + SCSS shell overrides), see the [MLflow fork](https://github.com/opendatahub-io/mlflow/tree/master/mlflow/server/js/src/common/styles/patternfly) — that pattern lives in the fork, not here.
12+
13+
---
14+
15+
## Invariant 1 — ECharts/canvas: use `.value`, not CSS vars
16+
17+
Canvas-based renderers cannot resolve CSS custom properties at paint time. Passing a `var(--pf-t--...)` string to an ECharts option or canvas `fillStyle` silently produces the wrong color.
18+
19+
```ts
20+
import { t_color_white, t_color_gray_95 } from '@patternfly/react-tokens';
21+
22+
// Correct — resolved hex value
23+
const textColor = isDark ? t_color_white.value : t_color_gray_95.value;
24+
25+
// These overrides are passed to generateChartsTheme(), not as top-level MUI theme properties
26+
const chartsTheme = generateChartsTheme(muiTheme, {
27+
echartsTheme: {
28+
color: palette, // hex array, never CSS var strings
29+
tooltip: {
30+
// CSS vars ARE fine for non-canvas properties (e.g. tooltip is DOM-rendered)
31+
backgroundColor: 'var(--pf-t--global--background--color--inverse--default)',
32+
},
33+
},
34+
thresholds: {
35+
defaultColor: textColor, // .value, not .var
36+
},
37+
});
38+
```
39+
40+
Use `@patternfly/react-tokens` `.value` anywhere the value is consumed by a canvas renderer. Use `var(--pf-t--*)` strings everywhere else.
41+
42+
---
43+
44+
## Invariant 2 — Dark mode source: `useThemeContext()` only
45+
46+
Never infer dark mode from `prefers-color-scheme`, a local prop, or any source other than the dashboard theme context.
47+
48+
```ts
49+
import { useThemeContext } from '@odh-dashboard/internal/app/ThemeContext';
50+
51+
const { theme: contextTheme } = useThemeContext();
52+
const theme: PatternFlyTheme = contextTheme === 'dark' ? 'dark' : 'light';
53+
```
54+
55+
The full theme object must recompute when context changes:
56+
57+
```ts
58+
return React.useMemo(() => {
59+
const muiTheme = getTheme(theme, { ...mapPatternFlyThemeToMUI(theme) });
60+
// ... build chartsTheme ...
61+
return { muiTheme, chartsTheme };
62+
}, [theme]);
63+
```
64+
65+
---
66+
67+
## Invariant 3 — `ThemeProvider` at the library boundary, not the app root
68+
69+
Placing an MUI `ThemeProvider` at the app root leaks MUI styles into PF-only components. Scope it as close to the library's render boundary as possible.
70+
71+
```tsx
72+
// Correct — scoped to the Perses render boundary
73+
// packages/observability/src/perses/PersesWrapper.tsx
74+
const { muiTheme, chartsTheme } = usePatternFlyTheme();
75+
return (
76+
<ThemeProvider theme={muiTheme}>
77+
<ChartsProvider chartsTheme={chartsTheme}>
78+
{children}
79+
</ChartsProvider>
80+
</ThemeProvider>
81+
);
82+
83+
// Wrong — app root wrapping bleeds MUI styles into unrelated PF components
84+
// frontend/src/app/App.tsx
85+
return <ThemeProvider theme={muiTheme}><App /></ThemeProvider>;
86+
```
87+
88+
One wrapper component per external library. Never scatter `ThemeProvider` across individual pages.

.github/workflows/cypress-e2e-test.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,11 @@ jobs:
857857
find ${{ github.workspace }}/packages/cypress/results -type d -empty -delete 2>/dev/null || true
858858
find ${{ github.workspace }}/packages/cypress/screenshots -type d -empty -delete 2>/dev/null || true
859859
find ${{ github.workspace }}/packages/cypress/videos -type d -empty -delete 2>/dev/null || true
860-
860+
861+
# Fix read-only envtest binaries and directories from previous BFF runs
862+
# (setup-envtest downloads k8s binaries with 0555 permissions, which blocks actions/checkout cleanup)
863+
chmod -R u+w ${{ github.workspace }}/packages/*/bff/bin ${{ github.workspace }}/packages/*/upstream/bff/bin 2>/dev/null || true
864+
861865
echo "✅ Cleanup complete (non-critical, continued on any errors)"
862866
863867
- name: Checkout code
@@ -1093,7 +1097,7 @@ jobs:
10931097
echo "🚀 Starting webpack dev server on port ${WEBPACK_PORT} ($CLUSTER_NAME)..."
10941098
10951099
# Start webpack with explicit dashboard host (ensures correct proxy target for all tests)
1096-
cd frontend && env ODH_DASHBOARD_HOST=${DASHBOARD_HOST} ODH_PORT=${WEBPACK_PORT} npm run start:dev:ext > /tmp/webpack_${WEBPACK_PORT}.log 2>&1 &
1100+
cd frontend && env ODH_DASHBOARD_HOST=${DASHBOARD_HOST} ODH_PORT=${WEBPACK_PORT} ODH_TOKEN_REFRESH=true npm run start:dev:ext > /tmp/webpack_${WEBPACK_PORT}.log 2>&1 &
10971101
SERVER_PID=$!
10981102
echo "SERVER_PID=$SERVER_PID" >> $GITHUB_ENV
10991103
echo "$SERVER_PID" > "$PORT_INFO_DIR/port-${WEBPACK_PORT}.pid"
@@ -1414,6 +1418,9 @@ jobs:
14141418
echo "✅ Cleaned up $BFF_KILLED_COUNT BFF process(es) for run_id: ${{ github.run_id }}"
14151419
fi
14161420
1421+
# Fix read-only envtest binaries and directories so next checkout can clean the workspace
1422+
chmod -R u+w ${{ github.workspace }}/packages/*/bff/bin ${{ github.workspace }}/packages/*/upstream/bff/bin 2>/dev/null || true
1423+
14171424
- name: Stop Cypress Servers
14181425
run: |
14191426
echo "🛑 Stopping webpack dev server for run_id: ${{ github.run_id }}..."

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Rules live in `.claude/rules/`. Read the relevant rule file before starting the
9595
| **React** | `react.md` | When writing React components, hooks, or pages |
9696
| **Security** | `security.md` | When working on auth, secrets, input validation, or K8s API interactions |
9797
| **Testing Standards** | `testing-standards.md` | When working across multiple test types or choosing a testing strategy |
98+
| **Third-Party Theming** | `third-party-theming.md` | When theming external libraries (Perses, MLflow, etc.) or mapping PF tokens into non-PF component systems |
9899
| **Unit Tests** | `unit-tests.md` | When creating or modifying Jest unit tests for utilities, hooks, or components |
99100

100101
## Agent Skills

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"type-check": "tsc --noEmit",
3535
"server": "NODE_ENV=production node ./dist/server.js",
3636
"lint": "eslint .",
37+
"lint:fix": "eslint . --fix",
3738
"test-unit": "jest --silent",
3839
"test-unit-coverage": "jest --silent --coverage"
3940
},

backend/src/utils/notebookUtils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,18 @@ const getImageTag = (image: ImageInfo, imageTagName: string): ImageTag => {
137137
};
138138
};
139139

140+
const getMlflowAnnotation = (fastify: KubeFastifyInstance): Record<string, string> => {
141+
const isMlflowFlagEnabled = getDashboardConfig().spec.dashboardConfig.mlflow;
142+
if (!isMlflowFlagEnabled) {
143+
return {};
144+
}
145+
const dscStatus = getClusterStatus(fastify);
146+
if (dscStatus?.components?.mlflowoperator?.managementState === 'Managed') {
147+
return { 'opendatahub.io/mlflow-instance': 'mlflow' };
148+
}
149+
return {};
150+
};
151+
140152
export const assembleNotebook = async (
141153
fastify: KubeFastifyInstance,
142154
data: NotebookData,
@@ -226,6 +238,7 @@ export const assembleNotebook = async (
226238
'opendatahub.io/hardware-profile-name': selectedHardwareProfile?.metadata.name || '',
227239
'opendatahub.io/hardware-profile-namespace':
228240
selectedHardwareProfile?.metadata.namespace || '',
241+
...getMlflowAnnotation(fastify),
229242
},
230243
name: name,
231244
namespace: namespace,
@@ -399,6 +412,7 @@ export const createNotebook = async (
399412
}
400413

401414
notebookAssembled.metadata.annotations['notebooks.opendatahub.io/inject-auth'] = 'true';
415+
402416
const notebookContainers = notebookAssembled.spec.template.spec.containers;
403417

404418
if (!notebookContainers[0]) {

frontend/config/webpack.dev.js

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -89,34 +89,39 @@ module.exports = smp.wrap(
8989
}
9090

9191
// Token refresh mechanism (for when oc user is switched during tests)
92-
// Only refreshes when token is actually used, with 5s minimum interval to avoid blocking
92+
// Gated behind ODH_TOKEN_REFRESH to avoid unexpected user switches in local dev
9393
let cachedToken = token;
94-
let lastTokenFetch = Date.now();
95-
const TOKEN_REFRESH_MIN_INTERVAL = 5000; // Don't refresh more than once per 5 seconds
94+
const tokenRefreshEnabled = process.env.ODH_TOKEN_REFRESH === 'true';
9695

97-
const getCurrentToken = () => {
98-
const now = Date.now();
99-
// Only refresh if enough time has passed since last fetch (prevents excessive blocking)
100-
if (now - lastTokenFetch > TOKEN_REFRESH_MIN_INTERVAL) {
101-
try {
102-
const newToken = execSync('oc whoami --show-token', {
103-
stdio: ['pipe', 'pipe', 'ignore'],
104-
})
105-
.toString()
106-
.trim();
107-
// Only update if token actually changed
108-
if (newToken !== cachedToken) {
109-
console.info('Token refreshed (oc user may have switched)');
110-
cachedToken = newToken;
96+
const getCurrentToken = (() => {
97+
if (!tokenRefreshEnabled) {
98+
return () => cachedToken;
99+
}
100+
101+
let lastTokenFetch = Date.now();
102+
const TOKEN_REFRESH_MIN_INTERVAL = 5000;
103+
return () => {
104+
const now = Date.now();
105+
if (now - lastTokenFetch > TOKEN_REFRESH_MIN_INTERVAL) {
106+
try {
107+
const newToken = execSync('oc whoami --show-token', {
108+
stdio: ['pipe', 'pipe', 'ignore'],
109+
})
110+
.toString()
111+
.trim();
112+
if (newToken !== cachedToken) {
113+
console.info('Token refreshed (oc user may have switched)');
114+
cachedToken = newToken;
115+
}
116+
} catch (e) {
117+
console.warn('Failed to refresh oc token, using cached token');
118+
} finally {
119+
lastTokenFetch = now;
111120
}
112-
lastTokenFetch = now;
113-
} catch (e) {
114-
// If refresh fails, keep using cached token
115-
console.warn('Failed to refresh oc token, using cached token');
116121
}
117-
}
118-
return cachedToken;
119-
};
122+
return cachedToken;
123+
};
124+
})();
120125

121126
const odhProject = process.env.OC_PROJECT || 'opendatahub';
122127
const app = process.env.ODH_APP || 'odh-dashboard';

frontend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@
6060
"test:cypress:e2e": "npx cypress run --project ../packages/cypress --env grepTags=\"${CY_TEST_TAGS}\",skipTags=\"${CY_SKIP_TAGS}\" --browser chrome",
6161
"test:cypress:e2e:open": "npx cypress open --project ../packages/cypress --env grepTags=\"${CY_TEST_TAGS}\",skipTags=\"${CY_SKIP_TAGS}\"",
6262
"cypress:server:wait": "npx wait-on -i 1000 http://localhost:9001",
63-
"lint": "eslint ."
63+
"lint": "eslint .",
64+
"lint:fix": "eslint . --fix"
6465
},
6566
"dependencies": {
6667
"@module-federation/runtime": "^0.15.0",

frontend/src/__mocks__/mockLLMInferenceServiceConfigK8sResource.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type MockLLMInferenceServiceConfigType = {
1010
modelUri?: string;
1111
modelName?: string;
1212
templateName?: string;
13+
disabled?: boolean;
1314
};
1415

1516
export const mockLLMInferenceServiceConfigK8sResource = ({
@@ -22,6 +23,7 @@ export const mockLLMInferenceServiceConfigK8sResource = ({
2223
modelUri = 'hf://test/model',
2324
modelName = 'test-model',
2425
templateName,
26+
disabled,
2527
}: MockLLMInferenceServiceConfigType): LLMInferenceServiceConfigKind => ({
2628
apiVersion: 'serving.kserve.io/v1alpha1',
2729
kind: 'LLMInferenceServiceConfig',
@@ -35,6 +37,7 @@ export const mockLLMInferenceServiceConfigK8sResource = ({
3537
? { 'opendatahub.io/recommended-accelerators': recommendedAccelerators }
3638
: {}),
3739
...(templateName ? { 'opendatahub.io/template-name': templateName } : {}),
40+
...(disabled ? { 'opendatahub.io/disabled': 'true' as const } : {}),
3841
},
3942
labels: {
4043
'opendatahub.io/config-type': configType,

0 commit comments

Comments
 (0)