Skip to content

Commit

Permalink
[Inventory][ECO] Entity type Remove Control groups filter (elastic#20…
Browse files Browse the repository at this point in the history
…2177)

closes elastic#201584

- Removes control group entity types filter
- Adds multi-select entity types filer
- Add kuery to url
- Remove unified entities page
- Adding telemetry when entity type is filtered
- Refactoring...



https://github.com/user-attachments/assets/98fb11ab-76e7-497b-af86-86378c6bfd7f

---------

Co-authored-by: Irene Blanco <[email protected]>
  • Loading branch information
cauemarcondes and iblancof authored Dec 2, 2024
1 parent 10f5056 commit d8f3f4c
Show file tree
Hide file tree
Showing 41 changed files with 716 additions and 934 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class EntitiesSynthtraceKibanaClient {
});
const entityDefinition: EntityDefinitionResponse = await response.json();

const hasEntityDefinitionsInstalled = entityDefinition.definitions.find(
const hasEntityDefinitionsInstalled = entityDefinition.definitions?.find(
(definition) => definition.type === 'service'
)?.state.installed;

Expand Down
45 changes: 0 additions & 45 deletions x-pack/plugins/observability_solution/inventory/common/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
* 2.0.
*/
import { ENTITY_LATEST, entitiesAliasPattern, type EntityMetadata } from '@kbn/entities-schema';
import { decode, encode } from '@kbn/rison';
import { isRight } from 'fp-ts/lib/Either';
import * as t from 'io-ts';

export const entityColumnIdsRt = t.union([
Expand All @@ -19,49 +17,6 @@ export const entityColumnIdsRt = t.union([

export type EntityColumnIds = t.TypeOf<typeof entityColumnIdsRt>;

export const entityViewRt = t.union([t.literal('unified'), t.literal('grouped')]);

const paginationRt = t.record(t.string, t.number);
export const entityPaginationRt = new t.Type<Record<string, number> | undefined, string, unknown>(
'entityPaginationRt',
paginationRt.is,
(input, context) => {
switch (typeof input) {
case 'string': {
try {
const decoded = decode(input);
const validation = paginationRt.decode(decoded);
if (isRight(validation)) {
return t.success(validation.right);
}

return t.failure(input, context);
} catch (e) {
return t.failure(input, context);
}
}

case 'undefined':
return t.success(input);

default: {
const validation = paginationRt.decode(input);

if (isRight(validation)) {
return t.success(validation.right);
}

return t.failure(input, context);
}
}
},
(o) => encode(o)
);

export type EntityView = t.TypeOf<typeof entityViewRt>;

export type EntityPagination = t.TypeOf<typeof entityPaginationRt>;

export const defaultEntitySortField: EntityColumnIds = 'alertsCount';

export const MAX_NUMBER_OF_ENTITIES = 500;
Expand Down
60 changes: 60 additions & 0 deletions x-pack/plugins/observability_solution/inventory/common/rt_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 { decode, encode } from '@kbn/rison';
import { isRight } from 'fp-ts/lib/Either';
import * as t from 'io-ts';

const validate = (validationRt: t.Any) => (input: unknown, context: t.Context) => {
switch (typeof input) {
case 'string': {
try {
const decoded = decode(input);
const validation = validationRt.decode(decoded);
if (isRight(validation)) {
return t.success(validation.right);
}

return t.failure(input, context);
} catch (e) {
return t.failure(input, context);
}
}

case 'undefined':
return t.success(input);

default: {
const validation = validationRt.decode(input);

if (isRight(validation)) {
return t.success(validation.right);
}

return t.failure(input, context);
}
}
};

const entityTypeCheckOptions = t.union([t.literal('on'), t.literal('off'), t.literal('mixed')]);
export type EntityTypeCheckOptions = t.TypeOf<typeof entityTypeCheckOptions>;

const entityTypeRt = t.record(t.string, entityTypeCheckOptions);
export type EntityType = t.TypeOf<typeof entityTypeRt>;
export const entityTypesRt = new t.Type<
Record<string, 'on' | 'off' | 'mixed'> | undefined,
string,
unknown
>('entityTypesRt', entityTypeRt.is, validate(entityTypeRt), (o) => encode(o));

const paginationRt = t.record(t.string, t.number);
export type EntityPagination = t.TypeOf<typeof entityPaginationRt>;
export const entityPaginationRt = new t.Type<Record<string, number> | undefined, string, unknown>(
'entityPaginationRt',
paginationRt.is,
validate(paginationRt),
(o) => encode(o)
);
Original file line number Diff line number Diff line change
Expand Up @@ -80,25 +80,6 @@ describe('Home page', () => {
cy.contains('foo');
});

it('Shows inventory page with unified view of entities', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities');
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.contains('Group entities by: Type');
cy.getByTestSubj('groupSelectorDropdown').click();
cy.getByTestSubj('panelUnified').click();
cy.wait('@getEntities');
cy.contains('server1');
cy.contains('host');
cy.contains('synth-node-trace-logs');
cy.contains('service');
cy.contains('foo');
cy.contains('container');
});

it('Navigates to apm when clicking on a service type entity', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
Expand Down Expand Up @@ -148,69 +129,69 @@ describe('Home page', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.intercept('POST', 'internal/controls/optionsList/entities-*-latest').as(
'entityTypeControlGroupOptions'
);
cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities');
cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes');
cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups');
cy.visitKibana('/app/inventory');
cy.wait('@getEntitiesTypes');
cy.wait('@getEEMStatus');
cy.getByTestSubj('optionsList-control-entity.type').click();
cy.wait('@entityTypeControlGroupOptions');
cy.getByTestSubj('optionsList-control-selection-service').click();
cy.getByTestSubj('entityTypes_multiSelect_filter').click();
cy.getByTestSubj('entityTypes_multiSelect_filter_selection_service').click();
cy.wait('@getGroups');
cy.getByTestSubj('inventoryGroupTitle_entity.type_service').click();
cy.wait('@getEntities');
cy.get('server1').should('not.exist');
cy.contains('synth-node-trace-logs');
cy.contains('foo').should('not.exist');
cy.getByTestSubj('entityTypes_multiSelect_filter').click();
cy.getByTestSubj('entityTypes_multiSelect_filter_selection_service').click();
cy.getByTestSubj('inventoryGroupTitle_entity.type_service').should('not.exist');
});

it('Filters entities by host type', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.intercept('POST', 'internal/controls/optionsList/entities-*-latest').as(
'entityTypeControlGroupOptions'
);
cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities');
cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes');
cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups');
cy.visitKibana('/app/inventory');
cy.wait('@getEntitiesTypes');
cy.wait('@getEEMStatus');
cy.getByTestSubj('optionsList-control-entity.type').click();
cy.wait('@entityTypeControlGroupOptions');
cy.getByTestSubj('optionsList-control-selection-host').click();
cy.getByTestSubj('entityTypes_multiSelect_filter').click();
cy.getByTestSubj('entityTypes_multiSelect_filter_selection_host').click();
cy.wait('@getGroups');
cy.getByTestSubj('inventoryGroupTitle_entity.type_host').click();
cy.wait('@getEntities');
cy.contains('server1');
cy.contains('synth-node-trace-logs').should('not.exist');
cy.contains('foo').should('not.exist');
cy.getByTestSubj('entityTypes_multiSelect_filter').click();
cy.getByTestSubj('entityTypes_multiSelect_filter_selection_host').click();
cy.getByTestSubj('inventoryGroupTitle_entity.type_host').should('not.exist');
});

it('Filters entities by container type', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.intercept('POST', 'internal/controls/optionsList/entities-*-latest').as(
'entityTypeControlGroupOptions'
);
cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities');
cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes');
cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups');
cy.visitKibana('/app/inventory');
cy.wait('@getEntitiesTypes');
cy.wait('@getEEMStatus');
cy.getByTestSubj('optionsList-control-entity.type').click();
cy.wait('@entityTypeControlGroupOptions');
cy.getByTestSubj('optionsList-control-selection-container').click();
cy.getByTestSubj('entityTypes_multiSelect_filter').click();
cy.getByTestSubj('entityTypes_multiSelect_filter_selection_container').click();
cy.wait('@getGroups');
cy.getByTestSubj('inventoryGroupTitle_entity.type_container').click();
cy.wait('@getEntities');
cy.contains('server1').should('not.exist');
cy.contains('synth-node-trace-logs').should('not.exist');
cy.contains('foo');
cy.getByTestSubj('entityTypes_multiSelect_filter').click();
cy.getByTestSubj('entityTypes_multiSelect_filter_selection_container').click();
cy.getByTestSubj('inventoryGroupTitle_entity.type_container').should('not.exist');
});

it('Navigates to discover with actions button in the entities list', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,31 @@ Cypress.Commands.add('loginAsSuperUser', () => {
Cypress.Commands.add(
'loginAs',
({ username, password }: { username: string; password: string }) => {
const kibanaUrl = Cypress.env('KIBANA_URL');
cy.log(`Logging in as ${username} on ${kibanaUrl}`);
cy.visit('/');
cy.request({
log: true,
method: 'POST',
url: `${kibanaUrl}/internal/security/login`,
body: {
providerType: 'basic',
providerName: 'basic',
currentURL: `${kibanaUrl}/login`,
params: { username, password },
cy.session(
username,
() => {
const kibanaUrl = Cypress.env('KIBANA_URL');
cy.log(`Logging in as ${username} on ${kibanaUrl}`);
cy.visit('/');
cy.request({
log: true,
method: 'POST',
url: `${kibanaUrl}/internal/security/login`,
body: {
providerType: 'basic',
providerName: 'basic',
currentURL: `${kibanaUrl}/login`,
params: { username, password },
},
headers: {
'kbn-xsrf': 'e2e_test',
},
});
cy.visit('/');
},
headers: {
'kbn-xsrf': 'e2e_test',
},
});
cy.visit('/');
{
cacheAcrossSpecs: true,
}
);
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"ruleRegistry",
"share"
],
"requiredBundles": ["kibanaReact","controls"],
"requiredBundles": ["kibanaReact"],
"optionalPlugins": ["spaces", "cloud"],
"extraPublicDirs": []
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,33 @@ describe('BadgeFilterWithPopover', () => {
});

it('opens the popover when the badge is clicked', () => {
render(<BadgeFilterWithPopover field={field} value={value} />);
render(<BadgeFilterWithPopover field={field} value={value} onFilter={jest.fn()} />);
expect(screen.queryByTestId(popoverContentDataTestId)).not.toBeInTheDocument();
fireEvent.click(screen.getByText(value));
expect(screen.queryByTestId(popoverContentDataTestId)).toBeInTheDocument();
expect(screen.queryByTestId(popoverContentTitleTestId)?.textContent).toBe(`${field}:${value}`);
});

it('copies value to clipboard when the "Copy value" button is clicked', () => {
render(<BadgeFilterWithPopover field={field} value={value} />);
render(<BadgeFilterWithPopover field={field} value={value} onFilter={jest.fn()} />);
fireEvent.click(screen.getByText(value));
fireEvent.click(screen.getByTestId('inventoryBadgeFilterWithPopoverCopyValueButton'));
expect(copyToClipboard).toHaveBeenCalledWith(value);
});

it('Filter for an entity', () => {
const handleFilter = jest.fn();
render(<BadgeFilterWithPopover field={field} value={value} onFilter={handleFilter} />);
fireEvent.click(screen.getByText(value));
fireEvent.click(screen.getByTestId('inventoryBadgeFilterWithPopoverFilterForButton'));
expect(handleFilter).toHaveBeenCalledWith(value, 'on');
});

it('Filter out an entity', () => {
const handleFilter = jest.fn();
render(<BadgeFilterWithPopover field={field} value={value} onFilter={handleFilter} />);
fireEvent.click(screen.getByText(value));
fireEvent.click(screen.getByTestId('inventoryBadgeFilterWithPopoverFilterOutButton'));
expect(handleFilter).toHaveBeenCalledWith(value, 'off');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,18 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
import React, { useState } from 'react';
import { useUnifiedSearchContext } from '../../hooks/use_unified_search_context';
import type { EntityTypeCheckOptions } from '../../../common/rt_types';

interface Props {
field: string;
value: string;
onFilter: (value: string, checked: EntityTypeCheckOptions) => void;
}

export function BadgeFilterWithPopover({ field, value }: Props) {
export function BadgeFilterWithPopover({ field, value, onFilter }: Props) {
const [isOpen, setIsOpen] = useState(false);
const theme = useEuiTheme();
const { addFilter } = useUnifiedSearchContext();

return (
<EuiPopover
Expand Down Expand Up @@ -82,7 +81,7 @@ export function BadgeFilterWithPopover({ field, value }: Props) {
data-test-subj="inventoryBadgeFilterWithPopoverFilterForButton"
iconType="plusInCircle"
onClick={() => {
addFilter({ fieldName: ENTITY_TYPE, operation: '+', value });
onFilter(value, 'on');
}}
>
{i18n.translate('xpack.inventory.badgeFilterWithPopover.filterForButtonEmptyLabel', {
Expand All @@ -92,10 +91,10 @@ export function BadgeFilterWithPopover({ field, value }: Props) {
</EuiFlexItem>
<EuiFlexItem>
<EuiButtonEmpty
data-test-subj="inventoryBadgeFilterWithPopoverFilterForButton"
data-test-subj="inventoryBadgeFilterWithPopoverFilterOutButton"
iconType="minusInCircle"
onClick={() => {
addFilter({ fieldName: ENTITY_TYPE, operation: '-', value });
onFilter(value, 'off');
}}
>
{i18n.translate('xpack.inventory.badgeFilterWithPopover.filterForButtonEmptyLabel', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const Grid: Story<EntityGridStoriesArgs> = (args) => {
onChangePage={setPageIndex}
onChangeSort={setSort}
pageIndex={pageIndex}
onFilterByType={() => {}}
/>
</EuiFlexItem>
</EuiFlexGroup>
Expand All @@ -99,6 +100,7 @@ export const EmptyGrid: Story<EntityGridStoriesArgs> = (args) => {
onChangePage={setPageIndex}
onChangeSort={setSort}
pageIndex={pageIndex}
onFilterByType={() => {}}
/>
);
};
Expand Down
Loading

0 comments on commit d8f3f4c

Please sign in to comment.