mirror of
https://github.com/grafana/grafana.git
synced 2024-11-24 09:50:29 -06:00
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:
parent
a25516fbe3
commit
4eea5d5190
@ -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>
|
||||
);
|
||||
|
@ -20,14 +20,6 @@ jest.mock('../../core/app_events', () => ({
|
||||
|
||||
const defaultProps: Props = {
|
||||
...getRouteComponentProps({}),
|
||||
navModel: {
|
||||
main: {
|
||||
text: 'foo',
|
||||
},
|
||||
node: {
|
||||
text: 'foo',
|
||||
},
|
||||
},
|
||||
search: '',
|
||||
isLoading: false,
|
||||
alertRules: [],
|
||||
|
@ -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">
|
||||
|
24
public/app/features/alerting/unified/AlertWarning.tsx
Normal file
24
public/app/features/alerting/unified/AlertWarning.tsx
Normal 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)};
|
||||
`,
|
||||
});
|
53
public/app/features/alerting/unified/ExistingRuleEditor.tsx
Normal file
53
public/app/features/alerting/unified/ExistingRuleEditor.tsx
Normal 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} />;
|
||||
}
|
@ -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;
|
||||
|
@ -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' });
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
`,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user