mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
9bc68562d4
commit
e0416cc0f8
@ -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.'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -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,
|
||||||
|
@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -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}` : ''}`;
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user