LibraryPanels: Separates name from panel title (#38707)

* LibraryPanels: Separates name from panel title

* WIP

* Chore: fixes update for duplicate lib panels

* Chore: reverts implementation

* Chore: show library options only for library panels

* Chore: ui fixes after PR comments

* Chore: fixes issue when creating library panels
pull/38785/head
Hugo Häggmark 4 years ago committed by GitHub
parent 6cfb640a0b
commit 55e20bbf04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      pkg/services/libraryelements/database.go
  2. 4
      pkg/services/libraryelements/libraryelements_create_test.go
  3. 16
      pkg/services/libraryelements/libraryelements_get_all_test.go
  4. 16
      pkg/services/libraryelements/libraryelements_patch_test.go
  5. 22
      public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx
  6. 48
      public/app/features/dashboard/components/PanelEditor/getLibraryPanelOptions.tsx
  7. 15
      public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx
  8. 22
      public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx
  9. 15
      public/app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo.tsx
  10. 15
      public/app/features/library-panels/state/api.ts
  11. 2
      public/app/features/library-panels/utils.ts

@ -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 {

@ -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,

@ -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,

@ -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",
}

@ -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> = (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> = (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> = (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) {

@ -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 (
<Input
id="LibraryPanelFrameName"
defaultValue={panel.libraryPanel.name}
onBlur={(e) =>
onPanelConfigChange('libraryPanel', { ...panel.libraryPanel, name: e.currentTarget.value })
}
/>
);
},
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Information',
render: function renderLibraryPanelInformation() {
return <LibraryPanelInformation panel={panel} formatDate={dashboard.formatDate} />;
},
})
);
}
return descriptor;
}

@ -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 <LibraryPanelInformation panel={panel} formatDate={dashboard.formatDate} />;
},
})
);
}
return descriptor
.addItem(
new OptionsPaneItemDescriptor({

@ -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' : ''}
>
<Input name="name" value={panelTitle} onChange={(e) => setPanelTitle(e.currentTarget.value)} />
<Input name="name" value={panelName} onChange={(e) => setPanelName(e.currentTarget.value)} />
</Field>
<Field label="Save in folder" description="Library panel permissions are derived from the folder permissions">
<FolderPicker onChange={({ id }) => setFolderId(id)} initialFolderId={initialFolderId} />

@ -18,11 +18,12 @@ export const LibraryPanelInformation: React.FC<Props> = ({ panel, formatDate })
}
return (
<>
<p className={styles.libraryPanelInfo}>
<div className={styles.info}>
<div className={styles.libraryPanelInfo}>
{`Used on ${panel.libraryPanel.meta.connectedDashboards} `}
{panel.libraryPanel.meta.connectedDashboards === 1 ? 'dashboard' : 'dashboards'}
<br />
</div>
<div className={styles.libraryPanelInfo}>
Last edited on {formatDate?.(panel.libraryPanel.meta.updated, 'L') ?? panel.libraryPanel.meta.updated} by
{panel.libraryPanel.meta.updatedBy.avatarUrl && (
<img
@ -34,17 +35,19 @@ export const LibraryPanelInformation: React.FC<Props> = ({ panel, formatDate })
/>
)}
{panel.libraryPanel.meta.updatedBy.name}
</p>
</>
</div>
</div>
);
};
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%;

@ -59,7 +59,7 @@ export async function addLibraryPanel(
): Promise<LibraryElementDTO> {
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<LibraryElementDTO> {
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;
}

@ -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<Library
}
if (panel.libraryPanel && panel.libraryPanel.uid === undefined) {
panel.libraryPanel.name = panel.title;
return addLibraryPanel(panel, folderId!);
}

Loading…
Cancel
Save