Skip to content

Commit

Permalink
[Inventory][ECO] Entities table (elastic#193272)
Browse files Browse the repository at this point in the history
Real data:
<img width="1237" alt="Screenshot 2024-09-18 at 14 23 17"
src="https://github.com/user-attachments/assets/ecc496aa-1c43-4c3c-9ac8-d6e4e6cb8aad">

Storybook:
<img width="1256" alt="Screenshot 2024-09-18 at 14 23 22"
src="https://github.com/user-attachments/assets/03d9f940-7b3f-4aea-9221-42b1c07119d1">

Tooltips:
<img width="1250" alt="Screenshot 2024-09-18 at 13 49 19"
src="https://github.com/user-attachments/assets/dc99b4cc-4eba-4815-8892-8e3fe7a041bb">


- Use ESQL to fetch the top 500 entities sorted by last seen property.
- Display 20 entities per page.
- Sorting is handles by the server and saved on the URL
- Current page is saved on the URL
- Filter entities types `service`, `host` or `container`
- Filter only entities from the built in definition
- LIMITATION: The EuiGrid doesn't have an embedded loading state, for
now, I'm switching the entire view to display a loading spinner while
data is being fetched.
- PLUS: Storybook created with mock data.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
cauemarcondes and kibanamachine committed Sep 18, 2024
1 parent 5040e35 commit e3f3c68
Show file tree
Hide file tree
Showing 13 changed files with 3,501 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
*/

import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
import type { ESQLSearchResponse, ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
import { withSpan } from '@kbn/apm-utils';
import type { EsqlQueryRequest } from '@elastic/elasticsearch/lib/api/types';

type SearchRequest = ESSearchRequest & {
index: string | string[];
Expand All @@ -24,6 +25,7 @@ export interface ObservabilityElasticsearchClient {
operationName: string,
parameters: TSearchRequest
): Promise<InferSearchResponseOf<TDocument, TSearchRequest>>;
esql(operationName: string, parameters: EsqlQueryRequest): Promise<ESQLSearchResponse>;
client: ElasticsearchClient;
}

Expand All @@ -38,6 +40,26 @@ export function createObservabilityEsClient({
}): ObservabilityElasticsearchClient {
return {
client,
esql(operationName: string, parameters: EsqlQueryRequest) {
logger.trace(() => `Request (${operationName}):\n${JSON.stringify(parameters, null, 2)}`);
return withSpan({ name: operationName, labels: { plugin } }, () => {
return client.esql.query(
{ ...parameters },
{
querystring: {
drop_null_columns: true,
},
}
);
})
.then((response) => {
logger.trace(() => `Response (${operationName}):\n${JSON.stringify(response, null, 2)}`);
return response as unknown as ESQLSearchResponse;
})
.catch((error) => {
throw error;
});
},
search<TDocument = unknown, TSearchRequest extends SearchRequest = SearchRequest>(
operationName: string,
parameters: SearchRequest
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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 type { ESQLSearchResponse } from '@kbn/es-types';

export function esqlResultToPlainObjects<T extends Record<string, any>>(
result: ESQLSearchResponse
): T[] {
return result.values.map((row) => {
return row.reduce<Record<string, unknown>>((acc, value, index) => {
const column = result.columns[index];
acc[column.name] = value;
return acc;
}, {});
}) as T[];
}
29 changes: 10 additions & 19 deletions x-pack/plugins/observability_solution/inventory/common/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,14 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';

export interface LatestEntity {
agent: {
name: string[];
};
data_stream: {
type: string[];
};
cloud: {
availability_zone: string[];
};
entity: {
firstSeenTimestamp: string;
lastSeenTimestamp: string;
type: string;
displayName: string;
id: string;
identityFields: string[];
};
}
export const entityTypeRt = t.union([
t.literal('service'),
t.literal('host'),
t.literal('container'),
]);

export type EntityType = t.TypeOf<typeof entityTypeRt>;

export const MAX_NUMBER_OF_ENTITIES = 500;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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.
*/

export const ENTITY_LAST_SEEN = 'entity.lastSeenTimestamp';
export const ENTITY_ID = 'entity.id';
export const ENTITY_TYPE = 'entity.type';
export const ENTITY_DISPLAY_NAME = 'entity.displayName';
export const ENTITY_DEFINITION_ID = 'entity.definitionId';
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 { EuiDataGridSorting } from '@elastic/eui';
import { Meta, Story } from '@storybook/react';
import React, { useMemo, useState } from 'react';
import { orderBy } from 'lodash';
import { EntitiesGrid } from '.';
import { ENTITY_LAST_SEEN } from '../../../common/es_fields/entities';
import { entitiesMock } from './mock/entities_mock';

const stories: Meta<{}> = {
title: 'app/inventory/entities_grid',
component: EntitiesGrid,
};
export default stories;

export const Example: Story<{}> = () => {
const [pageIndex, setPageIndex] = useState(0);
const [sort, setSort] = useState<EuiDataGridSorting['columns'][0]>({
id: ENTITY_LAST_SEEN,
direction: 'desc',
});

const sortedItems = useMemo(
() => orderBy(entitiesMock, sort.id, sort.direction),
[sort.direction, sort.id]
);

return (
<EntitiesGrid
entities={sortedItems}
loading={false}
sortDirection={sort.direction}
sortField={sort.id}
onChangePage={setPageIndex}
onChangeSort={setSort}
pageIndex={pageIndex}
/>
);
};

export const EmptyGridExample: Story<{}> = () => {
const [pageIndex, setPageIndex] = useState(0);
const [sort, setSort] = useState<EuiDataGridSorting['columns'][0]>({
id: ENTITY_LAST_SEEN,
direction: 'desc',
});

return (
<EntitiesGrid
entities={[]}
loading={false}
sortDirection={sort.direction}
sortField={sort.id}
onChangePage={setPageIndex}
onChangeSort={setSort}
pageIndex={pageIndex}
/>
);
};
Loading

0 comments on commit e3f3c68

Please sign in to comment.