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.
This commit is contained in:
Linus Pahl
2023-12-21 10:07:02 +01:00
committed by GitHub
parent 769c3bf263
commit 16f3d66021
13 changed files with 97 additions and 69 deletions

View File

@@ -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]);

View File

@@ -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 (
<BrandContainer className={className}>
<BrandLink to={activePerspective.brandLink}>
<BrandLink to={activePerspective.welcomeRoute}>
<ActiveBrandComponent />
</BrandLink>
{children}

View File

@@ -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: () => <div>Default perspective</div>,
brandLink: '',
},
{
id: 'example-perspective',
title: 'Example Perspective',
brandComponent: () => <div />,
brandLink: '/example-perspective',
},
]);
asMock(usePerspectives).mockReturnValue(mockedPerspectives);
asMock(useActivePerspective).mockReturnValue({
activePerspective: 'default',
activePerspective: mockedPerspectives[0],
setActivePerspective,
});
});
@@ -70,10 +59,10 @@ describe('PerspectivesSwitcher', () => {
render(<PerspectivesSwitcher />);
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);
});
});

View File

@@ -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]);

View File

@@ -24,7 +24,7 @@ const perspectivesBindings = {
{
id: 'default',
title: 'General',
brandLink: Routes.WELCOME,
welcomeRoute: Routes.WELCOME,
brandComponent: DefaultBrand,
},
],

View File

@@ -20,7 +20,7 @@ import { singleton } from 'logic/singleton';
import type { Perspective } from 'components/perspectives/types';
type PerspectivesContextType = {
activePerspective: string,
activePerspective: Perspective,
availablePerspectives: Array<Perspective>
setActivePerspective: (perspectiveId: string) => void
}

View File

@@ -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<Perspective>, 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<Perspective>) => {
const [persistedPerspective, setPersistedPerspective] = usePersistedSetting('perspective');
const [activePerspective, setActivePerspective] = useState(persistedPerspective ?? 'default');
const [activePerspective, setActivePerspective] = useState<string>(
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 (
<PerspectivesContext.Provider value={contextValue}>

View File

@@ -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 () => {

View File

@@ -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 () => {

View File

@@ -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,

View File

@@ -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();

View File

@@ -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: () => <div />,
brandLink: '',
},
],
perspectives: [defaultPerspective],
};
beforeEach(() => {

View File

@@ -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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import React from 'react';
export const defaultPerspective = {
id: 'default',
title: 'Default Perspective',
brandComponent: () => <div>Default perspective</div>,
welcomeRoute: '',
};
export const examplePerspective = {
id: 'example-perspective',
title: 'Example Perspective',
brandComponent: () => <div />,
welcomeRoute: '',
};
export const unavailablePerspective = {
id: 'unavailable-perspective',
title: 'Unavailable Perspective',
brandComponent: () => <div />,
welcomeRoute: '',
useCondition: () => false,
};