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