Dashboard Schema V2: Support alerting for v2 schema in alert rule editor (#113891)
* implement alerting for v2 schema * clean up * clarify * add support for library panels * comment * include missing library panel when v1 returns v2; cleanup * Use type union * remove unneccesary check * add AlertStatesDataLayer so that alert state is displayed next to panel title * fix test * cleanup undefined
This commit is contained in:
@@ -8,7 +8,6 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button, Field, Input, Stack, Text, TextArea, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { DashboardModel } from '../../../../dashboard/state/DashboardModel';
|
||||
import { AIImproveAnnotationsButtonComponent } from '../../enterprise-components/AI/AIGenImproveAnnotationsButton/addAIImproveAnnotationsButton';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { Annotation, annotationLabels } from '../../utils/constants';
|
||||
@@ -19,7 +18,7 @@ import DashboardAnnotationField from './DashboardAnnotationField';
|
||||
import { DashboardPicker, PanelDTO, getVisualPanels } from './DashboardPicker';
|
||||
import { NeedHelpInfo } from './NeedHelpInfo';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
import { useDashboardQuery } from './useDashboardQuery';
|
||||
import { DashboardResponse, useDashboardQuery } from './useDashboardQuery';
|
||||
|
||||
const AnnotationsStep = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
@@ -40,22 +39,22 @@ const AnnotationsStep = () => {
|
||||
const selectedDashboardUid = annotations.find((annotation) => annotation.key === Annotation.dashboardUID)?.value;
|
||||
const selectedPanelId = Number(annotations.find((annotation) => annotation.key === Annotation.panelID)?.value);
|
||||
|
||||
const [selectedDashboard, setSelectedDashboard] = useState<DashboardModel | undefined>(undefined);
|
||||
const [selectedDashboard, setSelectedDashboard] = useState<DashboardResponse | undefined>(undefined);
|
||||
const [selectedPanel, setSelectedPanel] = useState<PanelDTO | undefined>(undefined);
|
||||
|
||||
const { dashboardModel, isFetching: isDashboardFetching } = useDashboardQuery(selectedDashboardUid);
|
||||
const { dashboard, isFetching: isDashboardFetching } = useDashboardQuery(selectedDashboardUid);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDashboardFetching || !dashboardModel) {
|
||||
if (isDashboardFetching || !dashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedDashboard(dashboardModel);
|
||||
setSelectedDashboard(dashboard);
|
||||
|
||||
const allPanels = getVisualPanels(dashboardModel);
|
||||
const allPanels = getVisualPanels(dashboard);
|
||||
const currentPanel = allPanels.find((panel) => panel.id === selectedPanelId);
|
||||
setSelectedPanel(currentPanel);
|
||||
}, [selectedPanelId, dashboardModel, isDashboardFetching]);
|
||||
}, [selectedPanelId, dashboard, isDashboardFetching]);
|
||||
|
||||
const setSelectedDashboardAndPanelId = (dashboardUid: string, panelId: number) => {
|
||||
const updatedAnnotations = produce(annotations, (draft) => {
|
||||
|
||||
+6
-6
@@ -3,11 +3,11 @@ import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { Icon, Text, useStyles2 } from '@grafana/ui';
|
||||
import { DashboardDataDTO } from 'app/types/dashboard';
|
||||
|
||||
import { makeDashboardLink, makePanelLink } from '../../utils/misc';
|
||||
|
||||
import { PanelDTO } from './DashboardPicker';
|
||||
import { PanelDTO, getDashboardTitle, getDashboardUid } from './DashboardPicker';
|
||||
import { DashboardResponse } from './useDashboardQuery';
|
||||
|
||||
const DashboardAnnotationField = ({
|
||||
dashboard,
|
||||
@@ -17,7 +17,7 @@ const DashboardAnnotationField = ({
|
||||
onEditClick,
|
||||
onDeleteClick,
|
||||
}: {
|
||||
dashboard?: DashboardDataDTO;
|
||||
dashboard?: DashboardResponse;
|
||||
panel?: PanelDTO;
|
||||
dashboardUid: string; //fallback
|
||||
panelId: string; //fallback
|
||||
@@ -26,8 +26,8 @@ const DashboardAnnotationField = ({
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const dashboardLink = makeDashboardLink(dashboard?.uid || dashboardUid);
|
||||
const panelLink = makePanelLink(dashboard?.uid || dashboardUid, panel?.id?.toString() || panelId);
|
||||
const dashboardLink = makeDashboardLink(getDashboardUid(dashboard) || dashboardUid);
|
||||
const panelLink = makePanelLink(getDashboardUid(dashboard) || dashboardUid, panel?.id?.toString() || panelId);
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{dashboard && (
|
||||
@@ -38,7 +38,7 @@ const DashboardAnnotationField = ({
|
||||
rel="noreferrer"
|
||||
data-testid="dashboard-annotation"
|
||||
>
|
||||
{dashboard.title} <Icon name={'external-link-alt'} />
|
||||
{getDashboardTitle(dashboard)} <Icon name={'external-link-alt'} />
|
||||
</a>
|
||||
)}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { FixedSizeList } from 'react-window';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -18,11 +19,13 @@ import {
|
||||
clearButtonStyles,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { AnnoKeyFolderTitle } from 'app/features/apiserver/types';
|
||||
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
|
||||
import { isDashboardV2Resource } from 'app/features/dashboard/api/utils';
|
||||
import { getGrafanaSearcher } from 'app/features/search/service/searcher';
|
||||
import { DashboardDTO } from 'app/types/dashboard';
|
||||
|
||||
import { DashboardModel } from '../../../../dashboard/state/DashboardModel';
|
||||
|
||||
import { useDashboardQuery } from './useDashboardQuery';
|
||||
import { DashboardResponse, useDashboardQuery } from './useDashboardQuery';
|
||||
|
||||
export interface PanelDTO {
|
||||
id?: number;
|
||||
@@ -74,7 +77,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
const [debouncedDashboardFilter, setDebouncedDashboardFilter] = useState('');
|
||||
const [panelFilter, setPanelFilter] = useState('');
|
||||
const { value, loading: isDashSearchFetching } = useFilteredDashboards(debouncedDashboardFilter);
|
||||
const { dashboardModel, isFetching: isDashboardFetching } = useDashboardQuery(selectedDashboardUid);
|
||||
const { dashboard, isFetching: isDashboardFetching } = useDashboardQuery(selectedDashboardUid);
|
||||
const handleDashboardChange = useCallback((dashboardUid: string) => {
|
||||
setSelectedDashboardUid(dashboardUid);
|
||||
setSelectedPanelId(undefined);
|
||||
@@ -82,7 +85,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
|
||||
const { dashboards: filteredDashboards = [], locationInfo: locationInfo = {} } = value || {};
|
||||
|
||||
const allDashboardPanels = getVisualPanels(dashboardModel);
|
||||
const allDashboardPanels = getVisualPanels(dashboard);
|
||||
|
||||
const filteredPanels =
|
||||
allDashboardPanels
|
||||
@@ -197,7 +200,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
contentClassName={styles.modalContent}
|
||||
>
|
||||
{/* This alert shows if the selected dashboard is not found in the first page of dashboards */}
|
||||
{!selectedDashboardIsInPageResult && dashboardUid && dashboardModel && (
|
||||
{!selectedDashboardIsInPageResult && dashboardUid && dashboard && (
|
||||
<Alert
|
||||
title={t('alerting.dashboard-picker.title-current-selection', 'Current selection')}
|
||||
severity="info"
|
||||
@@ -209,9 +212,9 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
<Trans
|
||||
i18nKey="alerting.dashboard-picker.current-selection-dashboard"
|
||||
values={{
|
||||
dashboardTitle: dashboardModel.title,
|
||||
dashboardUid: dashboardModel.uid,
|
||||
folderTitle: dashboardModel.meta?.folderTitle ?? fallbackDashboardsString,
|
||||
dashboardTitle: getDashboardTitle(dashboard),
|
||||
dashboardUid: getDashboardUid(dashboard),
|
||||
folderTitle: getDashboardFolderTitle(dashboard) ?? fallbackDashboardsString,
|
||||
}}
|
||||
>
|
||||
Dashboard: {'{{dashboardTitle}}'} ({'{{ dashboardUid }}'}) in folder {'{{ folderTitle }}'}
|
||||
@@ -318,13 +321,35 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
);
|
||||
};
|
||||
|
||||
export function getVisualPanels(dashboardModel: DashboardModel | undefined) {
|
||||
if (!dashboardModel) {
|
||||
export function getVisualPanels(dashboardDTO: DashboardResponse | undefined) {
|
||||
if (!dashboardDTO || !('dashboard' in dashboardDTO)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const panelsWithoutRows = dashboardModel.panels.filter((panel) => panel.type !== 'row');
|
||||
const panelsNestedInRows = dashboardModel.panels
|
||||
// process v2 dashboard
|
||||
if (isDashboardV2Resource(dashboardDTO)) {
|
||||
return Object.values(dashboardDTO.spec.elements).map((element) => ({
|
||||
id: element.spec.id,
|
||||
title: element.spec.title,
|
||||
type: element.kind === 'Panel' ? element.spec.vizConfig.group : 'LibraryPanel',
|
||||
...(element.kind === 'LibraryPanel' && {
|
||||
libraryPanel: {
|
||||
uid: element.spec.libraryPanel.uid,
|
||||
name: element.spec.libraryPanel.name,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
// process v1 dashboard
|
||||
const { dashboard } = dashboardDTO;
|
||||
|
||||
if (!dashboard || !dashboard.panels) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const panelsWithoutRows = dashboard.panels.filter((panel) => panel.type !== 'row');
|
||||
const panelsNestedInRows = dashboard.panels
|
||||
.filter((rowPanel) => rowPanel.collapsed)
|
||||
.flatMap((collapsedRow) => collapsedRow.panels ?? []);
|
||||
|
||||
@@ -332,6 +357,46 @@ export function getVisualPanels(dashboardModel: DashboardModel | undefined) {
|
||||
return allDashboardPanels;
|
||||
}
|
||||
|
||||
export function getDashboardTitle(dashboardDTO: DashboardResponse | undefined) {
|
||||
if (!dashboardDTO || !('dashboard' in dashboardDTO)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isDashboardV2Resource(dashboardDTO)) {
|
||||
return dashboardDTO.spec.title;
|
||||
}
|
||||
|
||||
return dashboardDTO.dashboard.title;
|
||||
}
|
||||
|
||||
export function getDashboardUid(dashboardDTO: DashboardResponse | undefined) {
|
||||
if (!dashboardDTO || !('dashboard' in dashboardDTO)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isDashboardV2Resource(dashboardDTO)) {
|
||||
return dashboardDTO.metadata.name;
|
||||
}
|
||||
|
||||
return dashboardDTO.dashboard.uid;
|
||||
}
|
||||
|
||||
export function getDashboardFolderTitle(
|
||||
dashboardDTO: DashboardDTO | DashboardWithAccessInfo<DashboardV2Spec> | undefined
|
||||
) {
|
||||
if (!dashboardDTO || !('dashboard' in dashboardDTO)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isDashboardV2Resource(dashboardDTO)) {
|
||||
return dashboardDTO.metadata.annotations?.[AnnoKeyFolderTitle];
|
||||
}
|
||||
|
||||
const { meta } = dashboardDTO;
|
||||
|
||||
return meta.folderTitle;
|
||||
}
|
||||
|
||||
const isValidPanel = (panel: PanelDTO): boolean => {
|
||||
const hasValidID = typeof panel.id === 'number';
|
||||
const isValidPanelType = typeof panel.type === 'string';
|
||||
|
||||
@@ -1,36 +1,47 @@
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
|
||||
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
|
||||
import { isDashboardV2Resource } from 'app/features/dashboard/api/utils';
|
||||
import { DashboardDTO } from 'app/types/dashboard';
|
||||
|
||||
import { DashboardModel } from '../../../../dashboard/state/DashboardModel';
|
||||
|
||||
const convertToDashboardModel = memoizeOne((dashboardDTO: DashboardDTO) => {
|
||||
export type DashboardResponse = DashboardDTO | DashboardWithAccessInfo<DashboardV2Spec>;
|
||||
|
||||
const ensureV1PanelsHaveIds = memoizeOne((dashboardDTO: DashboardDTO): DashboardResponse => {
|
||||
// RTKQuery freezes all returned objects. DashboardModel constructor runs migrations which might change the internal object
|
||||
// Hence we need to add structuredClone to make a deep copy of the API response object
|
||||
const { dashboard, meta } = structuredClone(dashboardDTO);
|
||||
return new DashboardModel(dashboard, meta);
|
||||
const dashboardDTOClone = structuredClone(dashboardDTO);
|
||||
const model = new DashboardModel(dashboardDTOClone.dashboard, dashboardDTOClone.meta);
|
||||
|
||||
dashboardDTOClone.dashboard.panels = model.panels;
|
||||
|
||||
return dashboardDTOClone;
|
||||
});
|
||||
|
||||
export function useDashboardQuery(dashboardUid?: string) {
|
||||
const [dashboardModel, setDashboardModel] = useState<DashboardModel>();
|
||||
const [dashboard, setDashboard] = useState<DashboardResponse>();
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
useEffect(() => {
|
||||
if (dashboardUid) {
|
||||
setIsFetching(true);
|
||||
getDashboardAPI()
|
||||
.getDashboardDTO(dashboardUid)
|
||||
.then((dashboard) => {
|
||||
if (!('dashboard' in dashboard)) {
|
||||
console.error('Something went wrong, unexpected dashboard format');
|
||||
.then((dashboardDTO) => {
|
||||
if ('dashboard' in dashboardDTO) {
|
||||
setDashboard(ensureV1PanelsHaveIds(dashboardDTO));
|
||||
} else if (isDashboardV2Resource(dashboardDTO)) {
|
||||
setDashboard(dashboardDTO);
|
||||
} else {
|
||||
setDashboardModel(convertToDashboardModel(dashboard));
|
||||
console.error('Something went wrong, unexpected dashboard format');
|
||||
}
|
||||
setIsFetching(false);
|
||||
});
|
||||
}
|
||||
}, [dashboardUid]);
|
||||
|
||||
return { dashboardModel, isFetching };
|
||||
return { dashboard, isFetching };
|
||||
}
|
||||
|
||||
+11
@@ -64,6 +64,7 @@ import { DashboardMeta } from 'app/types/dashboard';
|
||||
|
||||
import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
|
||||
import { dashboardAnalyticsInitializer } from '../behaviors/DashboardAnalyticsInitializerBehavior';
|
||||
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
|
||||
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||
import { DashboardControls } from '../scene/DashboardControls';
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
@@ -123,6 +124,15 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
||||
return new DashboardAnnotationsDataLayer(layerState);
|
||||
});
|
||||
|
||||
// Create alert states data layer if unified alerting is enabled
|
||||
let alertStatesLayer: AlertStatesDataLayer | undefined;
|
||||
if (config.unifiedAlertingEnabled) {
|
||||
alertStatesLayer = new AlertStatesDataLayer({
|
||||
key: 'alert-states',
|
||||
name: 'Alert States',
|
||||
});
|
||||
}
|
||||
|
||||
const isDashboardEditable = Boolean(dashboard.editable);
|
||||
const canSave = dto.access.canSave !== false;
|
||||
let dashboardId: number | undefined = undefined;
|
||||
@@ -232,6 +242,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
||||
],
|
||||
$data: new DashboardDataLayerSet({
|
||||
annotationLayers,
|
||||
alertStatesLayer,
|
||||
}),
|
||||
controls: new DashboardControls({
|
||||
timePicker: new SceneTimePicker({
|
||||
|
||||
Reference in New Issue
Block a user