diff --git a/public/app/features/dashboard/components/GenAI/GenAIButton.test.tsx b/public/app/features/dashboard/components/GenAI/GenAIButton.test.tsx index 164892a221f..9c711d9fa94 100644 --- a/public/app/features/dashboard/components/GenAI/GenAIButton.test.tsx +++ b/public/app/features/dashboard/components/GenAI/GenAIButton.test.tsx @@ -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.' ); }); diff --git a/public/app/features/dashboard/components/GenAI/GenAIButton.tsx b/public/app/features/dashboard/components/GenAI/GenAIButton.tsx index a8bbad79b50..336afb7f16d 100644 --- a/public/app/features/dashboard/components/GenAI/GenAIButton.tsx +++ b/public/app/features/dashboard/components/GenAI/GenAIButton.tsx @@ -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) => { if (streamStatus === StreamStatus.GENERATING) { setStopGeneration(true); @@ -192,13 +205,7 @@ export const GenAIButton = ({
{isGenerating && } {isFirstHistoryEntry ? ( - + {button} ) : ( diff --git a/public/app/features/dashboard/components/GenAI/GenAIDashDescriptionButton.tsx b/public/app/features/dashboard/components/GenAI/GenAIDashDescriptionButton.tsx index feeb7a08a0b..6ba4f378266 100644 --- a/public/app/features/dashboard/components/GenAI/GenAIDashDescriptionButton.tsx +++ b/public/app/features/dashboard/components/GenAI/GenAIDashDescriptionButton.tsx @@ -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 ( ); }; -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, }, ]; diff --git a/public/app/features/dashboard/components/GenAI/GenAIDashTitleButton.tsx b/public/app/features/dashboard/components/GenAI/GenAIDashTitleButton.tsx index 9888c514ce4..6e5f0b49842 100644 --- a/public/app/features/dashboard/components/GenAI/GenAIDashTitleButton.tsx +++ b/public/app/features/dashboard/components/GenAI/GenAIDashTitleButton.tsx @@ -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 ( ); }; -function getMessages(): Message[] { - const dashboard = getDashboardSrv().getCurrent()!; - +function getMessages(dashboard: DashboardModel): Message[] { return [ { content: TITLE_GENERATION_STANDARD_PROMPT, diff --git a/public/app/features/dashboard/components/GenAI/utils.test.ts b/public/app/features/dashboard/components/GenAI/utils.test.ts index 8b3c06003a8..39741a10a2b 100644 --- a/public/app/features/dashboard/components/GenAI/utils.test.ts +++ b/public/app/features/dashboard/components/GenAI/utils.test.ts @@ -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', + ]); + }); +}); diff --git a/public/app/features/dashboard/components/GenAI/utils.ts b/public/app/features/dashboard/components/GenAI/utils.ts index 2b1707caacc..78a2cede187 100644 --- a/public/app/features/dashboard/components/GenAI/utils.ts +++ b/public/app/features/dashboard/components/GenAI/utils.ts @@ -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}` : ''}`; diff --git a/public/app/features/dashboard/utils/dashboard.ts b/public/app/features/dashboard/utils/dashboard.ts index 4fd87d68b48..5627bb9209c 100644 --- a/public/app/features/dashboard/utils/dashboard.ts +++ b/public/app/features/dashboard/utils/dashboard.ts @@ -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 = { 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,