diff --git a/public/app/features/alerting/unified/integration/AlertRulesDrawerContent.tsx b/public/app/features/alerting/unified/integration/AlertRulesDrawerContent.tsx index d3b0ea88796..3c13303b960 100644 --- a/public/app/features/alerting/unified/integration/AlertRulesDrawerContent.tsx +++ b/public/app/features/alerting/unified/integration/AlertRulesDrawerContent.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { useAsync } from 'react-use'; import { LoadingPlaceholder } from '@grafana/ui'; -import { useDispatch } from 'app/types'; +import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager'; +import { DashboardRoutes, useDispatch } from 'app/types'; import { RulesTable } from '../components/rules/RulesTable'; import { useCombinedRuleNamespaces } from '../hooks/useCombinedRuleNamespaces'; @@ -10,14 +11,26 @@ import { fetchPromAndRulerRulesAction } from '../state/actions'; import { Annotation } from '../utils/constants'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import LegacyAlertsDeprecationNotice from './LegacyAlertsDeprecationNotice'; + interface Props { dashboardUid: string; } export default function AlertRulesDrawerContent({ dashboardUid }: Props) { const dispatch = useDispatch(); + const dashboardStateManager = getDashboardScenePageStateManager(); - const { loading } = useAsync(async () => { + const { value: dashboard, loading: loadingDashboardState } = useAsync(() => { + return dashboardStateManager + .fetchDashboard({ + uid: dashboardUid, + route: DashboardRoutes.Normal, + }) + .then((data) => (data ? data.dashboard : undefined)); + }, [dashboardStateManager]); + + const { loading: loadingAlertRules } = useAsync(async () => { await dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME })); }, [dispatch]); @@ -27,12 +40,17 @@ export default function AlertRulesDrawerContent({ dashboardUid }: Props) { .flatMap((g) => g.rules) .filter((rule) => rule.annotations[Annotation.dashboardUID] === dashboardUid); + const loading = loadingDashboardState || loadingAlertRules; + return ( <> {loading ? ( ) : ( - + <> + + + )} ); diff --git a/public/app/features/alerting/unified/integration/LegacyAlertsDeprecationNotice.tsx b/public/app/features/alerting/unified/integration/LegacyAlertsDeprecationNotice.tsx new file mode 100644 index 00000000000..66fa4595f03 --- /dev/null +++ b/public/app/features/alerting/unified/integration/LegacyAlertsDeprecationNotice.tsx @@ -0,0 +1,92 @@ +import { isEmpty } from 'lodash'; +import React from 'react'; +import { useToggle } from 'react-use'; + +import { config } from '@grafana/runtime'; +import { Dashboard, Panel, RowPanel } from '@grafana/schema'; +import { Alert, Collapse, Column, InteractiveTable, TextLink } from '@grafana/ui'; + +import { makePanelLink } from '../utils/misc'; + +interface DeprecationNoticeProps { + dashboard: Dashboard; +} + +const usingLegacyAlerting = !config.unifiedAlertingEnabled; + +export default function LegacyAlertsDeprecationNotice({ dashboard }: DeprecationNoticeProps) { + // if the user is still using legacy alerting we don't need to show any notice at all – they will probably keep using legacy alerting and do not intend to upgrade. + if (usingLegacyAlerting) { + return null; + } + + const panelsWithLegacyAlerts = getLegacyAlertPanelsFromDashboard(dashboard); + + // don't show anything when the user has no legacy alerts defined + const hasLegacyAlerts = !isEmpty(panelsWithLegacyAlerts); + if (!hasLegacyAlerts) { + return null; + } + + return dashboard.uid ? : null; +} + +/** + * This function uses two different ways to detect legacy alerts based on what dashboard system is being used. + * + * 1. if using the older (non-scenes) dashboard system we can simply check for "alert" in the panel definition. + * 2. for dashboard scenes the alerts are no longer added to the model but we can check for "alertThreshold" in the panel options object + */ +function getLegacyAlertPanelsFromDashboard(dashboard: Dashboard): Panel[] { + const panelsWithLegacyAlerts = dashboard.panels?.filter((panel) => { + const hasAlertDefinition = 'alert' in panel; + const hasAlertThreshold = 'options' in panel && panel.options ? 'alertThreshold' in panel.options : false; + return hasAlertDefinition || hasAlertThreshold; + }); + + return panelsWithLegacyAlerts ?? []; +} + +interface Props { + dashboardUid: string; + panels: Panel[]; +} + +function LegacyAlertsWarning({ dashboardUid, panels }: Props) { + const [isOpen, toggleCollapsible] = useToggle(false); + + const columns: Array> = [ + { id: 'id', header: 'ID' }, + { + id: 'title', + header: 'Title', + cell: (cell) => ( + + {cell.value} + + ), + }, + ]; + + return ( + +

+ You have legacy alert rules in this dashboard that were deprecated in Grafana 11 and are no longer supported. +

+

+ Refer to{' '} + + our documentation + {' '} + on how to migrate legacy alert rules and how to import and export using Grafana Alerting. +

+ + + String(panel.id)} pageSize={5} /> + +
+ ); +} diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts index e8e1a7e8dcb..2fd698b3796 100644 --- a/public/app/features/alerting/unified/utils/misc.ts +++ b/public/app/features/alerting/unified/utils/misc.ts @@ -157,8 +157,15 @@ export function makeDashboardLink(dashboardUID: string): string { return createUrl(`/d/${encodeURIComponent(dashboardUID)}`); } -export function makePanelLink(dashboardUID: string, panelId: string): string { - return createUrl(`/d/${encodeURIComponent(dashboardUID)}`, { viewPanel: panelId }); +type PanelLinkParams = { + viewPanel?: string; + editPanel?: string; + tab?: 'alert' | 'transform' | 'query'; +}; + +export function makePanelLink(dashboardUID: string, panelId: string, queryParams: PanelLinkParams = {}): string { + const panelParams = new URLSearchParams(queryParams); + return createUrl(`/d/${encodeURIComponent(dashboardUID)}`, panelParams); } // keep retrying fn if it's error passes shouldRetry(error) and timeout has not elapsed yet diff --git a/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx b/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx index bd7626b5d61..819d5704ad5 100644 --- a/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx +++ b/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx @@ -3,9 +3,11 @@ import React from 'react'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { SceneComponentProps, SceneObjectBase, sceneUtils } from '@grafana/scenes'; +import { Dashboard } from '@grafana/schema'; import { Button, CodeEditor, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { Trans } from 'app/core/internationalization'; +import LegacyAlertsDeprecationNotice from 'app/features/alerting/unified/integration/LegacyAlertsDeprecationNotice'; import { getPrettyJSON } from 'app/features/inspector/utils/utils'; import { DashboardDTO } from 'app/types'; @@ -38,9 +40,13 @@ export class JsonModelEditView extends SceneObjectBase i return getDashboardSceneFor(this); } - public getJsonText(): string { + public getSaveModel(): Dashboard { const dashboard = this.getDashboard(); - const jsonData = transformSceneToSaveModel(dashboard); + return transformSceneToSaveModel(dashboard); + } + + public getJsonText(): string { + const jsonData = this.getSaveModel(); return getPrettyJSON(jsonData); } @@ -67,6 +73,7 @@ export class JsonModelEditView extends SceneObjectBase i const { jsonText } = model.useState(); const styles = useStyles2(getStyles); + const saveModel = model.getSaveModel(); return ( @@ -76,6 +83,7 @@ export class JsonModelEditView extends SceneObjectBase i The JSON model below is the data structure that defines the dashboard. This includes dashboard settings, panel settings, layout, queries, and so on. + (JSON.stringify(dashboard.getSaveModelClone(), null, 2)); + const dashboardSaveModel = dashboard.getSaveModelClone(); + const [dashboardJson, setDashboardJson] = useState(JSON.stringify(dashboardSaveModel, null, 2)); const pageNav = sectionNav.node.parentItem; const onClick = async () => { @@ -29,6 +31,7 @@ export function JsonEditorSettings({ dashboard, sectionNav }: SettingsPageProps) The JSON model below is the data structure that defines the dashboard. This includes dashboard settings, panel settings, layout, queries, and so on. +