diff --git a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx index b7ef11375f0..52d8cab8b70 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx @@ -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(undefined); + const [selectedDashboard, setSelectedDashboard] = useState(undefined); const [selectedPanel, setSelectedPanel] = useState(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) => { diff --git a/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx b/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx index 26d32af3104..3354a8b102c 100644 --- a/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx @@ -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 (
{dashboard && ( @@ -38,7 +38,7 @@ const DashboardAnnotationField = ({ rel="noreferrer" data-testid="dashboard-annotation" > - {dashboard.title} + {getDashboardTitle(dashboard)} )} diff --git a/public/app/features/alerting/unified/components/rule-editor/DashboardPicker.tsx b/public/app/features/alerting/unified/components/rule-editor/DashboardPicker.tsx index afd4a4e80f2..68fa9d2df4d 100644 --- a/public/app/features/alerting/unified/components/rule-editor/DashboardPicker.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DashboardPicker.tsx @@ -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 && ( 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 | 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'; diff --git a/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts b/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts index 3427bd0486b..22c95aa797f 100644 --- a/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts +++ b/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts @@ -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; + +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(); + const [dashboard, setDashboard] = useState(); 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 }; } diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 0534ad76e56..683f22c16fb 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -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