From 5b53b37634cd56ebcdb9a99f6b2bfc8820e6c21a Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 27 Sep 2024 15:39:29 +0300 Subject: [PATCH] Routing: Update components using props.match to use hooks (#93792) * RuleViewed: Get params from hook * ProviderConfigPage: Use hooks for redux logic * Update NewDashboardWithDS * Update StorageFolderPage * Update StoragePage * Cleanup * Update PublicDashboardPage * Update RuleEditor * Update BrowseFolderAlertingPage * Update BrowseFolderLibraryPanelsPage * Update SoloPanelPage * Fix test * Add useParams mocks * Update ServiceAccountPage * Simplify mocks * Cleanup * Reuse types for path params * Remove mock for router compat in test * Switch to element --------- Co-authored-by: Tom Ratcliffe --- .../features/alerting/unified/RuleEditor.tsx | 16 ++-- .../unified/RuleEditorExisting.test.tsx | 16 ++-- .../features/alerting/unified/RuleViewer.tsx | 12 +-- .../auth-config/ProviderConfigPage.tsx | 41 +++------- .../BrowseFolderAlertingPage.test.tsx | 33 +++----- .../BrowseFolderAlertingPage.tsx | 8 +- .../BrowseFolderLibraryPanelsPage.test.tsx | 32 +++----- .../BrowseFolderLibraryPanelsPage.tsx | 5 +- .../containers/NewDashboardWithDS.tsx | 6 +- .../containers/PublicDashboardPage.test.tsx | 15 ++-- .../containers/PublicDashboardPage.tsx | 10 ++- .../PublicDashboardPageProxy.test.tsx | 6 +- .../dashboard/containers/SoloPanelPage.tsx | 81 ++++++++----------- .../ServiceAccountPage.test.tsx | 34 ++------ .../serviceaccounts/ServiceAccountPage.tsx | 8 +- .../features/storage/StorageFolderPage.tsx | 8 +- public/app/features/storage/StoragePage.tsx | 3 +- 17 files changed, 135 insertions(+), 199 deletions(-) diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/RuleEditor.tsx index 4dfa212e4e3..9b482e07c04 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/RuleEditor.tsx @@ -1,9 +1,9 @@ import { useCallback } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { useAsync } from 'react-use'; import { NavModelItem } from '@grafana/data'; import { withErrorBoundary } from '@grafana/ui'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { useDispatch } from 'app/types'; import { RuleIdentifier } from 'app/types/unified-alerting'; @@ -17,10 +17,10 @@ import { fetchRulesSourceBuildInfoAction } from './state/actions'; import { useRulesAccess } from './utils/accessControlHooks'; import * as ruleId from './utils/rule-id'; -type RuleEditorProps = GrafanaRouteComponentProps<{ +type RuleEditorPathParams = { id?: string; type?: 'recording' | 'alerting' | 'grafana-recording'; -}>; +}; const defaultPageNav: Partial = { icon: 'bell', @@ -28,7 +28,7 @@ const defaultPageNav: Partial = { }; // sadly we only get the "type" when a new rule is being created, when editing an existing recording rule we can't actually know it from the URL -const getPageNav = (identifier?: RuleIdentifier, type?: 'recording' | 'alerting' | 'grafana-recording') => { +const getPageNav = (identifier?: RuleIdentifier, type?: RuleEditorPathParams['type']) => { if (type === 'recording' || type === 'grafana-recording') { if (identifier) { // this branch should never trigger actually, the type param isn't used when editing rules @@ -46,12 +46,12 @@ const getPageNav = (identifier?: RuleIdentifier, type?: 'recording' | 'alerting' } }; -const RuleEditor = ({ match }: RuleEditorProps) => { +const RuleEditor = () => { const dispatch = useDispatch(); const [searchParams] = useURLSearchParams(); - - const { type } = match.params; - const id = ruleId.getRuleIdFromPathname(match.params); + const params = useParams(); + const { type } = params; + const id = ruleId.getRuleIdFromPathname(params); const identifier = ruleId.tryParse(id, true); const copyFromId = searchParams.get('copyFrom') ?? undefined; diff --git a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx index 67b966d1ef8..a3aeec38bdc 100644 --- a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx @@ -1,4 +1,4 @@ -import { Route } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom-v5-compat'; import { ui } from 'test/helpers/alertingRuleEditor'; import { render, screen } from 'test/test-utils'; @@ -43,10 +43,15 @@ const mocks = { setupMswServer(); -function renderRuleEditor(identifier?: string) { - return render(, { - historyOptions: { initialEntries: [identifier ? `/alerting/${identifier}/edit` : `/alerting/new`] }, - }); +function renderRuleEditor(identifier: string) { + return render( + + } /> + , + { + historyOptions: { initialEntries: [`/alerting/${identifier}/edit`] }, + } + ); } describe('RuleEditor grafana managed rules', () => { @@ -106,7 +111,6 @@ describe('RuleEditor grafana managed rules', () => { // mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); mocks.searchFolders.mockResolvedValue([folder, slashedFolder] as DashboardSearchHit[]); - const { user } = renderRuleEditor(grafanaRulerRule.grafana_alert.uid); // check that it's filled in diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 7e9aa0152b2..9584539e41b 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -1,10 +1,10 @@ import { useMemo } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { NavModelItem } from '@grafana/data'; import { isFetchError } from '@grafana/runtime'; import { Alert, withErrorBoundary } from '@grafana/ui'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertRuleProvider } from './components/rule-viewer/RuleContext'; @@ -13,13 +13,9 @@ import { useCombinedRule } from './hooks/useCombinedRule'; import { stringifyErrorLike } from './utils/misc'; import { getRuleIdFromPathname, parse as parseRuleId } from './utils/rule-id'; -type RuleViewerProps = GrafanaRouteComponentProps<{ - id: string; - sourceName: string; -}>; - -const RuleViewer = (props: RuleViewerProps): JSX.Element => { - const id = getRuleIdFromPathname(props.match.params); +const RuleViewer = (): JSX.Element => { + const params = useParams(); + const id = getRuleIdFromPathname(params); const [activeTab] = useActiveTab(); const instancesTab = activeTab === ActiveTab.Instances; diff --git a/public/app/features/auth-config/ProviderConfigPage.tsx b/public/app/features/auth-config/ProviderConfigPage.tsx index 82f3ec5072b..7e8e23b5e0f 100644 --- a/public/app/features/auth-config/ProviderConfigPage.tsx +++ b/public/app/features/auth-config/ProviderConfigPage.tsx @@ -1,13 +1,11 @@ import { useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useParams } from 'react-router-dom-v5-compat'; import { NavModelItem } from '@grafana/data'; import { Badge, Stack, Text } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; - -import { PageNotFound } from '../../core/components/PageNotFound/PageNotFound'; -import { StoreState } from '../../types'; +import { PageNotFound } from 'app/core/components/PageNotFound/PageNotFound'; +import { useDispatch, useSelector } from 'app/types'; import { ProviderConfigForm } from './ProviderConfigForm'; import { UIMap } from './constants'; @@ -34,33 +32,18 @@ const getPageNav = (config?: SSOProvider): NavModelItem => { }; }; -interface RouteProps extends GrafanaRouteComponentProps<{ provider: string }> {} - -function mapStateToProps(state: StoreState, props: RouteProps) { - const { isLoading, providers } = state.authConfig; - const { provider } = props.match.params; - const config = providers.find((config) => config.provider === provider); - return { - config, - isLoading, - provider, - }; -} - -const mapDispatchToProps = { - loadProviders, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); -export type Props = ConnectedProps; - /** * Separate the Page logic from the Content logic for easier testing. */ -export const ProviderConfigPage = ({ config, loadProviders, isLoading, provider }: Props) => { +export const ProviderConfigPage = () => { + const dispatch = useDispatch(); + const { isLoading, providers } = useSelector((store) => store.authConfig); + const { provider = '' } = useParams(); + const config = providers.find((config) => config.provider === provider); + useEffect(() => { - loadProviders(provider); - }, [loadProviders, provider]); + dispatch(loadProviders(provider)); + }, [dispatch, provider]); if (!config || !config.provider || !UIMap[config.provider]) { return ; @@ -88,4 +71,4 @@ export const ProviderConfigPage = ({ config, loadProviders, isLoading, provider ); }; -export default connector(ProviderConfigPage); +export default ProviderConfigPage; diff --git a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx index 69b22171aba..95f10cc443e 100644 --- a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx @@ -1,13 +1,13 @@ import { render as rtlRender, screen } from '@testing-library/react'; import { http, HttpResponse } from 'msw'; import { SetupServer, setupServer } from 'msw/node'; +import { useParams } from 'react-router-dom-v5-compat'; import { TestProvider } from 'test/helpers/TestProvider'; import { contextSrv } from 'app/core/core'; -import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { backendSrv } from 'app/core/services/backend_srv'; -import BrowseFolderAlertingPage, { OwnProps } from './BrowseFolderAlertingPage'; +import BrowseFolderAlertingPage from './BrowseFolderAlertingPage'; import { getPrometheusRulesResponse, getRulerRulesResponse } from './fixtures/alertRules.fixture'; import * as permissions from './permissions'; @@ -23,7 +23,10 @@ jest.mock('@grafana/runtime', () => ({ unifiedAlertingEnabled: true, }, })); - +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: jest.fn(), +})); const mockFolderName = 'myFolder'; const mockFolderUid = '12345'; @@ -31,7 +34,7 @@ const mockRulerRulesResponse = getRulerRulesResponse(mockFolderName, mockFolderU const mockPrometheusRulesResponse = getPrometheusRulesResponse(mockFolderName); describe('browse-dashboards BrowseFolderAlertingPage', () => { - let props: OwnProps; + (useParams as jest.Mock).mockReturnValue({ uid: mockFolderUid }); let server: SetupServer; const mockPermissions = { canCreateDashboards: true, @@ -68,18 +71,6 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => { beforeEach(() => { jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions); jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); - props = { - ...getRouteComponentProps({ - match: { - params: { - uid: mockFolderUid, - }, - isExact: false, - path: '', - url: '', - }, - }), - }; }); afterEach(() => { @@ -88,12 +79,12 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => { }); it('displays the folder title', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument(); }); it('displays the "Folder actions" button', async () => { - render(); + render(); expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument(); }); @@ -107,13 +98,13 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => { canSetPermissions: false, }; }); - render(); + render(); expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); }); it('displays all the folder tabs and shows the "Alert rules" tab as selected', async () => { - render(); + render(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toBeInTheDocument(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toHaveAttribute('aria-selected', 'false'); @@ -125,7 +116,7 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => { }); it('displays the alert rules returned by the API', async () => { - render(); + render(); const ruleName = mockPrometheusRulesResponse.data.groups[0].rules[0].name; expect(await screen.findByRole('link', { name: ruleName })).toBeInTheDocument(); diff --git a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx index 4c137afff9b..74931bc2170 100644 --- a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { Page } from 'app/core/components/Page/Page'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { buildNavModel, getAlertingTabID } from 'app/features/folders/state/navModel'; import { useSelector } from 'app/types'; @@ -10,10 +10,8 @@ import { AlertsFolderView } from '../alerting/unified/AlertsFolderView'; import { useGetFolderQuery, useSaveFolderMutation } from './api/browseDashboardsAPI'; import { FolderActionsButton } from './components/FolderActionsButton'; -export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {} - -export function BrowseFolderAlertingPage({ match }: OwnProps) { - const { uid: folderUID } = match.params; +export function BrowseFolderAlertingPage() { + const { uid: folderUID = '' } = useParams(); const { data: folderDTO } = useGetFolderQuery(folderUID); const folder = useSelector((state) => state.folder); const [saveFolder] = useSaveFolderMutation(); diff --git a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx index bb758e875e8..dec629c0181 100644 --- a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx @@ -1,13 +1,13 @@ import { render as rtlRender, screen } from '@testing-library/react'; import { http, HttpResponse } from 'msw'; import { SetupServer, setupServer } from 'msw/node'; +import { useParams } from 'react-router-dom-v5-compat'; import { TestProvider } from 'test/helpers/TestProvider'; import { contextSrv } from 'app/core/core'; -import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { backendSrv } from 'app/core/services/backend_srv'; -import BrowseFolderLibraryPanelsPage, { OwnProps } from './BrowseFolderLibraryPanelsPage'; +import BrowseFolderLibraryPanelsPage from './BrowseFolderLibraryPanelsPage'; import { getLibraryElementsResponse } from './fixtures/libraryElements.fixture'; import * as permissions from './permissions'; @@ -23,6 +23,10 @@ jest.mock('@grafana/runtime', () => ({ unifiedAlertingEnabled: true, }, })); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: jest.fn(), +})); const mockFolderName = 'myFolder'; const mockFolderUid = '12345'; @@ -31,7 +35,7 @@ const mockLibraryElementsResponse = getLibraryElementsResponse(1, { }); describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { - let props: OwnProps; + (useParams as jest.Mock).mockReturnValue({ uid: mockFolderUid }); let server: SetupServer; const mockPermissions = { canCreateDashboards: true, @@ -70,18 +74,6 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { beforeEach(() => { jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions); jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); - props = { - ...getRouteComponentProps({ - match: { - params: { - uid: mockFolderUid, - }, - isExact: false, - path: '', - url: '', - }, - }), - }; }); afterEach(() => { @@ -90,12 +82,12 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { }); it('displays the folder title', async () => { - render(); + render(); expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument(); }); it('displays the "Folder actions" button', async () => { - render(); + render(); expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument(); }); @@ -109,13 +101,13 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { canSetPermissions: false, }; }); - render(); + render(); expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); }); it('displays all the folder tabs and shows the "Library panels" tab as selected', async () => { - render(); + render(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toBeInTheDocument(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toHaveAttribute('aria-selected', 'false'); @@ -127,7 +119,7 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { }); it('displays the library panels returned by the API', async () => { - render(); + render(); expect(await screen.findByText(mockLibraryElementsResponse.elements[0].name)).toBeInTheDocument(); }); diff --git a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.tsx b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.tsx index 48caa971c62..0211f45a1a9 100644 --- a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { Page } from 'app/core/components/Page/Page'; @@ -13,8 +14,8 @@ import { useGetFolderQuery, useSaveFolderMutation } from './api/browseDashboards export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {} -export function BrowseFolderLibraryPanelsPage({ match }: OwnProps) { - const { uid: folderUID } = match.params; +export function BrowseFolderLibraryPanelsPage() { + const { uid: folderUID = '' } = useParams(); const { data: folderDTO } = useGetFolderQuery(folderUID); const [selected, setSelected] = useState(undefined); const [saveFolder] = useSaveFolderMutation(); diff --git a/public/app/features/dashboard/containers/NewDashboardWithDS.tsx b/public/app/features/dashboard/containers/NewDashboardWithDS.tsx index 5da67c8fe32..e1f543bf3ca 100644 --- a/public/app/features/dashboard/containers/NewDashboardWithDS.tsx +++ b/public/app/features/dashboard/containers/NewDashboardWithDS.tsx @@ -1,15 +1,15 @@ import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { useDispatch } from 'app/types'; import { setInitialDatasource } from '../state/reducers'; -export default function NewDashboardWithDS(props: GrafanaRouteComponentProps<{ datasourceUid: string }>) { +export default function NewDashboardWithDS() { const [error, setError] = useState(null); - const { datasourceUid } = props.match.params; + const { datasourceUid } = useParams(); const dispatch = useDispatch(); useEffect(() => { diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx index e2619b6f757..6968530b55d 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx @@ -1,7 +1,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; -import { match, Router } from 'react-router-dom'; +import { Router } from 'react-router-dom'; import { useEffectOnce } from 'react-use'; import { Props as AutoSizerProps } from 'react-virtualized-auto-sizer'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; @@ -34,8 +34,8 @@ jest.mock('app/features/dashboard/dashgrid/LazyLoader', () => { }); jest.mock('react-virtualized-auto-sizer', () => { - // // // The size of the children need to be small enough to be outside the view. - // // // So it does not trigger the query to be run by the PanelQueryRunner. + // The size of the children need to be small enough to be outside the view. + // So it does not trigger the query to be run by the PanelQueryRunner. return ({ children }: AutoSizerProps) => children({ height: 1, @@ -55,13 +55,16 @@ 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({ - match: { params: { accessToken: 'an-access-token' }, isExact: true, url: '', path: '' }, route: { routeName: DashboardRoutes.Public, path: '/public-dashboards/:accessToken', @@ -250,7 +253,7 @@ describe('PublicDashboardPage', () => { describe('When public dashboard changes', () => { it('Should init again', async () => { const { rerender } = setup(); - rerender({ match: { params: { accessToken: 'another-new-access-token' } } as unknown as match }); + rerender({}); await waitFor(() => { expect(initDashboard).toHaveBeenCalledTimes(2); }); diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.tsx index 7d12cf4bf08..794baab1454 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import { useEffect } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { usePrevious } from 'react-use'; import { GrafanaTheme2, PageLayoutType, TimeZone } from '@grafana/data'; @@ -52,7 +53,8 @@ const Toolbar = ({ dashboard }: { dashboard: DashboardModel }) => { }; const PublicDashboardPage = (props: Props) => { - const { match, route, location } = props; + const { route, location } = props; + const { accessToken } = useParams(); const dispatch = useDispatch(); const context = useGrafana(); const prevProps = usePrevious(props); @@ -65,11 +67,11 @@ const PublicDashboardPage = (props: Props) => { initDashboard({ routeName: route.routeName, fixUrl: false, - accessToken: match.params.accessToken, + accessToken, keybindingSrv: context.keybindings, }) ); - }, [route.routeName, match.params.accessToken, context.keybindings, dispatch]); + }, [route.routeName, accessToken, context.keybindings, dispatch]); useEffect(() => { if (prevProps?.location.search !== location.search) { @@ -88,7 +90,7 @@ const PublicDashboardPage = (props: Props) => { getTimeSrv().setAutoRefresh(urlParams.refresh); } } - }, [prevProps, location.search, props.queryParams, dashboard?.timepicker.hidden, match.params.accessToken]); + }, [prevProps, location.search, props.queryParams, dashboard?.timepicker.hidden, accessToken]); if (!dashboard) { return ; diff --git a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx index 49193505061..dd8ea8b4826 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx @@ -25,10 +25,14 @@ jest.mock('@grafana/runtime', () => ({ }), })); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: () => ({ accessToken: 'an-access-token' }), +})); + function setup(props: Partial) { const context = getGrafanaContextMock(); const store = configureStore({}); - return render( diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx index 76e38b778c8..48dc90beba1 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -1,15 +1,16 @@ import { css } from '@emotion/css'; -import { Component } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import { useParams } from 'react-router-dom-v5-compat'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; import { Alert, useStyles2 } from '@grafana/ui'; -import { GrafanaContext, GrafanaContextType } from 'app/core/context/GrafanaContext'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { StoreState } from 'app/types'; +import { useGrafana } from '../../../core/context/GrafanaContext'; import { DashboardPanel } from '../dashgrid/DashboardPanel'; import { initDashboard } from '../state/initDashboard'; @@ -37,69 +38,55 @@ export interface State { notFound: boolean; } -export class SoloPanelPage extends Component { - declare context: GrafanaContextType; - static contextType = GrafanaContext; +export const SoloPanelPage = ({ route, queryParams, dashboard, initDashboard }: Props) => { + const [panel, setPanel] = useState(null); + const [notFound, setNotFound] = useState(false); + const { keybindings } = useGrafana(); - state: State = { - panel: null, - notFound: false, - }; + const { slug, uid, type } = useParams(); - componentDidMount() { - const { match, route } = this.props; - - this.props.initDashboard({ - urlSlug: match.params.slug, - urlUid: match.params.uid, - urlType: match.params.type, + useEffect(() => { + initDashboard({ + urlSlug: slug, + urlUid: uid, + urlType: type, routeName: route.routeName, fixUrl: false, - keybindingSrv: this.context.keybindings, + keybindingSrv: keybindings, }); - } + }, [slug, uid, type, route.routeName, initDashboard, keybindings]); - getPanelId(): number { - return parseInt(this.props.queryParams.panelId ?? '0', 10); - } + const getPanelId = useCallback(() => { + return parseInt(queryParams.panelId ?? '0', 10); + }, [queryParams.panelId]); - componentDidUpdate(prevProps: Props) { - const { dashboard } = this.props; - - if (!dashboard) { - return; - } - - // we just got a new dashboard - if (!prevProps.dashboard || prevProps.dashboard.uid !== dashboard.uid) { - const panel = dashboard.getPanelByUrlId(this.props.queryParams.panelId); + useEffect(() => { + if (dashboard) { + const panel = dashboard.getPanelByUrlId(queryParams.panelId); if (!panel) { - this.setState({ notFound: true }); + setNotFound(true); return; } if (panel) { dashboard.exitViewPanel(panel); } - - this.setState({ panel }); + setPanel(panel); dashboard.initViewPanel(panel); } - } + }, [dashboard, queryParams.panelId]); - render() { - return ( - - ); - } -} + return ( + + ); +}; export interface SoloPanelProps extends State { dashboard: DashboardModel | null; diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx index 2e6478f738a..fe3e5cb42f8 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx @@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; -import { RouteDescriptor } from 'app/core/navigation/types'; import { ApiKey, OrgRole, ServiceAccountDTO } from 'app/types'; import { ServiceAccountPageUnconnected, Props } from './ServiceAccountPage'; @@ -15,6 +14,11 @@ jest.mock('app/core/core', () => ({ }, })); +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useParams: () => ({ id: '1' }), +})); + const setup = (propOverrides: Partial) => { const createServiceAccountTokenMock = jest.fn(); const deleteServiceAccountMock = jest.fn(); @@ -23,38 +27,10 @@ const setup = (propOverrides: Partial) => { const loadServiceAccountTokensMock = jest.fn(); const updateServiceAccountMock = jest.fn(); - const mockLocation = { - search: '', - pathname: '', - state: undefined, - hash: '', - }; const props: Props = { serviceAccount: {} as ServiceAccountDTO, tokens: [], isLoading: false, - match: { - params: { id: '1' }, - isExact: true, - path: '/org/serviceaccounts/1', - url: 'http://localhost:3000/org/serviceaccounts/1', - }, - history: { - length: 0, - action: 'PUSH', - location: mockLocation, - push: jest.fn(), - replace: jest.fn(), - go: jest.fn(), - goBack: jest.fn(), - goForward: jest.fn(), - block: jest.fn(), - listen: jest.fn(), - createHref: jest.fn(), - }, - location: mockLocation, - queryParams: {}, - route: {} as RouteDescriptor, timezone: '', createServiceAccountToken: createServiceAccountTokenMock, deleteServiceAccount: deleteServiceAccountMock, diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index 3ec2b9f5185..d68bdb53233 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import { useParams } from 'react-router-dom-v5-compat'; import { getTimeZone, NavModelItem } from '@grafana/data'; import { Button, ConfirmModal, IconButton, 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 { AccessControlAction, ApiKey, ServiceAccountDTO, StoreState } from 'app/types'; import { ServiceAccountPermissions } from './ServiceAccountPermissions'; @@ -22,7 +22,7 @@ import { updateServiceAccount, } from './state/actionsServiceAccountPage'; -interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { +interface OwnProps { serviceAccount?: ServiceAccountDTO; tokens: ApiKey[]; isLoading: boolean; @@ -51,7 +51,6 @@ const connector = connect(mapStateToProps, mapDispatchToProps); export type Props = OwnProps & ConnectedProps; export const ServiceAccountPageUnconnected = ({ - match, serviceAccount, tokens, timezone, @@ -67,8 +66,9 @@ export const ServiceAccountPageUnconnected = ({ const [isTokenModalOpen, setIsTokenModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDisableModalOpen, setIsDisableModalOpen] = useState(false); + const { id = '' } = useParams(); - const serviceAccountId = parseInt(match.params.id, 10); + const serviceAccountId = parseInt(id, 10); const tokenActionsDisabled = serviceAccount.isDisabled || serviceAccount.isExternal || diff --git a/public/app/features/storage/StorageFolderPage.tsx b/public/app/features/storage/StorageFolderPage.tsx index b19e4b4791f..1808f6c51c2 100644 --- a/public/app/features/storage/StorageFolderPage.tsx +++ b/public/app/features/storage/StorageFolderPage.tsx @@ -1,16 +1,14 @@ +import { useParams } from 'react-router-dom-v5-compat'; import { useAsync } from 'react-use'; import { DataFrame, NavModel, NavModelItem } from '@grafana/data'; import { Card, Icon, Spinner } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getGrafanaStorage } from './storage'; -export interface Props extends GrafanaRouteComponentProps<{ slug: string }> {} - -export function StorageFolderPage(props: Props) { - const slug = props.match.params.slug ?? ''; +export function StorageFolderPage() { + const { slug = '' } = useParams(); const listing = useAsync((): Promise => { return getGrafanaStorage().list('content/' + slug); }, [slug]); diff --git a/public/app/features/storage/StoragePage.tsx b/public/app/features/storage/StoragePage.tsx index ac37ab0c9f5..3217e58ccc3 100644 --- a/public/app/features/storage/StoragePage.tsx +++ b/public/app/features/storage/StoragePage.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import { useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; import { useAsync } from 'react-use'; import { DataFrame, GrafanaTheme2, isDataFrame, ValueLinkConfig } from '@grafana/data'; @@ -46,7 +47,7 @@ const getParentPath = (path: string) => { export default function StoragePage(props: Props) { const styles = useStyles2(getStyles); const navModel = useNavModel('storage'); - const path = props.match.params.path ?? ''; + const { path = '' } = useParams(); const view = props.queryParams.view ?? StorageView.Data; const setPath = (p: string, view?: StorageView) => { let url = ('/admin/storage/' + p).replace('//', '/');