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 <tom.ratcliffe@grafana.com>
This commit is contained in:
Alex Khomenko 2024-09-27 15:39:29 +03:00 committed by GitHub
parent 9fc4436418
commit 5b53b37634
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 135 additions and 199 deletions

View File

@ -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<NavModelItem> = {
icon: 'bell',
@ -28,7 +28,7 @@ const defaultPageNav: Partial<NavModelItem> = {
};
// 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<RuleEditorPathParams>();
const { type } = params;
const id = ruleId.getRuleIdFromPathname(params);
const identifier = ruleId.tryParse(id, true);
const copyFromId = searchParams.get('copyFrom') ?? undefined;

View File

@ -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(<Route path={['/alerting/new', '/alerting/:id/edit']} component={RuleEditor} />, {
historyOptions: { initialEntries: [identifier ? `/alerting/${identifier}/edit` : `/alerting/new`] },
});
function renderRuleEditor(identifier: string) {
return render(
<Routes>
<Route path="/alerting/:id/edit" element={<RuleEditor />} />
</Routes>,
{
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

View File

@ -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;

View File

@ -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<typeof connector>;
/**
* 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 <PageNotFound />;
@ -88,4 +71,4 @@ export const ProviderConfigPage = ({ config, loadProviders, isLoading, provider
);
};
export default connector(ProviderConfigPage);
export default ProviderConfigPage;

View File

@ -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(<BrowseFolderAlertingPage {...props} />);
render(<BrowseFolderAlertingPage />);
expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument();
});
it('displays the "Folder actions" button', async () => {
render(<BrowseFolderAlertingPage {...props} />);
render(<BrowseFolderAlertingPage />);
expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument();
});
@ -107,13 +98,13 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => {
canSetPermissions: false,
};
});
render(<BrowseFolderAlertingPage {...props} />);
render(<BrowseFolderAlertingPage />);
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(<BrowseFolderAlertingPage {...props} />);
render(<BrowseFolderAlertingPage />);
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(<BrowseFolderAlertingPage {...props} />);
render(<BrowseFolderAlertingPage />);
const ruleName = mockPrometheusRulesResponse.data.groups[0].rules[0].name;
expect(await screen.findByRole('link', { name: ruleName })).toBeInTheDocument();

View File

@ -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();

View File

@ -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(<BrowseFolderLibraryPanelsPage {...props} />);
render(<BrowseFolderLibraryPanelsPage />);
expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument();
});
it('displays the "Folder actions" button', async () => {
render(<BrowseFolderLibraryPanelsPage {...props} />);
render(<BrowseFolderLibraryPanelsPage />);
expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument();
});
@ -109,13 +101,13 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => {
canSetPermissions: false,
};
});
render(<BrowseFolderLibraryPanelsPage {...props} />);
render(<BrowseFolderLibraryPanelsPage />);
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(<BrowseFolderLibraryPanelsPage {...props} />);
render(<BrowseFolderLibraryPanelsPage />);
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(<BrowseFolderLibraryPanelsPage {...props} />);
render(<BrowseFolderLibraryPanelsPage />);
expect(await screen.findByText(mockLibraryElementsResponse.elements[0].name)).toBeInTheDocument();
});

View File

@ -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<LibraryElementDTO | undefined>(undefined);
const [saveFolder] = useSaveFolderMutation();

View File

@ -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<string | null>(null);
const { datasourceUid } = props.match.params;
const { datasourceUid } = useParams();
const dispatch = useDispatch();
useEffect(() => {

View File

@ -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<Props>, initialState?: Partial<appTypes.StoreState>) => {
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);
});

View File

@ -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 <DashboardLoading initPhase={dashboardState.initPhase} />;

View File

@ -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<PublicDashboardPageProxyProps>) {
const context = getGrafanaContextMock();
const store = configureStore({});
return render(
<GrafanaContext.Provider value={context}>
<Provider store={store}>

View File

@ -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<Props, State> {
declare context: GrafanaContextType;
static contextType = GrafanaContext;
export const SoloPanelPage = ({ route, queryParams, dashboard, initDashboard }: Props) => {
const [panel, setPanel] = useState<State['panel']>(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 (
<SoloPanel
dashboard={this.props.dashboard}
notFound={this.state.notFound}
panel={this.state.panel}
panelId={this.getPanelId()}
timezone={this.props.queryParams.timezone}
/>
);
}
}
return (
<SoloPanel
dashboard={dashboard}
notFound={notFound}
panel={panel}
panelId={getPanelId()}
timezone={queryParams.timezone}
/>
);
};
export interface SoloPanelProps extends State {
dashboard: DashboardModel | null;

View File

@ -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<Props>) => {
const createServiceAccountTokenMock = jest.fn();
const deleteServiceAccountMock = jest.fn();
@ -23,38 +27,10 @@ const setup = (propOverrides: Partial<Props>) => {
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,

View File

@ -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<typeof connector>;
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 ||

View File

@ -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<DataFrame | undefined> => {
return getGrafanaStorage().list('content/' + slug);
}, [slug]);

View File

@ -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('//', '/');