diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 9a12d6c5780..3fa51f77d85 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -41,9 +41,7 @@ func syncFieldsWithModel(libraryElement *LibraryElement) error { return err } - if models.LibraryElementKind(libraryElement.Kind) == models.PanelElement { - model["title"] = libraryElement.Name - } else if models.LibraryElementKind(libraryElement.Kind) == models.VariableElement { + if models.LibraryElementKind(libraryElement.Kind) == models.VariableElement { model["name"] = libraryElement.Name } if model["type"] != nil { diff --git a/pkg/services/libraryelements/libraryelements_create_test.go b/pkg/services/libraryelements/libraryelements_create_test.go index e200c471be0..080905e5b25 100644 --- a/pkg/services/libraryelements/libraryelements_create_test.go +++ b/pkg/services/libraryelements/libraryelements_create_test.go @@ -59,7 +59,7 @@ func TestCreateLibraryElement(t *testing.T) { } }) - testScenario(t, "When an admin tries to create a library panel where name and panel title differ, it should update panel title", + testScenario(t, "When an admin tries to create a library panel where name and panel title differ, it should not update panel title", func(t *testing.T, sc scenarioContext) { command := getCreatePanelCommand(1, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) @@ -78,7 +78,7 @@ func TestCreateLibraryElement(t *testing.T) { "datasource": "${DS_GDEV-TESTDATA}", "description": "A description", "id": float64(1), - "title": "Library Panel Name", + "title": "Text - Library Panel", "type": "text", }, Version: 1, diff --git a/pkg/services/libraryelements/libraryelements_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go index 2358b18a21c..6f4d80c94f2 100644 --- a/pkg/services/libraryelements/libraryelements_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -229,7 +229,7 @@ func TestGetAllLibraryElements(t *testing.T) { "datasource": "${DS_GDEV-TESTDATA}", "description": "A description", "id": float64(1), - "title": "Text - Library Panel2", + "title": "Text - Library Panel", "type": "text", }, Version: 1, @@ -293,7 +293,7 @@ func TestGetAllLibraryElements(t *testing.T) { "datasource": "${DS_GDEV-TESTDATA}", "description": "A description", "id": float64(1), - "title": "Text - Library Panel2", + "title": "Text - Library Panel", "type": "text", }, Version: 1, @@ -549,7 +549,7 @@ func TestGetAllLibraryElements(t *testing.T) { "datasource": "${DS_GDEV-TESTDATA}", "description": "A description", "id": float64(1), - "title": "Text - Library Panel2", + "title": "Text - Library Panel", "type": "text", }, Version: 1, @@ -679,7 +679,7 @@ func TestGetAllLibraryElements(t *testing.T) { "datasource": "${DS_GDEV-TESTDATA}", "description": "A description", "id": float64(1), - "title": "Text - Library Panel2", + "title": "Text - Library Panel", "type": "text", }, Version: 1, @@ -743,7 +743,7 @@ func TestGetAllLibraryElements(t *testing.T) { "datasource": "${DS_GDEV-TESTDATA}", "description": "A description", "id": float64(1), - "title": "Text - Library Panel2", + "title": "Text - Library Panel", "type": "text", }, Version: 1, @@ -872,7 +872,7 @@ func TestGetAllLibraryElements(t *testing.T) { "datasource": "${DS_GDEV-TESTDATA}", "description": "A description", "id": float64(1), - "title": "Text - Library Panel2", + "title": "Text - Library Panel", "type": "text", }, Version: 1, @@ -1018,7 +1018,7 @@ func TestGetAllLibraryElements(t *testing.T) { "datasource": "${DS_GDEV-TESTDATA}", "description": "A Library Panel", "id": float64(1), - "title": "Some Other", + "title": "Text - Library Panel", "type": "text", }, Version: 1, @@ -1119,7 +1119,7 @@ func TestGetAllLibraryElements(t *testing.T) { "datasource": "${DS_GDEV-TESTDATA}", "description": "A description", "id": float64(1), - "title": "Text - Library Panel2", + "title": "Text - Library Panel", "type": "text", }, Version: 1, diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index 2bda2b43019..4b2c3d58eda 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -54,7 +54,7 @@ func TestPatchLibraryElement(t *testing.T) { "datasource": "${DS_GDEV-TESTDATA}", "description": "An updated description", "id": float64(1), - "title": "Panel - New name", + "title": "Model - New name", "type": "graph", }, Version: 2, @@ -101,7 +101,7 @@ func TestPatchLibraryElement(t *testing.T) { } }) - scenarioWithPanel(t, "When an admin tries to patch a library panel with name only, it should change name successfully, sync title and return correct result", + scenarioWithPanel(t, "When an admin tries to patch a library panel with name only, it should change name successfully and return correct result", func(t *testing.T, sc scenarioContext) { cmd := patchLibraryElementCommand{ FolderID: -1, @@ -115,14 +115,14 @@ func TestPatchLibraryElement(t *testing.T) { sc.initialResult.Result.Name = "New Name" sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar - sc.initialResult.Result.Model["title"] = "New Name" + sc.initialResult.Result.Model["title"] = "Text - Library Panel" sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } }) - scenarioWithPanel(t, "When an admin tries to patch a library panel with model only, it should change model successfully, sync name, type and description fields and return correct result", + scenarioWithPanel(t, "When an admin tries to patch a library panel with model only, it should change model successfully, sync type and description fields and return correct result", func(t *testing.T, sc scenarioContext) { cmd := patchLibraryElementCommand{ FolderID: -1, @@ -136,7 +136,7 @@ func TestPatchLibraryElement(t *testing.T) { sc.initialResult.Result.Type = "graph" sc.initialResult.Result.Description = "New description" sc.initialResult.Result.Model = map[string]interface{}{ - "title": "Text - Library Panel", + "title": "New Model Title", "name": "New Model Name", "type": "graph", "description": "New description", @@ -149,7 +149,7 @@ func TestPatchLibraryElement(t *testing.T) { } }) - scenarioWithPanel(t, "When an admin tries to patch a library panel with model.description only, it should change model successfully, sync name, type and description fields and return correct result", + scenarioWithPanel(t, "When an admin tries to patch a library panel with model.description only, it should change model successfully, sync type and description fields and return correct result", func(t *testing.T, sc scenarioContext) { cmd := patchLibraryElementCommand{ FolderID: -1, @@ -163,7 +163,6 @@ func TestPatchLibraryElement(t *testing.T) { sc.initialResult.Result.Type = "text" sc.initialResult.Result.Description = "New description" sc.initialResult.Result.Model = map[string]interface{}{ - "title": "Text - Library Panel", "type": "text", "description": "New description", } @@ -175,7 +174,7 @@ func TestPatchLibraryElement(t *testing.T) { } }) - scenarioWithPanel(t, "When an admin tries to patch a library panel with model.type only, it should change model successfully, sync name, type and description fields and return correct result", + scenarioWithPanel(t, "When an admin tries to patch a library panel with model.type only, it should change model successfully, sync type and description fields and return correct result", func(t *testing.T, sc scenarioContext) { cmd := patchLibraryElementCommand{ FolderID: -1, @@ -189,7 +188,6 @@ func TestPatchLibraryElement(t *testing.T) { sc.initialResult.Result.Type = "graph" sc.initialResult.Result.Description = "A description" sc.initialResult.Result.Model = map[string]interface{}{ - "title": "Text - Library Panel", "type": "graph", "description": "A description", } diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx index 4a44b352900..c191748feb3 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx @@ -12,6 +12,9 @@ import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor'; import { OptionSearchEngine } from './state/OptionSearchEngine'; import { AngularPanelOptions } from './AngularPanelOptions'; import { getRecentOptions } from './state/getRecentOptions'; +import { isPanelModelLibraryPanel } from '../../../library-panels/guard'; +import { getLibraryPanelOptionsCategory } from './getLibraryPanelOptions'; + interface Props { plugin: PanelPlugin; panel: PanelModel; @@ -28,8 +31,13 @@ export const OptionsPaneOptions: React.FC = (props) => { const [listMode, setListMode] = useState(OptionFilter.All); const styles = useStyles2(getStyles); - const [panelFrameOptions, vizOptions, justOverrides] = useMemo( - () => [getPanelFrameCategory(props), getVizualizationOptions(props), getFieldOverrideCategories(props)], + const [panelFrameOptions, vizOptions, justOverrides, libraryPanelOptions] = useMemo( + () => [ + getPanelFrameCategory(props), + getVizualizationOptions(props), + getFieldOverrideCategories(props), + getLibraryPanelOptionsCategory(props), + ], // eslint-disable-next-line react-hooks/exhaustive-deps [panel.configRev, props.data] @@ -38,7 +46,9 @@ export const OptionsPaneOptions: React.FC = (props) => { const mainBoxElements: React.ReactNode[] = []; const isSearching = searchQuery.length > 0; const optionRadioFilters = useMemo(getOptionRadioFilters, []); - const allOptions = [panelFrameOptions, ...vizOptions]; + const allOptions = isPanelModelLibraryPanel(panel) + ? [libraryPanelOptions, panelFrameOptions, ...vizOptions] + : [panelFrameOptions, ...vizOptions]; if (isSearching) { mainBoxElements.push(renderSearchHits(allOptions, justOverrides, searchQuery)); @@ -54,7 +64,11 @@ export const OptionsPaneOptions: React.FC = (props) => { } else { switch (listMode) { case OptionFilter.All: - // Panel frame options first + if (isPanelModelLibraryPanel(panel)) { + // Library Panel options first + mainBoxElements.push(libraryPanelOptions.render()); + } + // Panel frame options second mainBoxElements.push(panelFrameOptions.render()); // If angular add those options next if (props.plugin.angularPanelCtrl) { diff --git a/public/app/features/dashboard/components/PanelEditor/getLibraryPanelOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getLibraryPanelOptions.tsx new file mode 100644 index 00000000000..d92669f3f72 --- /dev/null +++ b/public/app/features/dashboard/components/PanelEditor/getLibraryPanelOptions.tsx @@ -0,0 +1,48 @@ +import { Input } from '@grafana/ui'; +import React from 'react'; +import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor'; +import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor'; +import { OptionPaneRenderProps } from './types'; +import { isPanelModelLibraryPanel } from '../../../library-panels/guard'; +import { LibraryPanelInformation } from 'app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo'; + +export function getLibraryPanelOptionsCategory(props: OptionPaneRenderProps): OptionsPaneCategoryDescriptor { + const { panel, onPanelConfigChange, dashboard } = props; + const descriptor = new OptionsPaneCategoryDescriptor({ + title: 'Library panel options', + id: 'Library panel options', + isOpenDefault: true, + }); + + if (isPanelModelLibraryPanel(panel)) { + descriptor + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Name', + value: panel.libraryPanel.name, + popularRank: 1, + render: function renderName() { + return ( + + onPanelConfigChange('libraryPanel', { ...panel.libraryPanel, name: e.currentTarget.value }) + } + /> + ); + }, + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Information', + render: function renderLibraryPanelInformation() { + return ; + }, + }) + ); + } + + return descriptor; +} diff --git a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx index 375a716779e..116cbbfb481 100644 --- a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx @@ -5,28 +5,15 @@ import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect'; import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor'; import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor'; import { OptionPaneRenderProps } from './types'; -import { isPanelModelLibraryPanel } from '../../../library-panels/guard'; -import { LibraryPanelInformation } from 'app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo'; export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPaneCategoryDescriptor { - const { panel, onPanelConfigChange, dashboard } = props; + const { panel, onPanelConfigChange } = props; const descriptor = new OptionsPaneCategoryDescriptor({ title: 'Panel options', id: 'Panel options', isOpenDefault: true, }); - if (isPanelModelLibraryPanel(panel)) { - descriptor.addItem( - new OptionsPaneItemDescriptor({ - title: 'Library panel information', - render: function renderLibraryPanelInformation() { - return ; - }, - }) - ); - } - return descriptor .addItem( new OptionsPaneItemDescriptor({ diff --git a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx index a2d041be3c2..06aa19ce23a 100644 --- a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx @@ -14,35 +14,35 @@ interface AddLibraryPanelContentsProps { export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: AddLibraryPanelContentsProps) => { const [folderId, setFolderId] = useState(initialFolderId); - const [panelTitle, setPanelTitle] = useState(panel.title); - const [debouncedPanelTitle, setDebouncedPanelTitle] = useState(panel.title); + const [panelName, setPanelName] = useState(panel.title); + const [debouncedPanelName, setDebouncedPanelName] = useState(panel.title); const [waiting, setWaiting] = useState(false); - useEffect(() => setWaiting(true), [panelTitle]); - useDebounce(() => setDebouncedPanelTitle(panelTitle), 350, [panelTitle]); + useEffect(() => setWaiting(true), [panelName]); + useDebounce(() => setDebouncedPanelName(panelName), 350, [panelName]); const { saveLibraryPanel } = usePanelSave(); const onCreate = useCallback(() => { - panel.title = panelTitle; + panel.libraryPanel = { uid: undefined, name: panelName }; saveLibraryPanel(panel, folderId!).then((res) => { if (!(res instanceof Error)) { onDismiss(); } }); - }, [panel, panelTitle, folderId, onDismiss, saveLibraryPanel]); - const isValidTitle = useAsync(async () => { + }, [panel, panelName, folderId, onDismiss, saveLibraryPanel]); + const isValidName = useAsync(async () => { try { - return !(await getLibraryPanelByName(panelTitle)).some((lp) => lp.folderId === folderId); + return !(await getLibraryPanelByName(panelName)).some((lp) => lp.folderId === folderId); } catch (err) { err.isHandled = true; return true; } finally { setWaiting(false); } - }, [debouncedPanelTitle, folderId]); + }, [debouncedPanelName, folderId]); const invalidInput = - !isValidTitle?.value && isValidTitle.value !== undefined && panelTitle === debouncedPanelTitle && !waiting; + !isValidName?.value && isValidName.value !== undefined && panelName === debouncedPanelName && !waiting; return ( <> @@ -51,7 +51,7 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A invalid={invalidInput} error={invalidInput ? 'Library panel with this name already exists' : ''} > - setPanelTitle(e.currentTarget.value)} /> + setPanelName(e.currentTarget.value)} /> setFolderId(id)} initialFolderId={initialFolderId} /> diff --git a/public/app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo.tsx b/public/app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo.tsx index c422632521e..462f92fa0d0 100644 --- a/public/app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo.tsx +++ b/public/app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo.tsx @@ -18,11 +18,12 @@ export const LibraryPanelInformation: React.FC = ({ panel, formatDate }) } return ( - <> -

+

+
{`Used on ${panel.libraryPanel.meta.connectedDashboards} `} {panel.libraryPanel.meta.connectedDashboards === 1 ? 'dashboard' : 'dashboards'} -
+
+
Last edited on {formatDate?.(panel.libraryPanel.meta.updated, 'L') ?? panel.libraryPanel.meta.updated} by {panel.libraryPanel.meta.updatedBy.avatarUrl && ( = ({ panel, formatDate }) /> )} {panel.libraryPanel.meta.updatedBy.name} -

- +
+
); }; const getStyles = (theme: GrafanaTheme) => { return { + info: css` + line-height: 1; + `, libraryPanelInfo: css` color: ${theme.colors.textSemiWeak}; font-size: ${theme.typography.size.sm}; - margin-left: ${theme.spacing.xxs}; `, userAvatar: css` border-radius: 50%; diff --git a/public/app/features/library-panels/state/api.ts b/public/app/features/library-panels/state/api.ts index 56131c07577..40d1302c6f1 100644 --- a/public/app/features/library-panels/state/api.ts +++ b/public/app/features/library-panels/state/api.ts @@ -59,7 +59,7 @@ export async function addLibraryPanel( ): Promise { const { result } = await getBackendSrv().post(`/api/library-elements`, { folderId, - name: panelSaveModel.title, + name: panelSaveModel.libraryPanel.name, model: panelSaveModel, kind: LibraryElementKind.Panel, }); @@ -70,12 +70,15 @@ export async function updateLibraryPanel( panelSaveModel: PanelModelWithLibraryPanel, folderId: number ): Promise { - const { result } = await getBackendSrv().patch(`/api/library-elements/${panelSaveModel.libraryPanel.uid}`, { + const { uid, name, version } = panelSaveModel.libraryPanel; + const kind = LibraryElementKind.Panel; + const model = panelSaveModel; + const { result } = await getBackendSrv().patch(`/api/library-elements/${uid}`, { folderId, - name: panelSaveModel.title, - model: panelSaveModel, - version: panelSaveModel.libraryPanel.version, - kind: LibraryElementKind.Panel, + name, + model, + version, + kind, }); return result; } diff --git a/public/app/features/library-panels/utils.ts b/public/app/features/library-panels/utils.ts index c412b749a24..b7602f0581a 100644 --- a/public/app/features/library-panels/utils.ts +++ b/public/app/features/library-panels/utils.ts @@ -42,6 +42,7 @@ function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryElementDT ...updated.model, configRev: 0, // reset config rev, since changes have been saved libraryPanel: toPanelModelLibraryPanel(updated), + title: panel.title, }); panel.refresh(); } @@ -52,7 +53,6 @@ function saveOrUpdateLibraryPanel(panel: any, folderId: number): Promise