Routing: Update AppRootPage (#95052)

* Update router

* Fix rendering case

* Switch useLocation

* Remove redundant await
pull/95162/head
Alex Khomenko 1 year ago committed by GitHub
parent 2d27703955
commit 7fe710b141
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 443
      public/app/features/plugins/components/AppRootPage.test.tsx
  2. 3
      public/app/features/plugins/components/AppRootPage.tsx

@ -1,13 +1,12 @@
import { act, screen } from '@testing-library/react'; import { act, screen } from '@testing-library/react';
import { Component } from 'react'; import { Component } from 'react';
import { Route, Router } from 'react-router-dom'; import { Routes, Route, Link } from 'react-router-dom-v5-compat';
import { render } from 'test/test-utils'; import { render } from 'test/test-utils';
import { AppPlugin, PluginType, AppRootProps, NavModelItem, PluginIncludeType, OrgRole } from '@grafana/data'; import { AppPlugin, PluginType, AppRootProps, NavModelItem, PluginIncludeType, OrgRole } from '@grafana/data';
import { getMockPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { getMockPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { locationService, setEchoSrv } from '@grafana/runtime'; import { setEchoSrv } from '@grafana/runtime';
import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute'; import { GrafanaRouteWrapper } from 'app/core/navigation/GrafanaRoute';
import { RouteDescriptor } from 'app/core/navigation/types';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { Echo } from 'app/core/services/echo/Echo'; import { Echo } from 'app/core/services/echo/Echo';
@ -62,7 +61,7 @@ class RootComponent extends Component<AppRootProps> {
} }
} }
async function renderUnderRouter(page = '') { function renderUnderRouter(page = '') {
const appPluginNavItem: NavModelItem = { const appPluginNavItem: NavModelItem = {
text: 'App', text: 'App',
id: 'plugin-page-app', id: 'plugin-page-app',
@ -90,19 +89,35 @@ async function renderUnderRouter(page = '') {
const registries = setupPluginExtensionRegistries(); const registries = setupPluginExtensionRegistries();
const pagePath = page ? `/${page}` : ''; const pagePath = page ? `/${page}` : '';
const route = { const route = {
path: `/a/:pluginId/*`,
component: () => <AppRootPage pluginId="my-awesome-plugin" pluginNavSection={appsSection} />, component: () => <AppRootPage pluginId="my-awesome-plugin" pluginNavSection={appsSection} />,
} as unknown as RouteDescriptor; };
await act(async () => { const Foo = () => {
locationService.push(`/a/my-awesome-plugin${pagePath}`); return <Link to={`/a/my-awesome-plugin${pagePath}`}>Navigate</Link>;
}); };
render( return render(
<Router history={locationService.getHistory()}> <ExtensionRegistriesProvider registries={registries}>
<ExtensionRegistriesProvider registries={registries}> <Routes>
<Route path={`/a/:pluginId${pagePath}`} exact render={(props) => <GrafanaRoute {...props} route={route} />} /> <Route
</ExtensionRegistriesProvider> path={route.path}
</Router> element={
<>
<GrafanaRouteWrapper route={route} />
{/* Add Link to trigger navigation instead of using locationService */}
<Link to={'/foo'}>Navigate</Link>
</>
}
/>
<Route path={'/foo'} element={<Foo />} />
</Routes>
</ExtensionRegistriesProvider>,
{
historyOptions: {
initialEntries: [`/a/my-awesome-plugin${pagePath}`],
},
}
); );
} }
@ -122,9 +137,7 @@ describe('AppRootPage', () => {
jest.spyOn(console, 'error').mockImplementation(); jest.spyOn(console, 'error').mockImplementation();
getPluginSettingsMock.mockRejectedValue(new Error('Unknown Plugin')); getPluginSettingsMock.mockRejectedValue(new Error('Unknown Plugin'));
// Renders once for the first time // Renders once for the first time
await act(async () => { renderUnderRouter();
await renderUnderRouter();
});
expect(await screen.findByText('App not found')).toBeVisible(); expect(await screen.findByText('App not found')).toBeVisible();
}); });
@ -138,19 +151,19 @@ describe('AppRootPage', () => {
importAppPluginMock.mockResolvedValue(plugin); importAppPluginMock.mockResolvedValue(plugin);
// Renders once for the first time // Renders once for the first time
await renderUnderRouter(); renderUnderRouter();
expect(await screen.findByText('my great component')).toBeVisible(); expect(await screen.findByText('my great component')).toBeVisible();
expect(RootComponent.timesRendered).toEqual(1); expect(RootComponent.timesRendered).toEqual(1);
// Does not render again when navigating to a non-plugin path // Does not render again when navigating to a non-plugin path
await act(async () => { await act(async () => {
locationService.push('/foo'); screen.getByRole('link', { name: 'Navigate' }).click();
}); });
expect(RootComponent.timesRendered).toEqual(1); expect(RootComponent.timesRendered).toEqual(1);
// Renders it again when navigating back to a plugin path // Renders it again when navigating back to a plugin path
await act(async () => { await act(async () => {
locationService.push('/a/my-awesome-plugin'); screen.getByRole('link', { name: 'Navigate' }).click();
}); });
expect(RootComponent.timesRendered).toEqual(2); expect(RootComponent.timesRendered).toEqual(2);
}); });
@ -219,188 +232,234 @@ describe('AppRootPage', () => {
it('an User should not be able to see page with not existing role', async () => { it('an User should not be able to see page with not existing role', async () => {
contextSrv.user.orgRole = OrgRole.Editor; contextSrv.user.orgRole = OrgRole.Editor;
await renderUnderRouter('mistake-page'); renderUnderRouter('mistake-page');
expect(await screen.findByText('Access denied')).toBeVisible();
});
it('a None role user should only have access to pages with actions defined or undefined', async () => {
contextSrv.user.orgRole = OrgRole.None;
// has access to a plugin entry page by default
await renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
// does not have access to a page with an action but no role
await renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// does not have access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// has access to a page without roles
await renderUnderRouter('page-without-role');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to Viewer page
await renderUnderRouter('viewer-page');
expect(await screen.findByText('Access denied')).toBeVisible();
contextSrv.user.permissions = {
'grafana-awesomeapp.user-settings:read': true,
};
// has access to a page with an action but no role
await renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('a Viewer should only have access to pages with "Viewer" roles', async () => {
contextSrv.user.orgRole = OrgRole.Viewer;
contextSrv.user.permissions = {};
// Viewer has access to a plugin entry page by default
await renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
// Viewer does not have access to a page with an action but no role
await renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// Viewer does not have access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// Viewer has access to a page without roles
await renderUnderRouter('page-without-role');
expect(await screen.findByText('my great component')).toBeVisible();
// Viewer has access to Viewer page
await renderUnderRouter('viewer-page');
expect(await screen.findByText('my great component')).toBeVisible();
// Viewer does not have access to Editor page
await renderUnderRouter('editor-page');
expect(await screen.findByText('Access denied')).toBeVisible();
// Viewer does not have access to a Admin page
await renderUnderRouter('admin-page');
expect(await screen.findByText('Access denied')).toBeVisible(); expect(await screen.findByText('Access denied')).toBeVisible();
}); });
it('an Editor should have access to pages with both "Viewer" and "Editor" roles', async () => { describe('Plugin page access control', () => {
contextSrv.user.orgRole = OrgRole.Editor; beforeEach(() => {
contextSrv.isEditor = true; // Reset context and permissions before each test
contextSrv.user.permissions = {}; contextSrv.user.orgRole = OrgRole.None;
contextSrv.user.permissions = {};
// does not have access to a page with an action but no role contextSrv.isEditor = false;
await renderUnderRouter('page-with-action-no-role'); contextSrv.isGrafanaAdmin = false;
expect(await screen.findByText('Access denied')).toBeVisible(); });
// does not have access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// has access to a plugin entry page by default
await renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to a page without roles
await renderUnderRouter('page-without-role');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to Viewer page
await renderUnderRouter('viewer-page');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to Editor page
await renderUnderRouter('editor-page');
expect(await screen.findByText('my great component')).toBeVisible();
// does not have access to a Admin page
await renderUnderRouter('admin-page');
expect(await screen.findByText('Access denied')).toBeVisible();
contextSrv.user.permissions = {
'grafana-awesomeapp.user-settings:read': true,
};
// has access to a page with an action but no role
await renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('an Admin should have access to pages with both "Viewer" and "Editor" roles', async () => {
contextSrv.user.orgRole = OrgRole.Admin;
contextSrv.user.permissions = {};
// does not have access to a page with an action but no role
await renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// does not have access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// has access to a plugin entry page by default
await renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to a page without roles
await renderUnderRouter('page-without-role');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to Viewer page
await renderUnderRouter('viewer-page');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to Editor page
await renderUnderRouter('editor-page');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to a Admin page
await renderUnderRouter('admin-page');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('a Grafana Admin should be able to see any page without action specifier', async () => { it('should allow access to plugin entry page by default', async () => {
contextSrv.isGrafanaAdmin = true; renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
});
// Viewer has access to a plugin entry page it('should deny access to page with action but no role when user has no permissions', async () => {
await renderUnderRouter(''); renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('my great component')).toBeVisible(); expect(await screen.findByText('Access denied')).toBeVisible();
});
// Viewer does not have access to a page with an action but no role it('should deny access to page with action and role when user has no permissions', async () => {
await renderUnderRouter('page-with-action-no-role'); renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible(); expect(await screen.findByText('Access denied')).toBeVisible();
});
// Viewer does not have access to a page with an action and role it('should allow access to page without roles', async () => {
await renderUnderRouter('page-with-action-and-role'); renderUnderRouter('page-without-role');
expect(await screen.findByText('Access denied')).toBeVisible(); expect(await screen.findByText('my great component')).toBeVisible();
});
// Admin has access to a page without roles it('should deny access to viewer page when user has no permissions', async () => {
await renderUnderRouter('page-without-role'); renderUnderRouter('viewer-page');
expect(await screen.findByText('my great component')).toBeVisible(); expect(await screen.findByText('Access denied')).toBeVisible();
});
// Admin has access to Viewer page describe('with user permissions', () => {
await renderUnderRouter('viewer-page'); beforeEach(() => {
expect(await screen.findByText('my great component')).toBeVisible(); contextSrv.user.permissions = {
'grafana-awesomeapp.user-settings:read': true,
};
});
it('should allow access to page with action but no role when user has permissions', async () => {
renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should allow access to page with action and role when user has permissions', async () => {
renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('my great component')).toBeVisible();
});
});
describe('Viewer role access', () => {
beforeEach(() => {
contextSrv.user.orgRole = OrgRole.Viewer;
});
it('should allow access to plugin entry page', async () => {
renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should deny access to page with action but no role', async () => {
renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('Access denied')).toBeVisible();
});
it('should deny access to page with action and role', async () => {
renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
});
it('should allow access to page without roles', async () => {
renderUnderRouter('page-without-role');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should allow access to viewer page', async () => {
renderUnderRouter('viewer-page');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should deny access to editor page', async () => {
renderUnderRouter('editor-page');
expect(await screen.findByText('Access denied')).toBeVisible();
});
it('should deny access to admin page', async () => {
renderUnderRouter('admin-page');
expect(await screen.findByText('Access denied')).toBeVisible();
});
});
describe('Editor role access', () => {
beforeEach(() => {
contextSrv.user.orgRole = OrgRole.Editor;
contextSrv.isEditor = true;
});
describe('without permissions', () => {
it('should deny access to pages with actions', async () => {
renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('Access denied')).toBeVisible();
renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
});
it('should allow access to basic pages', async () => {
renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
renderUnderRouter('page-without-role');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should allow access to viewer and editor pages', async () => {
renderUnderRouter('viewer-page');
expect(await screen.findByText('my great component')).toBeVisible();
renderUnderRouter('editor-page');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should deny access to admin page', async () => {
renderUnderRouter('admin-page');
expect(await screen.findByText('Access denied')).toBeVisible();
});
});
describe('with permissions', () => {
beforeEach(() => {
contextSrv.user.permissions = {
'grafana-awesomeapp.user-settings:read': true,
};
});
it('should allow access to pages with actions', async () => {
renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('my great component')).toBeVisible();
renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('my great component')).toBeVisible();
});
});
});
// Admin has access to Editor page describe('Admin role access', () => {
await renderUnderRouter('editor-page'); beforeEach(() => {
expect(await screen.findByText('my great component')).toBeVisible(); contextSrv.user.orgRole = OrgRole.Admin;
});
describe('without permissions', () => {
it('should deny access to pages with actions', async () => {
renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('Access denied')).toBeVisible();
renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
});
it('should allow access to plugin entry page', async () => {
renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should allow access to page without role', async () => {
renderUnderRouter('page-without-role');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should allow access to viewer page', async () => {
renderUnderRouter('viewer-page');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should allow access to editor page', async () => {
renderUnderRouter('editor-page');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should allow access to admin page', async () => {
renderUnderRouter('admin-page');
expect(await screen.findByText('my great component')).toBeVisible();
});
});
});
// Admin has access to a Admin page describe('Grafana Admin access', () => {
await renderUnderRouter('admin-page'); beforeEach(() => {
expect(await screen.findByText('my great component')).toBeVisible(); contextSrv.isGrafanaAdmin = true;
});
it('should deny access to pages with actions', async () => {
renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('Access denied')).toBeVisible();
renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
});
it('should allow access to plugin entry page', async () => {
renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should allow access to page without role', async () => {
renderUnderRouter('page-without-role');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should allow access to viewer page', async () => {
renderUnderRouter('viewer-page');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should allow access to editor page', async () => {
renderUnderRouter('editor-page');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('should allow access to admin page', async () => {
renderUnderRouter('admin-page');
expect(await screen.findByText('my great component')).toBeVisible();
});
});
}); });
}); });
}); });

@ -2,8 +2,7 @@
import { AnyAction, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { AnyAction, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { useCallback, useEffect, useMemo, useReducer } from 'react'; import { useCallback, useEffect, useMemo, useReducer } from 'react';
import * as React from 'react'; import * as React from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom-v5-compat';
import { useParams } from 'react-router-dom-v5-compat';
import { import {
AppEvents, AppEvents,

Loading…
Cancel
Save