mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
9fc4436418
commit
5b53b37634
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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(() => {
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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} />;
|
||||
|
@ -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}>
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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 ||
|
||||
|
@ -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]);
|
||||
|
@ -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('//', '/');
|
||||
|
Loading…
Reference in New Issue
Block a user