LibraryPanels: No save modal when user is on same dashboard (#31606)

This commit is contained in:
Hugo Häggmark 2021-03-02 16:37:36 +01:00 committed by GitHub
parent a8bf1d68e3
commit d306f417d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 165 additions and 93 deletions

View File

@ -14,8 +14,9 @@ import { updateLocation } from 'app/core/actions';
import { addPanel } from 'app/features/dashboard/state/reducers'; import { addPanel } from 'app/features/dashboard/state/reducers';
import { DashboardModel, PanelModel } from '../../state'; import { DashboardModel, PanelModel } from '../../state';
import { LibraryPanelsView } from '../../../library-panels/components/LibraryPanelsView/LibraryPanelsView'; 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 { 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 } }; export type PanelPluginInfo = { id: any; defaults: { gridPos: { w: any; h: any }; title: any } };
@ -119,7 +120,7 @@ export const AddPanelWidgetUnconnected: React.FC<Props> = ({ panel, dashboard, u
const newPanel: PanelModel = { const newPanel: PanelModel = {
...panelInfo.model, ...panelInfo.model,
gridPos, gridPos,
libraryPanel: _.pick(panelInfo, 'name', 'uid', 'meta'), libraryPanel: toPanelModelLibraryPanel(panelInfo),
}; };
dashboard.addPanel(newPanel); dashboard.addPanel(newPanel);

View File

@ -29,10 +29,10 @@ import { DashboardPanel } from '../../dashgrid/DashboardPanel';
import { import {
exitPanelEditor, exitPanelEditor,
updateSourcePanel,
initPanelEditor, initPanelEditor,
panelEditorCleanUp, panelEditorCleanUp,
updatePanelEditorUIState, updatePanelEditorUIState,
updateSourcePanel,
} from './state/actions'; } from './state/actions';
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers'; import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
@ -49,6 +49,14 @@ import { DashboardModel, PanelModel } from '../../state';
import { PanelOptionsChangedEvent } from 'app/types/events'; import { PanelOptionsChangedEvent } from 'app/types/events';
import { UnlinkModal } from '../../../library-panels/components/UnlinkModal/UnlinkModal'; import { UnlinkModal } from '../../../library-panels/components/UnlinkModal/UnlinkModal';
import { SaveLibraryPanelModal } from 'app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal'; 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 { interface OwnProps {
dashboard: DashboardModel; dashboard: DashboardModel;
@ -79,6 +87,7 @@ const mapDispatchToProps = {
setDiscardChanges, setDiscardChanges,
updatePanelEditorUIState, updatePanelEditorUIState,
updateTimeZoneForSession, updateTimeZoneForSession,
notifyApp,
}; };
const connector = connect(mapStateToProps, mapDispatchToProps); const connector = connect(mapStateToProps, mapDispatchToProps);
@ -129,13 +138,28 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
}); });
}; };
onSavePanel = () => { onSaveLibraryPanel = async () => {
const panelId = this.props.panel.libraryPanel?.uid; if (!isPanelModelLibraryPanel(this.props.panel)) {
if (!panelId) {
// New library panel, no need to display modal // New library panel, no need to display modal
return; 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, { appEvents.emit(CoreEvents.showModalReact, {
component: SaveLibraryPanelModal, component: SaveLibraryPanelModal,
props: { props: {
@ -289,7 +313,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
</ToolbarButton>, </ToolbarButton>,
this.props.panel.libraryPanel ? ( this.props.panel.libraryPanel ? (
<ToolbarButton <ToolbarButton
onClick={this.onSavePanel} onClick={this.onSaveLibraryPanel}
variant="primary" variant="primary"
title="Apply changes and save library panel" title="Apply changes and save library panel"
key="save-panel" key="save-panel"

View File

@ -6,17 +6,17 @@ import { getNextRefIdChar } from 'app/core/utils/query';
// Types // Types
import { import {
DataConfigSource, DataConfigSource,
DataFrameDTO,
DataLink, DataLink,
DataLinkBuiltInVars,
DataQuery, DataQuery,
DataTransformerConfig, DataTransformerConfig,
EventBus,
EventBusSrv,
FieldConfigSource, FieldConfigSource,
PanelPlugin, PanelPlugin,
ScopedVars, ScopedVars,
EventBus,
EventBusSrv,
DataFrameDTO,
urlUtil, urlUtil,
DataLinkBuiltInVars,
} from '@grafana/data'; } from '@grafana/data';
import { EDIT_PANEL_ID } from 'app/core/constants'; import { EDIT_PANEL_ID } from 'app/core/constants';
import config from 'app/core/config'; import config from 'app/core/config';
@ -36,7 +36,7 @@ import {
isStandardFieldProp, isStandardFieldProp,
restoreCustomOverrideRules, restoreCustomOverrideRules,
} from './getPanelOptionsWithDefaults'; } from './getPanelOptionsWithDefaults';
import { LibraryPanelDTO } from 'app/features/library-panels/state/api'; import { PanelModelLibraryPanel } from '../../library-panels/types';
export interface GridPos { export interface GridPos {
x: number; x: number;
@ -151,7 +151,7 @@ export class PanelModel implements DataConfigSource {
links?: DataLink[]; links?: DataLink[];
transparent: boolean; transparent: boolean;
libraryPanel?: { uid: undefined; name: string } | Pick<LibraryPanelDTO, 'uid' | 'name' | 'meta'>; libraryPanel?: { uid: undefined; name: string } | PanelModelLibraryPanel;
// non persisted // non persisted
isViewing: boolean; isViewing: boolean;

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { Icon, IconButton, ConfirmModal, Tooltip, useStyles, Card } from '@grafana/ui'; import { Icon, IconButton, ConfirmModal, Tooltip, useStyles, Card } from '@grafana/ui';
import { css } from 'emotion'; import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { LibraryPanelDTO } from '../../state/api'; import { LibraryPanelDTO } from '../../types';
export interface LibraryPanelCardProps { export interface LibraryPanelCardProps {
libraryPanel: LibraryPanelDTO; libraryPanel: LibraryPanelDTO;

View File

@ -1,10 +1,12 @@
import { Icon, Input, Button, stylesFactory, useStyles } from '@grafana/ui';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useDebounce } from 'react-use'; import { useDebounce } from 'react-use';
import { cx, css } from 'emotion'; import { css, cx } from 'emotion';
import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard'; import { Button, Icon, Input, stylesFactory, useStyles } from '@grafana/ui';
import { DateTimeInput, GrafanaTheme } from '@grafana/data'; 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 { interface LibraryPanelViewProps {
className?: string; className?: string;

View File

@ -1,14 +1,16 @@
import React, { useState } from 'react';
import { css } from 'emotion';
import pick from 'lodash/pick';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { Button, stylesFactory, useStyles } from '@grafana/ui'; import { Button, stylesFactory, useStyles } from '@grafana/ui';
import { OptionsGroup } from 'app/features/dashboard/components/PanelEditor/OptionsGroup'; import { OptionsGroup } from 'app/features/dashboard/components/PanelEditor/OptionsGroup';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { css } from 'emotion';
import React, { useState } from 'react';
import { AddLibraryPanelModal } from '../AddLibraryPanelModal/AddLibraryPanelModal'; import { AddLibraryPanelModal } from '../AddLibraryPanelModal/AddLibraryPanelModal';
import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView'; import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView';
import pick from 'lodash/pick';
import { LibraryPanelDTO } from '../../state/api';
import { PanelQueriesChangedEvent } from 'app/types/events'; import { PanelQueriesChangedEvent } from 'app/types/events';
import { LibraryPanelDTO } from '../../types';
import { toPanelModelLibraryPanel } from '../../utils';
interface Props { interface Props {
panel: PanelModel; panel: PanelModel;
@ -23,7 +25,7 @@ export const PanelLibraryOptionsGroup: React.FC<Props> = ({ panel, dashboard })
panel.restoreModel({ panel.restoreModel({
...panelInfo.model, ...panelInfo.model,
...pick(panel, 'gridPos', 'id'), ...pick(panel, 'gridPos', 'id'),
libraryPanel: pick(panelInfo, 'uid', 'name', 'meta'), libraryPanel: toPanelModelLibraryPanel(panelInfo),
}); });
// dummy change for re-render // dummy change for re-render

View File

@ -5,7 +5,8 @@ import { css } from 'emotion';
import { useAsync, useDebounce } from 'react-use'; import { useAsync, useDebounce } from 'react-use';
import { getBackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from 'app/core/services/backend_srv';
import { usePanelSave } from '../../utils/usePanelSave'; import { usePanelSave } from '../../utils/usePanelSave';
import { getLibraryPanelConnectedDashboards, PanelModelWithLibraryPanel } from '../../state/api'; import { getLibraryPanelConnectedDashboards } from '../../state/api';
import { PanelModelWithLibraryPanel } from '../../types';
interface Props { interface Props {
panel: PanelModelWithLibraryPanel; panel: PanelModelWithLibraryPanel;

View File

@ -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);
}

View File

@ -1,36 +1,5 @@
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { LibraryPanelDTO, PanelModelWithLibraryPanel } from '../types';
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<LibraryPanelDTO, 'uid' | 'name' | 'meta' | 'version'>;
}
export async function getLibraryPanels(): Promise<LibraryPanelDTO[]> { export async function getLibraryPanels(): Promise<LibraryPanelDTO[]> {
const { result } = await getBackendSrv().get(`/api/library-panels`); const { result } = await getBackendSrv().get(`/api/library-panels`);

View File

@ -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<LibraryPanelDTO, 'uid' | 'name' | 'meta' | 'version'>;
export interface PanelModelWithLibraryPanel extends PanelModel {
libraryPanel: PanelModelLibraryPanel;
}

View File

@ -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<LibraryPanelDTO> {
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<LibraryPanelDTO> {
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!);
}

View File

@ -1,52 +1,27 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import useAsyncFn from 'react-use/lib/useAsyncFn'; 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 { PanelModel } from 'app/features/dashboard/state';
import { addLibraryPanel, updateLibraryPanel } from '../state/api'; import {
createPanelLibraryErrorNotification,
const saveLibraryPanels = (panel: any, folderId: number) => { createPanelLibrarySuccessNotification,
if (!panel.libraryPanel) { saveAndRefreshLibraryPanel,
return Promise.reject(); } from '../utils';
} import { notifyApp } from 'app/core/actions';
if (panel.libraryPanel && panel.libraryPanel.uid === undefined) {
panel.libraryPanel.name = panel.title;
return addLibraryPanel(panel, folderId!);
}
return updateLibraryPanel(panel, folderId!);
};
export const usePanelSave = () => { export const usePanelSave = () => {
const dispatch = useDispatch();
const [state, saveLibraryPanel] = useAsyncFn(async (panel: PanelModel, folderId: number) => { const [state, saveLibraryPanel] = useAsyncFn(async (panel: PanelModel, folderId: number) => {
let panelSaveModel = panel.getSaveModel(); return await saveAndRefreshLibraryPanel(panel, folderId);
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;
}, []); }, []);
useEffect(() => { useEffect(() => {
if (state.error) { 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) { if (state.value) {
appEvents.emit(AppEvents.alertSuccess, ['Library panel saved']); dispatch(notifyApp(createPanelLibrarySuccessNotification('Library panel saved')));
} }
}, [state]); }, [state]);