From fee75847e264165caa53a13adbf0d4c3fc6d837f Mon Sep 17 00:00:00 2001 From: yelias Date: Wed, 27 Nov 2024 16:50:56 +0200 Subject: [PATCH] feat(ws): Notebooks 2.0 // Frontend // Namespace selector Signed-off-by: yelias --- .../cypress/tests/e2e/NamespaceSelector.cy.ts | 41 +++++++++++++ workspaces/frontend/src/app/App.tsx | 27 ++++++--- workspaces/frontend/src/app/NavSidebar.tsx | 5 +- .../app/context/NamespaceContextProvider.tsx | 59 +++++++++++++++++++ .../frontend/src/app/hooks/useMount.tsx | 9 +++ .../shared/components/NamespaceSelector.tsx | 57 ++++++++++++++++++ 6 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 workspaces/frontend/src/__tests__/cypress/cypress/tests/e2e/NamespaceSelector.cy.ts create mode 100644 workspaces/frontend/src/app/context/NamespaceContextProvider.tsx create mode 100644 workspaces/frontend/src/app/hooks/useMount.tsx create mode 100644 workspaces/frontend/src/shared/components/NamespaceSelector.tsx 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;