Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ObsUx][Inventory] Add actions column with link to discover for inventory #199306

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const entityColumnIdsRt = t.union([
t.literal(ENTITY_LAST_SEEN),
t.literal(ENTITY_TYPE),
t.literal('alertsCount'),
t.literal('actions'),
]);

export type EntityColumnIds = t.TypeOf<typeof entityColumnIdsRt>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,22 @@ describe('Home page', () => {
cy.getByTestSubj('inventoryGroup_entity.type_host').should('not.exist');
cy.getByTestSubj('inventoryGroup_entity.type_service').should('not.exist');
});

it('Navigates to discover with actions button in the entities list', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.contains('container');
cy.getByTestSubj('inventoryGroupTitle_entity.type_container').click();
cy.getByTestSubj('inventoryEntityActionsButton-foo').click();
cy.getByTestSubj('inventoryEntityActionOpenInDiscover').click();
cy.url().should(
'include',
"query:'container.id:%20foo%20AND%20entity.definition_id%20:%20builtin*"
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,17 @@ const entityLastSeenLabel = i18n.translate(
defaultMessage: 'Last seen',
}
);
const entityLastSeenToolip = i18n.translate(
const entityLastSeenTooltip = i18n.translate(
'xpack.inventory.entitiesGrid.euiDataGrid.lastSeenTooltip',
{
defaultMessage: 'Timestamp of last received data for entity (entity.lastSeenTimestamp)',
}
);

const entityActionsLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.actionsLabel', {
defaultMessage: 'Actions',
});

const CustomHeaderCell = ({ title, tooltipContent }: { title: string; tooltipContent: string }) => (
<>
<span>{title}</span>
Expand All @@ -68,8 +72,10 @@ const CustomHeaderCell = ({ title, tooltipContent }: { title: string; tooltipCon

export const getColumns = ({
showAlertsColumn,
showActions,
}: {
showAlertsColumn: boolean;
showActions: boolean;
}): EuiDataGridColumn[] => {
return [
...(showAlertsColumn
Expand Down Expand Up @@ -103,11 +109,24 @@ export const getColumns = ({
// keep it for accessibility purposes
displayAsText: entityLastSeenLabel,
display: (
<CustomHeaderCell title={entityLastSeenLabel} tooltipContent={entityLastSeenToolip} />
<CustomHeaderCell title={entityLastSeenLabel} tooltipContent={entityLastSeenTooltip} />
),
defaultSortDirection: 'desc',
isSortable: true,
schema: 'datetime',
},
...(showActions
? [
{
id: 'actions',
// keep it for accessibility purposes
displayAsText: entityActionsLabel,
display: (
<CustomHeaderCell title={entityActionsLabel} tooltipContent={entityActionsLabel} />
),
initialWidth: 100,
},
]
: []),
];
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { BadgeFilterWithPopover } from '../badge_filter_with_popover';
import { getColumns } from './grid_columns';
import { AlertsBadge } from '../alerts_badge/alerts_badge';
import { EntityName } from './entity_name';
import { EntityActions } from '../entity_actions';
import { useDiscoverRedirect } from '../../hooks/use_discover_redirect';

type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>;
type LatestEntities = InventoryEntitiesAPIReturnType['entities'];
Expand Down Expand Up @@ -53,6 +55,8 @@ export function EntitiesGrid({
onChangeSort,
onFilterByType,
}: Props) {
const { getDiscoverRedirectUrl } = useDiscoverRedirect();

const onSort: EuiDataGridSorting['onSort'] = useCallback(
(newSortingColumns) => {
const lastItem = last(newSortingColumns);
Expand All @@ -68,12 +72,14 @@ export function EntitiesGrid({
[entities]
);

const showActions = useMemo(() => !!getDiscoverRedirectUrl(), [getDiscoverRedirectUrl]);

const columnVisibility = useMemo(
() => ({
visibleColumns: getColumns({ showAlertsColumn }).map(({ id }) => id),
visibleColumns: getColumns({ showAlertsColumn, showActions }).map(({ id }) => id),
setVisibleColumns: () => {},
}),
[showAlertsColumn]
[showAlertsColumn, showActions]
);

const renderCellValue = useCallback(
Expand All @@ -85,6 +91,7 @@ export function EntitiesGrid({

const columnEntityTableId = columnId as EntityColumnIds;
const entityType = entity[ENTITY_TYPE];
const discoverUrl = getDiscoverRedirectUrl(entity);

switch (columnEntityTableId) {
case 'alertsCount':
Expand Down Expand Up @@ -127,11 +134,20 @@ export function EntitiesGrid({
);
case ENTITY_DISPLAY_NAME:
return <EntityName entity={entity} />;
case 'actions':
return (
discoverUrl && (
<EntityActions
discoverUrl={discoverUrl}
entityIdentifyingValue={entity[ENTITY_DISPLAY_NAME]}
/>
)
);
default:
return entity[columnId as EntityColumnIds] || '';
}
},
[entities, onFilterByType]
[entities, getDiscoverRedirectUrl, onFilterByType]
);

if (loading) {
Expand All @@ -146,7 +162,7 @@ export function EntitiesGrid({
'xpack.inventory.entitiesGrid.euiDataGrid.inventoryEntitiesGridLabel',
{ defaultMessage: 'Inventory entities grid' }
)}
columns={getColumns({ showAlertsColumn })}
columns={getColumns({ showAlertsColumn, showActions })}
columnVisibility={columnVisibility}
rowCount={entities.length}
renderCellValue={renderCellValue}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useBoolean } from '@kbn/react-hooks';

interface Props {
discoverUrl: string;
entityIdentifyingValue?: string;
}

export const EntityActions = ({ discoverUrl, entityIdentifyingValue }: Props) => {
const [isPopoverOpen, { toggle: togglePopover, off: closePopover }] = useBoolean(false);
const actionButtonTestSubject = entityIdentifyingValue
? `inventoryEntityActionsButton-${entityIdentifyingValue}`
: 'inventoryEntityActionsButton';

const actions = [
<EuiContextMenuItem
data-test-subj="inventoryEntityActionOpenInDiscover"
key={`openInDiscover-${entityIdentifyingValue}`}
color="text"
icon="discoverApp"
href={discoverUrl}
>
{i18n.translate('xpack.inventory.entityActions.discoverLink', {
defaultMessage: 'Open in discover',
})}
</EuiContextMenuItem>,
];

return (
<>
<EuiPopover
isOpen={isPopoverOpen}
panelPaddingSize="none"
anchorPosition="upCenter"
button={
<EuiButtonIcon
data-test-subj={actionButtonTestSubject}
aria-label={i18n.translate(
'xpack.inventory.entityActions.euiButtonIcon.showActionsLabel',
{ defaultMessage: 'Show actions' }
)}
iconType="boxesHorizontal"
color="text"
onClick={togglePopover}
/>
}
closePopover={closePopover}
>
<EuiContextMenuPanel items={actions} size="s" />
</EuiPopover>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,56 +7,16 @@

import { EuiButton } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/public';
import { buildPhrasesFilter, PhrasesFilter } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';

import {
ENTITY_DEFINITION_ID,
ENTITY_DISPLAY_NAME,
ENTITY_LAST_SEEN,
ENTITY_TYPE,
} from '@kbn/observability-shared-plugin/common';
import { EntityColumnIds } from '../../../common/entities';
import { useInventoryParams } from '../../hooks/use_inventory_params';
import { useKibana } from '../../hooks/use_kibana';

const ACTIVE_COLUMNS: EntityColumnIds[] = [ENTITY_DISPLAY_NAME, ENTITY_TYPE, ENTITY_LAST_SEEN];
import React from 'react';
import { useDiscoverRedirect } from '../../hooks/use_discover_redirect';

export function DiscoverButton({ dataView }: { dataView: DataView }) {
const {
services: { share, application },
} = useKibana();
const {
query: { kuery, entityTypes },
} = useInventoryParams('/*');

const discoverLocator = useMemo(
() => share.url.locators.get('DISCOVER_APP_LOCATOR'),
[share.url.locators]
);

const filters: PhrasesFilter[] = [];

const entityTypeField = dataView.getFieldByName(ENTITY_TYPE);

if (entityTypes && entityTypeField) {
const entityTypeFilter = buildPhrasesFilter(entityTypeField, entityTypes, dataView);
filters.push(entityTypeFilter);
}

const kueryWithEntityDefinitionFilters = [kuery, `${ENTITY_DEFINITION_ID} : builtin*`]
.filter(Boolean)
.join(' AND ');
const { getDiscoverRedirectUrl } = useDiscoverRedirect();

const discoverLink = discoverLocator?.getRedirectUrl({
indexPatternId: dataView?.id ?? '',
columns: ACTIVE_COLUMNS,
query: { query: kueryWithEntityDefinitionFilters, language: 'kuery' },
filters,
});
const discoverLink = getDiscoverRedirectUrl();

if (!application.capabilities.discover?.show || !discoverLink) {
if (!discoverLink) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ import React, { useCallback, useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Query } from '@kbn/es-query';
import { useInventorySearchBarContext } from '../../context/inventory_search_bar_context_provider';
import { useAdHocInventoryDataView } from '../../hooks/use_adhoc_inventory_data_view';
import { useInventoryParams } from '../../hooks/use_inventory_params';
import { useKibana } from '../../hooks/use_kibana';
import { EntityTypesControls } from './entity_types_controls';
import { DiscoverButton } from './discover_button';
import { getKqlFieldsWithFallback } from '../../utils/get_kql_field_names_with_fallback';

export function SearchBar() {
const { searchBarContentSubject$, refreshSubject$ } = useInventorySearchBarContext();
const { refreshSubject$, searchBarContentSubject$, dataView } = useInventorySearchBarContext();
const {
services: {
unifiedSearch,
Expand All @@ -36,8 +35,6 @@ export function SearchBar() {

const { SearchBar: UnifiedSearchBar } = unifiedSearch.ui;

const { dataView } = useAdHocInventoryDataView();

const syncSearchBarWithUrl = useCallback(() => {
const query = kuery ? { query: kuery, language: 'kuery' } : undefined;
if (query && !deepEqual(queryStringService.getQuery(), query)) {
Expand Down Expand Up @@ -107,7 +104,7 @@ export function SearchBar() {
refreshSubject$.next();
}
},
[entityTypes, registerSearchSubmittedEvent, searchBarContentSubject$, refreshSubject$]
[searchBarContentSubject$, entityTypes, registerSearchSubmittedEvent, refreshSubject$]
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
*/
import React, { createContext, useContext, type ReactChild } from 'react';
import { Subject } from 'rxjs';
import { DataView } from '@kbn/data-views-plugin/common';
import { useAdHocInventoryDataView } from '../../hooks/use_adhoc_inventory_data_view';

interface InventorySearchBarContextType {
searchBarContentSubject$: Subject<{
kuery?: string;
entityTypes?: string[];
}>;
refreshSubject$: Subject<void>;
dataView?: DataView;
}

const InventorySearchBarContext = createContext<InventorySearchBarContextType>({
Expand All @@ -21,9 +24,14 @@ const InventorySearchBarContext = createContext<InventorySearchBarContextType>({
});

export function InventorySearchBarContextProvider({ children }: { children: ReactChild }) {
const { dataView } = useAdHocInventoryDataView();
return (
<InventorySearchBarContext.Provider
value={{ searchBarContentSubject$: new Subject(), refreshSubject$: new Subject() }}
value={{
searchBarContentSubject$: new Subject(),
refreshSubject$: new Subject(),
dataView,
}}
>
{children}
</InventorySearchBarContext.Provider>
Expand Down
Loading