Navigation: Use navid and pagnav in alert rules pages (#55722)

* add navid and pagenav to edit/add/view alert rules

* move ruleeditor smaller component to its own file

* fix form alignments with new layout

* fixed broken test

* reuse AlertingPageWrapper
This commit is contained in:
Leo 2022-10-04 14:36:36 +02:00 committed by GitHub
parent a25516fbe3
commit 4eea5d5190
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 144 additions and 115 deletions

View File

@ -191,7 +191,7 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
getOptionLabel={(o) => {
if (o.meta && isUnsignedPluginSignature(o.meta.signature) && o !== value) {
return (
<HorizontalGroup align="center" justify="space-between">
<HorizontalGroup align="center" justify="space-between" height="auto">
<span>{o.label}</span> <PluginSignatureBadge status={o.meta.signature} />
</HorizontalGroup>
);

View File

@ -20,14 +20,6 @@ jest.mock('../../core/app_events', () => ({
const defaultProps: Props = {
...getRouteComponentProps({}),
navModel: {
main: {
text: 'foo',
},
node: {
text: 'foo',
},
},
search: '',
isLoading: false,
alertRules: [],

View File

@ -7,7 +7,6 @@ import { Button, FilterInput, LinkButton, Select, VerticalGroup } from '@grafana
import appEvents from 'app/core/app_events';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { AlertRule, StoreState } from 'app/types';
import { ShowModalReactEvent } from '../../types/events';
@ -21,7 +20,6 @@ import { getAlertRuleItems, getSearchQuery } from './state/selectors';
function mapStateToProps(state: StoreState) {
return {
navModel: getNavModel(state.navIndex, 'alert-list'),
alertRules: getAlertRuleItems(state),
search: getSearchQuery(state.alertRules),
isLoading: state.alertRules.isLoading,
@ -94,10 +92,10 @@ export class AlertRuleListUnconnected extends PureComponent<Props> {
};
render() {
const { navModel, alertRules, search, isLoading } = this.props;
const { alertRules, search, isLoading } = this.props;
return (
<Page navModel={navModel}>
<Page navId="alert-list">
<Page.Contents isLoading={isLoading}>
<div className="page-action-bar">
<div className="gf-form gf-form--grow">

View File

@ -0,0 +1,24 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, LinkButton, useStyles2 } from '@grafana/ui';
interface AlertWarningProps {
title: string;
children: React.ReactNode;
}
export function AlertWarning({ title, children }: AlertWarningProps) {
return (
<Alert className={useStyles2(warningStyles).warning} severity="warning" title={title}>
<p>{children}</p>
<LinkButton href="alerting/list">To rule list</LinkButton>
</Alert>
);
}
const warningStyles = (theme: GrafanaTheme2) => ({
warning: css`
margin: ${theme.spacing(4)};
`,
});

View File

@ -0,0 +1,53 @@
import React, { useEffect } from 'react';
import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { useCleanup } from 'app/core/hooks/useCleanup';
import { useDispatch } from 'app/types';
import { RuleIdentifier } from 'app/types/unified-alerting';
import { AlertWarning } from './AlertWarning';
import { AlertRuleForm } from './components/rule-editor/AlertRuleForm';
import { useIsRuleEditable } from './hooks/useIsRuleEditable';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchEditableRuleAction } from './state/actions';
import { initialAsyncRequestState } from './utils/redux';
import * as ruleId from './utils/rule-id';
interface ExistingRuleEditorProps {
identifier: RuleIdentifier;
}
export function ExistingRuleEditor({ identifier }: ExistingRuleEditorProps) {
useCleanup((state) => (state.unifiedAlerting.ruleForm.existingRule = initialAsyncRequestState));
const { loading, result, error, dispatched } = useUnifiedAlertingSelector((state) => state.ruleForm.existingRule);
const dispatch = useDispatch();
const { isEditable } = useIsRuleEditable(ruleId.ruleIdentifierToRuleSourceName(identifier), result?.rule);
useEffect(() => {
if (!dispatched) {
dispatch(fetchEditableRuleAction(identifier));
}
}, [dispatched, dispatch, identifier]);
if (loading || isEditable === undefined) {
return <LoadingPlaceholder text="Loading rule..." />;
}
if (error) {
return (
<Alert severity="error" title="Failed to load rule">
{error.message}
</Alert>
);
}
if (!result) {
return <AlertWarning title="Rule not found">Sorry! This rule does not exist.</AlertWarning>;
}
if (isEditable === false) {
return <AlertWarning title="Cannot edit rule">Sorry! You do not have permission to edit this rule.</AlertWarning>;
}
return <AlertRuleForm existing={result} />;
}

View File

@ -13,7 +13,7 @@ import { getRulesSourceByName } from './utils/datasource';
import { createViewLink } from './utils/misc';
type RedirectToRuleViewerProps = GrafanaRouteComponentProps<{ name?: string; sourceName?: string }>;
const pageTitle = 'Alerting / Find rule';
const pageTitle = 'Find rule';
export function RedirectToRuleViewer(props: RedirectToRuleViewerProps): JSX.Element | null {
const { name, sourceName } = props.match.params;

View File

@ -1,115 +1,72 @@
import { css } from '@emotion/css';
import React, { FC, useEffect } from 'react';
import React, { FC } from 'react';
import { useAsync } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, LinkButton, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { useCleanup } from 'app/core/hooks/useCleanup';
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';
import { AlertWarning } from './AlertWarning';
import { ExistingRuleEditor } from './ExistingRuleEditor';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { AlertRuleForm } from './components/rule-editor/AlertRuleForm';
import { useIsRuleEditable } from './hooks/useIsRuleEditable';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchAllPromBuildInfoAction, fetchEditableRuleAction } from './state/actions';
import { fetchAllPromBuildInfoAction } from './state/actions';
import { useRulesAccess } from './utils/accessControlHooks';
import { initialAsyncRequestState } from './utils/redux';
import * as ruleId from './utils/rule-id';
interface ExistingRuleEditorProps {
identifier: RuleIdentifier;
}
type RuleEditorProps = GrafanaRouteComponentProps<{ id?: string }>;
const ExistingRuleEditor: FC<ExistingRuleEditorProps> = ({ identifier }) => {
useCleanup((state) => (state.unifiedAlerting.ruleForm.existingRule = initialAsyncRequestState));
const { loading, result, error, dispatched } = useUnifiedAlertingSelector((state) => state.ruleForm.existingRule);
const dispatch = useDispatch();
const { isEditable } = useIsRuleEditable(ruleId.ruleIdentifierToRuleSourceName(identifier), result?.rule);
useEffect(() => {
if (!dispatched) {
dispatch(fetchEditableRuleAction(identifier));
}
}, [dispatched, dispatch, identifier]);
if (loading || isEditable === undefined) {
return (
<Page.Contents>
<LoadingPlaceholder text="Loading rule..." />
</Page.Contents>
);
}
if (error) {
return (
<Page.Contents>
<Alert severity="error" title="Failed to load rule">
{error.message}
</Alert>
</Page.Contents>
);
}
if (!result) {
return <AlertWarning title="Rule not found">Sorry! This rule does not exist.</AlertWarning>;
}
if (isEditable === false) {
return <AlertWarning title="Cannot edit rule">Sorry! You do not have permission to edit this rule.</AlertWarning>;
}
return <AlertRuleForm existing={result} />;
const defaultPageNav: Partial<NavModelItem> = {
icon: 'bell',
id: 'alert-rule-view',
breadcrumbs: [{ title: 'Alert rules', url: 'alerting/list' }],
};
type RuleEditorProps = GrafanaRouteComponentProps<{ id?: string }>;
const getPageNav = (state: 'edit' | 'add') => {
if (state === 'edit') {
return { ...defaultPageNav, id: 'alert-rule-edit', text: 'Edit rule' };
} else if (state === 'add') {
return { ...defaultPageNav, id: 'alert-rule-add', text: 'Add rule' };
}
return undefined;
};
const RuleEditor: FC<RuleEditorProps> = ({ match }) => {
const dispatch = useDispatch();
const { id } = match.params;
const identifier = ruleId.tryParse(id, true);
const { loading } = useAsync(async () => {
const { loading = true } = useAsync(async () => {
await dispatch(fetchAllPromBuildInfoAction());
}, [dispatch]);
const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess();
if (!identifier && !canCreateGrafanaRules && !canCreateCloudRules) {
return <AlertWarning title="Cannot create rules">Sorry! You are not allowed to create rules.</AlertWarning>;
}
const getContent = () => {
if (loading) {
return;
}
if (identifier && !canEditRules(identifier.ruleSourceName)) {
return <AlertWarning title="Cannot edit rules">Sorry! You are not allowed to edit rules.</AlertWarning>;
}
if (!identifier && !canCreateGrafanaRules && !canCreateCloudRules) {
return <AlertWarning title="Cannot create rules">Sorry! You are not allowed to create rules.</AlertWarning>;
}
if (loading) {
return (
<Page.Contents>
<LoadingPlaceholder text="Loading..." />
</Page.Contents>
);
}
if (identifier && !canEditRules(identifier.ruleSourceName)) {
return <AlertWarning title="Cannot edit rules">Sorry! You are not allowed to edit rules.</AlertWarning>;
}
if (identifier) {
return <ExistingRuleEditor key={id} identifier={identifier} />;
}
if (identifier) {
return <ExistingRuleEditor key={id} identifier={identifier} />;
}
return <AlertRuleForm />;
return <AlertRuleForm />;
};
return (
<AlertingPageWrapper isLoading={loading} pageId="alert-list" pageNav={getPageNav(identifier ? 'edit' : 'add')}>
{getContent()}
</AlertingPageWrapper>
);
};
const AlertWarning: FC<{ title: string }> = ({ title, children }) => (
<Alert className={useStyles2(warningStyles).warning} severity="warning" title={title}>
<p>{children}</p>
<LinkButton href="alerting/list">To rule list</LinkButton>
</Alert>
);
const warningStyles = (theme: GrafanaTheme2) => ({
warning: css`
margin: ${theme.spacing(4)};
`,
});
export default withErrorBoundary(RuleEditor, { style: 'page' });

View File

@ -61,8 +61,8 @@ describe('RuleViewer', () => {
});
await renderRuleViewer();
expect(screen.getByText('Alerting / View rule')).toBeInTheDocument();
expect(screen.getByText('Test alert')).toBeInTheDocument();
expect(screen.getByText(/view rule/i)).toBeInTheDocument();
expect(screen.getByText(/test alert/i)).toBeInTheDocument();
});
it('should render page with cloud alert', async () => {
@ -74,8 +74,8 @@ describe('RuleViewer', () => {
error: undefined,
});
await renderRuleViewer();
expect(screen.getByText('Alerting / View rule')).toBeInTheDocument();
expect(screen.getByText('Cloud test alert')).toBeInTheDocument();
expect(screen.getByText(/view rule/i)).toBeInTheDocument();
expect(screen.getByText(/cloud test alert/i)).toBeInTheDocument();
});
});

View File

@ -43,7 +43,7 @@ type RuleViewerProps = GrafanaRouteComponentProps<{ id?: string; sourceName?: st
const errorMessage = 'Could not find data source for rule';
const errorTitle = 'Could not view rule';
const pageTitle = 'Alerting / View rule';
const pageTitle = 'View rule';
export function RuleViewer({ match }: RuleViewerProps) {
const styles = useStyles2(getStyles);

View File

@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { logInfo } from '@grafana/runtime';
import { Button, ConfirmModal, CustomScrollbar, PageToolbar, Spinner, useStyles2 } from '@grafana/ui';
import { Button, ConfirmModal, CustomScrollbar, Spinner, useStyles2, HorizontalGroup } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { useCleanup } from 'app/core/hooks/useCleanup';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
@ -110,7 +110,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
return (
<FormProvider {...formAPI}>
<form onSubmit={(e) => e.preventDefault()} className={styles.form}>
<PageToolbar title={`${existing ? 'Edit' : 'Create'} alert rule`} pageIcon="bell">
<HorizontalGroup height="auto" justify="flex-end">
<Link to={returnTo}>
<Button
variant="secondary"
@ -155,7 +155,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
{submitState.loading && <Spinner className={styles.buttonSpinner} inline={true} />}
Save and exit
</Button>
</PageToolbar>
</HorizontalGroup>
<div className={styles.contentOuter}>
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>
<div className={styles.contentInner}>
@ -212,9 +212,9 @@ const getStyles = (theme: GrafanaTheme2) => {
background: ${theme.colors.background.primary};
border: 1px solid ${theme.colors.border.weak};
border-radius: ${theme.shape.borderRadius()};
margin: ${theme.spacing(0, 2, 2)};
overflow: hidden;
flex: 1;
margin-top: ${theme.spacing(1)};
`,
flexRow: css`
display: flex;

View File

@ -1,9 +1,8 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { PageToolbar, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
type Props = {
@ -12,14 +11,21 @@ type Props = {
wrapInContent?: boolean;
};
const defaultPageNav: Partial<NavModelItem> = {
icon: 'bell',
id: 'alert-rule-view',
breadcrumbs: [{ title: 'Alert rules', url: 'alerting/list' }],
};
export function RuleViewerLayout(props: Props): JSX.Element | null {
const { wrapInContent = true, children, title } = props;
const styles = useStyles2(getPageStyles);
return (
<Page>
<PageToolbar title={title} pageIcon="bell" onGoBack={() => locationService.push('/alerting/list')} />
<div className={styles.content}>{wrapInContent ? <RuleViewerLayoutContent {...props} /> : children}</div>
<Page pageNav={{ ...defaultPageNav, text: title }} navId="alert-list">
<Page.Contents>
<div className={styles.content}>{wrapInContent ? <RuleViewerLayoutContent {...props} /> : children}</div>
</Page.Contents>
</Page>
);
}
@ -37,7 +43,6 @@ export function RuleViewerLayoutContent({ children, padding = 2 }: ContentProps)
const getPageStyles = (theme: GrafanaTheme2) => {
return {
content: css`
margin: ${theme.spacing(0, 2, 2)};
max-width: ${theme.breakpoints.values.xxl}px;
`,
};