[RCA] Start investigation from alert details page (#190307)

Resolves https://github.com/elastic/kibana/issues/190320 and
https://github.com/elastic/kibana/issues/190396

- Start investigation from Custom threshold alert details page
- Go to ongoing investigation instead of creating new one if one already
exists
- Initial investigation status is set as `ongoing`
- Investigation origin is set as `alert`

"Start investigation" is hidden for other alert types and when
investigate plugin is disabled.

### Testing
- Add the following in `kibana.dev.yml`
```
xpack.investigate.enabled: true
xpack.investigateApp.enabled: true
```
- Create Custom threshold rule
- Open Custom threshold alert details page
- Click on "Start investigation"
- Verify that a new saved object is created for the investigation


https://github.com/user-attachments/assets/6dfe8a5f-287b-4cc5-92ae-e4c315c7420b

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Kevin Delemme <kdelemme@gmail.com>
This commit is contained in:
Bena Kansara 2024-08-14 15:24:32 +02:00 committed by GitHub
parent abc8495337
commit 95736fb536
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 477 additions and 22 deletions

View file

@ -14,3 +14,11 @@ export type {
export { mergePlainObjects } from './utils/merge_plain_objects';
export { InvestigateWidgetColumnSpan } from './types';
export type { CreateInvestigationInput, CreateInvestigationResponse } from './schema/create';
export type { GetInvestigationParams } from './schema/get';
export type { FindInvestigationsResponse } from './schema/find';
export { createInvestigationParamsSchema } from './schema/create';
export { getInvestigationParamsSchema } from './schema/get';
export { findInvestigationsParamsSchema } from './schema/find';

View file

@ -6,14 +6,16 @@
*/
import * as t from 'io-ts';
import { investigationResponseSchema } from './investigation';
import { alertOriginSchema, blankOriginSchema } from './origin';
const createInvestigationParamsSchema = t.type({
body: t.type({
id: t.string,
title: t.string,
parameters: t.type({
params: t.type({
timeRange: t.type({ from: t.number, to: t.number }),
}),
origin: t.union([alertOriginSchema, blankOriginSchema]),
}),
});

View file

@ -9,6 +9,7 @@ import { investigationResponseSchema } from './investigation';
const findInvestigationsParamsSchema = t.partial({
query: t.partial({
alertId: t.string,
page: t.string,
perPage: t.string,
}),

View file

@ -5,15 +5,18 @@
* 2.0.
*/
import * as t from 'io-ts';
import { alertOriginSchema, blankOriginSchema } from './origin';
const investigationResponseSchema = t.type({
id: t.string,
title: t.string,
createdAt: t.number,
createdBy: t.string,
parameters: t.type({
params: t.type({
timeRange: t.type({ from: t.number, to: t.number }),
}),
origin: t.union([alertOriginSchema, blankOriginSchema]),
status: t.union([t.literal('ongoing'), t.literal('closed')]),
});
type InvestigationResponse = t.OutputOf<typeof investigationResponseSchema>;

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
const blankOriginSchema = t.type({ type: t.literal('blank') });
const alertOriginSchema = t.type({ type: t.literal('alert'), id: t.string });
type AlertOrigin = t.OutputOf<typeof alertOriginSchema>;
type BlankOrigin = t.OutputOf<typeof blankOriginSchema>;
export { alertOriginSchema, blankOriginSchema };
export type { AlertOrigin, BlankOrigin };

View file

@ -9,6 +9,7 @@ export const investigationKeys = {
all: ['investigation'] as const,
list: (params: { page: number; perPage: number }) =>
[...investigationKeys.all, 'list', params] as const,
fetch: (params: { id: string }) => [...investigationKeys.all, 'fetch', params] as const,
};
export type InvestigationKeys = typeof investigationKeys;

View file

@ -6,7 +6,7 @@
*/
import { useQuery } from '@tanstack/react-query';
import { FindInvestigationsResponse } from '../../common/schema/find';
import { FindInvestigationsResponse } from '@kbn/investigate-plugin/common';
import { investigationKeys } from './query_key_factory';
import { useKibana } from './use_kibana';

View file

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useQuery } from '@tanstack/react-query';
import { BASE_RAC_ALERTS_API_PATH, EcsFieldsResponse } from '@kbn/rule-registry-plugin/common';
import { useKibana } from './use_kibana';
export interface AlertParams {
id: string;
}
export interface UseFetchAlertResponse {
isInitialLoading: boolean;
isLoading: boolean;
isRefetching: boolean;
isSuccess: boolean;
isError: boolean;
data: EcsFieldsResponse | undefined | null;
}
export function useFetchAlert({ id }: AlertParams): UseFetchAlertResponse {
const {
core: {
http,
notifications: { toasts },
},
} = useKibana();
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
queryKey: ['fetchAlert', id],
queryFn: async ({ signal }) => {
return await http.get<EcsFieldsResponse>(BASE_RAC_ALERTS_API_PATH, {
query: {
id,
},
signal,
});
},
cacheTime: 0,
refetchOnWindowFocus: false,
retry: (failureCount, error) => {
if (String(error) === 'Error: Forbidden') {
return false;
}
return failureCount < 3;
},
onError: (error: Error) => {
toasts.addError(error, {
title: 'Something went wrong while fetching alert',
});
},
enabled: Boolean(id),
});
return {
data,
isInitialLoading,
isLoading,
isRefetching,
isSuccess,
isError,
};
}

View file

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useQuery } from '@tanstack/react-query';
import { GetInvestigationResponse } from '@kbn/investigate-plugin/common/schema/get';
import { investigationKeys } from './query_key_factory';
import { useKibana } from './use_kibana';
export interface FetchInvestigationParams {
id: string;
}
export interface UseFetchInvestigationResponse {
isInitialLoading: boolean;
isLoading: boolean;
isRefetching: boolean;
isSuccess: boolean;
isError: boolean;
data: GetInvestigationResponse | undefined;
}
export function useFetchInvestigation({
id,
}: FetchInvestigationParams): UseFetchInvestigationResponse {
const {
core: {
http,
notifications: { toasts },
},
} = useKibana();
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
queryKey: investigationKeys.fetch({ id }),
queryFn: async ({ signal }) => {
return await http.get<GetInvestigationResponse>(`/api/observability/investigations/${id}`, {
version: '2023-10-31',
signal,
});
},
cacheTime: 0,
refetchOnWindowFocus: false,
retry: (failureCount, error) => {
if (String(error) === 'Error: Forbidden') {
return false;
}
return failureCount < 3;
},
onError: (error: Error) => {
toasts.addError(error, {
title: 'Something went wrong while fetching Investigation',
});
},
});
return {
data,
isInitialLoading,
isLoading,
isRefetching,
isSuccess,
isError,
};
}

View file

@ -5,11 +5,16 @@
* 2.0.
*/
import { EuiButton } from '@elastic/eui';
import { EuiButton, EuiButtonEmpty, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ALERT_RULE_CATEGORY } from '@kbn/rule-data-utils/src/default_alerts_as_data';
import { AlertOrigin } from '@kbn/investigate-plugin/common/schema/origin';
import { paths } from '../../../common/paths';
import { useKibana } from '../../hooks/use_kibana';
import { useFetchInvestigation } from '../../hooks/use_get_investigation_details';
import { useInvestigateParams } from '../../hooks/use_investigate_params';
import { useFetchAlert } from '../../hooks/use_get_alert_details';
import { InvestigationDetails } from './components/investigation_details';
export function InvestigationDetailsPage() {
@ -22,8 +27,46 @@ export function InvestigationDetailsPage() {
},
} = useKibana();
const {
path: { id },
} = useInvestigateParams('/{id}');
const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate;
const {
data: investigationDetails,
isLoading: isFetchInvestigationLoading,
isError: isFetchInvestigationError,
} = useFetchInvestigation({ id });
const alertId = investigationDetails ? (investigationDetails.origin as AlertOrigin).id : '';
const {
data: alertDetails,
isLoading: isFetchAlertLoading,
isError: isFetchAlertError,
} = useFetchAlert({ id: alertId });
if (isFetchInvestigationLoading || isFetchAlertLoading) {
return (
<h1>
{i18n.translate('xpack.investigateApp.fetchInvestigation.loadingLabel', {
defaultMessage: 'Loading...',
})}
</h1>
);
}
if (isFetchInvestigationError || isFetchAlertError) {
return (
<h1>
{i18n.translate('xpack.investigateApp.fetchInvestigation.errorLabel', {
defaultMessage: 'Error while fetching investigation',
})}
</h1>
);
}
return (
<ObservabilityPageTemplate
pageHeader={{
@ -40,12 +83,26 @@ export function InvestigationDetailsPage() {
}),
},
],
pageTitle: i18n.translate('xpack.investigateApp.detailsPage.title', {
defaultMessage: 'New investigation',
}),
pageTitle: (
<>
{alertDetails && (
<EuiButtonEmpty
data-test-subj="investigationDetailsAlertLink"
iconType="arrowLeft"
size="xs"
href={basePath.prepend(`/app/observability/alerts/${alertId}`)}
>
<EuiText size="s">
{`[Alert] ${alertDetails?.[ALERT_RULE_CATEGORY]} breached`}
</EuiText>
</EuiButtonEmpty>
)}
{investigationDetails && <div>{investigationDetails.title}</div>}
</>
),
rightSideItems: [
<EuiButton fill data-test-subj="investigateAppInvestigateDetailsPageEscalateButton">
{i18n.translate('xpack.investigateApp.investigateDetailsPage.escalateButtonLabel', {
<EuiButton fill data-test-subj="investigationDetailsEscalateButton">
{i18n.translate('xpack.investigateApp.investigationDetails.escalateButtonLabel', {
defaultMessage: 'Escalate',
})}
</EuiButton>,

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { alertOriginSchema, blankOriginSchema } from '@kbn/investigate-plugin/common/schema/origin';
import * as t from 'io-ts';
export const investigationSchema = t.type({
@ -12,9 +13,11 @@ export const investigationSchema = t.type({
title: t.string,
createdAt: t.number,
createdBy: t.string,
parameters: t.type({
params: t.type({
timeRange: t.type({ from: t.number, to: t.number }),
}),
origin: t.union([alertOriginSchema, blankOriginSchema]),
status: t.union([t.literal('ongoing'), t.literal('closed')]),
});
export type Investigation = t.TypeOf<typeof investigationSchema>;

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import { findInvestigationsParamsSchema } from '../../common/schema/find';
import { createInvestigationParamsSchema } from '../../common/schema/create';
import { createInvestigationParamsSchema } from '@kbn/investigate-plugin/common';
import { findInvestigationsParamsSchema } from '@kbn/investigate-plugin/common';
import { getInvestigationParamsSchema } from '@kbn/investigate-plugin/common';
import { createInvestigation } from '../services/create_investigation';
import { investigationRepositoryFactory } from '../services/investigation_repository';
import { createInvestigateAppServerRoute } from './create_investigate_app_server_route';
import { findInvestigations } from '../services/find_investigations';
import { getInvestigationParamsSchema } from '../../common/schema/get';
import { getInvestigation } from '../services/get_investigation';
const createInvestigationRoute = createInvestigateAppServerRoute({

View file

@ -15,12 +15,18 @@ export const investigation: SavedObjectsType = {
name: SO_INVESTIGATION_TYPE,
hidden: false,
namespaceType: 'multiple-isolated',
switchToModelVersionAt: '8.10.0',
mappings: {
dynamic: false,
properties: {
id: { type: 'keyword' },
title: { type: 'text' },
origin: {
properties: {
type: { type: 'keyword' },
id: { type: 'keyword' },
},
},
status: { type: 'keyword' },
},
},
management: {

View file

@ -5,14 +5,27 @@
* 2.0.
*/
import { CreateInvestigationInput, CreateInvestigationResponse } from '../../common/schema/create';
import {
CreateInvestigationInput,
CreateInvestigationResponse,
} from '@kbn/investigate-plugin/common';
import { InvestigationRepository } from './investigation_repository';
enum InvestigationStatus {
ongoing = 'ongoing',
closed = 'closed',
}
export async function createInvestigation(
params: CreateInvestigationInput,
repository: InvestigationRepository
): Promise<CreateInvestigationResponse> {
const investigation = { ...params, createdAt: Date.now(), createdBy: 'elastic' };
const investigation = {
...params,
createdAt: Date.now(),
createdBy: 'elastic',
status: InvestigationStatus.ongoing,
};
await repository.save(investigation);
return investigation;

View file

@ -5,18 +5,18 @@
* 2.0.
*/
import { FindInvestigationsResponse } from '@kbn/investigate-plugin/common';
import {
FindInvestigationsParams,
FindInvestigationsResponse,
findInvestigationsResponseSchema,
} from '../../common/schema/find';
} from '@kbn/investigate-plugin/common/schema/find';
import { InvestigationRepository } from './investigation_repository';
export async function findInvestigations(
params: FindInvestigationsParams,
repository: InvestigationRepository
): Promise<FindInvestigationsResponse> {
const investigations = await repository.search(toPagination(params));
const investigations = await repository.search(toFilter(params), toPagination(params));
return findInvestigationsResponseSchema.encode(investigations);
}
@ -29,3 +29,10 @@ function toPagination(params: FindInvestigationsParams) {
perPage: params.perPage ? parseInt(params.perPage, 10) : DEFAULT_PER_PAGE,
};
}
function toFilter(params: FindInvestigationsParams) {
if (params.alertId) {
return `investigation.attributes.origin.id:(${params.alertId}) AND investigation.attributes.status: ongoing`;
}
return '';
}

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { GetInvestigationParams, GetInvestigationResponse } from '../../common/schema/get';
import { GetInvestigationParams } from '@kbn/investigate-plugin/common';
import { GetInvestigationResponse } from '@kbn/investigate-plugin/common/schema/get';
import { InvestigationRepository } from './investigation_repository';
export async function getInvestigation(

View file

@ -15,7 +15,7 @@ export interface InvestigationRepository {
save(investigation: Investigation): Promise<void>;
findById(id: string): Promise<Investigation>;
deleteById(id: string): Promise<void>;
search(pagination: Pagination): Promise<Paginated<Investigation>>;
search(filter: string, pagination: Pagination): Promise<Paginated<Investigation>>;
}
export function investigationRepositoryFactory({
@ -89,11 +89,12 @@ export function investigationRepositoryFactory({
await soClient.delete(SO_INVESTIGATION_TYPE, response.saved_objects[0].id);
},
async search(pagination: Pagination): Promise<Paginated<Investigation>> {
async search(filter: string, pagination: Pagination): Promise<Paginated<Investigation>> {
const response = await soClient.find<StoredInvestigation>({
type: SO_INVESTIGATION_TYPE,
page: pagination.page,
perPage: pagination.perPage,
filter,
});
return {

View file

@ -52,5 +52,7 @@
"@kbn/esql-datagrid",
"@kbn/server-route-repository-utils",
"@kbn/core-saved-objects-server",
"@kbn/rule-registry-plugin",
"@kbn/rule-data-utils",
],
}

View file

@ -51,6 +51,7 @@
"serverless",
"guidedOnboarding",
"observabilityAIAssistant",
"investigate"
],
"requiredBundles": [
"data",

View file

@ -34,6 +34,10 @@ const mockHttp = {
},
};
const mockNavigateToApp = {
mockNavigateToApp: jest.fn(),
};
const mockGetEditRuleFlyout = jest.fn(() => (
<div data-test-subj="edit-rule-flyout">mocked component</div>
));
@ -48,6 +52,7 @@ const mockKibana = () => {
},
cases: mockCases,
http: mockHttp,
application: mockNavigateToApp,
},
});
};

View file

@ -25,13 +25,22 @@ import {
ALERT_RULE_UUID,
ALERT_STATUS_ACTIVE,
ALERT_UUID,
ALERT_RULE_CATEGORY,
ALERT_START,
ALERT_END,
ALERT_RULE_TYPE_ID,
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
} from '@kbn/rule-data-utils';
import { v4 as uuidv4 } from 'uuid';
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
import { useKibana } from '../../../utils/kibana_react';
import { useFetchRule } from '../../../hooks/use_fetch_rule';
import type { TopAlert } from '../../../typings/alerts';
import { paths } from '../../../../common/locators/paths';
import { useBulkUntrackAlerts } from '../hooks/use_bulk_untrack_alerts';
import { useCreateInvestigation } from '../hooks/use_create_investigation';
import { useFetchInvestigationsByAlert } from '../hooks/use_fetch_investigations_by_alert';
export interface HeaderActionsProps {
alert: TopAlert | null;
@ -52,12 +61,18 @@ export function HeaderActions({
},
triggersActionsUi: { getEditRuleFlyout: EditRuleFlyout, getRuleSnoozeModal: RuleSnoozeModal },
http,
application: { navigateToApp },
investigate: investigatePlugin,
} = useKibana().services;
const { rule, refetch } = useFetchRule({
ruleId: alert?.fields[ALERT_RULE_UUID] || '',
});
const { data: investigations } = useFetchInvestigationsByAlert({
alertId: alert?.fields[ALERT_UUID] ?? '',
});
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const [ruleConditionsFlyoutOpen, setRuleConditionsFlyoutOpen] = useState<boolean>(false);
const [snoozeModalOpen, setSnoozeModalOpen] = useState<boolean>(false);
@ -109,9 +124,67 @@ export function HeaderActions({
setSnoozeModalOpen(true);
};
const { mutateAsync: createInvestigation } = useCreateInvestigation();
const alertStart = alert?.fields[ALERT_START];
const alertEnd = alert?.fields[ALERT_END];
const createOrOpenInvestigation = async () => {
if (!alert) return;
if (!investigations || investigations.results.length === 0) {
const paddedAlertTimeRange = getPaddedAlertTimeRange(alertStart!, alertEnd);
const investigationResponse = await createInvestigation({
investigation: {
id: uuidv4(),
title: `Investigate ${alert.fields[ALERT_RULE_CATEGORY]} breached`,
params: {
timeRange: {
from: new Date(paddedAlertTimeRange.from).getTime(),
to: new Date(paddedAlertTimeRange.to).getTime(),
},
},
origin: {
type: 'alert',
id: alert.fields[ALERT_UUID],
},
},
});
navigateToApp('investigate', { path: `/${investigationResponse.id}`, replace: false });
} else {
navigateToApp('investigate', {
path: `/${investigations.results[0].id}`,
replace: false,
});
}
};
return (
<>
<EuiFlexGroup direction="row" gutterSize="s" justifyContent="flexEnd">
{Boolean(investigatePlugin) &&
alert?.fields[ALERT_RULE_TYPE_ID] === OBSERVABILITY_THRESHOLD_RULE_TYPE_ID && (
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => {
createOrOpenInvestigation();
}}
fill
data-test-subj="investigate-alert-button"
>
<EuiText size="s">
{i18n.translate('xpack.observability.alertDetails.investigateAlert', {
defaultMessage:
!investigations || investigations.results.length === 0
? 'Start investigation'
: 'Ongoing investigation',
})}
</EuiText>
</EuiButton>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton
fill

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { QueryKey, useMutation } from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import {
CreateInvestigationInput,
CreateInvestigationResponse,
} from '@kbn/investigate-plugin/common';
import { FindInvestigationsResponse } from '@kbn/investigate-plugin/common';
import { useKibana } from '../../../utils/kibana_react';
type ServerError = IHttpFetchError<ResponseErrorBody>;
export function useCreateInvestigation() {
const {
http,
notifications: { toasts },
} = useKibana().services;
return useMutation<
CreateInvestigationResponse,
ServerError,
{ investigation: CreateInvestigationInput },
{ previousData?: FindInvestigationsResponse; queryKey?: QueryKey }
>(
['createInvestigation'],
({ investigation }) => {
const body = JSON.stringify(investigation);
return http.post<CreateInvestigationResponse>(`/api/observability/investigations`, { body });
},
{
onError: (error, { investigation }, context) => {
toasts.addError(new Error(error.body?.message ?? error.message), {
title: i18n.translate('xpack.observability.create.errorNotification', {
defaultMessage: 'Something went wrong while creating investigation',
}),
});
},
}
);
}

View file

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useQuery } from '@tanstack/react-query';
import { FindInvestigationsResponse } from '@kbn/investigate-plugin/common';
import { useKibana } from '../../../utils/kibana_react';
export interface InvestigationsByAlertParams {
alertId: string;
}
export interface UseFetchInvestigationsByAlertResponse {
isInitialLoading: boolean;
isLoading: boolean;
isRefetching: boolean;
isSuccess: boolean;
isError: boolean;
data: FindInvestigationsResponse | undefined;
}
export function useFetchInvestigationsByAlert({
alertId,
}: InvestigationsByAlertParams): UseFetchInvestigationsByAlertResponse {
const {
http,
notifications: { toasts },
investigate: investigatePlugin,
} = useKibana().services;
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
queryKey: ['fetchInvestigationsByAlert', alertId],
queryFn: async ({ signal }) => {
return await http.get<FindInvestigationsResponse>('/api/observability/investigations', {
query: { alertId },
version: '2023-10-31',
signal,
});
},
cacheTime: 0,
refetchOnWindowFocus: false,
retry: (failureCount, error) => {
if (String(error) === 'Error: Forbidden') {
return false;
}
return failureCount < 3;
},
onError: (error: Error) => {
toasts.addError(error, {
title: 'Something went wrong while fetching Investigations',
});
},
enabled: Boolean(investigatePlugin),
});
return {
data,
isInitialLoading,
isLoading,
isRefetching,
isSuccess,
isError,
};
}

View file

@ -70,6 +70,7 @@ import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
import { LicenseManagementUIPluginSetup } from '@kbn/license-management-plugin/public';
import { InvestigatePublicStart } from '@kbn/investigate-plugin/public';
import { observabilityAppId, observabilityFeatureId } from '../common';
import {
ALERTS_PATH,
@ -161,6 +162,7 @@ export interface ObservabilityPublicPluginsStart {
theme: CoreStart['theme'];
dataViewFieldEditor: DataViewFieldEditorStart;
toastNotifications: ToastsStart;
investigate?: InvestigatePublicStart;
}
export type ObservabilityPublicStart = ReturnType<Plugin['start']>;

View file

@ -110,6 +110,7 @@
"@kbn/license-management-plugin",
"@kbn/observability-alerting-rule-utils",
"@kbn/core-ui-settings-server-mocks",
"@kbn/investigate-plugin",
],
"exclude": [
"target/**/*"