Skip to content

Commit

Permalink
feat: support navigating to discover in alerting popover (#316)
Browse files Browse the repository at this point in the history
* feat: support navigate to discover in alerting popover

Signed-off-by: tygao <tygao@amazon.com>

* feat: support MDS and fix test env error

Signed-off-by: tygao <tygao@amazon.com>

* doc: add changelog

Signed-off-by: tygao <tygao@amazon.com>

* add tests

Signed-off-by: tygao <tygao@amazon.com>

---------

Signed-off-by: tygao <tygao@amazon.com>
(cherry picked from commit 93744fe)
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md
  • Loading branch information
github-actions[bot] committed Sep 20, 2024
1 parent 96a9046 commit 732fca4
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 4 deletions.
135 changes: 135 additions & 0 deletions public/components/incontext_insight/generate_popover_body.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@ import { GeneratePopoverBody } from './generate_popover_body';
import { HttpSetup } from '../../../../../src/core/public';
import { SUMMARY_ASSISTANT_API } from '../../../common/constants/llm';
import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks';
import { coreMock } from '../../../../../src/core/public/mocks';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';

jest.mock('../../services');

jest.mock('../../utils', () => ({
createIndexPatterns: jest.fn().mockResolvedValue('index pattern'),
buildUrlQuery: jest.fn().mockResolvedValue('query'),
}));

const mockToasts = {
addDanger: jest.fn(),
};
Expand All @@ -33,6 +40,36 @@ const mockHttpSetup: HttpSetup = ({
post: mockPost,
} as unknown) as HttpSetup; // Mocking HttpSetup

const mockDSL = `{
"query": {
"bool": {
"filter": [
{
"range": {
"timestamp": {
"from": "2024-09-06T04:02:52||-1h",
"to": "2024-09-06T04:02:52",
"include_lower": true,
"include_upper": true,
"boost": 1
}
}
},
{
"term": {
"FlightDelay": {
"value": "true",
"boost": 1
}
}
}
],
"adjust_pure_negative": true,
"boost": 1
}
}
}`;

describe('GeneratePopoverBody', () => {
const incontextInsightMock = {
contextProvider: jest.fn(),
Expand Down Expand Up @@ -240,4 +277,102 @@ describe('GeneratePopoverBody', () => {
// insight tip icon is not visible for this alert
expect(screen.queryAllByLabelText('How was this generated?')).toHaveLength(0);
});

it('should not display discover link if monitor type is not query_level_monitor or bucket_level_monitor', async () => {
incontextInsightMock.contextProvider = jest.fn().mockResolvedValue({
additionalInfo: {
dsl: mockDSL,
index: 'mock_index',
dataSourceId: `test-data-source-id`,
monitorType: 'mock_type',
},
});
mockPost.mockImplementation((path: string, body) => {
let value;
switch (path) {
case SUMMARY_ASSISTANT_API.SUMMARIZE:
value = {
summary: 'Generated summary content',
insightAgentIdExists: true,
};
break;

case SUMMARY_ASSISTANT_API.INSIGHT:
value = 'Generated insight content';
break;

default:
return null;
}
return Promise.resolve(value);
});

const { queryByText } = render(
<GeneratePopoverBody
incontextInsight={incontextInsightMock}
httpSetup={mockHttpSetup}
closePopover={closePopoverMock}
/>
);

await waitFor(() => {
expect(queryByText('Discover details')).not.toBeInTheDocument();
});
});

it('handle navigate to discover after clicking link', async () => {
incontextInsightMock.contextProvider = jest.fn().mockResolvedValue({
additionalInfo: {
dsl: mockDSL,
index: 'mock_index',
dataSourceId: `test-data-source-id`,
monitorType: 'query_level_monitor',
},
});
mockPost.mockImplementation((path: string, body) => {
let value;
switch (path) {
case SUMMARY_ASSISTANT_API.SUMMARIZE:
value = {
summary: 'Generated summary content',
insightAgentIdExists: true,
};
break;

case SUMMARY_ASSISTANT_API.INSIGHT:
value = 'Generated insight content';
break;

default:
return null;
}
return Promise.resolve(value);
});

const coreStart = coreMock.createStart();
const dataStart = dataPluginMock.createStartContract();
const getStartServices = jest.fn().mockResolvedValue([
coreStart,
{
data: dataStart,
},
]);
const { getByText } = render(
<GeneratePopoverBody
incontextInsight={incontextInsightMock}
httpSetup={mockHttpSetup}
closePopover={closePopoverMock}
getStartServices={getStartServices}
/>
);

await waitFor(() => {
const button = getByText('Discover details');
expect(button).toBeInTheDocument();
fireEvent.click(button);
expect(coreStart.application.navigateToUrl).toHaveBeenCalledWith(
'data-explorer/discover#?query'
);
});
});
});
74 changes: 71 additions & 3 deletions public/components/incontext_insight/generate_popover_body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { i18n } from '@osd/i18n';
import {
EuiFlexGroup,
Expand All @@ -17,24 +17,29 @@ import {
EuiSpacer,
EuiText,
EuiTitle,
EuiButton,
} from '@elastic/eui';
import { useEffectOnce } from 'react-use';
import { METRIC_TYPE } from '@osd/analytics';
import { MessageActions } from '../../tabs/chat/messages/message_action';
import { IncontextInsight as IncontextInsightInput } from '../../types';
import { getNotifications } from '../../services';
import { HttpSetup } from '../../../../../src/core/public';
import { HttpSetup, StartServicesAccessor } from '../../../../../src/core/public';
import { SUMMARY_ASSISTANT_API } from '../../../common/constants/llm';
import shiny_sparkle from '../../assets/shiny_sparkle.svg';
import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public';
import { reportMetric } from '../../utils/report_metric';
import { buildUrlQuery, createIndexPatterns } from '../../utils';
import { AssistantPluginStartDependencies } from '../../types';
import { UI_SETTINGS } from '../../../../../src/plugins/data/public';

export const GeneratePopoverBody: React.FC<{
incontextInsight: IncontextInsightInput;
httpSetup?: HttpSetup;
usageCollection?: UsageCollectionSetup;
closePopover: () => void;
}> = ({ incontextInsight, httpSetup, usageCollection, closePopover }) => {
getStartServices?: StartServicesAccessor<AssistantPluginStartDependencies>;
}> = ({ incontextInsight, httpSetup, usageCollection, closePopover, getStartServices }) => {
const [summary, setSummary] = useState('');
const [insight, setInsight] = useState('');
const [insightAvailable, setInsightAvailable] = useState(false);
Expand All @@ -43,6 +48,20 @@ export const GeneratePopoverBody: React.FC<{

const toasts = getNotifications().toasts;

const [displayDiscoverButton, setDisplayDiscoverButton] = useState(false);

useEffect(() => {
const getMonitorType = async () => {
const context = await incontextInsight.contextProvider?.();
const monitorType = context?.additionalInfo?.monitorType;
// Only this two types from alerting contain DSL and index.
const shoudDisplayDiscoverButton =
monitorType === 'query_level_monitor' || monitorType === 'bucket_level_monitor';
setDisplayDiscoverButton(shoudDisplayDiscoverButton);
};
getMonitorType();
}, [incontextInsight, setDisplayDiscoverButton]);

useEffectOnce(() => {
onGenerateSummary(
incontextInsight.suggestions && incontextInsight.suggestions.length > 0
Expand Down Expand Up @@ -154,6 +173,48 @@ export const GeneratePopoverBody: React.FC<{
return generateInsight();
};

const handleNavigateToDiscover = async () => {
const context = await incontextInsight?.contextProvider?.();
const dsl = context?.additionalInfo?.dsl;
const indexName = context?.additionalInfo?.index;
if (!dsl || !indexName) return;
const dslObject = JSON.parse(dsl);
const filters = dslObject?.query?.bool?.filter;
if (!filters) return;
const timeDslIndex = filters?.findIndex((filter: Record<string, string>) => filter?.range);
const timeDsl = filters[timeDslIndex]?.range;
const timeFieldName = Object.keys(timeDsl)[0];
if (!timeFieldName) return;
filters?.splice(timeDslIndex, 1);

if (getStartServices) {
const [coreStart, startDeps] = await getStartServices();
const newDiscoverEnabled = coreStart.uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED);
if (!newDiscoverEnabled) {
// Only new discover supports DQL with filters.
coreStart.uiSettings.set(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED, true);
}

const indexPattern = await createIndexPatterns(
startDeps.data,
indexName,
timeFieldName,
context?.dataSourceId
);
if (!indexPattern) return;
const query = await buildUrlQuery(
startDeps.data,
coreStart.savedObjects,
indexPattern,
dslObject,
timeDsl[timeFieldName],
context?.dataSourceId
);
// Navigate to new discover with query built to populate
coreStart.application.navigateToUrl(`data-explorer/discover#?${query}`);
}
};

const renderContent = () => {
const content = showInsight && insightAvailable ? insight : summary;
return content ? (
Expand Down Expand Up @@ -245,6 +306,13 @@ export const GeneratePopoverBody: React.FC<{
<>
{renderInnerTitle()}
{renderContent()}
{displayDiscoverButton && (
<EuiButton onClick={handleNavigateToDiscover}>
{i18n.translate('assistantDashboards.incontextInsight.discover', {
defaultMessage: 'Discover details',
})}
</EuiButton>
)}
</>
);
};
6 changes: 5 additions & 1 deletion public/components/incontext_insight/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,24 @@ import { getIncontextInsightRegistry, getNotifications } from '../../services';
// TODO: Replace with getChrome().logos.Chat.url
import chatIcon from '../../assets/chat.svg';
import sparkle from '../../assets/sparkle.svg';
import { HttpSetup } from '../../../../../src/core/public';
import { HttpSetup, StartServicesAccessor } from '../../../../../src/core/public';
import { GeneratePopoverBody } from './generate_popover_body';
import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public/plugin';
import { AssistantPluginStartDependencies } from '../../types';

export interface IncontextInsightProps {
children?: React.ReactNode;
httpSetup?: HttpSetup;
usageCollection?: UsageCollectionSetup;
getStartServices?: StartServicesAccessor<AssistantPluginStartDependencies>;
}

// TODO: add saved objects / config to store seed suggestions
export const IncontextInsight = ({
children,
httpSetup,
usageCollection,
getStartServices,
}: IncontextInsightProps) => {
const anchor = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
Expand Down Expand Up @@ -287,6 +290,7 @@ export const IncontextInsight = ({
httpSetup={httpSetup}
usageCollection={usageCollection}
closePopover={closePopover}
getStartServices={getStartServices}
/>
);
case 'summary':
Expand Down
1 change: 1 addition & 0 deletions public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ export class AssistantPlugin
{...props}
httpSetup={httpSetup}
usageCollection={setupDeps.usageCollection}
getStartServices={core.getStartServices}
/>
);
},
Expand Down
Loading

0 comments on commit 732fca4

Please sign in to comment.