diff --git a/packages/grafana-runtime/src/components/DataSourcePicker.tsx b/packages/grafana-runtime/src/components/DataSourcePicker.tsx index 8e13c6ff9e6..0ceba9231be 100644 --- a/packages/grafana-runtime/src/components/DataSourcePicker.tsx +++ b/packages/grafana-runtime/src/components/DataSourcePicker.tsx @@ -191,7 +191,7 @@ export class DataSourcePicker extends PureComponent { if (o.meta && isUnsignedPluginSignature(o.meta.signature) && o !== value) { return ( - + {o.label} ); diff --git a/public/app/features/alerting/AlertRuleList.test.tsx b/public/app/features/alerting/AlertRuleList.test.tsx index 84d97a55588..c146cdb322d 100644 --- a/public/app/features/alerting/AlertRuleList.test.tsx +++ b/public/app/features/alerting/AlertRuleList.test.tsx @@ -20,14 +20,6 @@ jest.mock('../../core/app_events', () => ({ const defaultProps: Props = { ...getRouteComponentProps({}), - navModel: { - main: { - text: 'foo', - }, - node: { - text: 'foo', - }, - }, search: '', isLoading: false, alertRules: [], diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index a19d2ab7bbd..12f07e39a48 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -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 { }; render() { - const { navModel, alertRules, search, isLoading } = this.props; + const { alertRules, search, isLoading } = this.props; return ( - +
diff --git a/public/app/features/alerting/unified/AlertWarning.tsx b/public/app/features/alerting/unified/AlertWarning.tsx new file mode 100644 index 00000000000..f5a4389c3e8 --- /dev/null +++ b/public/app/features/alerting/unified/AlertWarning.tsx @@ -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 ( + +

{children}

+ To rule list +
+ ); +} + +const warningStyles = (theme: GrafanaTheme2) => ({ + warning: css` + margin: ${theme.spacing(4)}; + `, +}); diff --git a/public/app/features/alerting/unified/ExistingRuleEditor.tsx b/public/app/features/alerting/unified/ExistingRuleEditor.tsx new file mode 100644 index 00000000000..612b7c345cd --- /dev/null +++ b/public/app/features/alerting/unified/ExistingRuleEditor.tsx @@ -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 ; + } + + if (error) { + return ( + + {error.message} + + ); + } + + if (!result) { + return Sorry! This rule does not exist.; + } + + if (isEditable === false) { + return Sorry! You do not have permission to edit this rule.; + } + + return ; +} diff --git a/public/app/features/alerting/unified/RedirectToRuleViewer.tsx b/public/app/features/alerting/unified/RedirectToRuleViewer.tsx index c2290067538..b7b79d889af 100644 --- a/public/app/features/alerting/unified/RedirectToRuleViewer.tsx +++ b/public/app/features/alerting/unified/RedirectToRuleViewer.tsx @@ -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; diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/RuleEditor.tsx index dd8d83ae705..a698b4af6e8 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/RuleEditor.tsx @@ -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 = ({ 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 ( - - - - ); - } - - if (error) { - return ( - - - {error.message} - - - ); - } - - if (!result) { - return Sorry! This rule does not exist.; - } - - if (isEditable === false) { - return Sorry! You do not have permission to edit this rule.; - } - - return ; +const defaultPageNav: Partial = { + 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 = ({ 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 Sorry! You are not allowed to create rules.; - } + const getContent = () => { + if (loading) { + return; + } - if (identifier && !canEditRules(identifier.ruleSourceName)) { - return Sorry! You are not allowed to edit rules.; - } + if (!identifier && !canCreateGrafanaRules && !canCreateCloudRules) { + return Sorry! You are not allowed to create rules.; + } - if (loading) { - return ( - - - - ); - } + if (identifier && !canEditRules(identifier.ruleSourceName)) { + return Sorry! You are not allowed to edit rules.; + } - if (identifier) { - return ; - } + if (identifier) { + return ; + } - return ; + return ; + }; + + return ( + + {getContent()} + + ); }; -const AlertWarning: FC<{ title: string }> = ({ title, children }) => ( - -

{children}

- To rule list -
-); - -const warningStyles = (theme: GrafanaTheme2) => ({ - warning: css` - margin: ${theme.spacing(4)}; - `, -}); - export default withErrorBoundary(RuleEditor, { style: 'page' }); diff --git a/public/app/features/alerting/unified/RuleViewer.test.tsx b/public/app/features/alerting/unified/RuleViewer.test.tsx index d0bc132bbda..d5d4d328cfb 100644 --- a/public/app/features/alerting/unified/RuleViewer.test.tsx +++ b/public/app/features/alerting/unified/RuleViewer.test.tsx @@ -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(); }); }); diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 1ea419f8d95..4bfdaa11dd8 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -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); diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx index 99959162b39..5c0069ffe16 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx @@ -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 = ({ existing }) => { return (
e.preventDefault()} className={styles.form}> - + - +
@@ -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; diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerLayout.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerLayout.tsx index 1306b7566f4..529abe2ce96 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerLayout.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerLayout.tsx @@ -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 = { + 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 ( - - locationService.push('/alerting/list')} /> -
{wrapInContent ? : children}
+ + +
{wrapInContent ? : children}
+
); } @@ -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; `, };