DashGPT: Autogenerate panel title and description on scenes (#84726)

Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
Ivan Ortega Alba
2024-03-22 12:56:19 +02:00
committed by GitHub
co-authored by nmarrs
parent 016d6f2f6d
commit 521d686ca4
8 changed files with 67 additions and 48 deletions
@@ -5,8 +5,10 @@ import { VizPanel } from '@grafana/scenes';
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import * as utils from '../utils/utils';
import { PanelOptions } from './PanelOptions';
import { VizPanelManager } from './VizPanelManager';
@@ -17,6 +19,9 @@ jest.mock('react-router-dom', () => ({
}),
}));
// Needed when the panel is not part of an DashboardScene
jest.spyOn(utils, 'getDashboardSceneFor').mockReturnValue(new DashboardScene({}));
describe('PanelOptions', () => {
it('gets library panel options when the editing a library panel', async () => {
const panel = new VizPanel({
@@ -329,6 +329,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"description": "This is a simple time series graph",
"fieldConfig": {
"defaults": {
"color": {
@@ -684,6 +685,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"description": "This is a simple time series graph",
"fieldConfig": {
"defaults": {
"color": {
@@ -115,6 +115,7 @@
}
],
"title": "Simple time series graph ",
"description": "This is a simple time series graph",
"type": "timeseries"
},
{
@@ -214,6 +214,7 @@ export function vizPanelToPanel(
id: getPanelIdForVizPanel(vizPanel),
type: vizPanel.state.pluginId,
title: vizPanel.state.title,
description: vizPanel.state.description ?? undefined,
gridPos,
options: vizPanel.state.options,
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
@@ -1,7 +1,8 @@
import React from 'react';
import { Panel } from '@grafana/schema';
import { getDashboardSrv } from '../../services/DashboardSrv';
import { PanelModel } from '../../state';
import { GenAIButton } from './GenAIButton';
import { EventTrackingSrc } from './tracking';
@@ -9,7 +10,7 @@ import { Message, Role, getFilteredPanelString } from './utils';
interface GenAIPanelDescriptionButtonProps {
onGenerate: (description: string) => void;
panel: PanelModel;
panel: Panel;
}
const PANEL_DESCRIPTION_CHAR_LIMIT = 200;
@@ -24,11 +25,9 @@ const DESCRIPTION_GENERATION_STANDARD_PROMPT =
`The description should be, at most, ${PANEL_DESCRIPTION_CHAR_LIMIT} characters.`;
export const GenAIPanelDescriptionButton = ({ onGenerate, panel }: GenAIPanelDescriptionButtonProps) => {
const messages = React.useMemo(() => getMessages(panel), [panel]);
return (
<GenAIButton
messages={messages}
messages={() => getMessages(panel)}
onGenerate={onGenerate}
eventTrackingSrc={EventTrackingSrc.panelDescription}
toggleTipTitle={'Improve your panel description'}
@@ -36,7 +35,7 @@ export const GenAIPanelDescriptionButton = ({ onGenerate, panel }: GenAIPanelDes
);
};
function getMessages(panel: PanelModel): Message[] {
function getMessages(panel: Panel): Message[] {
const dashboard = getDashboardSrv().getCurrent()!;
const panelString = getFilteredPanelString(panel);
@@ -1,7 +1,6 @@
import React from 'react';
import { getDashboardSrv } from '../../services/DashboardSrv';
import { PanelModel } from '../../state';
import { Dashboard, Panel } from '@grafana/schema';
import { GenAIButton } from './GenAIButton';
import { EventTrackingSrc } from './tracking';
@@ -9,7 +8,8 @@ import { Message, Role, getFilteredPanelString } from './utils';
interface GenAIPanelTitleButtonProps {
onGenerate: (title: string) => void;
panel: PanelModel;
panel: Panel;
dashboard: Dashboard;
}
const PANEL_TITLE_CHAR_LIMIT = 50;
@@ -19,12 +19,10 @@ const TITLE_GENERATION_STANDARD_PROMPT =
'Your goal is to write short, descriptive, and concise panel title.' +
`The title should be shorter than ${PANEL_TITLE_CHAR_LIMIT} characters.`;
export const GenAIPanelTitleButton = ({ onGenerate, panel }: GenAIPanelTitleButtonProps) => {
const messages = React.useMemo(() => getMessages(panel), [panel]);
export const GenAIPanelTitleButton = ({ onGenerate, panel, dashboard }: GenAIPanelTitleButtonProps) => {
return (
<GenAIButton
messages={messages}
messages={() => getMessages(panel, dashboard)}
onGenerate={onGenerate}
eventTrackingSrc={EventTrackingSrc.panelTitle}
toggleTipTitle={'Improve your panel title'}
@@ -32,8 +30,7 @@ export const GenAIPanelTitleButton = ({ onGenerate, panel }: GenAIPanelTitleButt
);
};
function getMessages(panel: PanelModel): Message[] {
const dashboard = getDashboardSrv().getCurrent()!;
function getMessages(panel: Panel, dashboard: Dashboard): Message[] {
const panelString = getFilteredPanelString(panel);
return [
@@ -1,4 +1,7 @@
import { pick } from 'lodash';
import { llms } from '@grafana/experimental';
import { Panel } from '@grafana/schema';
import { DashboardModel, PanelModel } from '../../state';
@@ -120,27 +123,16 @@ export function getDashboardPanelPrompt(dashboard: DashboardModel): string {
return panelPrompt;
}
export function getFilteredPanelString(panel: PanelModel): string {
const panelObj = panel.getSaveModel();
export function getFilteredPanelString(panel: Panel): string {
const keysToKeep: Array<keyof Panel> = ['datasource', 'title', 'description', 'targets', 'type'];
const keysToKeep = new Set([
'id',
'datasource',
'title',
'description',
'targets',
'thresholds',
'type',
'xaxis',
'yaxes',
]);
const filteredPanel: Partial<Panel> = {
...pick(panel, keysToKeep),
options: pick(panel.options, [
// For text panels, the content property helps generate the panel metadata
'content',
]),
};
const panelObjFiltered = Object.keys(panelObj).reduce((obj: { [key: string]: unknown }, key) => {
if (keysToKeep.has(key)) {
obj[key] = panelObj[key];
}
return obj;
}, {});
return JSON.stringify(panelObjFiltered, null, 2);
return JSON.stringify(filteredPanel, null, 2);
}
@@ -7,7 +7,12 @@ import { VizPanel } from '@grafana/scenes';
import { DataLinksInlineEditor, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui';
import { VizPanelManager, VizPanelManagerState } from 'app/features/dashboard-scene/panel-edit/VizPanelManager';
import { VizPanelLinks } from 'app/features/dashboard-scene/scene/PanelLinks';
import {
transformSceneToSaveModel,
vizPanelToPanel,
} from 'app/features/dashboard-scene/serialization/transformSceneToSaveModel';
import { dashboardSceneGraph } from 'app/features/dashboard-scene/utils/dashboardSceneGraph';
import { getDashboardSceneFor } from 'app/features/dashboard-scene/utils/utils';
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
import { GenAIPanelDescriptionButton } from '../GenAI/GenAIPanelDescriptionButton';
@@ -19,7 +24,7 @@ import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
import { OptionPaneRenderProps } from './types';
export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPaneCategoryDescriptor {
const { panel, onPanelConfigChange } = props;
const { dashboard, panel, onPanelConfigChange } = props;
const descriptor = new OptionsPaneCategoryDescriptor({
title: 'Panel options',
id: 'Panel options',
@@ -58,7 +63,13 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
/>
);
},
addon: config.featureToggles.dashgpt && <GenAIPanelTitleButton onGenerate={setPanelTitle} panel={panel} />,
addon: config.featureToggles.dashgpt && (
<GenAIPanelTitleButton
onGenerate={setPanelTitle}
panel={panel.getSaveModel()}
dashboard={dashboard.getSaveModelClone()}
/>
),
})
)
.addItem(
@@ -77,7 +88,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
);
},
addon: config.featureToggles.dashgpt && (
<GenAIPanelDescriptionButton onGenerate={setPanelDescription} panel={panel} />
<GenAIPanelDescriptionButton onGenerate={setPanelDescription} panel={panel.getSaveModel()} />
),
})
)
@@ -194,6 +205,7 @@ export function getPanelFrameCategory2(
const panelLinksObject = dashboardSceneGraph.getPanelLinks(panel);
const links = panelLinksObject?.state.rawLinks ?? [];
const dashboard = getDashboardSceneFor(panel);
return descriptor
.addItem(
@@ -202,34 +214,44 @@ export function getPanelFrameCategory2(
value: panel.state.title,
popularRank: 1,
render: function renderTitle() {
const { title } = panel.useState();
return (
<Input
id="PanelFrameTitle"
defaultValue={panel.state.title}
onBlur={(e) => panel.setState({ title: e.currentTarget.value })}
value={title}
onChange={(e) => panel.setState({ title: e.currentTarget.value })}
/>
);
},
// addon: config.featureToggles.dashgpt && <GenAIPanelTitleButton onGenerate={setPanelTitle} panel={panel} />,
addon: config.featureToggles.dashgpt && (
<GenAIPanelTitleButton
onGenerate={(title) => panel.setState({ title })}
panel={vizPanelToPanel(panel)}
dashboard={transformSceneToSaveModel(dashboard)}
/>
),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Description',
description: panel.state.description,
value: panel.state.description,
render: function renderDescription() {
const { description } = panel.useState();
return (
<TextArea
id="description-text-area"
defaultValue={panel.state.description}
onBlur={(e) => panel.setState({ description: e.currentTarget.value })}
value={description}
onChange={(e) => panel.setState({ description: e.currentTarget.value })}
/>
);
},
// addon: config.featureToggles.dashgpt && (
// <GenAIPanelDescriptionButton onGenerate={setPanelDescription} panel={panel} />
// ),
addon: config.featureToggles.dashgpt && (
<GenAIPanelDescriptionButton
onGenerate={(description) => panel.setState({ description })}
panel={vizPanelToPanel(panel)}
/>
),
})
)
.addItem(