Skip to content

Commit

Permalink
[ECO][Inventory v2] Ad hoc data view: Add get entities definition end…
Browse files Browse the repository at this point in the history
…point using sources (#204026)

Closes #202298 

This PR changes the way we get the entity index patterns to v2. It
creates an endpoint part of the inventory API which returns the index
patterns by entity type.

## Testing

### Test the endpoint: 
- Open Dev tools and add
` GET kbn:/internal/inventory/entity/definitions/sources`
- Response: 


![image](https://github.com/user-attachments/assets/3346c36e-dbc2-4e56-9ed6-d3d3a8f7d1a5)


### Test in the UI
- After the previous steps add some host data (oblt cluster /
metricbeat) or use synthtrace (for example use `node scripts/synthtrace
infra_hosts_with_apm_hosts --scenarioOpts.numInstances=10` or `node
scripts/synthtrace logs_traces_hosts.ts`)
- Go to Inventory and expand the host group
- Click on the actions button for any host and click on the Discover
link
- The correct dataview should be selected based on the index patterns in
the source definition
The same can be done for other entity types
- Test the search bar as well (the suggestions should be visible) and
now we should have 1 request to get the sources (instead of doing it on
click)



https://github.com/user-attachments/assets/93b5ac6c-9d64-44e0-b26e-6133477e0840




<!--ONMERGE {"backportTargets":["8.x"]} ONMERGE-->

---------

Co-authored-by: Carlos Crespo <[email protected]>
Co-authored-by: Sergi Romeu <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
4 people authored Jan 14, 2025
1 parent 902820e commit ffccfdc
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,28 @@ export const EntityActions = ({ entity, setShowActions }: Props) => {
? `inventoryEntityActionsButton-${entity.entityDisplayName}`
: 'inventoryEntityActionsButton';

const { getDiscoverEntitiesRedirectUrl, isEntityDefinitionLoading } = useDiscoverRedirect(entity);
const { getDiscoverEntitiesRedirectUrl } = useDiscoverRedirect(entity);
const discoverUrl = getDiscoverEntitiesRedirectUrl();

const actions: React.ReactElement[] = [];
const actions = [
<EuiContextMenuItem
data-test-subj="inventoryEntityActionExploreInDiscover"
key={`exploreInDiscover-${entity.entityDisplayName}`}
color="text"
icon="discoverApp"
href={discoverUrl}
>
{i18n.translate('xpack.inventory.entityActions.exploreInDiscoverLink', {
defaultMessage: 'Explore in Discover',
})}
</EuiContextMenuItem>,
];

if (!discoverUrl && !isEntityDefinitionLoading) {
if (!discoverUrl) {
setShowActions(false);
return null;
}

if (!isEntityDefinitionLoading) {
actions.push(
<EuiContextMenuItem
data-test-subj="inventoryEntityActionExploreInDiscover"
key={`exploreInDiscover-${entity.entityDisplayName}`}
color="text"
icon="discoverApp"
href={discoverUrl}
>
{i18n.translate('xpack.inventory.entityActions.exploreInDiscoverLink', {
defaultMessage: 'Explore in Discover',
})}
</EuiContextMenuItem>
);
}

return (
<EuiPopover
isOpen={isPopoverOpen}
Expand All @@ -65,7 +61,6 @@ export const EntityActions = ({ entity, setShowActions }: Props) => {
iconType="boxesHorizontal"
color="text"
onClick={togglePopover}
isLoading={isEntityDefinitionLoading}
/>
}
closePopover={closePopover}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import React, { useCallback, useState } from 'react';
import { EntityCountBadge } from './entity_count_badge';
import { GroupedEntitiesGrid } from './grouped_entities_grid';
import { useUnifiedSearchContext } from '../../hooks/use_unified_search_context';

const ENTITIES_COUNT_BADGE = i18n.translate(
'xpack.inventory.inventoryGroupPanel.entitiesBadgeLabel',
Expand All @@ -26,10 +27,12 @@ export interface Props {
export function EntityGroupAccordion({ groupValue, groupLabel, groupCount, isLoading }: Props) {
const { euiTheme } = useEuiTheme();
const [open, setOpen] = useState(false);
const { setSingleEntityType } = useUnifiedSearchContext();

const onToggle = useCallback(() => {
if (!open) setSingleEntityType(groupValue);
setOpen((opened) => !opened);
}, []);
}, [groupValue, open, setSingleEntityType]);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,17 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import type { InventoryEntity } from '../../common/entities';
import { useAdHocDataView } from './use_adhoc_data_view';
import { useFetchEntityDefinition } from './use_fetch_entity_definition';
import { useKibana } from './use_kibana';
import { useUnifiedSearchContext } from './use_unified_search_context';

export const useDiscoverRedirect = (entity: InventoryEntity) => {
const {
services: { share, application, entityManager },
} = useKibana();
const { entityDefinitions, isEntityDefinitionLoading } = useFetchEntityDefinition(
entity.entityDefinitionId as string
);

const title = useMemo(
() =>
!isEntityDefinitionLoading && entityDefinitions && entityDefinitions?.length > 0
? entityDefinitions[0]?.indexPatterns?.join(',')
: '',
[entityDefinitions, isEntityDefinitionLoading]
);

const { dataView } = useAdHocDataView(title);

const { discoverDataview } = useUnifiedSearchContext();
const { dataView } = discoverDataview;
const discoverLocator = share.url.locators.get('DISCOVER_APP_LOCATOR');

const getDiscoverEntitiesRedirectUrl = useCallback(() => {
Expand All @@ -37,19 +24,19 @@ export const useDiscoverRedirect = (entity: InventoryEntity) => {
})
: '';

return application.capabilities.discover?.show
return application.capabilities.discover?.show || !dataView
? discoverLocator?.getRedirectUrl({
indexPatternId: dataView?.id ?? '',
dataViewId: dataView?.id ?? '',
query: { query: entityKqlFilter, language: 'kuery' },
})
: undefined;
}, [
application.capabilities.discover?.show,
dataView?.id,
dataView,
discoverLocator,
entity,
entityManager.entityClient,
]);

return { getDiscoverEntitiesRedirectUrl, isEntityDefinitionLoading };
return { getDiscoverEntitiesRedirectUrl };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 { useInventoryAbortableAsync } from './use_inventory_abortable_async';
import { useKibana } from './use_kibana';

export const useFetchEntityDefinitionIndexPattern = () => {
const {
services: { inventoryAPIClient },
} = useKibana();

const { value = { definitionIndexPatterns: {} }, loading } = useInventoryAbortableAsync(
({ signal }) => {
return inventoryAPIClient.fetch('GET /internal/inventory/entity/definitions/sources', {
signal,
});
},
[inventoryAPIClient]
);

return {
definitionIndexPatterns: value?.definitionIndexPatterns,
isIndexPatternsLoading: loading,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,73 @@
* 2.0.
*/
import createContainer from 'constate';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { Subject } from 'rxjs';
import { ENTITIES_LATEST_ALIAS } from '../../common/entities';
import { useAdHocDataView } from './use_adhoc_data_view';
import { useInventoryDecodedQueryParams } from './use_inventory_decoded_query_params';
import { useInventoryAbortableAsync } from './use_inventory_abortable_async';
import { groupEntityTypesByStatus } from '../utils/group_entity_types_by_status';
import { useKibana } from './use_kibana';
import { useInventoryParams } from './use_inventory_params';
import { useFetchEntityDefinitionIndexPattern } from './use_fetch_entity_definition_index_patterns';

function useUnifiedSearch() {
const { dataView } = useAdHocDataView(ENTITIES_LATEST_ALIAS);
const {
services: { inventoryAPIClient },
} = useKibana();
const {
query: { kuery },
} = useInventoryParams('/');
const { entityTypes } = useInventoryDecodedQueryParams();
const { definitionIndexPatterns, isIndexPatternsLoading } =
useFetchEntityDefinitionIndexPattern();
const [singleEntityType, setSingleEntityType] = useState<string>('');

const { value, refresh, loading } = useInventoryAbortableAsync(
({ signal }) => {
const { entityTypesOff, entityTypesOn } = groupEntityTypesByStatus(entityTypes);
return inventoryAPIClient.fetch('GET /internal/inventory/entities/types', {
params: {
query: {
includeEntityTypes: entityTypesOn.length ? JSON.stringify(entityTypesOn) : undefined,
excludeEntityTypes: entityTypesOff.length ? JSON.stringify(entityTypesOff) : undefined,
kuery,
},
},
signal,
});
},
[entityTypes, inventoryAPIClient, kuery]
);

const entityTypeIds = useMemo(
() => value?.entityTypes.map((entityType) => entityType.id) ?? [],
[value?.entityTypes]
);
const allDefinitionIndexPatterns = useMemo(() => {
const filteredDefinitionIndexPatterns = entityTypeIds.flatMap(
(id) => definitionIndexPatterns?.[id] ?? []
);

return Array.from(new Set(filteredDefinitionIndexPatterns)).join(',');
}, [definitionIndexPatterns, entityTypeIds]);

const { dataView } = useAdHocDataView(allDefinitionIndexPatterns ?? '');
const discoverDataview = useAdHocDataView(
(definitionIndexPatterns[singleEntityType ?? ''] ?? []).join(',') ?? ''
);

const [refreshSubject$] = useState<Subject<void>>(new Subject());

return {
dataView,
definitionIndexPatterns,
refreshSubject$,
loading: loading || isIndexPatternsLoading,
refresh,
value,
discoverDataview,
setSingleEntityType,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,11 @@ import React from 'react';
import useEffectOnce from 'react-use/lib/useEffectOnce';
import { EntitiesSummary } from '../../components/entities_summary';
import { EntityGroupAccordion } from '../../components/entity_group_accordion';
import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async';
import { useInventoryDecodedQueryParams } from '../../hooks/use_inventory_decoded_query_params';
import { useInventoryParams } from '../../hooks/use_inventory_params';
import { useKibana } from '../../hooks/use_kibana';
import { useUnifiedSearchContext } from '../../hooks/use_unified_search_context';
import { GroupBySelector } from '../../components/group_by_selector';
import { groupEntityTypesByStatus } from '../../utils/group_entity_types_by_status';

export function InventoryPage() {
const {
services: { inventoryAPIClient },
} = useKibana();
const { refreshSubject$ } = useUnifiedSearchContext();
const {
query: { kuery },
} = useInventoryParams('/');
const { entityTypes } = useInventoryDecodedQueryParams();
const { value, refresh, loading } = useInventoryAbortableAsync(
({ signal }) => {
const { entityTypesOff, entityTypesOn } = groupEntityTypesByStatus(entityTypes);
return inventoryAPIClient.fetch('GET /internal/inventory/entities/types', {
params: {
query: {
includeEntityTypes: entityTypesOn.length ? JSON.stringify(entityTypesOn) : undefined,
excludeEntityTypes: entityTypesOff.length ? JSON.stringify(entityTypesOff) : undefined,
kuery,
},
},
signal,
});
},
[entityTypes, inventoryAPIClient, kuery]
);
const { refreshSubject$, value, refresh, loading } = useUnifiedSearchContext();

useEffectOnce(() => {
const refreshSubscription = refreshSubject$.subscribe(refresh);
Expand Down
Loading

0 comments on commit ffccfdc

Please sign in to comment.