diff --git a/package.json b/package.json index dd6ba11bc16..54a1d756b6c 100644 --- a/package.json +++ b/package.json @@ -271,6 +271,7 @@ "@formatjs/intl-durationformat": "^0.7.0", "@glideapps/glide-data-grid": "^6.0.0", "@grafana/alerting": "workspace:*", + "@grafana/assistant": "0.0.7", "@grafana/aws-sdk": "0.7.1", "@grafana/azure-sdk": "0.0.7", "@grafana/data": "workspace:*", diff --git a/public/app/features/logs/components/panel/LogLine.test.tsx b/public/app/features/logs/components/panel/LogLine.test.tsx index e31ba38cabf..89803a457bb 100644 --- a/public/app/features/logs/components/panel/LogLine.test.tsx +++ b/public/app/features/logs/components/panel/LogLine.test.tsx @@ -14,6 +14,11 @@ import { defaultProps, defaultValue } from './__mocks__/LogListContext'; import { LogListModel } from './processing'; import { LogLineVirtualization } from './virtualization'; +jest.mock('@grafana/assistant', () => ({ + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn(() => [true, jest.fn()]), +})); + jest.mock('./LogListContext'); jest.mock('../LogDetails'); diff --git a/public/app/features/logs/components/panel/LogLineDetails.test.tsx b/public/app/features/logs/components/panel/LogLineDetails.test.tsx index 2ce960fdd86..112ea2531ba 100644 --- a/public/app/features/logs/components/panel/LogLineDetails.test.tsx +++ b/public/app/features/logs/components/panel/LogLineDetails.test.tsx @@ -23,6 +23,13 @@ import { LogLineDetails, Props } from './LogLineDetails'; import { LogListContext, LogListContextData } from './LogListContext'; import { defaultValue } from './__mocks__/LogListContext'; +jest.mock('@grafana/assistant', () => { + return { + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn().mockReturnValue([true, jest.fn()]), + }; +}); + jest.mock('@grafana/runtime', () => { return { ...jest.requireActual('@grafana/runtime'), diff --git a/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx b/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx index a71b64fc31b..9a4e6eedce0 100644 --- a/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx +++ b/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx @@ -37,6 +37,8 @@ export const LogLineDetailsHeader = ({ focusLogLine, log, search, onSearch }: Pr onPinLine, onUnpinLine, wrapLogMessage, + isAssistantAvailable, + openAssistantByLog, } = useLogListContext(); const pinned = useLogIsPinned(log); const styles = useStyles2(getStyles, detailsMode, wrapLogMessage); @@ -149,6 +151,16 @@ export const LogLineDetailsHeader = ({ focusLogLine, log, search, onSearch }: Pr suffix={search !== '' ? clearSearch : undefined} />
+ {isAssistantAvailable && ( + openAssistantByLog?.(log)} + tabIndex={0} + /> + )} {focusLogLine && ( ({ + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn(() => [true, jest.fn()]), +})); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + isAssistantAvailable: true, +})); + const theme = createTheme(); const styles = getStyles(theme); const contextProps = { diff --git a/public/app/features/logs/components/panel/LogLineMenu.tsx b/public/app/features/logs/components/panel/LogLineMenu.tsx index 37a0df589d6..49b55b368d1 100644 --- a/public/app/features/logs/components/panel/LogLineMenu.tsx +++ b/public/app/features/logs/components/panel/LogLineMenu.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, MouseEvent } from 'react'; +import { MouseEvent, useCallback, useMemo, useRef } from 'react'; import { LogRowContextOptions, LogRowModel } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -45,6 +45,8 @@ export const LogLineMenu = ({ log, styles }: Props) => { logLineMenuCustomItems = [], logSupportsContext, toggleDetails, + isAssistantAvailable, + openAssistantByLog, } = useLogListContext(); const pinned = useLogIsPinned(log); const menuRef = useRef(null); @@ -108,6 +110,13 @@ export const LogLineMenu = ({ log, styles }: Props) => { {onPermalinkClick && log.rowId !== undefined && log.uid && ( )} + {isAssistantAvailable && ( + openAssistantByLog?.(log)} + icon="ai-sparkle" + label={t('logs.log-line-menu.open-assistant', 'Explain this log line in Assistant')} + /> + )} {logLineMenuCustomItems.map((item, i) => { if (isDivider(item)) { return ; @@ -134,6 +143,8 @@ export const LogLineMenu = ({ log, styles }: Props) => { showContext, toggleLogDetails, togglePinning, + isAssistantAvailable, + openAssistantByLog, ] ); diff --git a/public/app/features/logs/components/panel/LogList.test.tsx b/public/app/features/logs/components/panel/LogList.test.tsx index 3d76e63e413..4f740d8059b 100644 --- a/public/app/features/logs/components/panel/LogList.test.tsx +++ b/public/app/features/logs/components/panel/LogList.test.tsx @@ -9,6 +9,11 @@ import { createLogRow } from '../mocks/logRow'; import { LogList, Props } from './LogList'; +jest.mock('@grafana/assistant', () => ({ + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn(() => [true, jest.fn()]), +})); + jest.mock('@grafana/runtime', () => { return { ...jest.requireActual('@grafana/runtime'), diff --git a/public/app/features/logs/components/panel/LogListContext.tsx b/public/app/features/logs/components/panel/LogListContext.tsx index 153c53a0a80..f2c7455fddb 100644 --- a/public/app/features/logs/components/panel/LogListContext.tsx +++ b/public/app/features/logs/components/panel/LogListContext.tsx @@ -11,6 +11,7 @@ import { useState, } from 'react'; +import { createContext as createAssistantContext, ItemDataType, useAssistant } from '@grafana/assistant'; import { CoreApp, DataFrame, @@ -22,10 +23,11 @@ import { shallowCompare, store, } from '@grafana/data'; -import { config, reportInteraction } from '@grafana/runtime'; +import { t } from '@grafana/i18n'; +import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { PopoverContent } from '@grafana/ui'; -import { DownloadFormat, checkLogsError, checkLogsSampled, downloadLogs as download } from '../../utils'; +import { checkLogsError, checkLogsSampled, downloadLogs as download, DownloadFormat } from '../../utils'; import { getDisplayedFieldsForLogs } from '../otel/formats'; import { LogLineDetailsMode } from './LogLineDetails'; @@ -63,6 +65,8 @@ export interface LogListContextData extends Omit void; showDetails: LogListModel[]; toggleDetails: (log: LogListModel) => void; + isAssistantAvailable: boolean; + openAssistantByLog: ((log: LogListModel) => void) | undefined; } export const LogListContext = createContext({ @@ -100,6 +104,8 @@ export const LogListContext = createContext({ syntaxHighlighting: true, toggleDetails: () => {}, wrapLogMessage: false, + isAssistantAvailable: false, + openAssistantByLog: () => {}, }); export const useLogListContextData = (key: keyof LogListContextData) => { @@ -239,6 +245,47 @@ export const LogListContextProvider = ({ const [showDetails, setShowDetails] = useState([]); const [detailsWidth, setDetailsWidthState] = useState(getDetailsWidth(containerElement, logOptionsStorageKey)); const [detailsMode, setDetailsMode] = useState(detailsModeProp ?? 'sidebar'); + const [isAssistantAvailable, openAssistant] = useAssistant(); + + const openAssistantByLog = useCallback( + async (log: LogListModel) => { + if (!openAssistant) { + return; + } + + const datasource = await getDataSourceSrv().get(log.datasourceUid); + const context = []; + if (datasource) { + context.push( + createAssistantContext(ItemDataType.Datasource, { + datasourceUid: datasource.uid, + datasourceName: datasource.name, + datasourceType: datasource.type, + }) + ); + } + openAssistant({ + prompt: `${t('logs.log-line-menu.log-line-explainer', 'Explain this log line in a concise way')}: + + \`\`\` +${log.entry.replaceAll('`', '\\`')} + \`\`\` + `, + context: [ + ...context, + createAssistantContext(ItemDataType.Structured, { + title: t('logs.log-line-menu.log-line', 'Log line'), + data: { + labels: log.labels, + value: log.entry, + timestamp: log.timestamp, + }, + }), + ], + }); + }, + [openAssistant] + ); useEffect(() => { if (noInteractions) { @@ -569,6 +616,8 @@ export const LogListContextProvider = ({ syntaxHighlighting: logListState.syntaxHighlighting, toggleDetails, wrapLogMessage: logListState.wrapLogMessage, + isAssistantAvailable, + openAssistantByLog, }} > {children} diff --git a/public/app/features/logs/components/panel/LogListControls.test.tsx b/public/app/features/logs/components/panel/LogListControls.test.tsx index db8e59134b0..2acdf38abe0 100644 --- a/public/app/features/logs/components/panel/LogListControls.test.tsx +++ b/public/app/features/logs/components/panel/LogListControls.test.tsx @@ -17,6 +17,13 @@ jest.mock('../../utils', () => ({ downloadLogs: jest.fn(), })); +jest.mock('@grafana/assistant', () => { + return { + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn().mockReturnValue([true, jest.fn()]), + }; +}); + const fontSize: LogListFontSize = 'default'; const contextProps = { app: CoreApp.Unknown, @@ -31,6 +38,8 @@ const contextProps = { sortOrder: LogsSortOrder.Ascending, syntaxHighlighting: false, wrapLogMessage: false, + isAssistantAvailable: false, + openAssistantByLog: () => {}, }; describe('LogListControls', () => { diff --git a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx index 131afaa32cb..53b020bbe88 100644 --- a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx +++ b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx @@ -7,6 +7,13 @@ import { LogLineDetailsMode } from '../LogLineDetails'; import { LogListContextData, Props } from '../LogListContext'; import { LogListModel } from '../processing'; +jest.mock('@grafana/assistant', () => { + return { + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn().mockReturnValue([true, jest.fn()]), + }; +}); + export const LogListContext = createContext({ app: CoreApp.Unknown, closeDetails: () => {}, @@ -43,6 +50,8 @@ export const LogListContext = createContext({ setDetailsMode: function (mode: LogLineDetailsMode): void { throw new Error('Function not implemented.'); }, + isAssistantAvailable: false, + openAssistantByLog: () => {}, }); export const useLogListContextData = (key: keyof LogListContextData) => { @@ -97,6 +106,8 @@ export const defaultValue: LogListContextData = { showTime: false, sortOrder: LogsSortOrder.Ascending, wrapLogMessage: false, + isAssistantAvailable: false, + openAssistantByLog: () => {}, }; export const defaultProps: Props = { diff --git a/public/app/plugins/panel/logs-new/LogsPanel.test.tsx b/public/app/plugins/panel/logs-new/LogsPanel.test.tsx index 77570bf19ab..09997a66047 100644 --- a/public/app/plugins/panel/logs-new/LogsPanel.test.tsx +++ b/public/app/plugins/panel/logs-new/LogsPanel.test.tsx @@ -18,6 +18,11 @@ import { LogsPanel } from './LogsPanel'; type LogsPanelProps = ComponentProps; +jest.mock('@grafana/assistant', () => ({ + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn(() => [true, jest.fn()]), +})); + jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getAppEvents: jest.fn(), diff --git a/public/app/plugins/panel/logs/LogsPanel.test.tsx b/public/app/plugins/panel/logs/LogsPanel.test.tsx index 6a299326f85..64ef4a60a44 100644 --- a/public/app/plugins/panel/logs/LogsPanel.test.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.test.tsx @@ -57,6 +57,13 @@ jest.mock('@grafana/data', () => ({ hasLogsContextSupport: (ds: MockDataSourceApi) => hasLogsContextSupport(ds), })); +jest.mock('@grafana/assistant', () => { + return { + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn().mockReturnValue([true, jest.fn()]), + }; +}); + const defaultProps = { data: { error: undefined, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 7a1a46a7138..180039f2b38 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -9331,6 +9331,7 @@ "move-displayed-field-down": "Move down", "move-displayed-field-up": "Move up", "no-details": "No fields to display.", + "open-assistant": "Explain this log line in Assistant", "pin-line": "Pin log", "remove-displayed-field": "Remove field", "remove-log": "Remove log", @@ -9349,6 +9350,9 @@ "copy-log": "Copy log line", "hide-details": "Show log details", "icon-label": "Log menu", + "log-line": "Log line", + "log-line-explainer": "Explain this log line in a concise way", + "open-assistant": "Explain this log line in Assistant", "pin-to-outline": "Pin log", "show-context": "Show context", "show-details": "Hide log details", diff --git a/yarn.lock b/yarn.lock index 866a76ae882..7bcd86a7b5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3064,6 +3064,20 @@ __metadata: languageName: unknown linkType: soft +"@grafana/assistant@npm:0.0.7": + version: 0.0.7 + resolution: "@grafana/assistant@npm:0.0.7" + peerDependencies: + "@grafana/data": ">=12.0.0" + "@grafana/runtime": ">=12.0.0" + "@grafana/scenes": ">=5.41.0" + "@grafana/ui": ">=12.0.0" + react: ">=18.0.0" + rxjs: ">=7.0.0" + checksum: 10/c83523a032cb748867f683e87ad5b00a04bb4918ae5c7aafe5587b418b73b769a92de948335f0d9055d06036004e7a9794722f627cab3111b076eb27423d4344 + languageName: node + linkType: hard + "@grafana/async-query-data@npm:0.4.1": version: 0.4.1 resolution: "@grafana/async-query-data@npm:0.4.1" @@ -17832,6 +17846,7 @@ __metadata: "@formatjs/intl-durationformat": "npm:^0.7.0" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana/alerting": "workspace:*" + "@grafana/assistant": "npm:0.0.7" "@grafana/aws-sdk": "npm:0.7.1" "@grafana/azure-sdk": "npm:0.0.7" "@grafana/data": "workspace:*"