Routing: Update AppRootPage (#95052)
* Update router * Fix rendering case * Switch useLocation * Remove redundant await
This commit is contained in:
@@ -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();
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('a None role user should only have access to pages with actions defined or undefined', async () => {
|
describe('Plugin page access control', () => {
|
||||||
contextSrv.user.orgRole = OrgRole.None;
|
beforeEach(() => {
|
||||||
|
// Reset context and permissions before each test
|
||||||
|
contextSrv.user.orgRole = OrgRole.None;
|
||||||
|
contextSrv.user.permissions = {};
|
||||||
|
contextSrv.isEditor = false;
|
||||||
|
contextSrv.isGrafanaAdmin = false;
|
||||||
|
});
|
||||||
|
|
||||||
// has access to a plugin entry page by default
|
it('should allow access to plugin entry page by default', async () => {
|
||||||
await renderUnderRouter('');
|
renderUnderRouter('');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// does not have access to a page with an action but no role
|
it('should deny access to page with action but no role when user has no permissions', async () => {
|
||||||
await renderUnderRouter('page-with-action-no-role');
|
renderUnderRouter('page-with-action-no-role');
|
||||||
expect(await screen.findByText('Access denied')).toBeVisible();
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// does not have access to a page with an action and role
|
it('should deny access to page with action and role when user has no permissions', async () => {
|
||||||
await renderUnderRouter('page-with-action-and-role');
|
renderUnderRouter('page-with-action-and-role');
|
||||||
expect(await screen.findByText('Access denied')).toBeVisible();
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// has access to a page without roles
|
it('should allow access to page without roles', async () => {
|
||||||
await renderUnderRouter('page-without-role');
|
renderUnderRouter('page-without-role');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// has access to Viewer page
|
it('should deny access to viewer page when user has no permissions', async () => {
|
||||||
await renderUnderRouter('viewer-page');
|
renderUnderRouter('viewer-page');
|
||||||
expect(await screen.findByText('Access denied')).toBeVisible();
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
contextSrv.user.permissions = {
|
describe('with user permissions', () => {
|
||||||
'grafana-awesomeapp.user-settings:read': true,
|
beforeEach(() => {
|
||||||
};
|
contextSrv.user.permissions = {
|
||||||
|
'grafana-awesomeapp.user-settings:read': true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// has access to a page with an action but no role
|
it('should allow access to page with action but no role when user has permissions', async () => {
|
||||||
await renderUnderRouter('page-with-action-no-role');
|
renderUnderRouter('page-with-action-no-role');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// has access to a page with an action and role
|
it('should allow access to page with action and role when user has permissions', async () => {
|
||||||
await renderUnderRouter('page-with-action-and-role');
|
renderUnderRouter('page-with-action-and-role');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
describe('Viewer role access', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
contextSrv.user.orgRole = OrgRole.Viewer;
|
||||||
|
});
|
||||||
|
|
||||||
it('a Viewer should only have access to pages with "Viewer" roles', async () => {
|
it('should allow access to plugin entry page', async () => {
|
||||||
contextSrv.user.orgRole = OrgRole.Viewer;
|
renderUnderRouter('');
|
||||||
contextSrv.user.permissions = {};
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// Viewer has access to a plugin entry page by default
|
it('should deny access to page with action but no role', 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', 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();
|
||||||
|
});
|
||||||
|
|
||||||
// Viewer has access to a page without roles
|
it('should allow access to viewer page', async () => {
|
||||||
await renderUnderRouter('page-without-role');
|
renderUnderRouter('viewer-page');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// Viewer has access to Viewer page
|
it('should deny access to editor page', async () => {
|
||||||
await renderUnderRouter('viewer-page');
|
renderUnderRouter('editor-page');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// Viewer does not have access to Editor page
|
it('should deny access to admin page', async () => {
|
||||||
await renderUnderRouter('editor-page');
|
renderUnderRouter('admin-page');
|
||||||
expect(await screen.findByText('Access denied')).toBeVisible();
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Editor role access', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
contextSrv.user.orgRole = OrgRole.Editor;
|
||||||
|
contextSrv.isEditor = true;
|
||||||
|
});
|
||||||
|
|
||||||
// Viewer does not have access to a Admin page
|
describe('without permissions', () => {
|
||||||
await renderUnderRouter('admin-page');
|
it('should deny access to pages with actions', async () => {
|
||||||
expect(await screen.findByText('Access denied')).toBeVisible();
|
renderUnderRouter('page-with-action-no-role');
|
||||||
});
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
|
||||||
it('an Editor should have access to pages with both "Viewer" and "Editor" roles', async () => {
|
renderUnderRouter('page-with-action-and-role');
|
||||||
contextSrv.user.orgRole = OrgRole.Editor;
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
contextSrv.isEditor = true;
|
});
|
||||||
contextSrv.user.permissions = {};
|
|
||||||
|
|
||||||
// does not have access to a page with an action but no role
|
it('should allow access to basic pages', async () => {
|
||||||
await renderUnderRouter('page-with-action-no-role');
|
renderUnderRouter('');
|
||||||
expect(await screen.findByText('Access denied')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
// does not have access to a page with an action and role
|
renderUnderRouter('page-without-role');
|
||||||
await renderUnderRouter('page-with-action-and-role');
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
expect(await screen.findByText('Access denied')).toBeVisible();
|
});
|
||||||
|
|
||||||
// has access to a plugin entry page by default
|
it('should allow access to viewer and editor pages', async () => {
|
||||||
await renderUnderRouter('');
|
renderUnderRouter('viewer-page');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
// has access to a page without roles
|
renderUnderRouter('editor-page');
|
||||||
await renderUnderRouter('page-without-role');
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
});
|
||||||
|
|
||||||
// has access to Viewer page
|
it('should deny access to admin page', async () => {
|
||||||
await renderUnderRouter('viewer-page');
|
renderUnderRouter('admin-page');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// has access to Editor page
|
describe('with permissions', () => {
|
||||||
await renderUnderRouter('editor-page');
|
beforeEach(() => {
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
contextSrv.user.permissions = {
|
||||||
|
'grafana-awesomeapp.user-settings:read': true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// does not have access to a Admin page
|
it('should allow access to pages with actions', async () => {
|
||||||
await renderUnderRouter('admin-page');
|
renderUnderRouter('page-with-action-no-role');
|
||||||
expect(await screen.findByText('Access denied')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
contextSrv.user.permissions = {
|
renderUnderRouter('page-with-action-and-role');
|
||||||
'grafana-awesomeapp.user-settings:read': true,
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
};
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// has access to a page with an action but no role
|
describe('Admin role access', () => {
|
||||||
await renderUnderRouter('page-with-action-no-role');
|
beforeEach(() => {
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
contextSrv.user.orgRole = OrgRole.Admin;
|
||||||
|
});
|
||||||
|
|
||||||
// has access to a page with an action and role
|
describe('without permissions', () => {
|
||||||
await renderUnderRouter('page-with-action-and-role');
|
it('should deny access to pages with actions', async () => {
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
renderUnderRouter('page-with-action-no-role');
|
||||||
});
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
|
||||||
it('an Admin should have access to pages with both "Viewer" and "Editor" roles', async () => {
|
renderUnderRouter('page-with-action-and-role');
|
||||||
contextSrv.user.orgRole = OrgRole.Admin;
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
contextSrv.user.permissions = {};
|
});
|
||||||
|
|
||||||
// does not have access to a page with an action but no role
|
it('should allow access to plugin entry page', async () => {
|
||||||
await renderUnderRouter('page-with-action-no-role');
|
renderUnderRouter('');
|
||||||
expect(await screen.findByText('Access denied')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// does not have access to a page with an action and role
|
it('should allow access to page without role', 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();
|
||||||
|
});
|
||||||
|
|
||||||
// has access to a plugin entry page by default
|
it('should allow access to viewer page', async () => {
|
||||||
await renderUnderRouter('');
|
renderUnderRouter('viewer-page');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// has access to a page without roles
|
it('should allow access to editor page', async () => {
|
||||||
await renderUnderRouter('page-without-role');
|
renderUnderRouter('editor-page');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// has access to Viewer page
|
it('should allow access to admin page', async () => {
|
||||||
await renderUnderRouter('viewer-page');
|
renderUnderRouter('admin-page');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// has access to Editor page
|
describe('Grafana Admin access', () => {
|
||||||
await renderUnderRouter('editor-page');
|
beforeEach(() => {
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
contextSrv.isGrafanaAdmin = true;
|
||||||
|
});
|
||||||
|
|
||||||
// has access to a Admin page
|
it('should deny access to pages with actions', async () => {
|
||||||
await renderUnderRouter('admin-page');
|
renderUnderRouter('page-with-action-no-role');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
});
|
|
||||||
|
|
||||||
it('a Grafana Admin should be able to see any page without action specifier', async () => {
|
renderUnderRouter('page-with-action-and-role');
|
||||||
contextSrv.isGrafanaAdmin = true;
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// Viewer has access to a plugin entry page
|
it('should allow access to plugin entry page', async () => {
|
||||||
await renderUnderRouter('');
|
renderUnderRouter('');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// Viewer does not have access to a page with an action but no role
|
it('should allow access to page without role', async () => {
|
||||||
await renderUnderRouter('page-with-action-no-role');
|
renderUnderRouter('page-without-role');
|
||||||
expect(await screen.findByText('Access denied')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// Viewer does not have access to a page with an action and role
|
it('should allow access to viewer page', async () => {
|
||||||
await renderUnderRouter('page-with-action-and-role');
|
renderUnderRouter('viewer-page');
|
||||||
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 allow access to editor page', async () => {
|
||||||
await renderUnderRouter('page-without-role');
|
renderUnderRouter('editor-page');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// Admin has access to Viewer page
|
it('should allow access to admin page', async () => {
|
||||||
await renderUnderRouter('viewer-page');
|
renderUnderRouter('admin-page');
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
// Admin has access to Editor page
|
});
|
||||||
await renderUnderRouter('editor-page');
|
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
|
||||||
|
|
||||||
// Admin has access to a Admin page
|
|
||||||
await 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user