Skip to content

Commit b9dba5d

Browse files
authored
Merge branch '8.x' into backport/8.x/pr-198099
2 parents 8c6c6d4 + 4a27b85 commit b9dba5d

16 files changed

Lines changed: 549 additions & 21 deletions

File tree

x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
1515
import type { SharePluginStart } from '@kbn/share-plugin/public';
1616
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
1717
import type { InventoryKibanaContext } from '../public/hooks/use_kibana';
18-
import type { ITelemetryClient } from '../public/services/telemetry/types';
18+
import { ITelemetryClient } from '../public/services/telemetry/types';
1919

2020
export function getMockInventoryContext(): InventoryKibanaContext {
2121
const coreStart = coreMock.createStart();

x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,22 @@ interface EntityNameProps {
2525
}
2626

2727
export function EntityName({ entity }: EntityNameProps) {
28-
const { services } = useKibana();
28+
const {
29+
services: { telemetry, share },
30+
} = useKibana();
2931

3032
const assetDetailsLocator =
31-
services.share?.url.locators.get<AssetDetailsLocatorParams>(ASSET_DETAILS_LOCATOR_ID);
33+
share?.url.locators.get<AssetDetailsLocatorParams>(ASSET_DETAILS_LOCATOR_ID);
3234

3335
const serviceOverviewLocator =
34-
services.share?.url.locators.get<ServiceOverviewParams>('serviceOverviewLocator');
36+
share?.url.locators.get<ServiceOverviewParams>('serviceOverviewLocator');
37+
38+
const handleLinkClick = useCallback(() => {
39+
telemetry.reportEntityViewClicked({
40+
view_type: 'detail',
41+
entity_type: entity['entity.type'],
42+
});
43+
}, [entity, telemetry]);
3544

3645
const getEntityRedirectUrl = useCallback(() => {
3746
const type = entity[ENTITY_TYPE];
@@ -58,7 +67,12 @@ export function EntityName({ entity }: EntityNameProps) {
5867
}, [entity, assetDetailsLocator, serviceOverviewLocator]);
5968

6069
return (
61-
<EuiLink data-test-subj="entityNameLink" href={getEntityRedirectUrl()}>
70+
// eslint-disable-next-line @elastic/eui/href-or-on-click
71+
<EuiLink
72+
data-test-subj="entityNameLink"
73+
href={getEntityRedirectUrl()}
74+
onClick={handleLinkClick}
75+
>
6276
<EuiFlexGroup gutterSize="s" alignItems="center">
6377
<EuiFlexItem grow={0}>
6478
<EntityIcon entity={entity} />

x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,13 @@ export function EntitiesGrid({
8585
}
8686

8787
const columnEntityTableId = columnId as EntityColumnIds;
88+
const entityType = entity[ENTITY_TYPE];
89+
8890
switch (columnEntityTableId) {
8991
case 'alertsCount':
9092
return entity?.alertsCount ? <AlertsBadge entity={entity} /> : null;
9193

9294
case ENTITY_TYPE:
93-
const entityType = entity[columnEntityTableId];
9495
return (
9596
<BadgeFilterWithPopover
9697
field={ENTITY_TYPE}

x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/index.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* 2.0.
66
*/
77
import { i18n } from '@kbn/i18n';
8-
import React from 'react';
8+
import React, { useEffect } from 'react';
99
import { EuiFlexGroup, EuiFlexItem, EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';
1010
import {
1111
FeatureFeedbackButton,
@@ -18,6 +18,7 @@ import { useEntityManager } from '../../hooks/use_entity_manager';
1818
import { Welcome } from '../entity_enablement/welcome_modal';
1919
import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async';
2020
import { EmptyState } from '../empty_states/empty_state';
21+
import { useIsLoadingComplete } from '../../hooks/use_is_loading_complete';
2122

2223
const pageTitle = (
2324
<EuiFlexGroup gutterSize="s">
@@ -36,7 +37,7 @@ const INVENTORY_FEEDBACK_LINK = 'https://ela.st/feedback-new-inventory';
3637

3738
export function InventoryPageTemplate({ children }: { children: React.ReactNode }) {
3839
const {
39-
services: { observabilityShared, inventoryAPIClient, kibanaEnvironment },
40+
services: { observabilityShared, inventoryAPIClient, kibanaEnvironment, telemetry },
4041
} = useKibana();
4142

4243
const { PageTemplate: ObservabilityPageTemplate } = observabilityShared.navigation;
@@ -62,6 +63,23 @@ export function InventoryPageTemplate({ children }: { children: React.ReactNode
6263
[inventoryAPIClient]
6364
);
6465

66+
const isLoadingComplete = useIsLoadingComplete({
67+
loadingStates: [isEnablementLoading, hasDataLoading],
68+
});
69+
70+
useEffect(() => {
71+
if (isLoadingComplete) {
72+
const viewState = isEntityManagerEnabled
73+
? value.hasData
74+
? 'populated'
75+
: 'empty'
76+
: 'eem_disabled';
77+
telemetry.reportEntityInventoryViewed({
78+
view_state: viewState,
79+
});
80+
}
81+
}, [isEntityManagerEnabled, value.hasData, telemetry, isLoadingComplete]);
82+
6583
if (isEnablementLoading || hasDataLoading) {
6684
return (
6785
<ObservabilityPageTemplate

x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import { SearchBarOwnProps } from '@kbn/unified-search-plugin/public/search_bar'
99
import deepEqual from 'fast-deep-equal';
1010
import React, { useCallback, useEffect } from 'react';
1111
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
12+
import { Query } from '@kbn/es-query';
1213
import { EntityType } from '../../../common/entities';
1314
import { useInventorySearchBarContext } from '../../context/inventory_search_bar_context_provider';
1415
import { useAdHocInventoryDataView } from '../../hooks/use_adhoc_inventory_data_view';
1516
import { useInventoryParams } from '../../hooks/use_inventory_params';
1617
import { useKibana } from '../../hooks/use_kibana';
1718
import { EntityTypesControls } from './entity_types_controls';
1819
import { DiscoverButton } from './discover_button';
20+
import { getKqlFieldsWithFallback } from '../../utils/get_kql_field_names_with_fallback';
1921

2022
export function SearchBar() {
2123
const { searchBarContentSubject$ } = useInventorySearchBarContext();
@@ -25,6 +27,7 @@ export function SearchBar() {
2527
data: {
2628
query: { queryString: queryStringService },
2729
},
30+
telemetry,
2831
},
2932
} = useKibana();
3033

@@ -51,11 +54,41 @@ export function SearchBar() {
5154
syncSearchBarWithUrl();
5255
}, [syncSearchBarWithUrl]);
5356

57+
const registerSearchSubmittedEvent = useCallback(
58+
({
59+
searchQuery,
60+
searchIsUpdate,
61+
searchEntityTypes,
62+
}: {
63+
searchQuery?: Query;
64+
searchEntityTypes?: string[];
65+
searchIsUpdate?: boolean;
66+
}) => {
67+
telemetry.reportEntityInventorySearchQuerySubmitted({
68+
kuery_fields: getKqlFieldsWithFallback(searchQuery?.query as string),
69+
entity_types: searchEntityTypes || [],
70+
action: searchIsUpdate ? 'submit' : 'refresh',
71+
});
72+
},
73+
[telemetry]
74+
);
75+
76+
const registerEntityTypeFilteredEvent = useCallback(
77+
({ filterEntityTypes, filterKuery }: { filterEntityTypes: string[]; filterKuery?: string }) => {
78+
telemetry.reportEntityInventoryEntityTypeFiltered({
79+
entity_types: filterEntityTypes,
80+
kuery_fields: filterKuery ? getKqlFieldsWithFallback(filterKuery) : [],
81+
});
82+
},
83+
[telemetry]
84+
);
85+
5486
const handleEntityTypesChange = useCallback(
5587
(nextEntityTypes: EntityType[]) => {
5688
searchBarContentSubject$.next({ kuery, entityTypes: nextEntityTypes, refresh: false });
89+
registerEntityTypeFilteredEvent({ filterEntityTypes: nextEntityTypes, filterKuery: kuery });
5790
},
58-
[kuery, searchBarContentSubject$]
91+
[kuery, registerEntityTypeFilteredEvent, searchBarContentSubject$]
5992
);
6093

6194
const handleQuerySubmit = useCallback<NonNullable<SearchBarOwnProps['onQuerySubmit']>>(
@@ -65,8 +98,14 @@ export function SearchBar() {
6598
entityTypes,
6699
refresh: !isUpdate,
67100
});
101+
102+
registerSearchSubmittedEvent({
103+
searchQuery: query,
104+
searchEntityTypes: entityTypes,
105+
searchIsUpdate: isUpdate,
106+
});
68107
},
69-
[entityTypes, searchBarContentSubject$]
108+
[entityTypes, registerSearchSubmittedEvent, searchBarContentSubject$]
70109
);
71110

72111
return (
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { renderHook } from '@testing-library/react-hooks';
9+
import { useIsLoadingComplete } from './use_is_loading_complete';
10+
11+
describe('useIsLoadingComplete', () => {
12+
describe('initialization', () => {
13+
it('should initialize with undefined', () => {
14+
const { result } = renderHook(() => useIsLoadingComplete({ loadingStates: [false, false] }));
15+
expect(result.current).toBeUndefined();
16+
});
17+
18+
it('should handle an empty array of loadingStates', () => {
19+
const { result } = renderHook(() => useIsLoadingComplete({ loadingStates: [] }));
20+
expect(result.current).toBeUndefined();
21+
});
22+
23+
it('should handle a single loading state that is false', () => {
24+
const { result } = renderHook(() => useIsLoadingComplete({ loadingStates: [false] }));
25+
expect(result.current).toBeUndefined();
26+
});
27+
});
28+
29+
describe('loading states', () => {
30+
it('should set isLoadingComplete to false when some loadingStates are true', () => {
31+
const { result } = renderHook(() => useIsLoadingComplete({ loadingStates: [true, false] }));
32+
expect(result.current).toBe(false);
33+
});
34+
35+
it('should set isLoadingComplete to false when all loadingStates are true', () => {
36+
const { result } = renderHook(() => useIsLoadingComplete({ loadingStates: [true, true] }));
37+
expect(result.current).toBe(false);
38+
});
39+
40+
it('should handle a single loading state that is true', () => {
41+
const { result } = renderHook(() => useIsLoadingComplete({ loadingStates: [true] }));
42+
expect(result.current).toBe(false);
43+
});
44+
});
45+
46+
describe('loading completion', () => {
47+
it('should set isLoadingComplete to true when all loadingStates are false after being true', () => {
48+
const { result, rerender } = renderHook(
49+
({ loadingStates }) => useIsLoadingComplete({ loadingStates }),
50+
{
51+
initialProps: { loadingStates: [true, false] },
52+
}
53+
);
54+
55+
expect(result.current).toBe(false);
56+
57+
rerender({ loadingStates: [false, false] });
58+
59+
expect(result.current).toBe(true);
60+
});
61+
62+
it('should set isLoadingComplete to true when all loadingStates are false after being mixed', () => {
63+
const { result, rerender } = renderHook(
64+
({ loadingStates }) => useIsLoadingComplete({ loadingStates }),
65+
{
66+
initialProps: { loadingStates: [true, false] },
67+
}
68+
);
69+
70+
expect(result.current).toBe(false);
71+
72+
rerender({ loadingStates: [false, false] });
73+
74+
expect(result.current).toBe(true);
75+
});
76+
});
77+
78+
describe('mixed states', () => {
79+
it('should not change isLoadingComplete if loadingStates are mixed', () => {
80+
const { result, rerender } = renderHook(
81+
({ loadingStates }) => useIsLoadingComplete({ loadingStates }),
82+
{
83+
initialProps: { loadingStates: [true, true] },
84+
}
85+
);
86+
87+
expect(result.current).toBe(false);
88+
89+
rerender({ loadingStates: [true, false] });
90+
91+
expect(result.current).toBe(false);
92+
});
93+
94+
it('should not change isLoadingComplete if loadingStates change from all true to mixed', () => {
95+
const { result, rerender } = renderHook(
96+
({ loadingStates }) => useIsLoadingComplete({ loadingStates }),
97+
{
98+
initialProps: { loadingStates: [true, true] },
99+
}
100+
);
101+
102+
expect(result.current).toBe(false);
103+
104+
rerender({ loadingStates: [true, false] });
105+
106+
expect(result.current).toBe(false);
107+
});
108+
});
109+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { useState, useEffect } from 'react';
9+
10+
interface UseIsLoadingCompleteProps {
11+
loadingStates: boolean[];
12+
}
13+
14+
export const useIsLoadingComplete = ({ loadingStates }: UseIsLoadingCompleteProps) => {
15+
const [isLoadingComplete, setIsLoadingComplete] = useState<boolean | undefined>(undefined);
16+
17+
useEffect(() => {
18+
const someLoading = loadingStates.some((loading) => loading);
19+
const allLoaded = loadingStates.every((loading) => !loading);
20+
21+
if (isLoadingComplete === undefined && someLoading) {
22+
setIsLoadingComplete(false);
23+
} else if (isLoadingComplete === false && allLoaded) {
24+
setIsLoadingComplete(true);
25+
}
26+
}, [isLoadingComplete, loadingStates]);
27+
28+
return isLoadingComplete;
29+
};

x-pack/plugins/observability_solution/inventory/public/plugin.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class InventoryPlugin
4949
this.kibanaVersion = context.env.packageInfo.version;
5050
this.isServerlessEnv = context.env.packageInfo.buildFlavor === 'serverless';
5151
}
52+
5253
setup(
5354
coreSetup: CoreSetup<InventoryStartDependencies, InventoryPublicStart>,
5455
pluginsSetup: InventorySetupDependencies
@@ -58,6 +59,13 @@ export class InventoryPlugin
5859
'observability:entityCentricExperience',
5960
true
6061
);
62+
63+
this.telemetry.setup({
64+
analytics: coreSetup.analytics,
65+
});
66+
67+
const telemetry = this.telemetry.start();
68+
6169
const getStartServices = coreSetup.getStartServices();
6270

6371
const hideInventory$ = from(getStartServices).pipe(
@@ -105,9 +113,6 @@ export class InventoryPlugin
105113

106114
pluginsSetup.observabilityShared.navigation.registerSections(sections$);
107115

108-
this.telemetry.setup({ analytics: coreSetup.analytics });
109-
const telemetry = this.telemetry.start();
110-
111116
const isCloudEnv = !!pluginsSetup.cloud?.isCloudEnabled;
112117
const isServerlessEnv = pluginsSetup.cloud?.isServerlessEnabled || this.isServerlessEnv;
113118

0 commit comments

Comments
 (0)