diff --git a/clients/ui/frontend/src/app/App.tsx b/clients/ui/frontend/src/app/App.tsx
index 6dac1cd33b..6e37d1384b 100644
--- a/clients/ui/frontend/src/app/App.tsx
+++ b/clients/ui/frontend/src/app/App.tsx
@@ -23,6 +23,7 @@ import ToastNotifications from '~/app/standalone/ToastNotifications';
import AppNavSidebar from '~/app/standalone/AppNavSidebar';
import AppRoutes from '~/app/AppRoutes';
import { AppContext } from '~/app/context/AppContext';
+import { CustomThemeProvider } from '~/app/context/ThemeContext';
import { ModelRegistrySelectorContextProvider } from '~/app/context/ModelRegistrySelectorContext';
import { TransferJobNotificationsProvider } from '~/app/context/TransferJobNotificationsContext';
@@ -95,32 +96,34 @@ const App: React.FC = () => {
) : (
-
-
- {
- logout().then(() => window.location.reload());
- }}
- />
- ) : (
- ''
- )
- }
- isManagedSidebar={isStandalone}
- sidebar={isStandalone ? : sidebar}
- >
-
-
-
-
-
-
-
+
+
+
+ {
+ logout().then(() => window.location.reload());
+ }}
+ />
+ ) : (
+ ''
+ )
+ }
+ isManagedSidebar={isStandalone}
+ sidebar={isStandalone ? : sidebar}
+ >
+
+
+
+
+
+
+
+
);
};
diff --git a/clients/ui/frontend/src/app/app.css b/clients/ui/frontend/src/app/app.css
index 06f062d46b..af1293261c 100644
--- a/clients/ui/frontend/src/app/app.css
+++ b/clients/ui/frontend/src/app/app.css
@@ -3,3 +3,266 @@ body,
#root {
height: 100%;
}
+
+/* ==========================================================================
+ Premium Dark Theme Design System for Kubeflow Hub
+ ========================================================================== */
+
+html.dark-theme {
+ --pf-v6-global--BackgroundColor--100: #0b0f19;
+ --pf-v6-global--BackgroundColor--200: #111827;
+ --pf-v6-global--Color--100: #f8fafc;
+ --pf-v6-global--Color--200: #cbd5e1;
+ --pf-v6-global--BorderColor--100: #1e293b;
+ --pf-v6-global--link--Color: #38bdf8;
+ --pf-v6-global--link--Color--hover: #7dd3fc;
+ color-scheme: dark;
+}
+
+html.dark-theme,
+html.dark-theme body {
+ background-color: #0b0f19 !important;
+ color: #f8fafc !important;
+}
+
+/* Page Layout & Main Backgrounds */
+html.dark-theme .pf-v6-c-page,
+html.dark-theme .pf-v6-c-page__main,
+html.dark-theme .pf-v6-c-page__main-section,
+html.dark-theme .pf-v6-c-page__main-body {
+ background-color: #0b0f19 !important;
+ color: #f8fafc !important;
+}
+
+/* Header Masthead */
+html.dark-theme .pf-v6-c-masthead {
+ background-color: #0f172a !important;
+ color: #f8fafc !important;
+ border-bottom: 1px solid #1e293b !important;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
+}
+
+/* Sidebar Navigation */
+html.dark-theme .pf-v6-c-page__sidebar,
+html.dark-theme .pf-v6-c-nav {
+ background-color: #0b0f19 !important;
+ border-right: 1px solid #1e293b !important;
+}
+
+html.dark-theme .pf-v6-c-nav__link {
+ color: #cbd5e1 !important;
+ transition: all 0.2s ease;
+}
+
+html.dark-theme .pf-v6-c-nav__link:hover,
+html.dark-theme .pf-v6-c-nav__link.pf-m-current {
+ color: #38bdf8 !important;
+ background-color: #1e293b !important;
+ border-left: 3px solid #38bdf8 !important;
+}
+
+/* Fix Dropdowns & Menu Toggles (bella-namespace, user@example.com, Keyword filter) */
+html.dark-theme .pf-v6-c-menu-toggle,
+html.dark-theme .pf-v6-c-menu-toggle__button,
+html.dark-theme .pf-v6-c-menu-toggle__text,
+html.dark-theme .pf-v6-c-select,
+html.dark-theme button[id*="menu-toggle"] {
+ background-color: #1e293b !important;
+ color: #f8fafc !important;
+ border: 1px solid #334155 !important;
+ border-radius: 6px !important;
+}
+
+html.dark-theme .pf-v6-c-menu-toggle__controls,
+html.dark-theme .pf-v6-c-menu-toggle__toggle-icon,
+html.dark-theme .pf-v6-c-menu-toggle__icon,
+html.dark-theme .pf-v6-c-menu-toggle svg {
+ color: #94a3b8 !important;
+}
+
+html.dark-theme .pf-v6-c-menu-toggle:hover {
+ background-color: #334155 !important;
+ border-color: #475569 !important;
+}
+
+/* Description Text, Subtitles & Paragraphs */
+html.dark-theme .pf-v6-c-content p,
+html.dark-theme .pf-v6-c-content,
+html.dark-theme .pf-v6-c-page__main-section p,
+html.dark-theme .pf-v6-c-page__main-body p,
+html.dark-theme p,
+html.dark-theme span,
+html.dark-theme label,
+html.dark-theme small,
+html.dark-theme .pf-v6-c-form__helper-text {
+ color: #cbd5e1 !important;
+}
+
+/* Headings & Main Titles */
+html.dark-theme h1,
+html.dark-theme h2,
+html.dark-theme h3,
+html.dark-theme h4,
+html.dark-theme h5,
+html.dark-theme h6,
+html.dark-theme .pf-v6-c-title,
+html.dark-theme .pf-v6-c-page__main-title {
+ color: #f8fafc !important;
+ font-weight: 600;
+}
+
+/* Search Inputs & Form Controls */
+html.dark-theme .pf-v6-c-form-control,
+html.dark-theme .pf-v6-c-text-input-group,
+html.dark-theme input,
+html.dark-theme select,
+html.dark-theme textarea {
+ background-color: #1e293b !important;
+ color: #f8fafc !important;
+ border: 1px solid #334155 !important;
+ border-radius: 6px !important;
+}
+
+html.dark-theme input::placeholder,
+html.dark-theme .pf-v6-c-text-input-group__text-input::placeholder {
+ color: #94a3b8 !important;
+}
+
+/* Links & Interactive Actions */
+html.dark-theme a,
+html.dark-theme .pf-v6-c-button.pf-m-link,
+html.dark-theme .pf-v6-c-button.pf-m-inline {
+ color: #38bdf8 !important;
+ text-decoration: none;
+}
+
+html.dark-theme a:hover,
+html.dark-theme .pf-v6-c-button.pf-m-link:hover,
+html.dark-theme .pf-v6-c-button.pf-m-inline:hover {
+ color: #7dd3fc !important;
+ text-decoration: underline;
+}
+
+/* Tables & Data Grids */
+html.dark-theme table,
+html.dark-theme .pf-v6-c-table,
+html.dark-theme .MuiTable-root {
+ background-color: #111827 !important;
+ color: #f8fafc !important;
+ border-color: #1e293b !important;
+}
+
+html.dark-theme th,
+html.dark-theme .pf-v6-c-table__th,
+html.dark-theme .pf-v6-c-table__button,
+html.dark-theme .pf-v6-c-table__text,
+html.dark-theme .pf-v6-c-table__sort-indicator {
+ background-color: #0f172a !important;
+ color: #f8fafc !important;
+ font-weight: 600;
+ border-bottom: 1px solid #1e293b !important;
+}
+
+html.dark-theme th button,
+html.dark-theme .pf-v6-c-table__button span,
+html.dark-theme .pf-v6-c-table__button svg {
+ color: #f8fafc !important;
+}
+
+html.dark-theme tr,
+html.dark-theme .pf-v6-c-table__tr {
+ border-bottom: 1px solid #1e293b !important;
+ transition: background-color 0.15s ease;
+}
+
+html.dark-theme tr:hover,
+html.dark-theme .pf-v6-c-table__tr:hover {
+ background-color: #1e293b !important;
+}
+
+html.dark-theme td,
+html.dark-theme .pf-v6-c-table__td {
+ color: #f1f5f9 !important;
+}
+
+/* Badges & Tags */
+html.dark-theme .pf-v6-c-badge,
+html.dark-theme .pf-v6-c-label,
+html.dark-theme .MuiChip-root {
+ background-color: #1e293b !important;
+ color: #38bdf8 !important;
+ border: 1px solid #334155 !important;
+ border-radius: 9999px;
+ font-weight: 500;
+}
+
+/* Dropdowns & Menus */
+html.dark-theme .pf-v6-c-dropdown__menu,
+html.dark-theme .pf-v6-c-menu,
+html.dark-theme .MuiMenu-paper {
+ background-color: #1e293b !important;
+ color: #f8fafc !important;
+ border: 1px solid #334155 !important;
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5) !important;
+}
+
+html.dark-theme .pf-v6-c-dropdown__menu-item:hover,
+html.dark-theme .pf-v6-c-menu__item:hover,
+html.dark-theme .MuiMenuItem-root:hover {
+ background-color: #334155 !important;
+ color: #38bdf8 !important;
+}
+
+/* Global Border Cleanup in Dark Mode */
+html.dark-theme *,
+html.dark-theme *::before,
+html.dark-theme *::after {
+ border-color: #1e293b;
+}
+
+html.dark-theme .pf-v6-c-page__main,
+html.dark-theme .pf-v6-c-page__main-section,
+html.dark-theme .pf-v6-c-page__main-body,
+html.dark-theme .pf-v6-c-page__sidebar,
+html.dark-theme .pf-v6-c-masthead {
+ border-color: #1e293b !important;
+ box-shadow: none !important;
+}
+
+/* Pagination & Toolbar Dark Overrides */
+html.dark-theme .pf-v6-c-pagination,
+html.dark-theme .pf-v6-c-pagination__menu,
+html.dark-theme .pf-v6-c-toolbar,
+html.dark-theme .pf-v6-c-toolbar__content,
+html.dark-theme .pf-v6-c-toolbar__group,
+html.dark-theme .pf-v6-c-toolbar__item,
+html.dark-theme .pf-v6-c-options-menu,
+html.dark-theme .pf-v6-c-pagination__total-items {
+ background-color: #0b0f19 !important;
+ color: #cbd5e1 !important;
+ border-color: #1e293b !important;
+}
+
+html.dark-theme .pf-v6-c-pagination .pf-v6-c-button,
+html.dark-theme .pf-v6-c-pagination__nav-button {
+ color: #f8fafc !important;
+ background-color: transparent !important;
+}
+
+html.dark-theme .pf-v6-c-pagination .pf-v6-c-button.pf-m-disabled,
+html.dark-theme .pf-v6-c-pagination__nav-button.pf-m-disabled {
+ color: #475569 !important;
+}
+
+/* Toggle Button Styling */
+#theme-toggle-button {
+ transition: transform 0.2s ease, background-color 0.2s ease;
+}
+
+#theme-toggle-button:hover {
+ transform: scale(1.1);
+ background-color: rgba(255, 255, 255, 0.1);
+ border-radius: 50%;
+}
+
+
diff --git a/clients/ui/frontend/src/app/context/ThemeContext.tsx b/clients/ui/frontend/src/app/context/ThemeContext.tsx
new file mode 100644
index 0000000000..42390ffbf1
--- /dev/null
+++ b/clients/ui/frontend/src/app/context/ThemeContext.tsx
@@ -0,0 +1,81 @@
+import * as React from 'react';
+
+export type ThemeMode = 'light' | 'dark' | 'system';
+
+interface ThemeContextType {
+ themeMode: ThemeMode;
+ effectiveTheme: 'light' | 'dark';
+ setThemeMode: (mode: ThemeMode) => void;
+ toggleTheme: () => void;
+}
+
+const STORAGE_KEY = 'kubeflow_theme_mode';
+
+const ThemeContext = React.createContext(undefined);
+
+export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [themeMode, setThemeModeState] = React.useState(() => {
+ const saved = localStorage.getItem(STORAGE_KEY);
+ if (saved === 'light' || saved === 'dark' || saved === 'system') {
+ return saved as ThemeMode;
+ }
+ return 'system';
+ });
+
+ const [systemPrefersDark, setSystemPrefersDark] = React.useState(() => {
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+ });
+
+ React.useEffect(() => {
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ const handleChange = (e: MediaQueryListEvent) => {
+ setSystemPrefersDark(e.matches);
+ };
+
+ if (mediaQuery && mediaQuery.addEventListener) {
+ mediaQuery.addEventListener('change', handleChange);
+ return () => {
+ mediaQuery.removeEventListener('change', handleChange);
+ };
+ }
+ return undefined;
+ }, []);
+
+ const effectiveTheme: 'light' | 'dark' =
+ themeMode === 'system' ? (systemPrefersDark ? 'dark' : 'light') : themeMode;
+
+ React.useEffect(() => {
+ const root = document.documentElement;
+ if (effectiveTheme === 'dark') {
+ root.classList.add('dark-theme');
+ root.classList.add('pf-v6-theme-dark');
+ } else {
+ root.classList.remove('dark-theme');
+ root.classList.remove('pf-v6-theme-dark');
+ }
+ }, [effectiveTheme]);
+
+ const setThemeMode = (mode: ThemeMode) => {
+ setThemeModeState(mode);
+ localStorage.setItem(STORAGE_KEY, mode);
+ };
+
+ const toggleTheme = () => {
+ const nextMode = effectiveTheme === 'light' ? 'dark' : 'light';
+ setThemeMode(nextMode);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useCustomTheme = (): ThemeContextType => {
+ const context = React.useContext(ThemeContext);
+ if (!context) {
+ throw new Error('useCustomTheme must be used within a CustomThemeProvider');
+ }
+ return context;
+};
diff --git a/clients/ui/frontend/src/app/standalone/NavBar.tsx b/clients/ui/frontend/src/app/standalone/NavBar.tsx
index 52c84261a1..7971e8e43d 100644
--- a/clients/ui/frontend/src/app/standalone/NavBar.tsx
+++ b/clients/ui/frontend/src/app/standalone/NavBar.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import {
Brand,
+ Button,
Dropdown,
DropdownItem,
DropdownList,
@@ -18,10 +19,11 @@ import {
ToolbarGroup,
ToolbarItem,
} from '@patternfly/react-core';
-import { BarsIcon } from '@patternfly/react-icons';
+import { BarsIcon, MoonIcon, SunIcon } from '@patternfly/react-icons';
import { useThemeContext } from 'mod-arch-kubeflow';
import { images as sharedImages } from 'mod-arch-shared';
import GlobalNamespaceSelector from './GlobalNamespaceSelector';
+import { useCustomTheme } from '~/app/context/ThemeContext';
interface NavBarProps {
username?: string;
@@ -30,6 +32,7 @@ interface NavBarProps {
const NavBar: React.FC = ({ username, onLogout }) => {
const { isMUITheme } = useThemeContext();
+ const { effectiveTheme, toggleTheme } = useCustomTheme();
const [userMenuOpen, setUserMenuOpen] = React.useState(false);
@@ -72,8 +75,24 @@ const NavBar: React.FC = ({ username, onLogout }) => {
- {username && (
-
+
+
+
+
+ {username && (
= ({ username, onLogout }) => {
{userMenuItems}
-
- )}
+ )}
+
diff --git a/internal/core/inference_service.go b/internal/core/inference_service.go
index f524014a6c..cdbe5848fc 100644
--- a/internal/core/inference_service.go
+++ b/internal/core/inference_service.go
@@ -138,6 +138,7 @@ func (b *ModelRegistryService) GetInferenceServices(listOptions api.ListOptions,
OrderBy: listOptions.OrderBy,
SortOrder: listOptions.SortOrder,
NextPageToken: listOptions.NextPageToken,
+ FilterQuery: listOptions.FilterQuery,
},
Runtime: runtime,
ParentResourceID: parentResourceID,
diff --git a/internal/core/inference_service_test.go b/internal/core/inference_service_test.go
index ca7a310074..0eb7513798 100644
--- a/internal/core/inference_service_test.go
+++ b/internal/core/inference_service_test.go
@@ -1092,3 +1092,189 @@ func TestInferenceServiceRoundTrip(t *testing.T) {
assert.Equal(t, "new_value", finalProps["new_prop"].MetadataStringValue.StringValue)
})
}
+
+// TestGetInferenceServicesWithFilterQuery verifies that the filterQuery parameter
+// is correctly propagated and applied when listing InferenceServices.
+//
+// Regression test for: filterQuery silently ignored on GET /inference_services
+// Root cause was missing FilterQuery field propagation in GetInferenceServices
+// and missing GetRestEntityType() on InferenceServiceListOptions.
+func TestGetInferenceServicesWithFilterQuery(t *testing.T) {
+ _service, cleanup := SetupModelRegistryService(t)
+ defer cleanup()
+
+ // Create shared prerequisites
+ rm, err := _service.UpsertRegisteredModel(&openapi.RegisteredModel{Name: "filter-test-rm"})
+ require.NoError(t, err)
+
+ env, err := _service.UpsertServingEnvironment(&openapi.ServingEnvironment{Name: "filter-test-env"})
+ require.NoError(t, err)
+
+ // Create inference services with distinct properties for filtering
+ type svcDef struct {
+ name string
+ runtime string
+ extID string
+ }
+ svcDefs := []svcDef{
+ {"fraud-detector", "tensorflow", "ext-fraud-001"},
+ {"image-classifier", "pytorch", "ext-image-002"},
+ {"nlp-pipeline", "tensorflow", "ext-nlp-003"},
+ {"recommendation-engine", "sklearn", "ext-rec-004"},
+ }
+ for _, svc := range svcDefs {
+ name := svc.name
+ rt := svc.runtime
+ eid := svc.extID
+ _, err := _service.UpsertInferenceService(&openapi.InferenceService{
+ Name: &name,
+ ExternalId: &eid,
+ Runtime: &rt,
+ ServingEnvironmentId: *env.Id,
+ RegisteredModelId: *rm.Id,
+ })
+ require.NoError(t, err)
+ }
+
+ testCases := []struct {
+ name string
+ filterQuery string
+ expectedCount int
+ expectedNames []string
+ }{
+ {
+ name: "Filter by exact name",
+ filterQuery: "name = 'fraud-detector'",
+ expectedCount: 1,
+ expectedNames: []string{"fraud-detector"},
+ },
+ {
+ name: "Filter by name pattern",
+ filterQuery: "name LIKE '%-detector'",
+ expectedCount: 1,
+ expectedNames: []string{"fraud-detector"},
+ },
+ {
+ name: "Filter by externalId",
+ filterQuery: "externalId = 'ext-image-002'",
+ expectedCount: 1,
+ expectedNames: []string{"image-classifier"},
+ },
+ {
+ name: "Filter by runtime - tensorflow",
+ filterQuery: "runtime = 'tensorflow'",
+ expectedCount: 2,
+ expectedNames: []string{"fraud-detector", "nlp-pipeline"},
+ },
+ {
+ name: "Filter by runtime - pytorch",
+ filterQuery: "runtime = 'pytorch'",
+ expectedCount: 1,
+ expectedNames: []string{"image-classifier"},
+ },
+ {
+ name: "Complex filter with AND",
+ filterQuery: "runtime = 'tensorflow' AND name = 'fraud-detector'",
+ expectedCount: 1,
+ expectedNames: []string{"fraud-detector"},
+ },
+ {
+ name: "Complex filter with OR",
+ filterQuery: "runtime = 'pytorch' OR runtime = 'sklearn'",
+ expectedCount: 2,
+ expectedNames: []string{"image-classifier", "recommendation-engine"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ pageSize := int32(20)
+ fq := tc.filterQuery
+ result, err := _service.GetInferenceServices(api.ListOptions{
+ PageSize: &pageSize,
+ FilterQuery: &fq,
+ }, nil, nil)
+
+ require.NoError(t, err)
+ require.NotNil(t, result)
+
+ var matchedNames []string
+ for _, item := range result.Items {
+ if slices.Contains(tc.expectedNames, *item.Name) {
+ matchedNames = append(matchedNames, *item.Name)
+ }
+ }
+
+ assert.Equal(t, tc.expectedCount, len(matchedNames),
+ "filterQuery %q: expected %d items, got %d (filter may be silently ignored)",
+ tc.filterQuery, tc.expectedCount, len(matchedNames))
+ assert.ElementsMatch(t, tc.expectedNames, matchedNames,
+ "filterQuery %q: unexpected items returned", tc.filterQuery)
+ })
+ }
+
+ t.Run("Invalid filter syntax returns error", func(t *testing.T) {
+ invalidFilter := "invalid <<