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

[Inventory][ECO] Entities page search bar #193546

Merged
merged 41 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
941df5c
cleaning up
cauemarcondes Sep 16, 2024
0b8b973
inventory page and grid base
cauemarcondes Sep 16, 2024
05e3991
server
cauemarcondes Sep 16, 2024
bd3ab6c
Merge remote-tracking branch 'origin/inventory-entities-list' into in…
cauemarcondes Sep 16, 2024
1b0f65c
[CI] Auto-commit changed files from 'node scripts/yarn_deduplicate'
kibanamachine Sep 16, 2024
8fc47e7
Merge remote-tracking branch 'origin/inventory-entities-list' into in…
cauemarcondes Sep 16, 2024
36e9611
fixing deps
cauemarcondes Sep 16, 2024
2803cf0
Merge remote-tracking branch 'origin/inventory-entities-list' into in…
cauemarcondes Sep 16, 2024
218b977
refactoring and adding esql
cauemarcondes Sep 17, 2024
51ee09c
entities grid
cauemarcondes Sep 18, 2024
ca2856a
Merge branch 'main' of github.com:elastic/kibana into inventory-entit…
cauemarcondes Sep 18, 2024
1a7e3d9
removing apm
cauemarcondes Sep 18, 2024
7448e2e
[CI] Auto-commit changed files from 'node scripts/yarn_deduplicate'
kibanamachine Sep 18, 2024
d99030b
fixing ci
cauemarcondes Sep 18, 2024
0def3e0
PR comments
cauemarcondes Sep 18, 2024
df5b7db
adding i18n
cauemarcondes Sep 18, 2024
ce7946d
pr comments
cauemarcondes Sep 18, 2024
caccca2
Merge branch 'inventory-entities-list-2' of github.com:cauemarcondes/…
cauemarcondes Sep 18, 2024
6ab74f1
adding tootilp to grid
cauemarcondes Sep 18, 2024
b2b47c5
tooltip impro
cauemarcondes Sep 18, 2024
6a29a5e
pr comments
cauemarcondes Sep 18, 2024
30cfb14
pr comments
cauemarcondes Sep 18, 2024
e12a14c
Merge remote-tracking branch 'origin/inventory-entities-list-2' into …
cauemarcondes Sep 18, 2024
0c1ec0d
adding unified search
cauemarcondes Sep 18, 2024
07a3c27
adding search bar and entity type filter
cauemarcondes Sep 19, 2024
3c35d2f
adding types
cauemarcondes Sep 19, 2024
aada2f8
adding kuery support
cauemarcondes Sep 20, 2024
f207e5a
Merge branch 'main' of github.com:elastic/kibana into inventory-entit…
cauemarcondes Sep 20, 2024
e749a6e
fixing query
cauemarcondes Sep 20, 2024
e7e9786
[CI] Auto-commit changed files from 'node scripts/yarn_deduplicate'
kibanamachine Sep 20, 2024
789d5b8
fixing CI
cauemarcondes Sep 20, 2024
e1bba19
Merge branch 'inventory-entities-search' of github.com:cauemarcondes/…
cauemarcondes Sep 20, 2024
88be83c
refac
cauemarcondes Sep 20, 2024
3b59659
Update x-pack/plugins/observability_solution/inventory/public/compone…
cauemarcondes Sep 20, 2024
f8b420e
fixing padding
cauemarcondes Sep 23, 2024
73ffc4b
Merge branch 'inventory-entities-search' of github.com:cauemarcondes/…
cauemarcondes Sep 23, 2024
b3d3350
Merge branch 'main' of github.com:elastic/kibana into inventory-entit…
cauemarcondes Sep 23, 2024
31ee0df
merge problems
cauemarcondes Sep 23, 2024
ddf8602
wrapping subject into a context
cauemarcondes Sep 23, 2024
777a2d9
fixing CI
cauemarcondes Sep 23, 2024
fdb3acc
Merge branch 'main' of github.com:elastic/kibana into inventory-entit…
cauemarcondes Sep 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
*/

import { coreMock } from '@kbn/core/public/mocks';
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 { 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 { InventoryKibanaContext } from '../public/hooks/use_kibana';

export function getMockInventoryContext(): InventoryKibanaContext {
Expand All @@ -19,6 +22,9 @@ export function getMockInventoryContext(): InventoryKibanaContext {
start: {
observabilityShared: {} as unknown as ObservabilitySharedPluginStart,
inference: {} as unknown as InferencePublicStart,
unifiedSearch: {} as unknown as UnifiedSearchPublicPluginStart,
dataViews: {} as unknown as DataViewsPublicPluginStart,
data: {} as unknown as DataPublicPluginStart,
},
},
services: {
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('');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
"observabilityShared",
"entityManager",
"inference",
"dataViews"
],
"requiredBundles": [
"kibanaReact"
"dataViews",
"unifiedSearch",
"data"
],
"requiredBundles": ["kibanaReact"],
"optionalPlugins": [],
"extraPublicDirs": []
}
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 @@ -6,7 +6,9 @@
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { useKibana } from '../../hooks/use_kibana';
import { SearchBar } from '../search_bar';

export function InventoryPageTemplate({ children }: { children: React.ReactNode }) {
const {
Expand All @@ -25,7 +27,12 @@ export function InventoryPageTemplate({ children }: { children: React.ReactNode
}),
}}
>
{children}
<EuiFlexGroup direction="column">
<EuiFlexItem>
<SearchBar />
</EuiFlexItem>
<EuiFlexItem>{children}</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 { useAbortableAsync } from '@kbn/observability-utils/hooks/use_abortable_async';
import React from 'react';
import { EntityType } from '../../../common/entities';
import { useKibana } from '../../hooks/use_kibana';
import { getEntityTypeLabel } from '../../utils/get_entity_type_label';
import { useInventoryParams } from '../../hooks/use_inventory_params';

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 } = useAbortableAsync(
({ 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={true}
cauemarcondes marked this conversation as resolved.
Show resolved Hide resolved
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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 { SearchBarOwnProps } from '@kbn/unified-search-plugin/public/search_bar';
import deepEqual from 'fast-deep-equal';
import React, { useCallback, useEffect } from 'react';
import { Subject } from 'rxjs';
import { i18n } from '@kbn/i18n';
import { EntityType } from '../../../common/entities';
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';

export const searchBarContentSubject$ = new Subject<{
kuery?: string;
entityTypes?: EntityType[];
refresh: boolean;
}>();
cauemarcondes marked this conversation as resolved.
Show resolved Hide resolved

export function SearchBar() {
const {
dependencies: {
start: {
unifiedSearch,
data: {
query: { queryString: queryStringService },
},
},
},
} = useKibana();

const {
query: { kuery, entityTypes },
} = useInventoryParams('/*');

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)) {
queryStringService.setQuery(query);
}

if (!query) {
queryStringService.clearQuery();
}
}, [kuery, queryStringService]);

useEffect(() => {
syncSearchBarWithUrl();
}, [syncSearchBarWithUrl]);

const handleEntityTypesChange = useCallback(
(nextEntityTypes: EntityType[]) => {
searchBarContentSubject$.next({ kuery, entityTypes: nextEntityTypes, refresh: false });
},
[kuery]
);

const handleQuerySubmit = useCallback<NonNullable<SearchBarOwnProps['onQuerySubmit']>>(
({ query }, isUpdate) => {
searchBarContentSubject$.next({
kuery: query?.query as string,
entityTypes,
refresh: !isUpdate,
});
},
[entityTypes]
);

return (
<UnifiedSearchBar
appName="Inventory"
showDatePicker={false}
showFilterBar={false}
indexPatterns={dataView ? [dataView] : undefined}
renderQueryInputAppend={() => <EntityTypesControls onChange={handleEntityTypesChange} />}
onQuerySubmit={handleQuerySubmit}
placeholder={i18n.translate('xpack.inventory.searchBar.placeholder', {
defaultMessage:
'Search for your entities by name or its metadata (e.g. entity.type : service)',
})}
/>
);
}
Loading