DashGPT: Disable GenAI title and description buttons for empty dashboards (#90341)

* Disable genai title and description buttons when dashboard doesn't have at least one panel with a title or description

* Fix test

* Additional tooltip tests

* address pr feedback

* Fix test: Use const for panel title

---------

Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
baldm0mma/backport_validation
Haris Rozajac 11 months ago committed by GitHub
parent 9bc68562d4
commit e0416cc0f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 42
      public/app/features/dashboard/components/GenAI/GenAIButton.test.tsx
  2. 21
      public/app/features/dashboard/components/GenAI/GenAIButton.tsx
  3. 23
      public/app/features/dashboard/components/GenAI/GenAIDashDescriptionButton.tsx
  4. 20
      public/app/features/dashboard/components/GenAI/GenAIDashTitleButton.tsx
  5. 47
      public/app/features/dashboard/components/GenAI/utils.test.ts
  6. 26
      public/app/features/dashboard/components/GenAI/utils.ts
  7. 6
      public/app/features/dashboard/utils/dashboard.ts

@ -133,6 +133,23 @@ describe('GenAIButton', () => {
await waitFor(() => expect(onClick).toHaveBeenCalledTimes(1));
});
it('should display the tooltip if provided', async () => {
const { getByRole, getByTestId } = setup({
tooltip: 'This is a tooltip',
onGenerate,
messages: [],
eventTrackingSrc,
});
// Wait for the check to be completed
const button = getByRole('button');
await userEvent.hover(button);
const tooltip = await waitFor(() => getByTestId(selectors.components.Tooltip.container));
expect(tooltip).toBeVisible();
expect(tooltip).toHaveTextContent('This is a tooltip');
});
});
describe('when it is generating data', () => {
@ -288,7 +305,30 @@ describe('GenAIButton', () => {
await userEvent.hover(tooltip);
expect(tooltip).toBeVisible();
expect(tooltip).toHaveTextContent(
'Failed to generate content using OpenAI. Please try again or if the problem persist, contact your organization admin.'
'Failed to generate content using OpenAI. Please try again or if the problem persists, contact your organization admin.'
);
});
it('error message should overwrite the tooltip content passed in tooltip prop', async () => {
const { getByRole, getByTestId } = setup({
tooltip: 'This is a tooltip',
onGenerate,
messages: [],
eventTrackingSrc,
});
// Wait for the check to be completed
const button = getByRole('button');
await userEvent.hover(button);
const tooltip = await waitFor(() => getByTestId(selectors.components.Tooltip.container));
expect(tooltip).toBeVisible();
// The tooltip keeps interactive to be able to click the link
await userEvent.hover(tooltip);
expect(tooltip).toBeVisible();
expect(tooltip).toHaveTextContent(
'Failed to generate content using OpenAI. Please try again or if the problem persists, contact your organization admin.'
);
});

@ -28,6 +28,13 @@ export interface GenAIButtonProps {
eventTrackingSrc: EventTrackingSrc;
// Whether the button should be disabled
disabled?: boolean;
/*
Tooltip to show when hovering over the button
Tooltip will be shown only before the improvement stage.
i.e once the button title changes to "Improve", the tooltip will not be shown because
toggletip will be enabled.
*/
tooltip?: string;
}
export const STOP_GENERATION_TEXT = 'Stop generating';
@ -41,6 +48,7 @@ export const GenAIButton = ({
temperature = 1,
eventTrackingSrc,
disabled,
tooltip,
}: GenAIButtonProps) => {
const styles = useStyles2(getStyles);
@ -55,6 +63,11 @@ export const GenAIButton = ({
const isButtonDisabled = disabled || (value && !value.enabled && !error);
const reportInteraction = (item: AutoGenerateItem) => reportAutoGenerateInteraction(eventTrackingSrc, item);
const showTooltip = error || tooltip ? undefined : false;
const tooltipContent = error
? 'Failed to generate content using OpenAI. Please try again or if the problem persists, contact your organization admin.'
: tooltip || '';
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (streamStatus === StreamStatus.GENERATING) {
setStopGeneration(true);
@ -192,13 +205,7 @@ export const GenAIButton = ({
<div className={styles.wrapper}>
{isGenerating && <Spinner size="sm" className={styles.spinner} />}
{isFirstHistoryEntry ? (
<Tooltip
show={error ? undefined : false}
interactive
content={
'Failed to generate content using OpenAI. Please try again or if the problem persist, contact your organization admin.'
}
>
<Tooltip show={showTooltip} interactive content={tooltipContent}>
{button}
</Tooltip>
) : (

@ -1,8 +1,15 @@
import { getDashboardSrv } from '../../services/DashboardSrv';
import { DashboardModel } from '../../state';
import { GenAIButton } from './GenAIButton';
import { EventTrackingSrc } from './tracking';
import { getDashboardPanelPrompt, Message, Role } from './utils';
import {
DASHBOARD_NEED_PANEL_TITLES_AND_DESCRIPTIONS_MESSAGE,
getDashboardPanelPrompt,
getPanelStrings,
Message,
Role,
} from './utils';
interface GenAIDashDescriptionButtonProps {
onGenerate: (description: string) => void;
@ -22,27 +29,29 @@ const DESCRIPTION_GENERATION_STANDARD_PROMPT =
'Respond with only the description of the dashboard.';
export const GenAIDashDescriptionButton = ({ onGenerate }: GenAIDashDescriptionButtonProps) => {
const dashboard = getDashboardSrv().getCurrent()!;
const panelStrings = getPanelStrings(dashboard);
return (
<GenAIButton
messages={getMessages}
messages={getMessages(dashboard)}
onGenerate={onGenerate}
eventTrackingSrc={EventTrackingSrc.dashboardDescription}
toggleTipTitle={'Improve your dashboard description'}
disabled={panelStrings.length === 0}
tooltip={panelStrings.length === 0 ? DASHBOARD_NEED_PANEL_TITLES_AND_DESCRIPTIONS_MESSAGE : undefined}
/>
);
};
function getMessages(): Message[] {
const dashboard = getDashboardSrv().getCurrent()!;
const panelPrompt = getDashboardPanelPrompt(dashboard);
function getMessages(dashboard: DashboardModel): Message[] {
return [
{
content: DESCRIPTION_GENERATION_STANDARD_PROMPT,
role: Role.system,
},
{
content: `The title of the dashboard is "${dashboard.title}"\n` + `${panelPrompt}`,
content: `The title of the dashboard is "${dashboard.title}"\n` + `${getDashboardPanelPrompt(dashboard)}`,
role: Role.user,
},
];

@ -1,8 +1,15 @@
import { getDashboardSrv } from '../../services/DashboardSrv';
import { DashboardModel } from '../../state';
import { GenAIButton } from './GenAIButton';
import { EventTrackingSrc } from './tracking';
import { getDashboardPanelPrompt, Message, Role } from './utils';
import {
DASHBOARD_NEED_PANEL_TITLES_AND_DESCRIPTIONS_MESSAGE,
getDashboardPanelPrompt,
getPanelStrings,
Message,
Role,
} from './utils';
interface GenAIDashTitleButtonProps {
onGenerate: (description: string) => void;
@ -22,19 +29,22 @@ const TITLE_GENERATION_STANDARD_PROMPT =
'Respond with only the title of the dashboard.';
export const GenAIDashTitleButton = ({ onGenerate }: GenAIDashTitleButtonProps) => {
const dashboard = getDashboardSrv().getCurrent()!;
const panelStrings = getPanelStrings(dashboard);
return (
<GenAIButton
messages={getMessages}
messages={getMessages(dashboard)}
onGenerate={onGenerate}
eventTrackingSrc={EventTrackingSrc.dashboardTitle}
toggleTipTitle={'Improve your dashboard title'}
disabled={panelStrings.length === 0}
tooltip={panelStrings.length === 0 ? DASHBOARD_NEED_PANEL_TITLES_AND_DESCRIPTIONS_MESSAGE : undefined}
/>
);
};
function getMessages(): Message[] {
const dashboard = getDashboardSrv().getCurrent()!;
function getMessages(dashboard: DashboardModel): Message[] {
return [
{
content: TITLE_GENERATION_STANDARD_PROMPT,

@ -2,8 +2,9 @@ import { llms } from '@grafana/experimental';
import { DASHBOARD_SCHEMA_VERSION } from '../../state/DashboardMigrator';
import { createDashboardModelFixture, createPanelSaveModel } from '../../state/__fixtures__/dashboardFixtures';
import { NEW_PANEL_TITLE } from '../../utils/dashboard';
import { getDashboardChanges, isLLMPluginEnabled, sanitizeReply } from './utils';
import { getDashboardChanges, getPanelStrings, isLLMPluginEnabled, sanitizeReply } from './utils';
// Mock the llms.openai module
jest.mock('@grafana/experimental', () => ({
@ -134,3 +135,47 @@ describe('sanitizeReply', () => {
expect(sanitizeReply('')).toBe('');
});
});
describe('getPanelStrings', () => {
function dashboardSetup(items: Array<{ title: string; description: string }>) {
return createDashboardModelFixture({
panels: items.map((item) => createPanelSaveModel(item)),
});
}
it('should return an empty array if all panels dont have title or descriptions', () => {
const dashboard = dashboardSetup([{ title: '', description: '' }]);
expect(getPanelStrings(dashboard)).toEqual([]);
});
it('should return an empty array if all panels have no description and panels that have title are titled "Panel title', () => {
const dashboard = dashboardSetup([{ title: NEW_PANEL_TITLE, description: '' }]);
expect(getPanelStrings(dashboard)).toEqual([]);
});
it('should return an array of panels if a panel has a title or description', () => {
const dashboard = dashboardSetup([
{ title: 'Graph panel', description: '' },
{ title: '', description: 'Logs' },
]);
expect(getPanelStrings(dashboard)).toEqual([
'- Panel 0\n- Title: Graph panel',
'- Panel 1\n- Title: \n- Description: Logs',
]);
});
it('returns an array with title and description if both are present', () => {
const dashboard = dashboardSetup([
{ title: 'Graph panel', description: 'Logs' },
{ title: 'Table panel', description: 'Metrics' },
]);
expect(getPanelStrings(dashboard)).toEqual([
'- Panel 0\n- Title: Graph panel\n- Description: Logs',
'- Panel 1\n- Title: Table panel\n- Description: Metrics',
]);
});
});

@ -5,6 +5,7 @@ import { config } from '@grafana/runtime';
import { Panel } from '@grafana/schema';
import { DashboardModel, PanelModel } from '../../state';
import { NEW_PANEL_TITLE } from '../../utils/dashboard';
import { getDashboardStringDiff } from './jsonDiffText';
@ -111,11 +112,7 @@ export const getFeedbackMessage = (previousResponse: string, feedback: string |
* @returns String for inclusion in prompts stating what the dashboard's panels are
*/
export function getDashboardPanelPrompt(dashboard: DashboardModel): string {
const getPanelString = (panel: PanelModel, idx: number) =>
`- Panel ${idx}
- Title: ${panel.title}${panel.description ? `\n- Description: ${panel.description}` : ''}`;
const panelStrings: string[] = dashboard.panels.map(getPanelString);
const panelStrings: string[] = getPanelStrings(dashboard);
let panelPrompt: string;
if (panelStrings.length <= 10) {
@ -158,3 +155,22 @@ export function getFilteredPanelString(panel: Panel): string {
return JSON.stringify(filteredPanel, null, 2);
}
export const DASHBOARD_NEED_PANEL_TITLES_AND_DESCRIPTIONS_MESSAGE =
'To generate this content your dashboard must contain at least one panel with a valid title or description.';
export function getPanelStrings(dashboard: DashboardModel): string[] {
const panelStrings = dashboard.panels
.filter(
(panel) =>
(panel.title.length > 0 && panel.title !== NEW_PANEL_TITLE) ||
(panel.description && panel.description.length > 0)
)
.map(getPanelString);
return panelStrings;
}
const getPanelString = (panel: PanelModel, idx: number) =>
`- Panel ${idx}
- Title: ${panel.title}${panel.description ? `\n- Description: ${panel.description}` : ''}`;

@ -8,10 +8,12 @@ import store from 'app/core/store';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { calculateNewPanelGridPos } from 'app/features/dashboard/utils/panel';
export const NEW_PANEL_TITLE = 'Panel Title';
export function onCreateNewPanel(dashboard: DashboardModel, datasource?: string): number | undefined {
const newPanel: Partial<PanelModel> = {
type: 'timeseries',
title: 'Panel Title',
title: NEW_PANEL_TITLE,
gridPos: calculateNewPanelGridPos(dashboard),
datasource: datasource ? { uid: datasource } : null,
isNew: true,
@ -68,7 +70,7 @@ export function onPasteCopiedPanel(dashboard: DashboardModel, panelPluginInfo?:
const newPanel = {
type: panelPluginInfo.id,
title: 'Panel Title',
title: NEW_PANEL_TITLE,
gridPos: {
x: gridPos.x,
y: gridPos.y,

Loading…
Cancel
Save