Skip to content

Commit

Permalink
[Inventory][ECO] Entities page search bar (elastic#193546)
Browse files Browse the repository at this point in the history
closes elastic#192329

https://github.com/user-attachments/assets/eb4e7aa6-14dd-48fb-a076-98ceec9cb335

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Carlos Crespo <[email protected]>
(cherry picked from commit 6a0fa96)
  • Loading branch information
cauemarcondes committed Sep 23, 2024
1 parent e3723fe commit c82c41a
Show file tree
Hide file tree
Showing 24 changed files with 626 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
*/

import { coreMock } from '@kbn/core/public/mocks';
import { EntityManagerPublicPluginStart } from '@kbn/entityManager-plugin/public';
import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { EntityManagerPublicPluginStart } from '@kbn/entityManager-plugin/public';
import type { InferencePublicStart } from '@kbn/inference-plugin/public';
import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { InventoryKibanaContext } from '../public/hooks/use_kibana';
import type { ITelemetryClient } from '../public/services/telemetry/types';
Expand All @@ -23,6 +26,9 @@ export function getMockInventoryContext(): InventoryKibanaContext {
inference: {} as unknown as InferencePublicStart,
share: {} as unknown as SharePluginStart,
telemetry: {} as unknown as ITelemetryClient,
unifiedSearch: {} as unknown as UnifiedSearchPublicPluginStart,
dataViews: {} as unknown as DataViewsPublicPluginStart,
data: {} as unknown as DataPublicPluginStart,
inventoryAPIClient: {
fetch: jest.fn(),
stream: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/
import React, { ComponentType, useMemo } from 'react';
import { InventoryContextProvider } from '../public/components/inventory_context_provider';
import { InventoryContextProvider } from '../public/context/inventory_context_provider';
import { getMockInventoryContext } from './get_mock_inventory_context';

export function KibanaReactStorybookDecorator(Story: ComponentType) {
Expand Down
30 changes: 30 additions & 0 deletions x-pack/plugins/observability_solution/inventory/common/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/
import * as t from 'io-ts';
import { ENTITY_LATEST, entitiesAliasPattern } from '@kbn/entities-schema';
import { isRight } from 'fp-ts/lib/Either';

export const entityTypeRt = t.union([
t.literal('service'),
Expand All @@ -15,3 +17,31 @@ export const entityTypeRt = t.union([
export type EntityType = t.TypeOf<typeof entityTypeRt>;

export const MAX_NUMBER_OF_ENTITIES = 500;

export const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({
type: '*',
dataset: ENTITY_LATEST,
});

const entityArrayRt = t.array(entityTypeRt);
export const entityTypesRt = new t.Type<EntityType[], string, unknown>(
'entityTypesRt',
entityArrayRt.is,
(input, context) => {
if (typeof input === 'string') {
const arr = input.split(',');
const validation = entityArrayRt.decode(arr);
if (isRight(validation)) {
return t.success(validation.right);
}
} else if (Array.isArray(input)) {
const validation = entityArrayRt.decode(input);
if (isRight(validation)) {
return t.success(validation.right);
}
}

return t.failure(input, context);
},
(arr) => arr.join()
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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 { isLeft, isRight } from 'fp-ts/lib/Either';
import { type EntityType, entityTypesRt } from './entities';

const validate = (input: unknown) => entityTypesRt.decode(input);

describe('entityTypesRt codec', () => {
it('should validate a valid string of entity types', () => {
const input = 'service,host,container';
const result = validate(input);
expect(isRight(result)).toBe(true);
if (isRight(result)) {
expect(result.right).toEqual(['service', 'host', 'container']);
}
});

it('should validate a valid array of entity types', () => {
const input = ['service', 'host', 'container'];
const result = validate(input);
expect(isRight(result)).toBe(true);
if (isRight(result)) {
expect(result.right).toEqual(['service', 'host', 'container']);
}
});

it('should fail validation when the string contains invalid entity types', () => {
const input = 'service,invalidType,host';
const result = validate(input);
expect(isLeft(result)).toBe(true);
});

it('should fail validation when the array contains invalid entity types', () => {
const input = ['service', 'invalidType', 'host'];
const result = validate(input);
expect(isLeft(result)).toBe(true);
});

it('should fail validation when input is not a string or array', () => {
const input = 123;
const result = validate(input);
expect(isLeft(result)).toBe(true);
});

it('should fail validation when the array contains non-string elements', () => {
const input = ['service', 123, 'host'];
const result = validate(input);
expect(isLeft(result)).toBe(true);
});

it('should fail validation an empty string', () => {
const input = '';
const result = validate(input);
expect(isLeft(result)).toBe(true);
});

it('should validate an empty array as valid', () => {
const input: unknown[] = [];
const result = validate(input);
expect(isRight(result)).toBe(true);
if (isRight(result)) {
expect(result.right).toEqual([]);
}
});

it('should fail validation when the string contains only commas', () => {
const input = ',,,';
const result = validate(input);
expect(isLeft(result)).toBe(true);
});

it('should fail validation for partial valid entities in a string', () => {
const input = 'service,invalidType';
const result = validate(input);
expect(isLeft(result)).toBe(true);
});

it('should fail validation for partial valid entities in an array', () => {
const input = ['service', 'invalidType'];
const result = validate(input);
expect(isLeft(result)).toBe(true);
});

it('should serialize a valid array back to a string', () => {
const input: EntityType[] = ['service', 'host'];
const serialized = entityTypesRt.encode(input);
expect(serialized).toBe('service,host');
});

it('should serialize an empty array back to an empty string', () => {
const input: EntityType[] = [];
const serialized = entityTypesRt.encode(input);
expect(serialized).toBe('');
});
});
2 changes: 2 additions & 0 deletions x-pack/plugins/observability_solution/inventory/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"entityManager",
"inference",
"dataViews",
"unifiedSearch",
"data",
"share"
],
"requiredBundles": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@
* 2.0.
*/

import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { type AppMountParameters, type CoreStart } from '@kbn/core/public';
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { InventoryContextProvider } from '../inventory_context_provider';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import React from 'react';
import { InventoryContextProvider } from '../../context/inventory_context_provider';
import { InventorySearchBarContextProvider } from '../../context/inventory_search_bar_context_provider';
import { inventoryRouter } from '../../routes/config';
import { HeaderActionMenuItems } from './header_action_menu';
import { InventoryStartDependencies } from '../../types';
import { InventoryServices } from '../../services/types';
import { InventoryStartDependencies } from '../../types';
import { HeaderActionMenuItems } from './header_action_menu';

export function AppRoot({
coreStart,
Expand All @@ -38,10 +39,12 @@ export function AppRoot({
return (
<InventoryContextProvider context={context}>
<RedirectAppLinks coreStart={coreStart}>
<RouterProvider history={history} router={inventoryRouter}>
<RouteRenderer />
<InventoryHeaderActionMenu appMountParameters={appMountParameters} />
</RouterProvider>
<InventorySearchBarContextProvider>
<RouterProvider history={history} router={inventoryRouter}>
<RouteRenderer />
<InventoryHeaderActionMenu appMountParameters={appMountParameters} />
</RouterProvider>
</InventorySearchBarContextProvider>
</RedirectAppLinks>
</InventoryContextProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
ENTITY_TYPE,
} from '../../../common/es_fields/entities';
import { APIReturnType } from '../../api';
import { getEntityTypeLabel } from '../../utils/get_entity_type_label';
import { EntityType } from '../../../common/entities';

type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>;

Expand Down Expand Up @@ -139,7 +141,11 @@ export function EntitiesGrid({
const columnEntityTableId = columnId as EntityColumnIds;
switch (columnEntityTableId) {
case ENTITY_TYPE:
return <EuiBadge color="hollow">{entity[columnEntityTableId]}</EuiBadge>;
return (
<EuiBadge color="hollow">
{getEntityTypeLabel(entity[columnEntityTableId] as EntityType)}
</EuiBadge>
);
case ENTITY_LAST_SEEN:
return (
<FormattedMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useKibana } from '../../hooks/use_kibana';
import { SearchBar } from '../search_bar';
import { getEntityManagerEnablement } from './no_data_config';
import { useEntityManager } from '../../hooks/use_entity_manager';
import { Welcome } from '../entity_enablement/welcome_modal';
Expand Down Expand Up @@ -43,10 +45,17 @@ export function InventoryPageTemplate({ children }: { children: React.ReactNode
}),
}}
>
{children}
{showWelcomedModal ? (
<Welcome onClose={toggleWelcomedModal} onConfirm={toggleWelcomedModal} />
) : null}
<EuiFlexGroup direction="column">
<EuiFlexItem>
<SearchBar />
</EuiFlexItem>
<EuiFlexItem>
{children}
{showWelcomedModal ? (
<Welcome onClose={toggleWelcomedModal} onConfirm={toggleWelcomedModal} />
) : null}
</EuiFlexItem>
</EuiFlexGroup>
</ObservabilityPageTemplate>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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 { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { EntityType } from '../../../common/entities';
import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async';
import { useInventoryParams } from '../../hooks/use_inventory_params';
import { useKibana } from '../../hooks/use_kibana';
import { getEntityTypeLabel } from '../../utils/get_entity_type_label';

interface Props {
onChange: (entityTypes: EntityType[]) => void;
}

const toComboBoxOption = (entityType: EntityType): EuiComboBoxOptionOption<EntityType> => ({
key: entityType,
label: getEntityTypeLabel(entityType),
});

export function EntityTypesControls({ onChange }: Props) {
const {
query: { entityTypes = [] },
} = useInventoryParams('/*');

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

const { value, loading } = useInventoryAbortableAsync(
({ signal }) => {
return inventoryAPIClient.fetch('GET /internal/inventory/entities/types', { signal });
},
[inventoryAPIClient]
);

const options = value?.entityTypes.map(toComboBoxOption);
const selectedOptions = entityTypes.map(toComboBoxOption);

return (
<EuiComboBox<EntityType>
isLoading={loading}
css={css`
max-width: 325px;
`}
aria-label={i18n.translate(
'xpack.inventory.entityTypesControls.euiComboBox.accessibleScreenReaderLabel',
{ defaultMessage: 'Entity types filter' }
)}
placeholder={i18n.translate(
'xpack.inventory.entityTypesControls.euiComboBox.placeHolderLabel',
{ defaultMessage: 'Types' }
)}
options={options}
selectedOptions={selectedOptions}
onChange={(newOptions) => {
onChange(newOptions.map((option) => option.key as EntityType));
}}
isClearable
/>
);
}
Loading

0 comments on commit c82c41a

Please sign in to comment.