From d306f417d3d014e044acdfda9bf7d31560117a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 2 Mar 2021 16:37:36 +0100 Subject: [PATCH] LibraryPanels: No save modal when user is on same dashboard (#31606) --- .../AddPanelWidget/AddPanelWidget.tsx | 5 +- .../components/PanelEditor/PanelEditor.tsx | 34 +++++++++-- .../features/dashboard/state/PanelModel.ts | 12 ++-- .../LibraryPanelCard/LibraryPanelCard.tsx | 2 +- .../LibraryPanelsView/LibraryPanelsView.tsx | 10 ++-- .../PanelLibraryOptionsGroup.tsx | 12 ++-- .../SaveLibraryPanelModal.tsx | 3 +- public/app/features/library-panels/guard.ts | 6 ++ .../app/features/library-panels/state/api.ts | 33 +---------- public/app/features/library-panels/types.ts | 33 +++++++++++ public/app/features/library-panels/utils.ts | 59 +++++++++++++++++++ .../library-panels/utils/usePanelSave.ts | 49 ++++----------- 12 files changed, 165 insertions(+), 93 deletions(-) create mode 100644 public/app/features/library-panels/guard.ts create mode 100644 public/app/features/library-panels/types.ts create mode 100644 public/app/features/library-panels/utils.ts diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index 3643a792f0a..05af4633684 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -14,8 +14,9 @@ import { updateLocation } from 'app/core/actions'; import { addPanel } from 'app/features/dashboard/state/reducers'; import { DashboardModel, PanelModel } from '../../state'; import { LibraryPanelsView } from '../../../library-panels/components/LibraryPanelsView/LibraryPanelsView'; -import { LibraryPanelDTO } from 'app/features/library-panels/state/api'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; +import { LibraryPanelDTO } from '../../../library-panels/types'; +import { toPanelModelLibraryPanel } from '../../../library-panels/utils'; export type PanelPluginInfo = { id: any; defaults: { gridPos: { w: any; h: any }; title: any } }; @@ -119,7 +120,7 @@ export const AddPanelWidgetUnconnected: React.FC = ({ panel, dashboard, u const newPanel: PanelModel = { ...panelInfo.model, gridPos, - libraryPanel: _.pick(panelInfo, 'name', 'uid', 'meta'), + libraryPanel: toPanelModelLibraryPanel(panelInfo), }; dashboard.addPanel(newPanel); diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 9709912423e..afe3961ec8b 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -29,10 +29,10 @@ import { DashboardPanel } from '../../dashgrid/DashboardPanel'; import { exitPanelEditor, - updateSourcePanel, initPanelEditor, panelEditorCleanUp, updatePanelEditorUIState, + updateSourcePanel, } from './state/actions'; import { updateTimeZoneForSession } from 'app/features/profile/state/reducers'; @@ -49,6 +49,14 @@ import { DashboardModel, PanelModel } from '../../state'; import { PanelOptionsChangedEvent } from 'app/types/events'; import { UnlinkModal } from '../../../library-panels/components/UnlinkModal/UnlinkModal'; import { SaveLibraryPanelModal } from 'app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal'; +import { isPanelModelLibraryPanel } from '../../../library-panels/guard'; +import { getLibraryPanelConnectedDashboards } from '../../../library-panels/state/api'; +import { + createPanelLibraryErrorNotification, + createPanelLibrarySuccessNotification, + saveAndRefreshLibraryPanel, +} from '../../../library-panels/utils'; +import { notifyApp } from '../../../../core/actions'; interface OwnProps { dashboard: DashboardModel; @@ -79,6 +87,7 @@ const mapDispatchToProps = { setDiscardChanges, updatePanelEditorUIState, updateTimeZoneForSession, + notifyApp, }; const connector = connect(mapStateToProps, mapDispatchToProps); @@ -129,13 +138,28 @@ export class PanelEditorUnconnected extends PureComponent { }); }; - onSavePanel = () => { - const panelId = this.props.panel.libraryPanel?.uid; - if (!panelId) { + onSaveLibraryPanel = async () => { + if (!isPanelModelLibraryPanel(this.props.panel)) { // New library panel, no need to display modal return; } + if (this.props.panel.libraryPanel.meta.connectedDashboards === 0) { + return; + } + + const connectedDashboards = await getLibraryPanelConnectedDashboards(this.props.panel.libraryPanel.uid); + if (connectedDashboards.length === 1 && connectedDashboards.indexOf(this.props.dashboard.id) !== -1) { + try { + await saveAndRefreshLibraryPanel(this.props.panel, this.props.dashboard.meta.folderId!); + this.props.updateSourcePanel(this.props.panel); + this.props.notifyApp(createPanelLibrarySuccessNotification('Library panel saved')); + } catch (err) { + this.props.notifyApp(createPanelLibraryErrorNotification(`Error saving library panel: "${err.statusText}"`)); + } + return; + } + appEvents.emit(CoreEvents.showModalReact, { component: SaveLibraryPanelModal, props: { @@ -289,7 +313,7 @@ export class PanelEditorUnconnected extends PureComponent { , this.props.panel.libraryPanel ? ( ; + libraryPanel?: { uid: undefined; name: string } | PanelModelLibraryPanel; // non persisted isViewing: boolean; diff --git a/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx b/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx index f38728c770f..00f08273d8e 100644 --- a/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx +++ b/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Icon, IconButton, ConfirmModal, Tooltip, useStyles, Card } from '@grafana/ui'; import { css } from 'emotion'; import { GrafanaTheme } from '@grafana/data'; -import { LibraryPanelDTO } from '../../state/api'; +import { LibraryPanelDTO } from '../../types'; export interface LibraryPanelCardProps { libraryPanel: LibraryPanelDTO; diff --git a/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx b/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx index 70c044ff31c..8ffb08393ec 100644 --- a/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx @@ -1,10 +1,12 @@ -import { Icon, Input, Button, stylesFactory, useStyles } from '@grafana/ui'; import React, { useEffect, useState } from 'react'; import { useDebounce } from 'react-use'; -import { cx, css } from 'emotion'; -import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard'; +import { css, cx } from 'emotion'; +import { Button, Icon, Input, stylesFactory, useStyles } from '@grafana/ui'; import { DateTimeInput, GrafanaTheme } from '@grafana/data'; -import { deleteLibraryPanel, getLibraryPanels, LibraryPanelDTO } from '../../state/api'; + +import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard'; +import { deleteLibraryPanel, getLibraryPanels } from '../../state/api'; +import { LibraryPanelDTO } from '../../types'; interface LibraryPanelViewProps { className?: string; diff --git a/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx b/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx index 97b55eff9e8..e3ec7471f52 100644 --- a/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx +++ b/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx @@ -1,14 +1,16 @@ +import React, { useState } from 'react'; +import { css } from 'emotion'; +import pick from 'lodash/pick'; import { GrafanaTheme } from '@grafana/data'; import { Button, stylesFactory, useStyles } from '@grafana/ui'; + import { OptionsGroup } from 'app/features/dashboard/components/PanelEditor/OptionsGroup'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; -import { css } from 'emotion'; -import React, { useState } from 'react'; import { AddLibraryPanelModal } from '../AddLibraryPanelModal/AddLibraryPanelModal'; import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView'; -import pick from 'lodash/pick'; -import { LibraryPanelDTO } from '../../state/api'; import { PanelQueriesChangedEvent } from 'app/types/events'; +import { LibraryPanelDTO } from '../../types'; +import { toPanelModelLibraryPanel } from '../../utils'; interface Props { panel: PanelModel; @@ -23,7 +25,7 @@ export const PanelLibraryOptionsGroup: React.FC = ({ panel, dashboard }) panel.restoreModel({ ...panelInfo.model, ...pick(panel, 'gridPos', 'id'), - libraryPanel: pick(panelInfo, 'uid', 'name', 'meta'), + libraryPanel: toPanelModelLibraryPanel(panelInfo), }); // dummy change for re-render diff --git a/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx b/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx index 6a3da834a4c..b2a72bed18e 100644 --- a/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx @@ -5,7 +5,8 @@ import { css } from 'emotion'; import { useAsync, useDebounce } from 'react-use'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { usePanelSave } from '../../utils/usePanelSave'; -import { getLibraryPanelConnectedDashboards, PanelModelWithLibraryPanel } from '../../state/api'; +import { getLibraryPanelConnectedDashboards } from '../../state/api'; +import { PanelModelWithLibraryPanel } from '../../types'; interface Props { panel: PanelModelWithLibraryPanel; diff --git a/public/app/features/library-panels/guard.ts b/public/app/features/library-panels/guard.ts new file mode 100644 index 00000000000..713aefbf411 --- /dev/null +++ b/public/app/features/library-panels/guard.ts @@ -0,0 +1,6 @@ +import { PanelModel } from '../dashboard/state'; +import { PanelModelWithLibraryPanel } from './types'; + +export function isPanelModelLibraryPanel(panel: PanelModel): panel is PanelModelWithLibraryPanel { + return Boolean(panel.libraryPanel?.uid); +} diff --git a/public/app/features/library-panels/state/api.ts b/public/app/features/library-panels/state/api.ts index 96f46d888e9..b52b3afd348 100644 --- a/public/app/features/library-panels/state/api.ts +++ b/public/app/features/library-panels/state/api.ts @@ -1,36 +1,5 @@ import { getBackendSrv } from '@grafana/runtime'; - -import { PanelModel } from '../../dashboard/state'; - -export interface LibraryPanelDTO { - id: number; - orgId: number; - folderId: number; - uid: string; - name: string; - model: any; - version: number; - meta: LibraryPanelDTOMeta; -} - -export interface LibraryPanelDTOMeta { - canEdit: boolean; - connectedDashboards: number; - created: string; - updated: string; - createdBy: LibraryPanelDTOMetaUser; - updatedBy: LibraryPanelDTOMetaUser; -} - -export interface LibraryPanelDTOMetaUser { - id: number; - name: string; - avatarUrl: string; -} - -export interface PanelModelWithLibraryPanel extends PanelModel { - libraryPanel: Pick; -} +import { LibraryPanelDTO, PanelModelWithLibraryPanel } from '../types'; export async function getLibraryPanels(): Promise { const { result } = await getBackendSrv().get(`/api/library-panels`); diff --git a/public/app/features/library-panels/types.ts b/public/app/features/library-panels/types.ts new file mode 100644 index 00000000000..3955de29639 --- /dev/null +++ b/public/app/features/library-panels/types.ts @@ -0,0 +1,33 @@ +import { PanelModel } from '../dashboard/state'; + +export interface LibraryPanelDTO { + id: number; + orgId: number; + folderId: number; + uid: string; + name: string; + model: any; + version: number; + meta: LibraryPanelDTOMeta; +} + +export interface LibraryPanelDTOMeta { + canEdit: boolean; + connectedDashboards: number; + created: string; + updated: string; + createdBy: LibraryPanelDTOMetaUser; + updatedBy: LibraryPanelDTOMetaUser; +} + +export interface LibraryPanelDTOMetaUser { + id: number; + name: string; + avatarUrl: string; +} + +export type PanelModelLibraryPanel = Pick; + +export interface PanelModelWithLibraryPanel extends PanelModel { + libraryPanel: PanelModelLibraryPanel; +} diff --git a/public/app/features/library-panels/utils.ts b/public/app/features/library-panels/utils.ts new file mode 100644 index 00000000000..f3b97234f3c --- /dev/null +++ b/public/app/features/library-panels/utils.ts @@ -0,0 +1,59 @@ +import { LibraryPanelDTO, PanelModelLibraryPanel } from './types'; +import { PanelModel } from '../dashboard/state'; +import { addLibraryPanel, updateLibraryPanel } from './state/api'; +import { createErrorNotification, createSuccessNotification } from '../../core/copy/appNotification'; +import { AppNotification } from '../../types'; + +export function createPanelLibraryErrorNotification(message: string): AppNotification { + return createErrorNotification(message); +} + +export function createPanelLibrarySuccessNotification(message: string): AppNotification { + return createSuccessNotification(message); +} + +export function toPanelModelLibraryPanel(libraryPanelDto: LibraryPanelDTO): PanelModelLibraryPanel { + const { uid, name, meta, version } = libraryPanelDto; + return { uid, name, meta, version }; +} + +export async function saveAndRefreshLibraryPanel(panel: PanelModel, folderId: number): Promise { + const panelSaveModel = toPanelSaveModel(panel); + const savedPanel = await saveOrUpdateLibraryPanel(panelSaveModel, folderId); + updatePanelModelWithUpdate(panel, savedPanel); + return savedPanel; +} + +function toPanelSaveModel(panel: PanelModel): any { + let panelSaveModel = panel.getSaveModel(); + panelSaveModel = { + libraryPanel: { + name: panel.title, + uid: undefined, + }, + ...panelSaveModel, + }; + + return panelSaveModel; +} + +function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryPanelDTO): void { + panel.restoreModel({ + ...updated.model, + libraryPanel: toPanelModelLibraryPanel(updated), + }); + panel.refresh(); +} + +function saveOrUpdateLibraryPanel(panel: any, folderId: number): Promise { + if (!panel.libraryPanel) { + return Promise.reject(); + } + + if (panel.libraryPanel && panel.libraryPanel.uid === undefined) { + panel.libraryPanel.name = panel.title; + return addLibraryPanel(panel, folderId!); + } + + return updateLibraryPanel(panel, folderId!); +} diff --git a/public/app/features/library-panels/utils/usePanelSave.ts b/public/app/features/library-panels/utils/usePanelSave.ts index 5ce40b3a04c..8af7b01d88b 100644 --- a/public/app/features/library-panels/utils/usePanelSave.ts +++ b/public/app/features/library-panels/utils/usePanelSave.ts @@ -1,52 +1,27 @@ import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; import useAsyncFn from 'react-use/lib/useAsyncFn'; -import { AppEvents } from '@grafana/data'; -import appEvents from 'app/core/app_events'; + import { PanelModel } from 'app/features/dashboard/state'; -import { addLibraryPanel, updateLibraryPanel } from '../state/api'; - -const saveLibraryPanels = (panel: any, folderId: number) => { - if (!panel.libraryPanel) { - return Promise.reject(); - } - - if (panel.libraryPanel && panel.libraryPanel.uid === undefined) { - panel.libraryPanel.name = panel.title; - return addLibraryPanel(panel, folderId!); - } - - return updateLibraryPanel(panel, folderId!); -}; +import { + createPanelLibraryErrorNotification, + createPanelLibrarySuccessNotification, + saveAndRefreshLibraryPanel, +} from '../utils'; +import { notifyApp } from 'app/core/actions'; export const usePanelSave = () => { + const dispatch = useDispatch(); const [state, saveLibraryPanel] = useAsyncFn(async (panel: PanelModel, folderId: number) => { - let panelSaveModel = panel.getSaveModel(); - panelSaveModel = { - libraryPanel: { - name: panel.title, - uid: undefined, - }, - ...panelSaveModel, - }; - const savedPanel = await saveLibraryPanels(panelSaveModel, folderId); - panel.restoreModel({ - ...savedPanel.model, - libraryPanel: { - uid: savedPanel.uid, - name: savedPanel.name, - meta: savedPanel.meta, - }, - }); - panel.refresh(); - return savedPanel; + return await saveAndRefreshLibraryPanel(panel, folderId); }, []); useEffect(() => { if (state.error) { - appEvents.emit(AppEvents.alertError, [`Error saving library panel: "${state.error.message}"`]); + dispatch(notifyApp(createPanelLibraryErrorNotification(`Error saving library panel: "${state.error.message}"`))); } if (state.value) { - appEvents.emit(AppEvents.alertSuccess, ['Library panel saved']); + dispatch(notifyApp(createPanelLibrarySuccessNotification('Library panel saved'))); } }, [state]);