mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05:00
DashGPT: Autogenerate panel title and description on scenes (#84726)
Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
co-authored by
nmarrs
parent
016d6f2f6d
commit
521d686ca4
@@ -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({
|
||||
|
||||
+2
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user