diff --git a/public/app/features/admin/AdminEditOrgPage.tsx b/public/app/features/admin/AdminEditOrgPage.tsx index f83374d68b6..c557c3a8507 100644 --- a/public/app/features/admin/AdminEditOrgPage.tsx +++ b/public/app/features/admin/AdminEditOrgPage.tsx @@ -1,12 +1,12 @@ import { useState, useEffect } from 'react'; import { useForm } from 'react-hook-form'; +import { useParams } from 'react-router-dom-v5-compat'; import { useAsyncFn } from 'react-use'; import { NavModelItem } from '@grafana/data'; import { Field, Input, Button, Legend, Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { OrgUser, AccessControlAction, OrgRole } from 'app/types'; import { OrgUsersTable } from './Users/OrgUsersTable'; @@ -16,10 +16,9 @@ interface OrgNameDTO { orgName: string; } -interface Props extends GrafanaRouteComponentProps<{ id: string }> {} - -const AdminEditOrgPage = ({ match }: Props) => { - const orgId = parseInt(match.params.id, 10); +const AdminEditOrgPage = () => { + const { id = '' } = useParams(); + const orgId = parseInt(id, 10); const canWriteOrg = contextSrv.hasPermission(AccessControlAction.OrgsWrite); const canReadUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead); diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index dc40a882c73..dbfa918b49e 100644 --- a/public/app/features/admin/UserAdminPage.tsx +++ b/public/app/features/admin/UserAdminPage.tsx @@ -1,12 +1,12 @@ -import { PureComponent } from 'react'; +import { useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import { useParams } from 'react-router-dom-v5-compat'; import { NavModelItem } from '@grafana/data'; import { featureEnabled } from '@grafana/runtime'; import { Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { StoreState, UserDTO, UserOrg, UserSession, SyncInfo, UserAdminError, AccessControlAction } from 'app/types'; import { UserLdapSyncInfo } from './UserLdapSyncInfo'; @@ -30,7 +30,7 @@ import { syncLdapUser, } from './state/actions'; -interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { +interface OwnProps { user?: UserDTO; orgs: UserOrg[]; sessions: UserSession[]; @@ -39,136 +39,142 @@ interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { error?: UserAdminError; } -export class UserAdminPage extends PureComponent { - async componentDidMount() { - const { match, loadAdminUserPage } = this.props; - loadAdminUserPage(parseInt(match.params.id, 10)); - } +export const UserAdminPage = ({ + loadAdminUserPage, + user, + orgs, + sessions, + ldapSyncInfo, + isLoading, + updateUser, + setUserPassword, + deleteUser, + disableUser, + enableUser, + updateUserPermissions, + deleteOrgUser, + updateOrgUserRole, + addOrgUser, + revokeSession, + revokeAllSessions, + syncLdapUser, +}: Props) => { + const { id = '' } = useParams(); + useEffect(() => { + const userId = parseInt(id, 10); + loadAdminUserPage(userId); + }, [id, loadAdminUserPage]); - onUserUpdate = (user: UserDTO) => { - this.props.updateUser(user); + const onPasswordChange = (password: string) => { + if (user) { + setUserPassword(user.id, password); + } }; - onPasswordChange = (password: string) => { - const { user, setUserPassword } = this.props; - user && setUserPassword(user.id, password); + const onGrafanaAdminChange = (isGrafanaAdmin: boolean) => { + if (user) { + updateUserPermissions(user.id, isGrafanaAdmin); + } }; - onUserDelete = (userId: number) => { - this.props.deleteUser(userId); + const onOrgRemove = (orgId: number) => { + if (user) { + deleteOrgUser(user.id, orgId); + } }; - onUserDisable = (userId: number) => { - this.props.disableUser(userId); + const onOrgRoleChange = (orgId: number, newRole: string) => { + if (user) { + updateOrgUserRole(user.id, orgId, newRole); + } }; - onUserEnable = (userId: number) => { - this.props.enableUser(userId); + const onOrgAdd = (orgId: number, role: string) => { + if (user) { + addOrgUser(user, orgId, role); + } }; - onGrafanaAdminChange = (isGrafanaAdmin: boolean) => { - const { user, updateUserPermissions } = this.props; - user && updateUserPermissions(user.id, isGrafanaAdmin); + const onSessionRevoke = (tokenId: number) => { + if (user) { + revokeSession(tokenId, user.id); + } }; - onOrgRemove = (orgId: number) => { - const { user, deleteOrgUser } = this.props; - user && deleteOrgUser(user.id, orgId); + const onAllSessionsRevoke = () => { + if (user) { + revokeAllSessions(user.id); + } }; - onOrgRoleChange = (orgId: number, newRole: string) => { - const { user, updateOrgUserRole } = this.props; - user && updateOrgUserRole(user.id, orgId, newRole); + const onUserSync = () => { + if (user) { + syncLdapUser(user.id); + } }; - onOrgAdd = (orgId: number, role: string) => { - const { user, addOrgUser } = this.props; - user && addOrgUser(user, orgId, role); + const isLDAPUser = user?.isExternal && user?.authLabels?.includes('LDAP'); + const canReadSessions = contextSrv.hasPermission(AccessControlAction.UsersAuthTokenList); + const canReadLDAPStatus = contextSrv.hasPermission(AccessControlAction.LDAPStatusRead); + const authSource = user?.authLabels?.[0]; + const lockMessage = authSource ? `Synced via ${authSource}` : ''; + const pageNav: NavModelItem = { + text: user?.login ?? '', + icon: 'shield', + subTitle: 'Manage settings for an individual user.', }; - onSessionRevoke = (tokenId: number) => { - const { user, revokeSession } = this.props; - user && revokeSession(tokenId, user.id); - }; - - onAllSessionsRevoke = () => { - const { user, revokeAllSessions } = this.props; - user && revokeAllSessions(user.id); - }; - - onUserSync = () => { - const { user, syncLdapUser } = this.props; - user && syncLdapUser(user.id); - }; - - render() { - const { user, orgs, sessions, ldapSyncInfo, isLoading } = this.props; - const isLDAPUser = user?.isExternal && user?.authLabels?.includes('LDAP'); - const canReadSessions = contextSrv.hasPermission(AccessControlAction.UsersAuthTokenList); - const canReadLDAPStatus = contextSrv.hasPermission(AccessControlAction.LDAPStatusRead); - const authSource = user?.authLabels?.[0]; - const lockMessage = authSource ? `Synced via ${authSource}` : ''; - - const pageNav: NavModelItem = { - text: user?.login ?? '', - icon: 'shield', - subTitle: 'Manage settings for an individual user.', - }; - - return ( - - - - {user && ( - <> - - {isLDAPUser && - user?.isExternallySynced && - featureEnabled('ldapsync') && - ldapSyncInfo && - canReadLDAPStatus && ( - - )} - - - )} - - {orgs && ( - + + + {user && ( + <> + - )} - - {sessions && canReadSessions && ( - + )} + - )} - - - - ); - } -} + + )} + {orgs && ( + + )} + {sessions && canReadSessions && ( + + )} + + + + ); +}; const mapStateToProps = (state: StoreState) => ({ user: state.userAdmin.user, diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx index 68054e1744e..009f39a8b7c 100644 --- a/public/app/features/alerting/unified/Silences.test.tsx +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -1,3 +1,4 @@ +import { useParams } from 'react-router-dom-v5-compat'; import { render, screen, userEvent, waitFor, within } from 'test/test-utils'; import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector'; @@ -31,6 +32,11 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; jest.mock('app/core/services/context_srv'); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: jest.fn(), +})); + const TEST_TIMEOUT = 60000; const renderSilences = (location = '/alerting/silences/') => { @@ -314,17 +320,20 @@ describe('Silence create/edit', () => { }); it('shows an error when existing silence cannot be found', async () => { + (useParams as jest.Mock).mockReturnValue({ id: 'foo-bar' }); renderSilences('/alerting/silence/foo-bar/edit'); expect(await ui.existingSilenceNotFound.find()).toBeInTheDocument(); }); it('shows an error when user cannot edit/recreate silence', async () => { + (useParams as jest.Mock).mockReturnValue({ id: MOCK_SILENCE_ID_LACKING_PERMISSIONS }); renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_LACKING_PERMISSIONS}/edit`); expect(await ui.noPermissionToEdit.find()).toBeInTheDocument(); }); it('populates form with existing silence information', async () => { + (useParams as jest.Mock).mockReturnValue({ id: MOCK_SILENCE_ID_EXISTING }); renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_EXISTING}/edit`); // Await the first value to be populated, after which we can expect that all of the other @@ -335,6 +344,7 @@ describe('Silence create/edit', () => { }); it('populates form with existing silence information that has __alert_rule_uid__', async () => { + (useParams as jest.Mock).mockReturnValue({ id: MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID }); mockAlertRuleApi(server).getAlertRule(MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID, grafanaRulerRule); renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID}/edit`); expect(await screen.findByLabelText(/alert rule/i)).toHaveValue(grafanaRulerRule.grafana_alert.title); diff --git a/public/app/features/alerting/unified/Silences.tsx b/public/app/features/alerting/unified/Silences.tsx index c9a2cd10af7..5bfbb676278 100644 --- a/public/app/features/alerting/unified/Silences.tsx +++ b/public/app/features/alerting/unified/Silences.tsx @@ -1,4 +1,4 @@ -import { Route, RouteChildrenProps, Switch } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import { withErrorBoundary } from '@grafana/ui'; import { @@ -51,13 +51,7 @@ const Silences = () => { }} - {({ match }: RouteChildrenProps<{ id: string }>) => { - return ( - match?.params.id && ( - - ) - ); - }} + diff --git a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx index a24860ace5d..f64211b329f 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Route } from 'react-router-dom'; +import { Routes, Route } from 'react-router-dom-v5-compat'; import { Props } from 'react-virtualized-auto-sizer'; import { render, waitFor, waitForElementToBeRemoved, userEvent } from 'test/test-utils'; import { byRole, byTestId, byText } from 'testing-library-selector'; @@ -55,9 +55,14 @@ const dataSources = { }; function renderModifyExport(ruleId: string) { - render(, { - historyOptions: { initialEntries: [`/alerting/${ruleId}/modify-export`] }, - }); + render( + + } /> + , + { + historyOptions: { initialEntries: [`/alerting/${ruleId}/modify-export`] }, + } + ); } const server = setupMswServer(); diff --git a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx index 541bf1eb0e5..108532e5e41 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { useMemo } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { locationService } from '@grafana/runtime'; import { Alert, LoadingPlaceholder } from '@grafana/ui'; -import { GrafanaRouteComponentProps } from '../../../../../core/navigation/types'; import { RuleIdentifier } from '../../../../../types/unified-alerting'; import { useRuleWithLocation } from '../../hooks/useCombinedRule'; import { stringifyErrorLike } from '../../utils/misc'; @@ -15,12 +15,11 @@ import { createRelativeUrl } from '../../utils/url'; import { AlertingPageWrapper } from '../AlertingPageWrapper'; import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExportRuleForm'; -interface GrafanaModifyExportProps extends GrafanaRouteComponentProps<{ id?: string }> {} - -export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps) { +export default function GrafanaModifyExport() { + const { id } = useParams(); const ruleIdentifier = useMemo(() => { - return ruleId.tryParse(match.params.id, true); - }, [match.params.id]); + return ruleId.tryParse(id, true); + }, [id]); if (!ruleIdentifier) { return ( diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx index 56e1f533e76..e9ce5661d3c 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import { pickBy } from 'lodash'; import { useMemo, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { useParams } from 'react-router-dom-v5-compat'; import { useDebounce } from 'react-use'; import { @@ -41,7 +42,6 @@ import { SilencedInstancesPreview } from './SilencedInstancesPreview'; import { getDefaultSilenceFormValues, getFormFieldsForSilence } from './utils'; interface Props { - silenceId: string; alertManagerSourceName: string; } @@ -50,7 +50,8 @@ interface Props { * * Fetches silence details from API, based on `silenceId` */ -const ExistingSilenceEditor = ({ silenceId, alertManagerSourceName }: Props) => { +const ExistingSilenceEditor = ({ alertManagerSourceName }: Props) => { + const { id: silenceId = '' } = useParams(); const { data: silence, isLoading: getSilenceIsLoading, @@ -61,7 +62,6 @@ const ExistingSilenceEditor = ({ silenceId, alertManagerSourceName }: Props) => ruleMetadata: true, accessControl: true, }); - const ruleUid = silence?.matchers?.find((m) => m.name === MATCHER_ALERT_RULE_UID)?.value; const isGrafanaAlertManager = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME; diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx index 7269e217afa..467786f7181 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx @@ -4,15 +4,15 @@ import { HttpResponse, http } from 'msw'; import { setupServer, SetupServer } from 'msw/node'; import { ComponentProps } from 'react'; import * as React from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import AutoSizer from 'react-virtualized-auto-sizer'; import { TestProvider } from 'test/helpers/TestProvider'; import { selectors } from '@grafana/e2e-selectors'; import { contextSrv } from 'app/core/core'; -import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { backendSrv } from 'app/core/services/backend_srv'; -import BrowseDashboardsPage, { Props } from './BrowseDashboardsPage'; +import BrowseDashboardsPage from './BrowseDashboardsPage'; import { wellFormedTree } from './fixtures/dashboardsTreeItem.fixture'; import * as permissions from './permissions'; const [mockTree, { dashbdD, folderA, folderA_folderA }] = wellFormedTree(); @@ -44,6 +44,11 @@ jest.mock('react-virtualized-auto-sizer', () => { }; }); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: jest.fn().mockReturnValue({}), +})); + function render(...[ui, options]: Parameters) { const { rerender } = rtlRender( { }); describe('browse-dashboards BrowseDashboardsPage', () => { - let props: Props; let server: SetupServer; const mockPermissions = { canCreateDashboards: true, @@ -143,10 +147,6 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); beforeEach(() => { - props = { - ...getRouteComponentProps(), - }; - jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions); jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); }); @@ -158,17 +158,17 @@ describe('browse-dashboards BrowseDashboardsPage', () => { describe('at the root level', () => { it('displays "Dashboards" as the page title', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); }); it('displays a search input', async () => { - render(); + render(); expect(await screen.findByPlaceholderText('Search for dashboards and folders')).toBeInTheDocument(); }); it('shows the "New" button', async () => { - render(); + render(); expect(await screen.findByRole('button', { name: 'New' })).toBeInTheDocument(); }); @@ -180,25 +180,25 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canCreateFolders: false, }; }); - render(); + render(); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument(); }); it('does not show "Folder actions"', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); }); it('does not show an "Edit title" button', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Edit title' })).not.toBeInTheDocument(); }); it('does not show any tabs', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('tab', { name: 'Dashboards' })).not.toBeInTheDocument(); @@ -207,7 +207,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('displays the filters and hides the actions initially', async () => { - render(); + render(); await screen.findByPlaceholderText('Search for dashboards and folders'); expect(await screen.findByText('Sort')).toBeInTheDocument(); @@ -218,7 +218,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('selecting an item hides the filters and shows the actions instead', async () => { - render(); + render(); const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashbdD.item.uid)); await userEvent.click(checkbox); @@ -233,7 +233,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('navigating into a child item resets the selected state', async () => { - const { rerender } = render(); + const { rerender } = render(); const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(folderA.item.uid)); await userEvent.click(checkbox); @@ -242,9 +242,8 @@ describe('browse-dashboards BrowseDashboardsPage', () => { expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument(); - const updatedProps = { ...props }; - updatedProps.match.params = { uid: folderA.item.uid }; - rerender(); + (useParams as jest.Mock).mockReturnValue({ uid: folderA.item.uid }); + rerender(); // Check the filters are now visible again expect(await screen.findByText('Filter by tag')).toBeInTheDocument(); @@ -258,21 +257,21 @@ describe('browse-dashboards BrowseDashboardsPage', () => { describe('for a child folder', () => { beforeEach(() => { - props.match.params = { uid: folderA.item.uid }; + (useParams as jest.Mock).mockReturnValue({ uid: folderA.item.uid }); }); it('shows the folder name as the page title', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); }); it('displays a search input', async () => { - render(); + render(); expect(await screen.findByPlaceholderText('Search for dashboards and folders')).toBeInTheDocument(); }); it('shows the "New" button', async () => { - render(); + render(); expect(await screen.findByRole('button', { name: 'New' })).toBeInTheDocument(); }); @@ -284,13 +283,13 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canCreateFolders: false, }; }); - render(); + render(); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument(); }); it('shows the "Folder actions" button', async () => { - render(); + render(); expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument(); }); @@ -304,13 +303,13 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canViewPermissions: false, }; }); - render(); + render(); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); }); it('shows an "Edit title" button', async () => { - render(); + render(); expect(await screen.findByRole('button', { name: 'Edit title' })).toBeInTheDocument(); }); @@ -321,13 +320,13 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canEditFolders: false, }; }); - render(); + render(); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Edit title' })).not.toBeInTheDocument(); }); it('displays all the folder tabs and shows the "Dashboards" tab as selected', async () => { - render(); + render(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toBeInTheDocument(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toHaveAttribute('aria-selected', 'true'); @@ -339,7 +338,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('displays the filters and hides the actions initially', async () => { - render(); + render(); await screen.findByPlaceholderText('Search for dashboards and folders'); expect(await screen.findByText('Sort')).toBeInTheDocument(); @@ -350,7 +349,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('selecting an item hides the filters and shows the actions instead', async () => { - render(); + render(); const checkbox = await screen.findByTestId( selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid) diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index 87b8239db2f..cce7bfdcdc3 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -1,13 +1,12 @@ import { css } from '@emotion/css'; import { memo, useEffect, useMemo } from 'react'; -import { useLocation } from 'react-router-dom-v5-compat'; +import { useLocation, useParams } from 'react-router-dom-v5-compat'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { FilterInput, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { useDispatch } from 'app/types'; import { buildNavModel, getDashboardsTabID } from '../folders/state/navModel'; @@ -24,17 +23,9 @@ import { SearchView } from './components/SearchView'; import { getFolderPermissions } from './permissions'; import { setAllSelection, useHasSelection } from './state'; -export interface BrowseDashboardsPageRouteParams { - uid?: string; - slug?: string; -} - -export interface Props extends GrafanaRouteComponentProps {} - // New Browse/Manage/Search Dashboards views for nested folders - -const BrowseDashboardsPage = memo(({ match }: Props) => { - const { uid: folderUID } = match.params; +const BrowseDashboardsPage = memo(() => { + const { uid: folderUID } = useParams(); const dispatch = useDispatch(); const styles = useStyles2(getStyles); diff --git a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx index bd7e7b4fc64..44688f0aa2e 100644 --- a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx @@ -1,19 +1,12 @@ -import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'; +import { screen, waitForElementToBeRemoved } from '@testing-library/react'; +import { Route, Routes } from 'react-router-dom-v5-compat'; import { of } from 'rxjs'; -import { TestProvider } from 'test/helpers/TestProvider'; -import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; +import { render } from 'test/test-utils'; import { getDefaultTimeRange, LoadingState, PanelData, PanelProps } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { - config, - getPluginLinkExtensions, - locationService, - LocationServiceProvider, - setPluginImportUtils, - setRunRequest, -} from '@grafana/runtime'; +import { config, getPluginLinkExtensions, setPluginImportUtils, setRunRequest } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { DashboardRoutes } from 'app/types/dashboard'; @@ -37,27 +30,22 @@ jest.mock('@grafana/runtime', () => ({ const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions); -function setup(props?: Partial) { - const context = getGrafanaContextMock(); - +function setup(token = 'an-access-token') { const pubdashProps: PublicDashboardSceneProps = { ...getRouteComponentProps({ - match: { params: { accessToken: 'an-access-token' }, isExact: true, url: '', path: '' }, route: { routeName: DashboardRoutes.Public, path: '/public-dashboards/:accessToken', component: () => null, }, }), - ...props, }; return render( - - - - - + + } /> + , + { historyOptions: { initialEntries: [`/public-dashboards/${token}`] } } ); } @@ -190,9 +178,7 @@ describe('PublicDashboardScenePage', () => { dashboard: { ...simpleDashboard, timepicker: { hidden: true } }, meta: {}, }); - setup({ - match: { params: { accessToken }, isExact: true, url: '', path: '' }, - }); + setup(accessToken); await waitForDashboardGridToRender(); @@ -210,7 +196,7 @@ describe('given unavailable public dashboard', () => { dashboard: simpleDashboard, meta: { publicDashboardEnabled: false, dashboardNotFound: false }, }); - setup({ match: { params: { accessToken }, isExact: true, url: '', path: '' } }); + setup(accessToken); await waitForElementToBeRemoved(screen.getByTestId(publicDashboardSceneSelector.loadingPage)); @@ -226,7 +212,7 @@ describe('given unavailable public dashboard', () => { dashboard: simpleDashboard, meta: { dashboardNotFound: true }, }); - setup({ match: { params: { accessToken }, isExact: true, url: '', path: '' } }); + setup(accessToken); await waitForElementToBeRemoved(screen.getByTestId(publicDashboardSceneSelector.loadingPage)); diff --git a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx index ddf886eeee6..2b2895124c9 100644 --- a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; @@ -20,23 +21,26 @@ import { DashboardScene } from '../scene/DashboardScene'; import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager'; -export interface Props - extends GrafanaRouteComponentProps {} - const selectors = e2eSelectors.pages.PublicDashboardScene; -export function PublicDashboardScenePage({ match, route }: Props) { +export type Props = Omit< + GrafanaRouteComponentProps, + 'match' | 'history' +>; + +export function PublicDashboardScenePage({ route }: Props) { + const { accessToken = '' } = useParams(); const stateManager = getDashboardScenePageStateManager(); const styles = useStyles2(getStyles); const { dashboard, isLoading, loadError } = stateManager.useState(); useEffect(() => { - stateManager.loadDashboard({ uid: match.params.accessToken!, route: DashboardRoutes.Public }); + stateManager.loadDashboard({ uid: accessToken, route: DashboardRoutes.Public }); return () => { stateManager.clearState(); }; - }, [stateManager, match.params.accessToken, route.routeName]); + }, [stateManager, accessToken, route.routeName]); if (!dashboard) { return ( diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx index 6968530b55d..d3e509db17c 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx @@ -1,21 +1,17 @@ -import { render, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom-v5-compat'; import { useEffectOnce } from 'react-use'; import { Props as AutoSizerProps } from 'react-virtualized-auto-sizer'; -import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; +import { render } from 'test/test-utils'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; -import { locationService } from '@grafana/runtime'; import { Dashboard, DashboardCursorSync, FieldConfigSource, ThresholdsMode, Panel } from '@grafana/schema/src'; import config from 'app/core/config'; -import { GrafanaContext } from 'app/core/context/GrafanaContext'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import * as appTypes from 'app/types'; import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types'; -import { SafeDynamicImport } from '../../../core/components/DynamicImports/SafeDynamicImport'; import { configureStore } from '../../../store/configureStore'; import { Props as LazyLoaderProps } from '../dashgrid/LazyLoader'; import { DashboardModel } from '../state'; @@ -55,53 +51,38 @@ jest.mock('app/types', () => ({ useDispatch: () => jest.fn(), })); -jest.mock('react-router-dom-v5-compat', () => ({ - ...jest.requireActual('react-router-dom-v5-compat'), - useParams: jest.fn().mockReturnValue({ accessToken: 'an-access-token' }), -})); - const setup = (propOverrides?: Partial, initialState?: Partial) => { - const context = getGrafanaContextMock(); const store = configureStore(initialState); const props: Props = { ...getRouteComponentProps({ route: { routeName: DashboardRoutes.Public, path: '/public-dashboards/:accessToken', - component: SafeDynamicImport( - () => - import(/* webpackChunkName: "PublicDashboardPage"*/ 'app/features/dashboard/containers/PublicDashboardPage') - ), + component: () => null, }, }), }; Object.assign(props, propOverrides); - const { unmount, rerender } = render( - - - - - - - + render( + + } /> + , + { store, historyOptions: { initialEntries: [`/public-dashboards/an-access-token`] } } ); const wrappedRerender = (newProps: Partial) => { Object.assign(props, newProps); - return rerender( - - - - - - - + return render( + + } /> + , + { store, historyOptions: { initialEntries: [`/public-dashboards/an-access-token`] } } ); }; - return { rerender: wrappedRerender, unmount }; + return { rerender: wrappedRerender }; }; const selectors = e2eSelectors.components; diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.tsx index 794baab1454..391863db5aa 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.tsx @@ -27,7 +27,10 @@ import { getTimeSrv } from '../services/TimeSrv'; import { DashboardModel } from '../state'; import { initDashboard } from '../state/initDashboard'; -export type Props = GrafanaRouteComponentProps; +export type Props = Omit< + GrafanaRouteComponentProps, + 'match' | 'history' +>; const selectors = e2eSelectors.pages.PublicDashboard; diff --git a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx index dd8ea8b4826..04b3678a0a1 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx @@ -1,13 +1,10 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; -import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; +import { screen, waitFor } from '@testing-library/react'; +import { Routes, Route } from 'react-router-dom-v5-compat'; +import { render } from 'test/test-utils'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { LocationServiceProvider, config, locationService } from '@grafana/runtime'; -import { GrafanaContext } from 'app/core/context/GrafanaContext'; +import { config, locationService } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; -import { configureStore } from 'app/store/configureStore'; import { DashboardRoutes } from '../../../types'; @@ -31,25 +28,23 @@ jest.mock('react-router-dom-v5-compat', () => ({ })); function setup(props: Partial) { - const context = getGrafanaContextMock(); - const store = configureStore({}); return render( - - - - - null, path: '/:accessToken' }} - match={{ params: { accessToken: 'an-access-token' }, isExact: true, path: '/', url: '/' }} - {...props} - /> - - - - + + null, path: '/:accessToken' }} + {...props} + /> + } + /> + , + { + historyOptions: { initialEntries: [`/public-dashboards/an-access-token`] }, + } ); } diff --git a/public/app/features/dashboard/containers/PublicDashboardPageProxy.tsx b/public/app/features/dashboard/containers/PublicDashboardPageProxy.tsx index cb5020d9dda..50cffeea8ae 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPageProxy.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPageProxy.tsx @@ -1,15 +1,10 @@ import { config } from '@grafana/runtime'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { PublicDashboardScenePage } from '../../dashboard-scene/pages/PublicDashboardScenePage'; -import PublicDashboardPage from './PublicDashboardPage'; -import { PublicDashboardPageRouteParams, PublicDashboardPageRouteSearchParams } from './types'; +import PublicDashboardPage, { type Props } from './PublicDashboardPage'; -export type PublicDashboardPageProxyProps = GrafanaRouteComponentProps< - PublicDashboardPageRouteParams, - PublicDashboardPageRouteSearchParams ->; +export type PublicDashboardPageProxyProps = Props; function PublicDashboardPageProxy(props: PublicDashboardPageProxyProps) { if (config.featureToggles.publicDashboardsScene) { diff --git a/public/app/features/invites/SignupInvited.test.tsx b/public/app/features/invites/SignupInvited.test.tsx index 295b0779a95..9c7d8a684f5 100644 --- a/public/app/features/invites/SignupInvited.test.tsx +++ b/public/app/features/invites/SignupInvited.test.tsx @@ -2,11 +2,9 @@ import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render } from 'test/test-utils'; -import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; - import { backendSrv } from '../../core/services/backend_srv'; -import { SignupInvitedPage, Props } from './SignupInvited'; +import { SignupInvitedPage } from './SignupInvited'; jest.mock('app/core/core', () => ({ contextSrv: { @@ -19,6 +17,11 @@ jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => backendSrv, })); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: jest.fn().mockReturnValue({ code: 'some code' }), +})); + const defaultGet = { email: 'some.user@localhost', name: 'Some User', @@ -35,18 +38,7 @@ async function setupTestContext({ get = defaultGet }: { get?: typeof defaultGet const postSpy = jest.spyOn(backendSrv, 'post'); postSpy.mockResolvedValue([]); - const props: Props = { - ...getRouteComponentProps({ - match: { - params: { code: 'some code' }, - isExact: false, - path: '', - url: '', - }, - }), - }; - - render(); + render(); await waitFor(() => expect(getSpy).toHaveBeenCalled()); expect(getSpy).toHaveBeenCalledTimes(1); diff --git a/public/app/features/invites/SignupInvited.tsx b/public/app/features/invites/SignupInvited.tsx index 37b754f59bc..cfc9f023eed 100644 --- a/public/app/features/invites/SignupInvited.tsx +++ b/public/app/features/invites/SignupInvited.tsx @@ -1,5 +1,6 @@ import { css, cx } from '@emotion/css'; import { useState } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { useAsync } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; @@ -9,7 +10,6 @@ import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; import { getConfig } from 'app/core/config'; import { contextSrv } from 'app/core/core'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { w3cStandardEmailValidator } from '../admin/utils'; @@ -32,10 +32,8 @@ const navModel = { }, }; -export interface Props extends GrafanaRouteComponentProps<{ code: string }> {} - -export const SignupInvitedPage = ({ match }: Props) => { - const code = match.params.code; +export const SignupInvitedPage = () => { + const { code } = useParams(); const [initFormModel, setInitFormModel] = useState(); const [greeting, setGreeting] = useState(); const [invitedBy, setInvitedBy] = useState(); diff --git a/public/app/features/playlist/PlaylistEditPage.test.tsx b/public/app/features/playlist/PlaylistEditPage.test.tsx index 37d2f618493..aa5db979917 100644 --- a/public/app/features/playlist/PlaylistEditPage.test.tsx +++ b/public/app/features/playlist/PlaylistEditPage.test.tsx @@ -1,10 +1,8 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { History, Location } from 'history'; import { TestProvider } from 'test/helpers/TestProvider'; import { locationService } from '@grafana/runtime'; -import { RouteDescriptor } from 'app/core/navigation/types'; import { backendSrv } from 'app/core/services/backend_srv'; import { PlaylistEditPage } from './PlaylistEditPage'; @@ -24,11 +22,7 @@ jest.mock('app/core/components/TagFilter/TagFilter', () => ({ async function getTestContext({ name, interval, items, uid }: Partial = {}) { jest.clearAllMocks(); const playlist = { name, items, interval, uid } as unknown as Playlist; - const queryParams = {}; - const route = {} as RouteDescriptor; - const match = { isExact: false, path: '', url: '', params: { uid: 'foo' } }; - const location = {} as Location; - const history = {} as History; + const getMock = jest.spyOn(backendSrv, 'get'); const putMock = jest.spyOn(backendSrv, 'put').mockImplementation(() => Promise.resolve()); @@ -41,7 +35,7 @@ async function getTestContext({ name, interval, items, uid }: Partial const { rerender } = render( - + ); await waitFor(() => expect(getMock).toHaveBeenCalledTimes(1)); diff --git a/public/app/features/playlist/PlaylistEditPage.tsx b/public/app/features/playlist/PlaylistEditPage.tsx index ac8327a883d..ff88b01cc84 100644 --- a/public/app/features/playlist/PlaylistEditPage.tsx +++ b/public/app/features/playlist/PlaylistEditPage.tsx @@ -1,10 +1,10 @@ +import { useParams } from 'react-router-dom-v5-compat'; import { useAsync } from 'react-use'; import { NavModelItem } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; import { t, Trans } from 'app/core/internationalization'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { PlaylistForm } from './PlaylistForm'; import { getPlaylistAPI } from './api'; @@ -14,11 +14,10 @@ export interface RouteParams { uid: string; } -interface Props extends GrafanaRouteComponentProps {} - -export const PlaylistEditPage = ({ match }: Props) => { +export const PlaylistEditPage = () => { + const { uid = '' } = useParams(); const api = getPlaylistAPI(); - const playlist = useAsync(() => api.getPlaylist(match.params.uid), [match.params]); + const playlist = useAsync(() => api.getPlaylist(uid), [uid]); const onSubmit = async (playlist: Playlist) => { await api.updatePlaylist(playlist); diff --git a/public/app/features/playlist/PlaylistStartPage.tsx b/public/app/features/playlist/PlaylistStartPage.tsx index 9958f919d34..4bead3289dd 100644 --- a/public/app/features/playlist/PlaylistStartPage.tsx +++ b/public/app/features/playlist/PlaylistStartPage.tsx @@ -1,3 +1,5 @@ +import { useParams } from 'react-router-dom-v5-compat'; + import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { playlistSrv } from './PlaylistSrv'; @@ -6,6 +8,7 @@ interface Props extends GrafanaRouteComponentProps<{ uid: string }> {} // This is a react page that just redirects to new URLs export default function PlaylistStartPage({ match }: Props) { - playlistSrv.start(match.params.uid); + const { uid = '' } = useParams(); + playlistSrv.start(uid); return null; } diff --git a/public/app/features/plugins/components/AppRootPage.tsx b/public/app/features/plugins/components/AppRootPage.tsx index 9eabe95f823..7853f723a4a 100644 --- a/public/app/features/plugins/components/AppRootPage.tsx +++ b/public/app/features/plugins/components/AppRootPage.tsx @@ -3,6 +3,7 @@ import { AnyAction, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { useCallback, useEffect, useMemo, useReducer } from 'react'; import * as React from 'react'; import { useLocation, useRouteMatch } from 'react-router-dom'; +import { useParams } from 'react-router-dom-v5-compat'; import { AppEvents, @@ -38,7 +39,7 @@ import { buildPluginPageContext, PluginPageContext } from './PluginPageContext'; interface Props { // The ID of the plugin we would like to load and display - pluginId: string; + pluginId?: string; // The root navModelItem for the plugin (root = lives directly under 'home'). In case app does not need a nva model, // for example it's in some way embedded or shown in a sideview this can be undefined. pluginNavSection?: NavModelItem; @@ -55,6 +56,8 @@ interface State { const initialState: State = { loading: true, loadingError: false, pluginNav: null, plugin: null }; export function AppRootPage({ pluginId, pluginNavSection }: Props) { + const { pluginId: pluginIdParam = '' } = useParams(); + pluginId = pluginId || pluginIdParam; const addedLinksRegistry = useAddedLinksRegistry(); const addedComponentsRegistry = useAddedComponentsRegistry(); const exposedComponentsRegistry = useExposedComponentsRegistry(); diff --git a/public/app/features/plugins/routes.tsx b/public/app/features/plugins/routes.tsx index a5b232521a2..b4e81d38245 100644 --- a/public/app/features/plugins/routes.tsx +++ b/public/app/features/plugins/routes.tsx @@ -33,7 +33,7 @@ export function getAppPluginRoutes(): RouteDescriptor[] { { path: '/a/:pluginId', exact: false, // route everything under this path to the plugin, so it can define more routes under this path - component: ({ match }) => , + component: () => , }, ]; } diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index da39686354c..261b3cf5726 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -5,7 +5,6 @@ import { NavLandingPage } from 'app/core/components/NavLandingPage/NavLandingPag import { PageNotFound } from 'app/core/components/PageNotFound/PageNotFound'; import config from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; -import UserAdminPage from 'app/features/admin/UserAdminPage'; import LdapPage from 'app/features/admin/ldap/LdapPage'; import { getAlertingRoutes } from 'app/features/alerting/routes'; import { isAdmin, isLocalDevEnv, isOpenSourceEdition } from 'app/features/alerting/unified/utils/misc'; @@ -336,7 +335,9 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/admin/users/edit/:id', - component: UserAdminPage, + component: SafeDynamicImport( + () => import(/* webpackChunkName: "UserAdminPage" */ 'app/features/admin/UserAdminPage') + ), }, { path: '/admin/orgs',