From 16f3d660216db0e5062ec127d5e80de08d4fd39a Mon Sep 17 00:00:00 2001 From: Linus Pahl Date: Thu, 21 Dec 2023 10:07:02 +0100 Subject: [PATCH] Consider availability for initially selected perspective. (#17716) * Consider available perspectives for initially selected perspective. * Consider if perspective is available when defining active perspective. * `useActivePerspective` now return the complete perpective and not just the id. * Redirect to perpective welcome page on login. * Rename perpective `brandLink` to `welcomeRoute`. * Update tests. * Improving code * Create perspective fixtures. * Fixing fixture usage. --- .../src/components/navigation/MainNavbar.tsx | 2 +- .../perspectives/ActivePerspectiveBrand.tsx | 7 +--- .../PerspectivesSwitcher.test.tsx | 25 ++++-------- .../perspectives/PerspectivesSwitcher.tsx | 4 +- .../src/components/perspectives/bindings.ts | 2 +- .../contexts/PerspectivesContext.ts | 2 +- .../contexts/PerspectivesProvider.tsx | 27 ++++++++++--- .../hooks/useActivePerspective.test.tsx | 13 ++++-- .../hooks/usePerspectives.test.tsx | 26 ++---------- .../src/components/perspectives/types.ts | 2 +- .../src/pages/StartPage.tsx | 6 ++- .../src/routing/AppRouter.test.tsx | 10 +---- .../test/fixtures/perspectives.tsx | 40 +++++++++++++++++++ 13 files changed, 97 insertions(+), 69 deletions(-) create mode 100644 graylog2-web-interface/test/fixtures/perspectives.tsx diff --git a/graylog2-web-interface/src/components/navigation/MainNavbar.tsx b/graylog2-web-interface/src/components/navigation/MainNavbar.tsx index 9d7329251a..628e3c8f53 100644 --- a/graylog2-web-interface/src/components/navigation/MainNavbar.tsx +++ b/graylog2-web-interface/src/components/navigation/MainNavbar.tsx @@ -192,7 +192,7 @@ const useNavigationItems = () => { navigationItems.push(securityNavigation); } - const itemsForActivePerspective = filterByPerspective(navigationItems, activePerspective); + const itemsForActivePerspective = filterByPerspective(navigationItems, activePerspective?.id); return sortItemsByPosition(itemsForActivePerspective); }, [activePerspective, allNavigationItems, permissions]); diff --git a/graylog2-web-interface/src/components/perspectives/ActivePerspectiveBrand.tsx b/graylog2-web-interface/src/components/perspectives/ActivePerspectiveBrand.tsx index 03358b9260..e50673f648 100644 --- a/graylog2-web-interface/src/components/perspectives/ActivePerspectiveBrand.tsx +++ b/graylog2-web-interface/src/components/perspectives/ActivePerspectiveBrand.tsx @@ -21,7 +21,6 @@ import styled from 'styled-components'; import { Link } from 'components/common/router'; import { NAV_ITEM_HEIGHT } from 'theme/constants'; import useActivePerspective from 'components/perspectives/hooks/useActivePerspective'; -import usePerspectives from 'components/perspectives/hooks/usePerspectives'; const BrandContainer = styled.div` display: flex; @@ -35,9 +34,7 @@ const BrandLink = styled(Link)` `; const ActivePerspectiveBrand = ({ children, className }: PropsWithChildren<{ className?: string }>) => { - const perspectives = usePerspectives(); - const { activePerspective: activePerspectiveId } = useActivePerspective(); - const activePerspective = perspectives.find(({ id }) => id === activePerspectiveId); + const { activePerspective } = useActivePerspective(); const ActiveBrandComponent = activePerspective?.brandComponent; if (!ActiveBrandComponent) { @@ -46,7 +43,7 @@ const ActivePerspectiveBrand = ({ children, className }: PropsWithChildren<{ cla return ( - + {children} diff --git a/graylog2-web-interface/src/components/perspectives/PerspectivesSwitcher.test.tsx b/graylog2-web-interface/src/components/perspectives/PerspectivesSwitcher.test.tsx index e6d203a82c..dadf39ea4c 100644 --- a/graylog2-web-interface/src/components/perspectives/PerspectivesSwitcher.test.tsx +++ b/graylog2-web-interface/src/components/perspectives/PerspectivesSwitcher.test.tsx @@ -23,6 +23,7 @@ import usePerspectives from 'components/perspectives/hooks/usePerspectives'; import { asMock } from 'helpers/mocking'; import useActivePerspective from 'components/perspectives/hooks/useActivePerspective'; import mockHistory from 'helpers/mocking/mockHistory'; +import { defaultPerspective, examplePerspective } from 'fixtures/perspectives'; import PerspectivesSwitcher from './PerspectivesSwitcher'; @@ -34,28 +35,16 @@ jest.mock('routing/useHistory'); describe('PerspectivesSwitcher', () => { let history; const setActivePerspective = jest.fn(); + const mockedPerspectives = [defaultPerspective, examplePerspective]; beforeEach(() => { history = mockHistory(); asMock(useHistory).mockReturnValue(history); - asMock(usePerspectives).mockReturnValue([ - { - id: 'default', - title: 'Default Perspective', - brandComponent: () =>
Default perspective
, - brandLink: '', - }, - { - id: 'example-perspective', - title: 'Example Perspective', - brandComponent: () =>
, - brandLink: '/example-perspective', - }, - ]); + asMock(usePerspectives).mockReturnValue(mockedPerspectives); asMock(useActivePerspective).mockReturnValue({ - activePerspective: 'default', + activePerspective: mockedPerspectives[0], setActivePerspective, }); }); @@ -70,10 +59,10 @@ describe('PerspectivesSwitcher', () => { render(); userEvent.click(await screen.findByRole('button', { name: /change ui perspective/i })); - userEvent.click(await screen.findByText(/example perspective/i)); + userEvent.click(await screen.findByText(new RegExp(examplePerspective.title, 'i'))); - await waitFor(() => expect(history.push).toHaveBeenCalledWith('/example-perspective')); + await waitFor(() => expect(history.push).toHaveBeenCalledWith(examplePerspective.welcomeRoute)); - expect(setActivePerspective).toHaveBeenCalledWith('example-perspective'); + expect(setActivePerspective).toHaveBeenCalledWith(examplePerspective.id); }); }); diff --git a/graylog2-web-interface/src/components/perspectives/PerspectivesSwitcher.tsx b/graylog2-web-interface/src/components/perspectives/PerspectivesSwitcher.tsx index 90068d1002..bf876ba3aa 100644 --- a/graylog2-web-interface/src/components/perspectives/PerspectivesSwitcher.tsx +++ b/graylog2-web-interface/src/components/perspectives/PerspectivesSwitcher.tsx @@ -64,9 +64,9 @@ const Switcher = () => { const history = useHistory(); const onChangePerspective = useCallback((nextPerspectiveId: string) => () => { - const { brandLink } = perspectives.find(({ id }) => id === nextPerspectiveId); + const { welcomeRoute } = perspectives.find(({ id }) => id === nextPerspectiveId); - history.push(brandLink); + history.push(welcomeRoute); setActivePerspective(nextPerspectiveId); }, [history, perspectives, setActivePerspective]); diff --git a/graylog2-web-interface/src/components/perspectives/bindings.ts b/graylog2-web-interface/src/components/perspectives/bindings.ts index 932c387192..22e20a5942 100644 --- a/graylog2-web-interface/src/components/perspectives/bindings.ts +++ b/graylog2-web-interface/src/components/perspectives/bindings.ts @@ -24,7 +24,7 @@ const perspectivesBindings = { { id: 'default', title: 'General', - brandLink: Routes.WELCOME, + welcomeRoute: Routes.WELCOME, brandComponent: DefaultBrand, }, ], diff --git a/graylog2-web-interface/src/components/perspectives/contexts/PerspectivesContext.ts b/graylog2-web-interface/src/components/perspectives/contexts/PerspectivesContext.ts index 00058c513a..c0bc15499a 100644 --- a/graylog2-web-interface/src/components/perspectives/contexts/PerspectivesContext.ts +++ b/graylog2-web-interface/src/components/perspectives/contexts/PerspectivesContext.ts @@ -20,7 +20,7 @@ import { singleton } from 'logic/singleton'; import type { Perspective } from 'components/perspectives/types'; type PerspectivesContextType = { - activePerspective: string, + activePerspective: Perspective, availablePerspectives: Array setActivePerspective: (perspectiveId: string) => void } diff --git a/graylog2-web-interface/src/components/perspectives/contexts/PerspectivesProvider.tsx b/graylog2-web-interface/src/components/perspectives/contexts/PerspectivesProvider.tsx index 83a632f971..4d120def60 100644 --- a/graylog2-web-interface/src/components/perspectives/contexts/PerspectivesProvider.tsx +++ b/graylog2-web-interface/src/components/perspectives/contexts/PerspectivesProvider.tsx @@ -24,9 +24,16 @@ import { CurrentUserStore } from 'stores/users/CurrentUserStore'; import UserPreferencesContext from 'contexts/UserPreferencesContext'; import { PreferencesStore } from 'stores/users/PreferencesStore'; import Store from 'logic/local-storage/Store'; +import type { Perspective } from 'components/perspectives/types'; import PerspectivesContext from './PerspectivesContext'; +const DEFAULT_PERSPECTIVE = 'default'; + +const findPerspective = (perspectives: Array, perspectiveId: string) => ( + perspectives.find(({ id }) => id === perspectiveId) +); + const usePersistedSetting = (settingKey: string) => { const { userIsReadOnly, username } = useStore(CurrentUserStore, (userStore) => ({ username: userStore.currentUser?.username, @@ -51,23 +58,33 @@ const usePersistedSetting = (settingKey: string) => { return useMemo(() => [setting, setSetting], [setSetting, setting]); }; -const PerspectivesProvider = ({ children }: PropsWithChildren) => { +const useActivePerspectiveState = (availablePerspectives: Array) => { const [persistedPerspective, setPersistedPerspective] = usePersistedSetting('perspective'); - const [activePerspective, setActivePerspective] = useState(persistedPerspective ?? 'default'); + const [activePerspective, setActivePerspective] = useState( + findPerspective(availablePerspectives, persistedPerspective) ? persistedPerspective : DEFAULT_PERSPECTIVE, + ); const setActivePerspectiveWithPersistence = useCallback((newPerspective: string) => { setActivePerspective(newPerspective); return setPersistedPerspective(newPerspective); }, [setPersistedPerspective]); + + return { + activePerspective: findPerspective(availablePerspectives, activePerspective), + setActivePerspective: setActivePerspectiveWithPersistence, + }; +}; + +const PerspectivesProvider = ({ children }: PropsWithChildren) => { const allPerspectives = usePluginEntities('perspectives'); const availablePerspectives = allPerspectives .filter((perspective) => (perspective.useCondition ? !!perspective.useCondition() : true)); - + const { activePerspective, setActivePerspective } = useActivePerspectiveState(availablePerspectives); const contextValue = useMemo(() => ({ activePerspective, availablePerspectives, - setActivePerspective: setActivePerspectiveWithPersistence, - }), [activePerspective, availablePerspectives, setActivePerspectiveWithPersistence]); + setActivePerspective, + }), [activePerspective, availablePerspectives, setActivePerspective]); return ( diff --git a/graylog2-web-interface/src/components/perspectives/hooks/useActivePerspective.test.tsx b/graylog2-web-interface/src/components/perspectives/hooks/useActivePerspective.test.tsx index 1eac9dfdb7..44784f32b6 100644 --- a/graylog2-web-interface/src/components/perspectives/hooks/useActivePerspective.test.tsx +++ b/graylog2-web-interface/src/components/perspectives/hooks/useActivePerspective.test.tsx @@ -19,10 +19,17 @@ import { renderHook } from 'wrappedTestingLibrary/hooks'; import useActivePerspective from 'components/perspectives/hooks/useActivePerspective'; import PerspectivesProvider from 'components/perspectives/contexts/PerspectivesProvider'; +import usePluginEntities from 'hooks/usePluginEntities'; +import { asMock } from 'helpers/mocking'; +import { defaultPerspective } from 'fixtures/perspectives'; + +jest.mock('hooks/usePluginEntities'); describe('useActivePerspective', () => { - afterEach(() => { - jest.clearAllMocks(); + beforeEach(() => { + asMock(usePluginEntities).mockImplementation((entityKey) => ({ + perspectives: [defaultPerspective], + }[entityKey])); }); const wrapper = ({ children }: React.PropsWithChildren) => ( @@ -34,7 +41,7 @@ describe('useActivePerspective', () => { it('should return active perspective', async () => { const { result } = renderHook(() => useActivePerspective(), { wrapper }); - expect(result.current.activePerspective).toEqual('default'); + expect(result.current.activePerspective).toEqual(defaultPerspective); }); it('should throw error when being used outside of PerspectivesContext', async () => { diff --git a/graylog2-web-interface/src/components/perspectives/hooks/usePerspectives.test.tsx b/graylog2-web-interface/src/components/perspectives/hooks/usePerspectives.test.tsx index f0d912f6b2..373fec3266 100644 --- a/graylog2-web-interface/src/components/perspectives/hooks/usePerspectives.test.tsx +++ b/graylog2-web-interface/src/components/perspectives/hooks/usePerspectives.test.tsx @@ -19,28 +19,10 @@ import { renderHook } from 'wrappedTestingLibrary/hooks'; import usePerspectives from 'components/perspectives/hooks/usePerspectives'; import PerspectivesProvider from 'components/perspectives/contexts/PerspectivesProvider'; +import { defaultPerspective, examplePerspective, unavailablePerspective } from 'fixtures/perspectives'; -jest.mock('hooks/usePluginEntities', () => () => ([ - { - id: 'default', - title: 'Default Perspective', - brandComponent: () => {}, - brandLink: '', - }, - { - id: 'example-perspective', - title: 'Example Perspective', - brandComponent: () => {}, - brandLink: '', - }, - { - id: 'unavailable-perspective', - title: 'Unavailable Perspective', - brandComponent: () => {}, - brandLink: '', - useCondition: () => false, - }, -])); +const mockedPerspectives = [defaultPerspective, examplePerspective, unavailablePerspective]; +jest.mock('hooks/usePluginEntities', () => () => mockedPerspectives); describe('usePerspectives', () => { afterEach(() => { @@ -56,7 +38,7 @@ describe('usePerspectives', () => { it('should return available perspectives', async () => { const { result } = renderHook(() => usePerspectives(), { wrapper }); - expect(result.current.map(({ id }) => id)).toEqual(['default', 'example-perspective']); + expect(result.current.map(({ id }) => id)).toEqual([defaultPerspective.id, examplePerspective.id]); }); it('should throw error when being used outside of PerspectivesContext', async () => { diff --git a/graylog2-web-interface/src/components/perspectives/types.ts b/graylog2-web-interface/src/components/perspectives/types.ts index bf4129d815..13b65f0132 100644 --- a/graylog2-web-interface/src/components/perspectives/types.ts +++ b/graylog2-web-interface/src/components/perspectives/types.ts @@ -20,7 +20,7 @@ import type * as React from 'react'; export type Perspective = { id: string, title: string, - brandLink: string, + welcomeRoute: string, brandComponent: React.ComponentType<{ className?: string, disabled?: boolean, diff --git a/graylog2-web-interface/src/pages/StartPage.tsx b/graylog2-web-interface/src/pages/StartPage.tsx index 292b62858d..66c9ae3430 100644 --- a/graylog2-web-interface/src/pages/StartPage.tsx +++ b/graylog2-web-interface/src/pages/StartPage.tsx @@ -21,9 +21,11 @@ import Routes from 'routing/Routes'; import { CurrentUserStore } from 'stores/users/CurrentUserStore'; import { useStore } from 'stores/connect'; import useHistory from 'routing/useHistory'; +import useActivePerspective from 'components/perspectives/hooks/useActivePerspective'; const StartPage = () => { const { currentUser } = useStore(CurrentUserStore); + const { activePerspective } = useActivePerspective(); const isLoading = !currentUser; const history = useHistory(); @@ -49,8 +51,8 @@ const StartPage = () => { return; } - redirect(Routes.WELCOME); - }, [currentUser?.startpage, redirect]); + redirect(activePerspective.welcomeRoute); + }, [activePerspective, currentUser?.startpage, redirect]); useEffect(() => { CurrentUserStore.reload(); diff --git a/graylog2-web-interface/src/routing/AppRouter.test.tsx b/graylog2-web-interface/src/routing/AppRouter.test.tsx index 0a2c016c0f..8421539e5c 100644 --- a/graylog2-web-interface/src/routing/AppRouter.test.tsx +++ b/graylog2-web-interface/src/routing/AppRouter.test.tsx @@ -30,6 +30,7 @@ import usePluginEntities from 'hooks/usePluginEntities'; import AppConfig from 'util/AppConfig'; import GlobalContextProviders from 'contexts/GlobalContextProviders'; import HotkeysProvider from 'contexts/HotkeysProvider'; +import { defaultPerspective } from 'fixtures/perspectives'; import AppRouter from './AppRouter'; @@ -86,14 +87,7 @@ const mockRoutes = (routes: PluginExports['routes']) => { describe('AppRouter', () => { const defaultPlugins = { - perspectives: [ - { - id: 'default', - title: 'Default Perspective', - brandComponent: () =>
, - brandLink: '', - }, - ], + perspectives: [defaultPerspective], }; beforeEach(() => { diff --git a/graylog2-web-interface/test/fixtures/perspectives.tsx b/graylog2-web-interface/test/fixtures/perspectives.tsx new file mode 100644 index 0000000000..d33a16e358 --- /dev/null +++ b/graylog2-web-interface/test/fixtures/perspectives.tsx @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ + +import React from 'react'; + +export const defaultPerspective = { + id: 'default', + title: 'Default Perspective', + brandComponent: () =>
Default perspective
, + welcomeRoute: '', +}; + +export const examplePerspective = { + id: 'example-perspective', + title: 'Example Perspective', + brandComponent: () =>
, + welcomeRoute: '', +}; + +export const unavailablePerspective = { + id: 'unavailable-perspective', + title: 'Unavailable Perspective', + brandComponent: () =>
, + welcomeRoute: '', + useCondition: () => false, +};