diff --git a/workspaces/frontend/src/__tests__/cypress/cypress/tests/e2e/NamespaceSelector.cy.ts b/workspaces/frontend/src/__tests__/cypress/cypress/tests/e2e/NamespaceSelector.cy.ts
new file mode 100644
index 00000000..769e6a57
--- /dev/null
+++ b/workspaces/frontend/src/__tests__/cypress/cypress/tests/e2e/NamespaceSelector.cy.ts
@@ -0,0 +1,41 @@
+const namespaces = ['default', 'kubeflow', 'custom-namespace'];
+const mockNamespaces = {
+ data: [{ name: 'default' }, { name: 'kubeflow' }, { name: 'custom-namespace' }],
+};
+
+describe('Namespace Selector Dropdown', () => {
+ beforeEach(() => {
+ // Mock the namespaces and selected namespace
+ cy.intercept('GET', '/api/v1/namespaces', {
+ body: mockNamespaces,
+ });
+ cy.visit('/');
+ });
+
+ it('should open the namespace dropdown and select a namespace', () => {
+ cy.get('[data-testid="namespace-toggle"]').click();
+ cy.get('[data-testid="namespace-dropdown"]').should('be.visible');
+ namespaces.forEach((ns) => {
+ cy.get(`[data-testid="dropdown-item-${ns}"]`).should('exist').and('contain', ns);
+ });
+
+ cy.get('[data-testid="dropdown-item-kubeflow"]').click();
+
+ // Assert the selected namespace is updated
+ cy.get('[data-testid="namespace-toggle"]').should('contain', 'kubeflow');
+ });
+
+ it('should display the default namespace initially', () => {
+ cy.get('[data-testid="namespace-toggle"]').should('contain', 'default');
+ });
+
+ it('should navigate to notebook settings and retain the namespace', () => {
+ cy.get('[data-testid="namespace-toggle"]').click();
+ cy.get('[data-testid="dropdown-item-custom-namespace"]').click();
+ cy.get('[data-testid="namespace-toggle"]').should('contain', 'custom-namespace');
+ // Click on navigation button
+ cy.get('#Settings').click();
+ cy.get('[data-testid="nav-link-/notebookSettings"]').click();
+ cy.get('[data-testid="namespace-toggle"]').should('contain', 'custom-namespace');
+ });
+});
diff --git a/workspaces/frontend/src/app/App.tsx b/workspaces/frontend/src/app/App.tsx
index 59cd5713..3d6a03b6 100644
--- a/workspaces/frontend/src/app/App.tsx
+++ b/workspaces/frontend/src/app/App.tsx
@@ -11,6 +11,8 @@ import {
Title,
} from '@patternfly/react-core';
import { BarsIcon } from '@patternfly/react-icons';
+import { NamespaceProvider } from './context/NamespaceContextProvider';
+import NamespaceSelector from '../shared/components/NamespaceSelector';
import AppRoutes from './AppRoutes';
import NavSidebar from './NavSidebar';
@@ -18,7 +20,11 @@ const App: React.FC = () => {
const masthead = (
-
+
@@ -28,20 +34,23 @@ const App: React.FC = () => {
Kubeflow Notebooks 2.0
+
);
return (
- }
- >
-
-
+
+ }
+ >
+
+
+
);
};
diff --git a/workspaces/frontend/src/app/NavSidebar.tsx b/workspaces/frontend/src/app/NavSidebar.tsx
index bfa9ab1a..651c5d40 100644
--- a/workspaces/frontend/src/app/NavSidebar.tsx
+++ b/workspaces/frontend/src/app/NavSidebar.tsx
@@ -12,7 +12,9 @@ import { useNavData, isNavDataGroup, NavDataHref, NavDataGroup } from './AppRout
const NavHref: React.FC<{ item: NavDataHref }> = ({ item }) => (
- {item.label}
+
+ {item.label}
+
);
@@ -40,7 +42,6 @@ const NavGroup: React.FC<{ item: NavDataGroup }> = ({ item }) => {
const NavSidebar: React.FC = () => {
const navData = useNavData();
-
return (
diff --git a/workspaces/frontend/src/app/context/NamespaceContextProvider.tsx b/workspaces/frontend/src/app/context/NamespaceContextProvider.tsx
new file mode 100644
index 00000000..201e6d44
--- /dev/null
+++ b/workspaces/frontend/src/app/context/NamespaceContextProvider.tsx
@@ -0,0 +1,59 @@
+import React, {
+ useState,
+ useContext,
+ ReactNode,
+ useMemo,
+ useCallback,
+} from 'react';
+import useMount from '../hooks/useMount';
+
+interface NamespaceContextState {
+ namespaces: string[];
+ selectedNamespace: string;
+ setSelectedNamespace: (namespace: string) => void;
+}
+
+const NamespaceContext = React.createContext(
+ undefined
+);
+
+export const useNamespaceContext = () => {
+ const context = useContext(NamespaceContext);
+ if (!context) {
+ throw new Error(
+ "useNamespaceContext must be used within a NamespaceProvider"
+ );
+ }
+ return context;
+};
+
+interface NamespaceProviderProps {
+ children: ReactNode;
+}
+
+export const NamespaceProvider: React.FC = ({
+ children,
+}) => {
+ const [namespaces, setNamespaces] = useState([]);
+ const [selectedNamespace, setSelectedNamespace] = useState("");
+
+ // Todo: Need to replace with actual API call
+ const fetchNamespaces = useCallback(async () => {
+ const mockNamespaces = {
+ data: [{ name: 'default' }, { name: 'kubeflow' }, { name: 'custom-namespace' }],
+ };
+ const namespaceNames = mockNamespaces.data.map((ns) => ns.name);
+ setNamespaces(namespaceNames);
+ setSelectedNamespace(namespaceNames.length > 0 ? namespaceNames[0] : "");
+ }, []);
+ useMount(fetchNamespaces);
+ const namespacesContextValues = useMemo(
+ () => ({ namespaces, selectedNamespace, setSelectedNamespace }),
+ [namespaces, selectedNamespace]
+ );
+ return (
+
+ {children}
+
+ );
+};
diff --git a/workspaces/frontend/src/app/hooks/useMount.tsx b/workspaces/frontend/src/app/hooks/useMount.tsx
new file mode 100644
index 00000000..1760bc0f
--- /dev/null
+++ b/workspaces/frontend/src/app/hooks/useMount.tsx
@@ -0,0 +1,9 @@
+import { useEffect } from "react"
+
+const useMount = (callback:()=>void): void => {
+ useEffect(() => {
+ callback();
+ }, []);
+}
+
+export default useMount;
\ No newline at end of file
diff --git a/workspaces/frontend/src/shared/components/NamespaceSelector.tsx b/workspaces/frontend/src/shared/components/NamespaceSelector.tsx
new file mode 100644
index 00000000..ebc54213
--- /dev/null
+++ b/workspaces/frontend/src/shared/components/NamespaceSelector.tsx
@@ -0,0 +1,57 @@
+import React, { FC, useMemo, useState } from 'react';
+import {
+ Dropdown,
+ DropdownItem,
+ MenuToggle,
+ DropdownList,
+ DropdownProps,
+} from '@patternfly/react-core';
+import { useNamespaceContext } from '../../app/context/NamespaceContextProvider';
+
+const NamespaceSelector: FC = () => {
+ const { namespaces, selectedNamespace, setSelectedNamespace } = useNamespaceContext();
+ const [isOpen, setIsOpen] = useState(false);
+
+ const onSelect: DropdownProps['onSelect'] = (_event, value) => {
+ setSelectedNamespace(value as string);
+ setIsOpen(false);
+ };
+
+ const dropdownItems = useMemo(
+ () =>
+ namespaces.map((ns) => (
+
+ {ns}
+
+ )),
+ [namespaces],
+ );
+
+ return (
+ (
+ setIsOpen(!isOpen)}
+ isExpanded={isOpen}
+ className="namespace-select-toggle"
+ data-testid="namespace-toggle"
+ >
+ {selectedNamespace}
+
+ )}
+ isOpen={isOpen}
+ data-testid="namespace-dropdown"
+ >
+ {dropdownItems}
+
+ );
+};
+
+export default NamespaceSelector;