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>
This commit is contained in:
Haris Rozajac 2024-07-12 11:49:12 -06:00 committed by GitHub
parent 9bc68562d4
commit e0416cc0f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 157 additions and 28 deletions

View File

@ -133,6 +133,23 @@ describe('GenAIButton', () => {
await waitFor(() => expect(onClick).toHaveBeenCalledTimes(1)); 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', () => { describe('when it is generating data', () => {
@ -288,7 +305,30 @@ describe('GenAIButton', () => {
await userEvent.hover(tooltip); await userEvent.hover(tooltip);
expect(tooltip).toBeVisible(); expect(tooltip).toBeVisible();
expect(tooltip).toHaveTextContent( 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.'
); );
}); });

View File

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

View File

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

View File

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

View File

@ -2,8 +2,9 @@ import { llms } from '@grafana/experimental';
import { DASHBOARD_SCHEMA_VERSION } from '../../state/DashboardMigrator'; import { DASHBOARD_SCHEMA_VERSION } from '../../state/DashboardMigrator';
import { createDashboardModelFixture, createPanelSaveModel } from '../../state/__fixtures__/dashboardFixtures'; 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 // Mock the llms.openai module
jest.mock('@grafana/experimental', () => ({ jest.mock('@grafana/experimental', () => ({
@ -134,3 +135,47 @@ describe('sanitizeReply', () => {
expect(sanitizeReply('')).toBe(''); 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',
]);
});
});

View File

@ -5,6 +5,7 @@ import { config } from '@grafana/runtime';
import { Panel } from '@grafana/schema'; import { Panel } from '@grafana/schema';
import { DashboardModel, PanelModel } from '../../state'; import { DashboardModel, PanelModel } from '../../state';
import { NEW_PANEL_TITLE } from '../../utils/dashboard';
import { getDashboardStringDiff } from './jsonDiffText'; 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 * @returns String for inclusion in prompts stating what the dashboard's panels are
*/ */
export function getDashboardPanelPrompt(dashboard: DashboardModel): string { export function getDashboardPanelPrompt(dashboard: DashboardModel): string {
const getPanelString = (panel: PanelModel, idx: number) => const panelStrings: string[] = getPanelStrings(dashboard);
`- Panel ${idx}
- Title: ${panel.title}${panel.description ? `\n- Description: ${panel.description}` : ''}`;
const panelStrings: string[] = dashboard.panels.map(getPanelString);
let panelPrompt: string; let panelPrompt: string;
if (panelStrings.length <= 10) { if (panelStrings.length <= 10) {
@ -158,3 +155,22 @@ export function getFilteredPanelString(panel: Panel): string {
return JSON.stringify(filteredPanel, null, 2); 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}` : ''}`;

View File

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