mirror of
https://github.com/Graylog2/graylog2-server.git
synced 2026-03-13 09:32:21 +08:00
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:
@@ -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]);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const perspectivesBindings = {
|
||||
{
|
||||
id: 'default',
|
||||
title: 'General',
|
||||
brandLink: Routes.WELCOME,
|
||||
welcomeRoute: Routes.WELCOME,
|
||||
brandComponent: DefaultBrand,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
40
graylog2-web-interface/test/fixtures/perspectives.tsx
vendored
Normal file
40
graylog2-web-interface/test/fixtures/perspectives.tsx
vendored
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user