mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Inventory][ECO] Replace `Entity` with `InventoryEntityLatest` type (#198760)](https://github.com/elastic/kibana/pull/198760) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Carlos Crespo","email":"crespocarlos@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-11-12T17:47:00Z","message":"[Inventory][ECO] Replace `Entity` with `InventoryEntityLatest` type (#198760)\n\ncloses [#198758](https://github.com/elastic/kibana/issues/198758)\r\n\r\n## Summary\r\n\r\nThis PR removes the Entity type used across the Inventory and replaces\r\nit with `InventoryEntityLatest`, which provides strong typing for the\r\nlatest entity object. This change makes the code leverage TypeScript’s\r\nintellisense and autocompletion in the editor, making the code easier to\r\nwork with and more maintainable across the codebase.\r\n\r\n`InventoryEntityLatest` is the interface that the API returns and what\r\nthe UI consumes. Note that this is distinct from the index mapping\r\ndefined by `entityLatestSchema`, creating a separation layer between\r\nElasticsearch and the UI.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"c4d3de83162904d3db19e82720b2dd747dcfc5e6","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-infra_services"],"number":198760,"url":"https://github.com/elastic/kibana/pull/198760","mergeCommit":{"message":"[Inventory][ECO] Replace `Entity` with `InventoryEntityLatest` type (#198760)\n\ncloses [#198758](https://github.com/elastic/kibana/issues/198758)\r\n\r\n## Summary\r\n\r\nThis PR removes the Entity type used across the Inventory and replaces\r\nit with `InventoryEntityLatest`, which provides strong typing for the\r\nlatest entity object. This change makes the code leverage TypeScript’s\r\nintellisense and autocompletion in the editor, making the code easier to\r\nwork with and more maintainable across the codebase.\r\n\r\n`InventoryEntityLatest` is the interface that the API returns and what\r\nthe UI consumes. Note that this is distinct from the index mapping\r\ndefined by `entityLatestSchema`, creating a separation layer between\r\nElasticsearch and the UI.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"c4d3de83162904d3db19e82720b2dd747dcfc5e6"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/198760","number":198760,"mergeCommit":{"message":"[Inventory][ECO] Replace `Entity` with `InventoryEntityLatest` type (#198760)\n\ncloses [#198758](https://github.com/elastic/kibana/issues/198758)\r\n\r\n## Summary\r\n\r\nThis PR removes the Entity type used across the Inventory and replaces\r\nit with `InventoryEntityLatest`, which provides strong typing for the\r\nlatest entity object. This change makes the code leverage TypeScript’s\r\nintellisense and autocompletion in the editor, making the code easier to\r\nwork with and more maintainable across the codebase.\r\n\r\n`InventoryEntityLatest` is the interface that the API returns and what\r\nthe UI consumes. Note that this is distinct from the index mapping\r\ndefined by `entityLatestSchema`, creating a separation layer between\r\nElasticsearch and the UI.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"c4d3de83162904d3db19e82720b2dd747dcfc5e6"}}]}] BACKPORT-->
This commit is contained in:
parent
12714317ba
commit
a68248ccfc
40 changed files with 578 additions and 443 deletions
|
@ -58,6 +58,8 @@ export class K8sEntity extends Serializable<EntityFields> {
|
|||
'entity.definition_id': `builtin_${entityTypeWithSchema}`,
|
||||
'entity.identity_fields': identityFields,
|
||||
'entity.display_name': getDisplayName({ identityFields, fields }),
|
||||
'entity.definition_version': '1.0.0',
|
||||
'entity.schema_version': '1.0',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,6 @@ export function replaceTemplateStrings(text, params = {}) {
|
|||
filebeat: docLinks.links.filebeat.base,
|
||||
metricbeat: docLinks.links.metricbeat.base,
|
||||
heartbeat: docLinks.links.heartbeat.base,
|
||||
functionbeat: docLinks.links.functionbeat.base,
|
||||
winlogbeat: docLinks.links.winlogbeat.base,
|
||||
auditbeat: docLinks.links.auditbeat.base,
|
||||
},
|
||||
|
|
|
@ -11,9 +11,9 @@ import { arrayOfStringsSchema } from './common';
|
|||
export const entityBaseSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
identity_fields: arrayOfStringsSchema,
|
||||
identity_fields: z.union([arrayOfStringsSchema, z.string()]),
|
||||
display_name: z.string(),
|
||||
metrics: z.record(z.string(), z.number()),
|
||||
metrics: z.optional(z.record(z.string(), z.number())),
|
||||
definition_version: z.string(),
|
||||
schema_version: z.string(),
|
||||
definition_id: z.string(),
|
||||
|
@ -24,10 +24,13 @@ export interface MetadataRecord {
|
|||
}
|
||||
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||
|
||||
type Literal = z.infer<typeof literalSchema>;
|
||||
type Metadata = Literal | { [key: string]: Metadata } | Metadata[];
|
||||
interface Metadata {
|
||||
[key: string]: Metadata | Literal | Literal[];
|
||||
}
|
||||
export const entityMetadataSchema: z.ZodType<Metadata> = z.lazy(() =>
|
||||
z.union([literalSchema, z.array(entityMetadataSchema), z.record(entityMetadataSchema)])
|
||||
z.record(z.string(), z.union([literalSchema, z.array(literalSchema), entityMetadataSchema]))
|
||||
);
|
||||
|
||||
export const entityLatestSchema = z
|
||||
|
@ -39,3 +42,6 @@ export const entityLatestSchema = z
|
|||
),
|
||||
})
|
||||
.and(entityMetadataSchema);
|
||||
|
||||
export type EntityInstance = z.infer<typeof entityLatestSchema>;
|
||||
export type EntityMetadata = z.infer<typeof entityMetadataSchema>;
|
||||
|
|
|
@ -221,4 +221,50 @@ describe('joinByKey', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('deeply merges by unflatten keys', () => {
|
||||
const joined = joinByKey(
|
||||
[
|
||||
{
|
||||
service: {
|
||||
name: 'opbeans-node',
|
||||
metrics: {
|
||||
cpu: 0.1,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
service: {
|
||||
environment: 'prod',
|
||||
metrics: {
|
||||
memory: 0.5,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
'properties.foo'
|
||||
);
|
||||
|
||||
expect(joined).toEqual([
|
||||
{
|
||||
service: {
|
||||
name: 'opbeans-node',
|
||||
environment: 'prod',
|
||||
metrics: {
|
||||
cpu: 0.1,
|
||||
memory: 0.5,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,18 +18,29 @@ export type JoinedReturnType<
|
|||
}
|
||||
>;
|
||||
|
||||
type ArrayOrSingle<T> = T | T[];
|
||||
function getValueByPath(obj: any, path: string): any {
|
||||
return path.split('.').reduce((acc, keyPart) => {
|
||||
// Check if acc is a valid object and has the key
|
||||
return acc && Object.prototype.hasOwnProperty.call(acc, keyPart) ? acc[keyPart] : undefined;
|
||||
}, obj);
|
||||
}
|
||||
|
||||
type NestedKeys<T> = T extends object
|
||||
? { [K in keyof T]: K extends string ? `${K}` | `${K}.${NestedKeys<T[K]>}` : never }[keyof T]
|
||||
: never;
|
||||
|
||||
type ArrayOrSingle<T> = T | T[];
|
||||
type CombinedNestedKeys<T, U> = (NestedKeys<T> & NestedKeys<U>) | (keyof T & keyof U);
|
||||
export function joinByKey<
|
||||
T extends Record<string, any>,
|
||||
U extends UnionToIntersection<T>,
|
||||
V extends ArrayOrSingle<keyof T & keyof U>
|
||||
V extends ArrayOrSingle<CombinedNestedKeys<T, U>>
|
||||
>(items: T[], key: V): JoinedReturnType<T, U>;
|
||||
|
||||
export function joinByKey<
|
||||
T extends Record<string, any>,
|
||||
U extends UnionToIntersection<T>,
|
||||
V extends ArrayOrSingle<keyof T & keyof U>,
|
||||
V extends ArrayOrSingle<CombinedNestedKeys<T, U>>,
|
||||
W extends JoinedReturnType<T, U>,
|
||||
X extends (a: T, b: T) => ValuesType<W>
|
||||
>(items: T[], key: V, mergeFn: X): W;
|
||||
|
@ -45,7 +56,7 @@ export function joinByKey(
|
|||
items.forEach((current) => {
|
||||
// The key of the map is a stable JSON string of the values from given keys.
|
||||
// We need stable JSON string to support plain object values.
|
||||
const stableKey = stableStringify(keys.map((k) => current[k]));
|
||||
const stableKey = stableStringify(keys.map((k) => current[k] ?? getValueByPath(current, k)));
|
||||
|
||||
if (map.has(stableKey)) {
|
||||
const item = map.get(stableKey);
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server';
|
|||
import type { ESQLSearchResponse, ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
|
||||
import { withSpan } from '@kbn/apm-utils';
|
||||
import type { EsqlQueryRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { esqlResultToPlainObjects } from '../utils/esql_result_to_plain_objects';
|
||||
|
||||
type SearchRequest = ESSearchRequest & {
|
||||
index: string | string[];
|
||||
|
@ -16,6 +17,20 @@ type SearchRequest = ESSearchRequest & {
|
|||
size: number | boolean;
|
||||
};
|
||||
|
||||
type EsqlQueryParameters = EsqlQueryRequest & { parseOutput?: boolean };
|
||||
type EsqlOutputParameters = Omit<EsqlQueryRequest, 'format' | 'columnar'> & {
|
||||
parseOutput?: true;
|
||||
format?: 'json';
|
||||
columnar?: false;
|
||||
};
|
||||
|
||||
type EsqlParameters = EsqlOutputParameters | EsqlQueryParameters;
|
||||
|
||||
export type InferEsqlResponseOf<
|
||||
TOutput = unknown,
|
||||
TParameters extends EsqlParameters = EsqlParameters
|
||||
> = TParameters extends EsqlOutputParameters ? TOutput[] : ESQLSearchResponse;
|
||||
|
||||
/**
|
||||
* An Elasticsearch Client with a fully typed `search` method and built-in
|
||||
* APM instrumentation.
|
||||
|
@ -25,7 +40,14 @@ export interface ObservabilityElasticsearchClient {
|
|||
operationName: string,
|
||||
parameters: TSearchRequest
|
||||
): Promise<InferSearchResponseOf<TDocument, TSearchRequest>>;
|
||||
esql(operationName: string, parameters: EsqlQueryRequest): Promise<ESQLSearchResponse>;
|
||||
esql<TOutput = unknown, TQueryParams extends EsqlOutputParameters = EsqlOutputParameters>(
|
||||
operationName: string,
|
||||
parameters: TQueryParams
|
||||
): Promise<InferEsqlResponseOf<TOutput, TQueryParams>>;
|
||||
esql<TOutput = unknown, TQueryParams extends EsqlQueryParameters = EsqlQueryParameters>(
|
||||
operationName: string,
|
||||
parameters: TQueryParams
|
||||
): Promise<InferEsqlResponseOf<TOutput, TQueryParams>>;
|
||||
client: ElasticsearchClient;
|
||||
}
|
||||
|
||||
|
@ -40,11 +62,14 @@ export function createObservabilityEsClient({
|
|||
}): ObservabilityElasticsearchClient {
|
||||
return {
|
||||
client,
|
||||
esql(operationName: string, parameters: EsqlQueryRequest) {
|
||||
esql<TOutput = unknown, TSearchRequest extends EsqlParameters = EsqlParameters>(
|
||||
operationName: string,
|
||||
{ parseOutput = true, format = 'json', columnar = false, ...parameters }: TSearchRequest
|
||||
) {
|
||||
logger.trace(() => `Request (${operationName}):\n${JSON.stringify(parameters, null, 2)}`);
|
||||
return withSpan({ name: operationName, labels: { plugin } }, () => {
|
||||
return client.esql.query(
|
||||
{ ...parameters },
|
||||
{ ...parameters, format, columnar },
|
||||
{
|
||||
querystring: {
|
||||
drop_null_columns: true,
|
||||
|
@ -54,7 +79,11 @@ export function createObservabilityEsClient({
|
|||
})
|
||||
.then((response) => {
|
||||
logger.trace(() => `Response (${operationName}):\n${JSON.stringify(response, null, 2)}`);
|
||||
return response as unknown as ESQLSearchResponse;
|
||||
|
||||
const esqlResponse = response as unknown as ESQLSearchResponse;
|
||||
|
||||
const shouldParseOutput = parseOutput && !columnar && format === 'json';
|
||||
return shouldParseOutput ? esqlResultToPlainObjects<TOutput>(esqlResponse) : esqlResponse;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
|
|
|
@ -6,25 +6,28 @@
|
|||
*/
|
||||
|
||||
import type { ESQLSearchResponse } from '@kbn/es-types';
|
||||
import { unflattenObject } from '../../object/unflatten_object';
|
||||
|
||||
export function esqlResultToPlainObjects<T extends Record<string, any>>(
|
||||
export function esqlResultToPlainObjects<TDocument = unknown>(
|
||||
result: ESQLSearchResponse
|
||||
): T[] {
|
||||
): TDocument[] {
|
||||
return result.values.map((row) => {
|
||||
return row.reduce<Record<string, unknown>>((acc, value, index) => {
|
||||
const column = result.columns[index];
|
||||
return unflattenObject(
|
||||
row.reduce<Record<string, any>>((acc, value, index) => {
|
||||
const column = result.columns[index];
|
||||
|
||||
if (!column) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Removes the type suffix from the column name
|
||||
const name = column.name.replace(/\.(text|keyword)$/, '');
|
||||
if (!acc[name]) {
|
||||
acc[name] = value;
|
||||
}
|
||||
|
||||
if (!column) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Removes the type suffix from the column name
|
||||
const name = column.name.replace(/\.(text|keyword)$/, '');
|
||||
if (!acc[name]) {
|
||||
acc[name] = value;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}) as T[];
|
||||
}, {})
|
||||
) as TDocument;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,16 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EntityClient, EnitityInstance } from './entity_client';
|
||||
import { EntityClient } from './entity_client';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import type { EntityInstance } from '@kbn/entities-schema';
|
||||
|
||||
const commonEntityFields: EnitityInstance = {
|
||||
const commonEntityFields: EntityInstance = {
|
||||
entity: {
|
||||
last_seen_timestamp: '2023-10-09T00:00:00Z',
|
||||
id: '1',
|
||||
display_name: 'entity_name',
|
||||
definition_id: 'entity_definition_id',
|
||||
} as EnitityInstance['entity'],
|
||||
} as EntityInstance['entity'],
|
||||
};
|
||||
|
||||
describe('EntityClient', () => {
|
||||
|
@ -26,7 +27,7 @@ describe('EntityClient', () => {
|
|||
|
||||
describe('asKqlFilter', () => {
|
||||
it('should return the kql filter', () => {
|
||||
const entityLatest: EnitityInstance = {
|
||||
const entityLatest: EntityInstance = {
|
||||
entity: {
|
||||
...commonEntityFields.entity,
|
||||
identity_fields: ['service.name', 'service.environment'],
|
||||
|
@ -42,7 +43,7 @@ describe('EntityClient', () => {
|
|||
});
|
||||
|
||||
it('should return the kql filter when indentity_fields is composed by multiple fields', () => {
|
||||
const entityLatest: EnitityInstance = {
|
||||
const entityLatest: EntityInstance = {
|
||||
entity: {
|
||||
...commonEntityFields.entity,
|
||||
identity_fields: ['service.name', 'service.environment'],
|
||||
|
@ -59,7 +60,7 @@ describe('EntityClient', () => {
|
|||
});
|
||||
|
||||
it('should ignore fields that are not present in the entity', () => {
|
||||
const entityLatest: EnitityInstance = {
|
||||
const entityLatest: EntityInstance = {
|
||||
entity: {
|
||||
...commonEntityFields.entity,
|
||||
identity_fields: ['host.name', 'foo.bar'],
|
||||
|
@ -76,7 +77,7 @@ describe('EntityClient', () => {
|
|||
|
||||
describe('getIdentityFieldsValue', () => {
|
||||
it('should return identity fields values', () => {
|
||||
const entityLatest: EnitityInstance = {
|
||||
const entityLatest: EntityInstance = {
|
||||
entity: {
|
||||
...commonEntityFields.entity,
|
||||
identity_fields: ['service.name', 'service.environment'],
|
||||
|
@ -93,7 +94,7 @@ describe('EntityClient', () => {
|
|||
});
|
||||
|
||||
it('should return identity fields values when indentity_fields is composed by multiple fields', () => {
|
||||
const entityLatest: EnitityInstance = {
|
||||
const entityLatest: EntityInstance = {
|
||||
entity: {
|
||||
...commonEntityFields.entity,
|
||||
identity_fields: ['service.name', 'service.environment'],
|
||||
|
@ -112,7 +113,7 @@ describe('EntityClient', () => {
|
|||
});
|
||||
|
||||
it('should return identity fields when field is in the root', () => {
|
||||
const entityLatest: EnitityInstance = {
|
||||
const entityLatest: EntityInstance = {
|
||||
entity: {
|
||||
...commonEntityFields.entity,
|
||||
identity_fields: ['name'],
|
||||
|
@ -127,7 +128,7 @@ describe('EntityClient', () => {
|
|||
});
|
||||
|
||||
it('should throw an error when identity fields are missing', () => {
|
||||
const entityLatest: EnitityInstance = {
|
||||
const entityLatest: EntityInstance = {
|
||||
...commonEntityFields,
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
import { CoreSetup, CoreStart } from '@kbn/core/public';
|
||||
import {
|
||||
ClientRequestParamsOf,
|
||||
|
@ -14,7 +13,7 @@ import {
|
|||
isHttpFetchError,
|
||||
} from '@kbn/server-route-repository-client';
|
||||
import { type KueryNode, nodeTypes, toKqlExpression } from '@kbn/es-query';
|
||||
import { entityLatestSchema } from '@kbn/entities-schema';
|
||||
import type { EntityInstance, EntityMetadata } from '@kbn/entities-schema';
|
||||
import { castArray } from 'lodash';
|
||||
import {
|
||||
DisableManagedEntityResponse,
|
||||
|
@ -39,8 +38,6 @@ type CreateEntityDefinitionQuery = QueryParamOf<
|
|||
ClientRequestParamsOf<EntityManagerRouteRepository, 'PUT /internal/entities/managed/enablement'>
|
||||
>;
|
||||
|
||||
export type EnitityInstance = z.infer<typeof entityLatestSchema>;
|
||||
|
||||
export class EntityClient {
|
||||
public readonly repositoryClient: EntityManagerRepositoryClient['fetch'];
|
||||
|
||||
|
@ -90,8 +87,12 @@ export class EntityClient {
|
|||
}
|
||||
}
|
||||
|
||||
asKqlFilter(entityLatest: EnitityInstance) {
|
||||
const identityFieldsValue = this.getIdentityFieldsValue(entityLatest);
|
||||
asKqlFilter(
|
||||
entityInstance: {
|
||||
entity: Pick<EntityInstance['entity'], 'identity_fields'>;
|
||||
} & Required<EntityMetadata>
|
||||
) {
|
||||
const identityFieldsValue = this.getIdentityFieldsValue(entityInstance);
|
||||
|
||||
const nodes: KueryNode[] = Object.entries(identityFieldsValue).map(([identityField, value]) => {
|
||||
return nodeTypes.function.buildNode('is', identityField, value);
|
||||
|
@ -104,8 +105,12 @@ export class EntityClient {
|
|||
return toKqlExpression(kqlExpression);
|
||||
}
|
||||
|
||||
getIdentityFieldsValue(entityLatest: EnitityInstance) {
|
||||
const { identity_fields: identityFields } = entityLatest.entity;
|
||||
getIdentityFieldsValue(
|
||||
entityInstance: {
|
||||
entity: Pick<EntityInstance['entity'], 'identity_fields'>;
|
||||
} & Required<EntityMetadata>
|
||||
) {
|
||||
const { identity_fields: identityFields } = entityInstance.entity;
|
||||
|
||||
if (!identityFields) {
|
||||
throw new Error('Identity fields are missing');
|
||||
|
@ -114,7 +119,7 @@ export class EntityClient {
|
|||
return castArray(identityFields).reduce((acc, field) => {
|
||||
const value = field.split('.').reduce((obj: any, part: string) => {
|
||||
return obj && typeof obj === 'object' ? (obj as Record<string, any>)[part] : undefined;
|
||||
}, entityLatest);
|
||||
}, entityInstance);
|
||||
|
||||
if (value) {
|
||||
acc[field] = value;
|
||||
|
|
|
@ -116,8 +116,11 @@ describe.skip('Transaction details', () => {
|
|||
})}`
|
||||
);
|
||||
|
||||
cy.contains('Top 5 errors');
|
||||
cy.getByTestSubj('topErrorsForTransactionTable').contains('a', '[MockError] Foo').click();
|
||||
cy.contains('Top 5 errors', { timeout: 30000 });
|
||||
cy.getByTestSubj('topErrorsForTransactionTable')
|
||||
.should('be.visible')
|
||||
.contains('a', '[MockError] Foo', { timeout: 10000 })
|
||||
.click();
|
||||
cy.url().should('include', 'opbeans-java/errors');
|
||||
});
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ describe('getDataStreamTypes', () => {
|
|||
it('should return metrics and entity source_data_stream types when entityCentriExperienceEnabled is true and has entity data', async () => {
|
||||
(getHasMetricsData as jest.Mock).mockResolvedValue(true);
|
||||
(getLatestEntity as jest.Mock).mockResolvedValue({
|
||||
'source_data_stream.type': ['logs', 'metrics'],
|
||||
sourceDataStreamType: ['logs', 'metrics'],
|
||||
});
|
||||
|
||||
const params = {
|
||||
|
@ -118,7 +118,7 @@ describe('getDataStreamTypes', () => {
|
|||
it('should return entity source_data_stream types when has no metrics', async () => {
|
||||
(getHasMetricsData as jest.Mock).mockResolvedValue(false);
|
||||
(getLatestEntity as jest.Mock).mockResolvedValue({
|
||||
'source_data_stream.type': ['logs', 'traces'],
|
||||
sourceDataStreamType: ['logs', 'traces'],
|
||||
});
|
||||
|
||||
const params = {
|
||||
|
|
|
@ -7,11 +7,9 @@
|
|||
|
||||
import { type EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client';
|
||||
import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common';
|
||||
import {
|
||||
EntityDataStreamType,
|
||||
SOURCE_DATA_STREAM_TYPE,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
import { EntityDataStreamType } from '@kbn/observability-shared-plugin/common';
|
||||
import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
|
||||
import { castArray } from 'lodash';
|
||||
import { type InfraMetricsClient } from '../../lib/helpers/get_infra_metrics_client';
|
||||
import { getHasMetricsData } from './get_has_metrics_data';
|
||||
import { getLatestEntity } from './get_latest_entity';
|
||||
|
@ -45,15 +43,15 @@ export async function getDataStreamTypes({
|
|||
return Array.from(sourceDataStreams);
|
||||
}
|
||||
|
||||
const entity = await getLatestEntity({
|
||||
const latestEntity = await getLatestEntity({
|
||||
inventoryEsClient: obsEsClient,
|
||||
entityId,
|
||||
entityType,
|
||||
entityManagerClient,
|
||||
});
|
||||
|
||||
if (entity?.[SOURCE_DATA_STREAM_TYPE]) {
|
||||
[entity[SOURCE_DATA_STREAM_TYPE]].flat().forEach((item) => {
|
||||
if (latestEntity) {
|
||||
castArray(latestEntity.sourceDataStreamType).forEach((item) => {
|
||||
sourceDataStreams.add(item as EntityDataStreamType);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,20 +7,16 @@
|
|||
|
||||
import { ENTITY_LATEST, entitiesAliasPattern } from '@kbn/entities-schema';
|
||||
import { type EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client';
|
||||
import {
|
||||
ENTITY_TYPE,
|
||||
SOURCE_DATA_STREAM_TYPE,
|
||||
} from '@kbn/observability-shared-plugin/common/field_names/elasticsearch';
|
||||
import { ENTITY_TYPE, SOURCE_DATA_STREAM_TYPE } from '@kbn/observability-shared-plugin/common';
|
||||
import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
|
||||
import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects';
|
||||
|
||||
const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({
|
||||
type: '*',
|
||||
dataset: ENTITY_LATEST,
|
||||
});
|
||||
|
||||
interface Entity {
|
||||
[SOURCE_DATA_STREAM_TYPE]: string | string[];
|
||||
interface EntitySourceResponse {
|
||||
sourceDataStreamType?: string | string[];
|
||||
}
|
||||
|
||||
export async function getLatestEntity({
|
||||
|
@ -33,7 +29,7 @@ export async function getLatestEntity({
|
|||
entityType: 'host' | 'container';
|
||||
entityId: string;
|
||||
entityManagerClient: EntityClient;
|
||||
}): Promise<Entity | undefined> {
|
||||
}): Promise<EntitySourceResponse | undefined> {
|
||||
const { definitions } = await entityManagerClient.getEntityDefinitions({
|
||||
builtIn: true,
|
||||
type: entityType,
|
||||
|
@ -41,10 +37,12 @@ export async function getLatestEntity({
|
|||
|
||||
const hostOrContainerIdentityField = definitions[0]?.identityFields?.[0]?.field;
|
||||
if (hostOrContainerIdentityField === undefined) {
|
||||
return { [SOURCE_DATA_STREAM_TYPE]: [] };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', {
|
||||
const response = await inventoryEsClient.esql<{
|
||||
source_data_stream?: { type?: string | string[] };
|
||||
}>('get_latest_entities', {
|
||||
query: `FROM ${ENTITIES_LATEST_ALIAS}
|
||||
| WHERE ${ENTITY_TYPE} == ?
|
||||
| WHERE ${hostOrContainerIdentityField} == ?
|
||||
|
@ -53,5 +51,5 @@ export async function getLatestEntity({
|
|||
params: [entityType, entityId],
|
||||
});
|
||||
|
||||
return esqlResultToPlainObjects<Entity>(latestEntitiesEsqlResponse)[0];
|
||||
return { sourceDataStreamType: response[0].source_data_stream?.type };
|
||||
}
|
||||
|
|
|
@ -24,7 +24,14 @@ export function getMockInventoryContext(): InventoryKibanaContext {
|
|||
|
||||
return {
|
||||
...coreStart,
|
||||
entityManager: {} as unknown as EntityManagerPublicPluginStart,
|
||||
entityManager: {
|
||||
entityClient: {
|
||||
asKqlFilter: jest.fn(),
|
||||
getIdentityFieldsValue() {
|
||||
return 'entity_id';
|
||||
},
|
||||
},
|
||||
} as unknown as EntityManagerPublicPluginStart,
|
||||
observabilityShared: {} as unknown as ObservabilitySharedPluginStart,
|
||||
inference: {} as unknown as InferencePublicStart,
|
||||
share: {
|
||||
|
|
|
@ -4,24 +4,15 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { z } from '@kbn/zod';
|
||||
import { ENTITY_LATEST, entitiesAliasPattern, entityLatestSchema } from '@kbn/entities-schema';
|
||||
import {
|
||||
ENTITY_DEFINITION_ID,
|
||||
ENTITY_DISPLAY_NAME,
|
||||
ENTITY_ID,
|
||||
ENTITY_IDENTITY_FIELDS,
|
||||
ENTITY_LAST_SEEN,
|
||||
ENTITY_TYPE,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
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([
|
||||
t.literal(ENTITY_DISPLAY_NAME),
|
||||
t.literal(ENTITY_LAST_SEEN),
|
||||
t.literal(ENTITY_TYPE),
|
||||
t.literal('entityDisplayName'),
|
||||
t.literal('entityLastSeenTimestamp'),
|
||||
t.literal('entityType'),
|
||||
t.literal('alertsCount'),
|
||||
t.literal('actions'),
|
||||
]);
|
||||
|
@ -80,23 +71,20 @@ export const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({
|
|||
dataset: ENTITY_LATEST,
|
||||
});
|
||||
|
||||
export interface Entity {
|
||||
[ENTITY_LAST_SEEN]: string;
|
||||
[ENTITY_ID]: string;
|
||||
[ENTITY_TYPE]: string;
|
||||
[ENTITY_DISPLAY_NAME]: string;
|
||||
[ENTITY_DEFINITION_ID]: string;
|
||||
[ENTITY_IDENTITY_FIELDS]: string | string[];
|
||||
alertsCount?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type EntityGroup = {
|
||||
count: number;
|
||||
} & {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export type InventoryEntityLatest = z.infer<typeof entityLatestSchema> & {
|
||||
export type InventoryEntity = {
|
||||
entityId: string;
|
||||
entityType: string;
|
||||
entityIdentityFields: string | string[];
|
||||
entityDisplayName: string;
|
||||
entityDefinitionId: string;
|
||||
entityLastSeenTimestamp: string;
|
||||
entityDefinitionVersion: string;
|
||||
entitySchemaVersion: string;
|
||||
alertsCount?: number;
|
||||
};
|
||||
} & EntityMetadata;
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { AgentName } from '@kbn/elastic-agent-utils';
|
||||
import type { InventoryEntity } from '../entities';
|
||||
|
||||
interface BuiltinEntityMap {
|
||||
host: InventoryEntity & { cloud?: { provider?: string[] } };
|
||||
container: InventoryEntity & { cloud?: { provider?: string[] } };
|
||||
service: InventoryEntity & {
|
||||
agent?: { name: AgentName[] };
|
||||
service?: { environment?: string };
|
||||
};
|
||||
}
|
||||
|
||||
export const isBuiltinEntityOfType = <T extends keyof BuiltinEntityMap>(
|
||||
type: T,
|
||||
entity: InventoryEntity
|
||||
): entity is BuiltinEntityMap[T] => {
|
||||
return entity.entityType === type;
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* 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 { unflattenObject } from '@kbn/observability-utils/object/unflatten_object';
|
||||
import type { Entity, InventoryEntityLatest } from '../entities';
|
||||
|
||||
export function unflattenEntity(entity: Entity) {
|
||||
return unflattenObject(entity) as InventoryEntityLatest;
|
||||
}
|
|
@ -169,6 +169,7 @@ describe('Home page', () => {
|
|||
'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('@getEEMStatus');
|
||||
|
@ -181,8 +182,6 @@ describe('Home page', () => {
|
|||
cy.get('server1').should('not.exist');
|
||||
cy.contains('synth-node-trace-logs');
|
||||
cy.contains('foo').should('not.exist');
|
||||
cy.getByTestSubj('inventoryGroup_entity.type_host').should('not.exist');
|
||||
cy.getByTestSubj('inventoryGroup_entity.type_container').should('not.exist');
|
||||
});
|
||||
|
||||
it('Filters entities by host type', () => {
|
||||
|
@ -193,6 +192,7 @@ describe('Home page', () => {
|
|||
'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('@getEEMStatus');
|
||||
|
@ -205,8 +205,6 @@ describe('Home page', () => {
|
|||
cy.contains('server1');
|
||||
cy.contains('synth-node-trace-logs').should('not.exist');
|
||||
cy.contains('foo').should('not.exist');
|
||||
cy.getByTestSubj('inventoryGroup_entity.type_service').should('not.exist');
|
||||
cy.getByTestSubj('inventoryGroup_entity.type_container').should('not.exist');
|
||||
});
|
||||
|
||||
it('Filters entities by container type', () => {
|
||||
|
@ -217,6 +215,7 @@ describe('Home page', () => {
|
|||
'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('@getEEMStatus');
|
||||
|
@ -229,8 +228,6 @@ describe('Home page', () => {
|
|||
cy.contains('server1').should('not.exist');
|
||||
cy.contains('synth-node-trace-logs').should('not.exist');
|
||||
cy.contains('foo');
|
||||
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', () => {
|
||||
|
|
|
@ -8,11 +8,16 @@ import React from 'react';
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { AlertsBadge } from './alerts_badge';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import type { Entity } from '../../../common/entities';
|
||||
import type { InventoryEntity } from '../../../common/entities';
|
||||
|
||||
jest.mock('../../hooks/use_kibana');
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
|
||||
const commonEntityFields: Partial<InventoryEntity> = {
|
||||
entityLastSeenTimestamp: 'foo',
|
||||
entityId: '1',
|
||||
};
|
||||
|
||||
describe('AlertsBadge', () => {
|
||||
const mockAsKqlFilter = jest.fn();
|
||||
|
||||
|
@ -40,16 +45,19 @@ describe('AlertsBadge', () => {
|
|||
});
|
||||
|
||||
it('render alerts badge for a host entity', () => {
|
||||
const entity: Entity = {
|
||||
'entity.last_seen_timestamp': 'foo',
|
||||
'entity.id': '1',
|
||||
'entity.type': 'host',
|
||||
'entity.display_name': 'foo',
|
||||
'entity.identity_fields': 'host.name',
|
||||
'host.name': 'foo',
|
||||
'entity.definition_id': 'host',
|
||||
'cloud.provider': null,
|
||||
const entity: InventoryEntity = {
|
||||
...(commonEntityFields as InventoryEntity),
|
||||
entityType: 'host',
|
||||
entityDisplayName: 'foo',
|
||||
entityIdentityFields: 'host.name',
|
||||
entityDefinitionId: 'host',
|
||||
alertsCount: 1,
|
||||
host: {
|
||||
name: 'foo',
|
||||
},
|
||||
cloud: {
|
||||
provider: null,
|
||||
},
|
||||
};
|
||||
mockAsKqlFilter.mockReturnValue('host.name: foo');
|
||||
|
||||
|
@ -60,16 +68,22 @@ describe('AlertsBadge', () => {
|
|||
expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('1');
|
||||
});
|
||||
it('render alerts badge for a service entity', () => {
|
||||
const entity: Entity = {
|
||||
'entity.last_seen_timestamp': 'foo',
|
||||
'agent.name': 'node',
|
||||
'entity.id': '1',
|
||||
'entity.type': 'service',
|
||||
'entity.display_name': 'foo',
|
||||
'entity.identity_fields': 'service.name',
|
||||
'service.name': 'bar',
|
||||
'entity.definition_id': 'host',
|
||||
'cloud.provider': null,
|
||||
const entity: InventoryEntity = {
|
||||
...(commonEntityFields as InventoryEntity),
|
||||
entityType: 'service',
|
||||
entityDisplayName: 'foo',
|
||||
entityIdentityFields: 'service.name',
|
||||
entityDefinitionId: 'service',
|
||||
service: {
|
||||
name: 'bar',
|
||||
},
|
||||
agent: {
|
||||
name: 'node',
|
||||
},
|
||||
cloud: {
|
||||
provider: null,
|
||||
},
|
||||
|
||||
alertsCount: 5,
|
||||
};
|
||||
mockAsKqlFilter.mockReturnValue('service.name: bar');
|
||||
|
@ -81,17 +95,22 @@ describe('AlertsBadge', () => {
|
|||
expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('5');
|
||||
});
|
||||
it('render alerts badge for a service entity with multiple identity fields', () => {
|
||||
const entity: Entity = {
|
||||
'entity.last_seen_timestamp': 'foo',
|
||||
'agent.name': 'node',
|
||||
'entity.id': '1',
|
||||
'entity.type': 'service',
|
||||
'entity.display_name': 'foo',
|
||||
'entity.identity_fields': ['service.name', 'service.environment'],
|
||||
'service.name': 'bar',
|
||||
'service.environment': 'prod',
|
||||
'entity.definition_id': 'host',
|
||||
'cloud.provider': null,
|
||||
const entity: InventoryEntity = {
|
||||
...(commonEntityFields as InventoryEntity),
|
||||
entityType: 'service',
|
||||
entityDisplayName: 'foo',
|
||||
entityIdentityFields: ['service.name', 'service.environment'],
|
||||
entityDefinitionId: 'service',
|
||||
service: {
|
||||
name: 'bar',
|
||||
environment: 'prod',
|
||||
},
|
||||
agent: {
|
||||
name: 'node',
|
||||
},
|
||||
cloud: {
|
||||
provider: null,
|
||||
},
|
||||
alertsCount: 2,
|
||||
};
|
||||
|
||||
|
|
|
@ -8,11 +8,10 @@ import React from 'react';
|
|||
import rison from '@kbn/rison';
|
||||
import { EuiBadge, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { Entity } from '../../../common/entities';
|
||||
import { unflattenEntity } from '../../../common/utils/unflatten_entity';
|
||||
import type { InventoryEntity } from '../../../common/entities';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
|
||||
export function AlertsBadge({ entity }: { entity: Entity }) {
|
||||
export function AlertsBadge({ entity }: { entity: InventoryEntity }) {
|
||||
const {
|
||||
services: {
|
||||
http: { basePath },
|
||||
|
@ -22,7 +21,12 @@ export function AlertsBadge({ entity }: { entity: Entity }) {
|
|||
|
||||
const activeAlertsHref = basePath.prepend(
|
||||
`/app/observability/alerts?_a=${rison.encode({
|
||||
kuery: entityManager.entityClient.asKqlFilter(unflattenEntity(entity)),
|
||||
kuery: entityManager.entityClient.asKqlFilter({
|
||||
entity: {
|
||||
identity_fields: entity.entityIdentityFields,
|
||||
},
|
||||
...entity,
|
||||
}),
|
||||
status: 'active',
|
||||
})}`
|
||||
);
|
||||
|
|
|
@ -9,7 +9,7 @@ import { EuiButton, EuiDataGridSorting, EuiFlexGroup, EuiFlexItem } from '@elast
|
|||
import { Meta, Story } from '@storybook/react';
|
||||
import { orderBy } from 'lodash';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ENTITY_LAST_SEEN, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
|
||||
import { ENTITY_LAST_SEEN } from '@kbn/observability-shared-plugin/common';
|
||||
import { useArgs } from '@storybook/addons';
|
||||
import { EntitiesGrid } from '.';
|
||||
import { entitiesMock } from './mock/entities_mock';
|
||||
|
@ -45,7 +45,7 @@ export const Grid: Story<EntityGridStoriesArgs> = (args) => {
|
|||
const filteredAndSortedItems = useMemo(
|
||||
() =>
|
||||
orderBy(
|
||||
entityType ? entitiesMock.filter((mock) => mock[ENTITY_TYPE] === entityType) : entitiesMock,
|
||||
entityType ? entitiesMock.filter((mock) => mock.entityType === entityType) : entitiesMock,
|
||||
sort.id,
|
||||
sort.direction
|
||||
),
|
||||
|
|
|
@ -9,28 +9,22 @@ import React from 'react';
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { EntityName } from '.';
|
||||
import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect';
|
||||
import { Entity } from '../../../../common/entities';
|
||||
import {
|
||||
ENTITY_DEFINITION_ID,
|
||||
ENTITY_DISPLAY_NAME,
|
||||
ENTITY_ID,
|
||||
ENTITY_IDENTITY_FIELDS,
|
||||
ENTITY_LAST_SEEN,
|
||||
ENTITY_TYPE,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
import type { InventoryEntity } from '../../../../common/entities';
|
||||
|
||||
jest.mock('../../../hooks/use_detail_view_redirect');
|
||||
|
||||
const useDetailViewRedirectMock = useDetailViewRedirect as jest.Mock;
|
||||
|
||||
describe('EntityName', () => {
|
||||
const mockEntity: Entity = {
|
||||
[ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z',
|
||||
[ENTITY_ID]: '1',
|
||||
[ENTITY_DISPLAY_NAME]: 'entity_name',
|
||||
[ENTITY_DEFINITION_ID]: 'entity_definition_id',
|
||||
[ENTITY_IDENTITY_FIELDS]: ['service.name', 'service.environment'],
|
||||
[ENTITY_TYPE]: 'service',
|
||||
const mockEntity: InventoryEntity = {
|
||||
entityLastSeenTimestamp: '2023-10-09T00:00:00Z',
|
||||
entityId: '1',
|
||||
entityType: 'service',
|
||||
entityDisplayName: 'entity_name',
|
||||
entityIdentityFields: ['service.name', 'service.environment'],
|
||||
entityDefinitionId: 'entity_definition_id',
|
||||
entitySchemaVersion: '1',
|
||||
entityDefinitionVersion: '1',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -7,14 +7,13 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { ENTITY_DISPLAY_NAME } from '@kbn/observability-shared-plugin/common';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import type { Entity } from '../../../../common/entities';
|
||||
import type { InventoryEntity } from '../../../../common/entities';
|
||||
import { EntityIcon } from '../../entity_icon';
|
||||
import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect';
|
||||
|
||||
interface EntityNameProps {
|
||||
entity: Entity;
|
||||
entity: InventoryEntity;
|
||||
}
|
||||
|
||||
export function EntityName({ entity }: EntityNameProps) {
|
||||
|
@ -29,7 +28,7 @@ export function EntityName({ entity }: EntityNameProps) {
|
|||
const handleLinkClick = useCallback(() => {
|
||||
telemetry.reportEntityViewClicked({
|
||||
view_type: 'detail',
|
||||
entity_type: entity['entity.type'],
|
||||
entity_type: entity.entityType,
|
||||
});
|
||||
}, [entity, telemetry]);
|
||||
|
||||
|
@ -40,7 +39,7 @@ export function EntityName({ entity }: EntityNameProps) {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem className="eui-textTruncate">
|
||||
<span className="eui-textTruncate" data-test-subj="entityNameDisplayName">
|
||||
{entity[ENTITY_DISPLAY_NAME]}
|
||||
{entity.entityDisplayName}
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -8,11 +8,6 @@
|
|||
import { EuiButtonIcon, EuiDataGridColumn, EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ENTITY_DISPLAY_NAME,
|
||||
ENTITY_LAST_SEEN,
|
||||
ENTITY_TYPE,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
|
||||
const alertsLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel', {
|
||||
defaultMessage: 'Alerts',
|
||||
|
@ -76,12 +71,12 @@ export const getColumns = ({
|
|||
}: {
|
||||
showAlertsColumn: boolean;
|
||||
showActions: boolean;
|
||||
}): EuiDataGridColumn[] => {
|
||||
}) => {
|
||||
return [
|
||||
...(showAlertsColumn
|
||||
? [
|
||||
{
|
||||
id: 'alertsCount',
|
||||
id: 'alertsCount' as const,
|
||||
displayAsText: alertsLabel,
|
||||
isSortable: true,
|
||||
display: <CustomHeaderCell title={alertsLabel} tooltipContent={alertsTooltip} />,
|
||||
|
@ -91,21 +86,21 @@ export const getColumns = ({
|
|||
]
|
||||
: []),
|
||||
{
|
||||
id: ENTITY_DISPLAY_NAME,
|
||||
id: 'entityDisplayName' as const,
|
||||
// keep it for accessibility purposes
|
||||
displayAsText: entityNameLabel,
|
||||
display: <CustomHeaderCell title={entityNameLabel} tooltipContent={entityNameTooltip} />,
|
||||
isSortable: true,
|
||||
},
|
||||
{
|
||||
id: ENTITY_TYPE,
|
||||
id: 'entityType' as const,
|
||||
// keep it for accessibility purposes
|
||||
displayAsText: entityTypeLabel,
|
||||
display: <CustomHeaderCell title={entityTypeLabel} tooltipContent={entityTypeTooltip} />,
|
||||
isSortable: true,
|
||||
},
|
||||
{
|
||||
id: ENTITY_LAST_SEEN,
|
||||
id: 'entityLastSeenTimestamp' as const,
|
||||
// keep it for accessibility purposes
|
||||
displayAsText: entityLastSeenLabel,
|
||||
display: (
|
||||
|
@ -118,7 +113,7 @@ export const getColumns = ({
|
|||
...(showActions
|
||||
? [
|
||||
{
|
||||
id: 'actions',
|
||||
id: 'actions' as const,
|
||||
// keep it for accessibility purposes
|
||||
displayAsText: entityActionsLabel,
|
||||
display: (
|
||||
|
@ -128,5 +123,5 @@ export const getColumns = ({
|
|||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
] satisfies EuiDataGridColumn[];
|
||||
};
|
||||
|
|
|
@ -15,13 +15,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react';
|
||||
import { last } from 'lodash';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
ENTITY_DISPLAY_NAME,
|
||||
ENTITY_LAST_SEEN,
|
||||
ENTITY_TYPE,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
import { EntityColumnIds } from '../../../common/entities';
|
||||
import { APIReturnType } from '../../api';
|
||||
import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
|
||||
import { EntityColumnIds, InventoryEntity } from '../../../common/entities';
|
||||
import { BadgeFilterWithPopover } from '../badge_filter_with_popover';
|
||||
import { getColumns } from './grid_columns';
|
||||
import { AlertsBadge } from '../alerts_badge/alerts_badge';
|
||||
|
@ -29,12 +24,9 @@ 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'];
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
entities: LatestEntities;
|
||||
entities: InventoryEntity[];
|
||||
sortDirection: 'asc' | 'desc';
|
||||
sortField: string;
|
||||
pageIndex: number;
|
||||
|
@ -88,16 +80,17 @@ export function EntitiesGrid({
|
|||
}
|
||||
|
||||
const columnEntityTableId = columnId as EntityColumnIds;
|
||||
const entityType = entity[ENTITY_TYPE];
|
||||
const entityType = entity.entityType;
|
||||
const discoverUrl = getDiscoverRedirectUrl(entity);
|
||||
|
||||
switch (columnEntityTableId) {
|
||||
case 'alertsCount':
|
||||
return entity?.alertsCount ? <AlertsBadge entity={entity} /> : null;
|
||||
|
||||
case ENTITY_TYPE:
|
||||
case 'entityType':
|
||||
return <BadgeFilterWithPopover field={ENTITY_TYPE} value={entityType} />;
|
||||
case ENTITY_LAST_SEEN:
|
||||
|
||||
case 'entityLastSeenTimestamp':
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.inventory.entitiesGrid.euiDataGrid.lastSeen"
|
||||
|
@ -105,7 +98,7 @@ export function EntitiesGrid({
|
|||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
value={entity[columnEntityTableId]}
|
||||
value={entity.entityLastSeenTimestamp}
|
||||
month="short"
|
||||
day="numeric"
|
||||
year="numeric"
|
||||
|
@ -113,7 +106,7 @@ export function EntitiesGrid({
|
|||
),
|
||||
time: (
|
||||
<FormattedTime
|
||||
value={entity[columnEntityTableId]}
|
||||
value={entity.entityLastSeenTimestamp}
|
||||
hour12={false}
|
||||
hour="2-digit"
|
||||
minute="2-digit"
|
||||
|
@ -123,19 +116,19 @@ export function EntitiesGrid({
|
|||
}}
|
||||
/>
|
||||
);
|
||||
case ENTITY_DISPLAY_NAME:
|
||||
case 'entityDisplayName':
|
||||
return <EntityName entity={entity} />;
|
||||
case 'actions':
|
||||
return (
|
||||
discoverUrl && (
|
||||
<EntityActions
|
||||
discoverUrl={discoverUrl}
|
||||
entityIdentifyingValue={entity[ENTITY_DISPLAY_NAME]}
|
||||
entityIdentifyingValue={entity.entityDisplayName}
|
||||
/>
|
||||
)
|
||||
);
|
||||
default:
|
||||
return entity[columnId as EntityColumnIds] || '';
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[entities, getDiscoverRedirectUrl]
|
||||
|
|
|
@ -6,15 +6,7 @@
|
|||
*/
|
||||
|
||||
import { faker } from '@faker-js/faker';
|
||||
import {
|
||||
ENTITY_DISPLAY_NAME,
|
||||
ENTITY_TYPE,
|
||||
ENTITY_ID,
|
||||
ENTITY_LAST_SEEN,
|
||||
AGENT_NAME,
|
||||
CLOUD_PROVIDER,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
import { Entity } from '../../../../common/entities';
|
||||
import type { InventoryEntity } from '../../../../common/entities';
|
||||
|
||||
const idGenerator = () => {
|
||||
let id = 0;
|
||||
|
@ -33,38 +25,48 @@ function generateRandomTimestamp() {
|
|||
return randomDate.toISOString();
|
||||
}
|
||||
|
||||
const getEntity = (entityType: string, customFields: Record<string, any> = {}) => ({
|
||||
[ENTITY_LAST_SEEN]: generateRandomTimestamp(),
|
||||
[ENTITY_TYPE]: entityType,
|
||||
[ENTITY_DISPLAY_NAME]: faker.person.fullName(),
|
||||
[ENTITY_ID]: generateId(),
|
||||
...customFields,
|
||||
const indentityFieldsPerType: Record<string, string[]> = {
|
||||
host: ['host.name'],
|
||||
container: ['container.id'],
|
||||
service: ['service.name'],
|
||||
};
|
||||
|
||||
const getEntityLatest = (
|
||||
entityType: string,
|
||||
overrides?: Partial<InventoryEntity>
|
||||
): InventoryEntity => ({
|
||||
entityLastSeenTimestamp: generateRandomTimestamp(),
|
||||
entityType,
|
||||
entityDisplayName: faker.person.fullName(),
|
||||
entityId: generateId(),
|
||||
entityDefinitionId: faker.string.uuid(),
|
||||
entityDefinitionVersion: '1.0.0',
|
||||
entityIdentityFields: indentityFieldsPerType[entityType],
|
||||
entitySchemaVersion: '1.0.0',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const alertsMock = [
|
||||
{
|
||||
...getEntity('host'),
|
||||
alertsCount: 3,
|
||||
},
|
||||
{
|
||||
...getEntity('service'),
|
||||
alertsCount: 3,
|
||||
},
|
||||
|
||||
{
|
||||
...getEntity('host'),
|
||||
alertsCount: 10,
|
||||
},
|
||||
{
|
||||
...getEntity('host'),
|
||||
const alertsMock: InventoryEntity[] = [
|
||||
getEntityLatest('host', {
|
||||
alertsCount: 1,
|
||||
},
|
||||
}),
|
||||
getEntityLatest('service', {
|
||||
alertsCount: 3,
|
||||
}),
|
||||
getEntityLatest('host', {
|
||||
alertsCount: 10,
|
||||
}),
|
||||
getEntityLatest('host', {
|
||||
alertsCount: 1,
|
||||
}),
|
||||
];
|
||||
|
||||
const hostsMock = Array.from({ length: 20 }, () => getEntity('host', { [CLOUD_PROVIDER]: 'gcp' }));
|
||||
const containersMock = Array.from({ length: 20 }, () => getEntity('container'));
|
||||
const hostsMock = Array.from({ length: 20 }, () =>
|
||||
getEntityLatest('host', { cloud: { provider: 'gcp' } })
|
||||
);
|
||||
const containersMock = Array.from({ length: 20 }, () => getEntityLatest('container'));
|
||||
const servicesMock = Array.from({ length: 20 }, () =>
|
||||
getEntity('service', { [AGENT_NAME]: 'java' })
|
||||
getEntityLatest('service', { agent: { name: 'java' } })
|
||||
);
|
||||
|
||||
export const entitiesMock = [
|
||||
|
@ -72,4 +74,4 @@ export const entitiesMock = [
|
|||
...hostsMock,
|
||||
...containersMock,
|
||||
...servicesMock,
|
||||
] as Entity[];
|
||||
] as InventoryEntity[];
|
||||
|
|
|
@ -6,36 +6,23 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
AGENT_NAME,
|
||||
CLOUD_PROVIDER,
|
||||
ENTITY_TYPE,
|
||||
ENTITY_TYPES,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
import { type CloudProvider, CloudProviderIcon, AgentIcon } from '@kbn/custom-icons';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
|
||||
import type { AgentName } from '@kbn/elastic-agent-utils';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import type { Entity } from '../../../common/entities';
|
||||
import { castArray } from 'lodash';
|
||||
import type { InventoryEntity } from '../../../common/entities';
|
||||
import { isBuiltinEntityOfType } from '../../../common/utils/entity_type_guards';
|
||||
|
||||
interface EntityIconProps {
|
||||
entity: Entity;
|
||||
entity: InventoryEntity;
|
||||
}
|
||||
|
||||
type NotNullableCloudProvider = Exclude<CloudProvider, null>;
|
||||
|
||||
const getSingleValue = <T,>(value?: T | T[] | null): T | undefined => {
|
||||
return value == null ? undefined : Array.isArray(value) ? value[0] : value;
|
||||
};
|
||||
|
||||
export function EntityIcon({ entity }: EntityIconProps) {
|
||||
const entityType = entity[ENTITY_TYPE];
|
||||
const defaultIconSize = euiThemeVars.euiSizeL;
|
||||
|
||||
if (entityType === ENTITY_TYPES.HOST || entityType === ENTITY_TYPES.CONTAINER) {
|
||||
const cloudProvider = getSingleValue(
|
||||
entity[CLOUD_PROVIDER] as NotNullableCloudProvider | NotNullableCloudProvider[]
|
||||
);
|
||||
if (isBuiltinEntityOfType('host', entity) || isBuiltinEntityOfType('container', entity)) {
|
||||
const cloudProvider = castArray(entity.cloud?.provider)[0];
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
style={{ width: defaultIconSize, height: defaultIconSize }}
|
||||
|
@ -44,7 +31,7 @@ export function EntityIcon({ entity }: EntityIconProps) {
|
|||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<CloudProviderIcon
|
||||
cloudProvider={cloudProvider}
|
||||
cloudProvider={cloudProvider as CloudProvider | undefined}
|
||||
size="m"
|
||||
title={cloudProvider}
|
||||
role="presentation"
|
||||
|
@ -54,12 +41,11 @@ export function EntityIcon({ entity }: EntityIconProps) {
|
|||
);
|
||||
}
|
||||
|
||||
if (entityType === ENTITY_TYPES.SERVICE) {
|
||||
const agentName = getSingleValue(entity[AGENT_NAME] as AgentName | AgentName[]);
|
||||
return <AgentIcon agentName={agentName} role="presentation" />;
|
||||
if (isBuiltinEntityOfType('service', entity)) {
|
||||
return <AgentIcon agentName={castArray(entity.agent?.name)[0]} role="presentation" />;
|
||||
}
|
||||
|
||||
if (entityType.startsWith('kubernetes')) {
|
||||
if (entity.entityType.startsWith('kubernetes')) {
|
||||
return <EuiIcon type="logoKubernetes" size="l" />;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { EuiSpacer } from '@elastic/eui';
|
|||
import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
|
||||
import React from 'react';
|
||||
import useEffectOnce from 'react-use/lib/useEffectOnce';
|
||||
import { flattenObject } from '@kbn/observability-utils/object/flatten_object';
|
||||
import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { useUnifiedSearchContext } from '../../hooks/use_unified_search_context';
|
||||
|
@ -52,15 +53,18 @@ export function GroupedInventory() {
|
|||
<>
|
||||
<InventorySummary totalEntities={value.entitiesCount} totalGroups={value.groups.length} />
|
||||
<EuiSpacer size="m" />
|
||||
{value.groups.map((group) => (
|
||||
<InventoryGroupAccordion
|
||||
key={`${value.groupBy}-${group[value.groupBy]}`}
|
||||
groupBy={value.groupBy}
|
||||
groupValue={group[value.groupBy]}
|
||||
groupCount={group.count}
|
||||
isLoading={loading}
|
||||
/>
|
||||
))}
|
||||
{value.groups.map((group) => {
|
||||
const groupValue = flattenObject(group)[value.groupBy];
|
||||
return (
|
||||
<InventoryGroupAccordion
|
||||
key={`${value.groupBy}-${groupValue}`}
|
||||
groupBy={value.groupBy}
|
||||
groupValue={groupValue}
|
||||
groupCount={group.count}
|
||||
isLoading={loading}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,34 +9,24 @@ import { renderHook } from '@testing-library/react-hooks';
|
|||
import { useDetailViewRedirect } from './use_detail_view_redirect';
|
||||
import { useKibana } from './use_kibana';
|
||||
import {
|
||||
AGENT_NAME,
|
||||
CLOUD_PROVIDER,
|
||||
CONTAINER_ID,
|
||||
ENTITY_DEFINITION_ID,
|
||||
ENTITY_DISPLAY_NAME,
|
||||
ENTITY_ID,
|
||||
ENTITY_IDENTITY_FIELDS,
|
||||
ENTITY_LAST_SEEN,
|
||||
ENTITY_TYPE,
|
||||
HOST_NAME,
|
||||
ENTITY_TYPES,
|
||||
SERVICE_ENVIRONMENT,
|
||||
HOST_NAME,
|
||||
SERVICE_NAME,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
import { unflattenEntity } from '../../common/utils/unflatten_entity';
|
||||
import type { Entity } from '../../common/entities';
|
||||
import type { InventoryEntity } from '../../common/entities';
|
||||
|
||||
jest.mock('./use_kibana');
|
||||
jest.mock('../../common/utils/unflatten_entity');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
const unflattenEntityMock = unflattenEntity as jest.Mock;
|
||||
|
||||
const commonEntityFields: Partial<Entity> = {
|
||||
[ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z',
|
||||
[ENTITY_ID]: '1',
|
||||
[ENTITY_DISPLAY_NAME]: 'entity_name',
|
||||
[ENTITY_DEFINITION_ID]: 'entity_definition_id',
|
||||
const commonEntityFields: Partial<InventoryEntity> = {
|
||||
entityLastSeenTimestamp: '2023-10-09T00:00:00Z',
|
||||
entityId: '1',
|
||||
entityDisplayName: 'entity_name',
|
||||
entityDefinitionId: 'entity_definition_id',
|
||||
entityDefinitionVersion: '1',
|
||||
entitySchemaVersion: '1',
|
||||
};
|
||||
|
||||
describe('useDetailViewRedirect', () => {
|
||||
|
@ -66,17 +56,19 @@ describe('useDetailViewRedirect', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
unflattenEntityMock.mockImplementation((entity) => entity);
|
||||
});
|
||||
|
||||
it('getEntityRedirectUrl should return the correct URL for host entity', () => {
|
||||
const entity: Entity = {
|
||||
...(commonEntityFields as Entity),
|
||||
[ENTITY_IDENTITY_FIELDS]: [HOST_NAME],
|
||||
[ENTITY_TYPE]: 'host',
|
||||
[HOST_NAME]: 'host-1',
|
||||
[CLOUD_PROVIDER]: null,
|
||||
const entity: InventoryEntity = {
|
||||
...(commonEntityFields as InventoryEntity),
|
||||
entityType: 'host',
|
||||
entityIdentityFields: ['host.name'],
|
||||
host: {
|
||||
name: 'host-1',
|
||||
},
|
||||
cloud: {
|
||||
provider: null,
|
||||
},
|
||||
};
|
||||
|
||||
mockGetIdentityFieldsValue.mockReturnValue({ [HOST_NAME]: 'host-1' });
|
||||
|
@ -90,12 +82,16 @@ describe('useDetailViewRedirect', () => {
|
|||
});
|
||||
|
||||
it('getEntityRedirectUrl should return the correct URL for container entity', () => {
|
||||
const entity: Entity = {
|
||||
...(commonEntityFields as Entity),
|
||||
[ENTITY_IDENTITY_FIELDS]: [CONTAINER_ID],
|
||||
[ENTITY_TYPE]: 'container',
|
||||
[CONTAINER_ID]: 'container-1',
|
||||
[CLOUD_PROVIDER]: null,
|
||||
const entity: InventoryEntity = {
|
||||
...(commonEntityFields as InventoryEntity),
|
||||
entityType: 'container',
|
||||
entityIdentityFields: ['container.id'],
|
||||
container: {
|
||||
id: 'container-1',
|
||||
},
|
||||
cloud: {
|
||||
provider: null,
|
||||
},
|
||||
};
|
||||
|
||||
mockGetIdentityFieldsValue.mockReturnValue({ [CONTAINER_ID]: 'container-1' });
|
||||
|
@ -112,13 +108,17 @@ describe('useDetailViewRedirect', () => {
|
|||
});
|
||||
|
||||
it('getEntityRedirectUrl should return the correct URL for service entity', () => {
|
||||
const entity: Entity = {
|
||||
...(commonEntityFields as Entity),
|
||||
[ENTITY_IDENTITY_FIELDS]: [SERVICE_NAME],
|
||||
[ENTITY_TYPE]: 'service',
|
||||
[SERVICE_NAME]: 'service-1',
|
||||
[SERVICE_ENVIRONMENT]: 'prod',
|
||||
[AGENT_NAME]: 'node',
|
||||
const entity: InventoryEntity = {
|
||||
...(commonEntityFields as InventoryEntity),
|
||||
entityType: 'service',
|
||||
entityIdentityFields: ['service.name'],
|
||||
agent: {
|
||||
name: 'node',
|
||||
},
|
||||
service: {
|
||||
name: 'service-1',
|
||||
environment: 'prod',
|
||||
},
|
||||
};
|
||||
mockGetIdentityFieldsValue.mockReturnValue({ [SERVICE_NAME]: 'service-1' });
|
||||
mockGetRedirectUrl.mockReturnValue('service-overview-url');
|
||||
|
@ -145,10 +145,13 @@ describe('useDetailViewRedirect', () => {
|
|||
[ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs, 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013'],
|
||||
].forEach(([entityType, dashboardId]) => {
|
||||
it(`getEntityRedirectUrl should return the correct URL for ${entityType} entity`, () => {
|
||||
const entity: Entity = {
|
||||
...(commonEntityFields as Entity),
|
||||
[ENTITY_IDENTITY_FIELDS]: ['some.field'],
|
||||
[ENTITY_TYPE]: entityType,
|
||||
const entity: InventoryEntity = {
|
||||
...(commonEntityFields as InventoryEntity),
|
||||
entityType,
|
||||
entityIdentityFields: ['some.field'],
|
||||
some: {
|
||||
field: 'some-value',
|
||||
},
|
||||
};
|
||||
|
||||
mockAsKqlFilter.mockReturnValue('kql-query');
|
||||
|
|
|
@ -6,20 +6,17 @@
|
|||
*/
|
||||
import {
|
||||
ASSET_DETAILS_LOCATOR_ID,
|
||||
AssetDetailsLocatorParams,
|
||||
ENTITY_IDENTITY_FIELDS,
|
||||
ENTITY_TYPE,
|
||||
ENTITY_TYPES,
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_OVERVIEW_LOCATOR_ID,
|
||||
ServiceOverviewParams,
|
||||
type AssetDetailsLocatorParams,
|
||||
type ServiceOverviewParams,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
import { useCallback } from 'react';
|
||||
import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public';
|
||||
import type { DashboardLocatorParams } from '@kbn/dashboard-plugin/public';
|
||||
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
|
||||
import { castArray } from 'lodash';
|
||||
import type { Entity } from '../../common/entities';
|
||||
import { unflattenEntity } from '../../common/utils/unflatten_entity';
|
||||
import { isBuiltinEntityOfType } from '../../common/utils/entity_type_guards';
|
||||
import type { InventoryEntity } from '../../common/entities';
|
||||
import { useKibana } from './use_kibana';
|
||||
|
||||
const KUBERNETES_DASHBOARDS_IDS: Record<string, string> = {
|
||||
|
@ -44,52 +41,38 @@ export const useDetailViewRedirect = () => {
|
|||
const dashboardLocator = locators.get<DashboardLocatorParams>(DASHBOARD_APP_LOCATOR);
|
||||
const serviceOverviewLocator = locators.get<ServiceOverviewParams>(SERVICE_OVERVIEW_LOCATOR_ID);
|
||||
|
||||
const getSingleIdentityFieldValue = useCallback(
|
||||
(entity: Entity) => {
|
||||
const identityFields = castArray(entity[ENTITY_IDENTITY_FIELDS]);
|
||||
if (identityFields.length > 1) {
|
||||
throw new Error(`Multiple identity fields are not supported for ${entity[ENTITY_TYPE]}`);
|
||||
}
|
||||
|
||||
const identityField = identityFields[0];
|
||||
return entityManager.entityClient.getIdentityFieldsValue(unflattenEntity(entity))[
|
||||
identityField
|
||||
];
|
||||
},
|
||||
[entityManager.entityClient]
|
||||
);
|
||||
|
||||
const getDetailViewRedirectUrl = useCallback(
|
||||
(entity: Entity) => {
|
||||
const type = entity[ENTITY_TYPE];
|
||||
const identityValue = getSingleIdentityFieldValue(entity);
|
||||
(entity: InventoryEntity) => {
|
||||
const identityFieldsValue = entityManager.entityClient.getIdentityFieldsValue({
|
||||
entity: {
|
||||
identity_fields: entity.entityIdentityFields,
|
||||
},
|
||||
...entity,
|
||||
});
|
||||
const identityFields = castArray(entity.entityIdentityFields);
|
||||
|
||||
switch (type) {
|
||||
case ENTITY_TYPES.HOST:
|
||||
case ENTITY_TYPES.CONTAINER:
|
||||
return assetDetailsLocator?.getRedirectUrl({
|
||||
assetId: identityValue,
|
||||
assetType: type,
|
||||
});
|
||||
|
||||
case 'service':
|
||||
return serviceOverviewLocator?.getRedirectUrl({
|
||||
serviceName: identityValue,
|
||||
// service.environemnt is not part of entity.identityFields
|
||||
// we need to manually get its value
|
||||
environment: [entity[SERVICE_ENVIRONMENT] || undefined].flat()[0],
|
||||
});
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
if (isBuiltinEntityOfType('host', entity) || isBuiltinEntityOfType('container', entity)) {
|
||||
return assetDetailsLocator?.getRedirectUrl({
|
||||
assetId: identityFieldsValue[identityFields[0]],
|
||||
assetType: entity.entityType,
|
||||
});
|
||||
}
|
||||
|
||||
if (isBuiltinEntityOfType('service', entity)) {
|
||||
return serviceOverviewLocator?.getRedirectUrl({
|
||||
serviceName: identityFieldsValue[identityFields[0]],
|
||||
environment: entity.service?.environment,
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
[assetDetailsLocator, getSingleIdentityFieldValue, serviceOverviewLocator]
|
||||
[assetDetailsLocator, entityManager.entityClient, serviceOverviewLocator]
|
||||
);
|
||||
|
||||
const getDashboardRedirectUrl = useCallback(
|
||||
(entity: Entity) => {
|
||||
const type = entity[ENTITY_TYPE];
|
||||
(entity: InventoryEntity) => {
|
||||
const type = entity.entityType;
|
||||
const dashboardId = KUBERNETES_DASHBOARDS_IDS[type];
|
||||
|
||||
return dashboardId
|
||||
|
@ -97,7 +80,12 @@ export const useDetailViewRedirect = () => {
|
|||
dashboardId,
|
||||
query: {
|
||||
language: 'kuery',
|
||||
query: entityManager.entityClient.asKqlFilter(unflattenEntity(entity)),
|
||||
query: entityManager.entityClient.asKqlFilter({
|
||||
entity: {
|
||||
identity_fields: entity.entityIdentityFields,
|
||||
},
|
||||
...entity,
|
||||
}),
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
@ -106,7 +94,8 @@ export const useDetailViewRedirect = () => {
|
|||
);
|
||||
|
||||
const getEntityRedirectUrl = useCallback(
|
||||
(entity: Entity) => getDetailViewRedirectUrl(entity) ?? getDashboardRedirectUrl(entity),
|
||||
(entity: InventoryEntity) =>
|
||||
getDetailViewRedirectUrl(entity) ?? getDashboardRedirectUrl(entity),
|
||||
[getDashboardRedirectUrl, getDetailViewRedirectUrl]
|
||||
);
|
||||
|
||||
|
|
|
@ -11,12 +11,11 @@ import {
|
|||
ENTITY_TYPE,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
import { useCallback } from 'react';
|
||||
import type { Entity, EntityColumnIds } from '../../common/entities';
|
||||
import { unflattenEntity } from '../../common/utils/unflatten_entity';
|
||||
import type { InventoryEntity } from '../../common/entities';
|
||||
import { useKibana } from './use_kibana';
|
||||
import { useUnifiedSearchContext } from './use_unified_search_context';
|
||||
|
||||
const ACTIVE_COLUMNS: EntityColumnIds[] = [ENTITY_DISPLAY_NAME, ENTITY_TYPE, ENTITY_LAST_SEEN];
|
||||
const ACTIVE_COLUMNS = [ENTITY_DISPLAY_NAME, ENTITY_TYPE, ENTITY_LAST_SEEN];
|
||||
|
||||
export const useDiscoverRedirect = () => {
|
||||
const {
|
||||
|
@ -31,9 +30,14 @@ export const useDiscoverRedirect = () => {
|
|||
const discoverLocator = share.url.locators.get('DISCOVER_APP_LOCATOR');
|
||||
|
||||
const getDiscoverEntitiesRedirectUrl = useCallback(
|
||||
(entity?: Entity) => {
|
||||
(entity?: InventoryEntity) => {
|
||||
const entityKqlFilter = entity
|
||||
? entityManager.entityClient.asKqlFilter(unflattenEntity(entity))
|
||||
? entityManager.entityClient.asKqlFilter({
|
||||
entity: {
|
||||
identity_fields: entity.entityIdentityFields,
|
||||
},
|
||||
...entity,
|
||||
})
|
||||
: '';
|
||||
|
||||
const kueryWithEntityDefinitionFilters = [
|
||||
|
@ -65,7 +69,7 @@ export const useDiscoverRedirect = () => {
|
|||
);
|
||||
|
||||
const getDiscoverRedirectUrl = useCallback(
|
||||
(entity?: Entity) => getDiscoverEntitiesRedirectUrl(entity),
|
||||
(entity?: InventoryEntity) => getDiscoverEntitiesRedirectUrl(entity),
|
||||
[getDiscoverEntitiesRedirectUrl]
|
||||
);
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
|
||||
import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects';
|
||||
import {
|
||||
ENTITIES_LATEST_ALIAS,
|
||||
type EntityGroup,
|
||||
|
@ -32,10 +31,8 @@ export async function getEntityGroupsBy({
|
|||
const limit = `LIMIT ${MAX_NUMBER_OF_ENTITIES}`;
|
||||
const query = [from, ...where, group, sort, limit].join(' | ');
|
||||
|
||||
const groups = await inventoryEsClient.esql('get_entities_groups', {
|
||||
return inventoryEsClient.esql<EntityGroup>('get_entities_groups', {
|
||||
query,
|
||||
filter: esQuery,
|
||||
});
|
||||
|
||||
return esqlResultToPlainObjects<EntityGroup>(groups);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
|
||||
import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
|
||||
import type { EntityInstance } from '@kbn/entities-schema';
|
||||
import { ENTITIES_LATEST_ALIAS } from '../../../common/entities';
|
||||
import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper';
|
||||
|
||||
|
@ -15,12 +16,14 @@ export async function getEntityTypes({
|
|||
}: {
|
||||
inventoryEsClient: ObservabilityElasticsearchClient;
|
||||
}) {
|
||||
const entityTypesEsqlResponse = await inventoryEsClient.esql('get_entity_types', {
|
||||
const entityTypesEsqlResponse = await inventoryEsClient.esql<{
|
||||
entity: Pick<EntityInstance['entity'], 'type'>;
|
||||
}>('get_entity_types', {
|
||||
query: `FROM ${ENTITIES_LATEST_ALIAS}
|
||||
| ${getBuiltinEntityDefinitionIdESQLWhereClause()}
|
||||
| STATS count = COUNT(${ENTITY_TYPE}) BY ${ENTITY_TYPE}
|
||||
`,
|
||||
});
|
||||
|
||||
return entityTypesEsqlResponse.values.map(([_, val]) => val as string);
|
||||
return entityTypesEsqlResponse.map((response) => response.entity.type);
|
||||
}
|
||||
|
|
|
@ -5,52 +5,60 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Entity } from '../../../common/entities';
|
||||
import {
|
||||
ENTITY_DEFINITION_ID,
|
||||
ENTITY_DISPLAY_NAME,
|
||||
ENTITY_ID,
|
||||
ENTITY_IDENTITY_FIELDS,
|
||||
ENTITY_LAST_SEEN,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
import type { InventoryEntity } from '../../../common/entities';
|
||||
import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type';
|
||||
|
||||
const commonEntityFields = {
|
||||
[ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z',
|
||||
[ENTITY_ID]: '1',
|
||||
[ENTITY_DISPLAY_NAME]: 'entity_name',
|
||||
[ENTITY_DEFINITION_ID]: 'entity_definition_id',
|
||||
alertCount: 3,
|
||||
const commonEntityFields: Partial<InventoryEntity> = {
|
||||
entityLastSeenTimestamp: '2023-10-09T00:00:00Z',
|
||||
entityId: '1',
|
||||
entityDisplayName: 'entity_name',
|
||||
entityDefinitionId: 'entity_definition_id',
|
||||
alertsCount: 3,
|
||||
};
|
||||
|
||||
describe('getIdentityFields', () => {
|
||||
it('should return an empty Map when no entities are provided', () => {
|
||||
const result = getIdentityFieldsPerEntityType([]);
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
it('should return a Map with unique entity types and their respective identity fields', () => {
|
||||
const serviceEntity: Entity = {
|
||||
'agent.name': 'node',
|
||||
[ENTITY_IDENTITY_FIELDS]: ['service.name', 'service.environment'],
|
||||
'service.name': 'my-service',
|
||||
'entity.type': 'service',
|
||||
...commonEntityFields,
|
||||
const serviceEntity: InventoryEntity = {
|
||||
...(commonEntityFields as InventoryEntity),
|
||||
entityIdentityFields: ['service.name', 'service.environment'],
|
||||
entityType: 'service',
|
||||
agent: {
|
||||
name: 'node',
|
||||
},
|
||||
service: {
|
||||
name: 'my-service',
|
||||
},
|
||||
};
|
||||
|
||||
const hostEntity: Entity = {
|
||||
[ENTITY_IDENTITY_FIELDS]: ['host.name'],
|
||||
'host.name': 'my-host',
|
||||
'entity.type': 'host',
|
||||
'cloud.provider': null,
|
||||
...commonEntityFields,
|
||||
const hostEntity: InventoryEntity = {
|
||||
...(commonEntityFields as InventoryEntity),
|
||||
entityIdentityFields: ['host.name'],
|
||||
entityType: 'host',
|
||||
cloud: {
|
||||
provider: null,
|
||||
},
|
||||
host: {
|
||||
name: 'my-host',
|
||||
},
|
||||
};
|
||||
|
||||
const containerEntity: Entity = {
|
||||
[ENTITY_IDENTITY_FIELDS]: 'container.id',
|
||||
'host.name': 'my-host',
|
||||
'entity.type': 'container',
|
||||
'cloud.provider': null,
|
||||
'container.id': '123',
|
||||
...commonEntityFields,
|
||||
const containerEntity: InventoryEntity = {
|
||||
...(commonEntityFields as InventoryEntity),
|
||||
entityIdentityFields: ['container.id'],
|
||||
entityType: 'container',
|
||||
host: {
|
||||
name: 'my-host',
|
||||
},
|
||||
cloud: {
|
||||
provider: null,
|
||||
},
|
||||
container: {
|
||||
id: '123',
|
||||
},
|
||||
};
|
||||
|
||||
const mockEntities = [serviceEntity, hostEntity, containerEntity];
|
||||
|
|
|
@ -5,16 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
|
||||
import { Entity } from '../../../common/entities';
|
||||
import { castArray } from 'lodash';
|
||||
import type { InventoryEntity } from '../../../common/entities';
|
||||
|
||||
export type IdentityFieldsPerEntityType = Map<string, string[]>;
|
||||
|
||||
export const getIdentityFieldsPerEntityType = (entities: Entity[]) => {
|
||||
const identityFieldsPerEntityType: IdentityFieldsPerEntityType = new Map();
|
||||
export const getIdentityFieldsPerEntityType = (latestEntities: InventoryEntity[]) => {
|
||||
const identityFieldsPerEntityType = new Map<string, string[]>();
|
||||
|
||||
entities.forEach((entity) =>
|
||||
identityFieldsPerEntityType.set(entity[ENTITY_TYPE], [entity[ENTITY_IDENTITY_FIELDS]].flat())
|
||||
latestEntities.forEach((entity) =>
|
||||
identityFieldsPerEntityType.set(entity.entityType, castArray(entity.entityIdentityFields))
|
||||
);
|
||||
|
||||
return identityFieldsPerEntityType;
|
||||
|
|
|
@ -5,18 +5,32 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
|
||||
import {
|
||||
ENTITY_LAST_SEEN,
|
||||
ENTITY_TYPE,
|
||||
ENTITY_DISPLAY_NAME,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
import type { QueryDslQueryContainer, ScalarValue } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { ENTITY_LAST_SEEN, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
|
||||
import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
|
||||
import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects';
|
||||
import type { EntityInstance } from '@kbn/entities-schema';
|
||||
import {
|
||||
ENTITIES_LATEST_ALIAS,
|
||||
MAX_NUMBER_OF_ENTITIES,
|
||||
type Entity,
|
||||
type EntityColumnIds,
|
||||
InventoryEntity,
|
||||
} from '../../../common/entities';
|
||||
import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper';
|
||||
|
||||
type EntitySortableColumnIds = Extract<
|
||||
EntityColumnIds,
|
||||
'entityLastSeenTimestamp' | 'entityDisplayName' | 'entityType'
|
||||
>;
|
||||
const SORT_FIELDS_TO_ES_FIELDS: Record<EntitySortableColumnIds, string> = {
|
||||
entityLastSeenTimestamp: ENTITY_LAST_SEEN,
|
||||
entityDisplayName: ENTITY_DISPLAY_NAME,
|
||||
entityType: ENTITY_TYPE,
|
||||
} as const;
|
||||
|
||||
export async function getLatestEntities({
|
||||
inventoryEsClient,
|
||||
sortDirection,
|
||||
|
@ -29,9 +43,10 @@ export async function getLatestEntities({
|
|||
sortField: EntityColumnIds;
|
||||
esQuery?: QueryDslQueryContainer;
|
||||
entityTypes?: string[];
|
||||
}) {
|
||||
}): Promise<InventoryEntity[]> {
|
||||
// alertsCount doesn't exist in entities index. Ignore it and sort by entity.lastSeenTimestamp by default.
|
||||
const entitiesSortField = sortField === 'alertsCount' ? ENTITY_LAST_SEEN : sortField;
|
||||
const entitiesSortField =
|
||||
SORT_FIELDS_TO_ES_FIELDS[sortField as EntitySortableColumnIds] ?? ENTITY_LAST_SEEN;
|
||||
|
||||
const from = `FROM ${ENTITIES_LATEST_ALIAS}`;
|
||||
const where: string[] = [getBuiltinEntityDefinitionIdESQLWhereClause()];
|
||||
|
@ -47,11 +62,28 @@ export async function getLatestEntities({
|
|||
|
||||
const query = [from, ...where, sort, limit].join(' | ');
|
||||
|
||||
const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', {
|
||||
query,
|
||||
filter: esQuery,
|
||||
params,
|
||||
});
|
||||
const latestEntitiesEsqlResponse = await inventoryEsClient.esql<EntityInstance>(
|
||||
'get_latest_entities',
|
||||
{
|
||||
query,
|
||||
filter: esQuery,
|
||||
params,
|
||||
}
|
||||
);
|
||||
|
||||
return esqlResultToPlainObjects<Entity>(latestEntitiesEsqlResponse);
|
||||
return latestEntitiesEsqlResponse.map((lastestEntity) => {
|
||||
const { entity, ...metadata } = lastestEntity;
|
||||
|
||||
return {
|
||||
entityId: entity.id,
|
||||
entityType: entity.type,
|
||||
entityDefinitionId: entity.definition_id,
|
||||
entityDisplayName: entity.display_name,
|
||||
entityIdentityFields: entity.identity_fields,
|
||||
entityLastSeenTimestamp: entity.last_seen_timestamp,
|
||||
entityDefinitionVersion: entity.definition_version,
|
||||
entitySchemaVersion: entity.schema_version,
|
||||
...metadata,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { termQuery } from '@kbn/observability-plugin/server';
|
||||
import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
|
||||
import { ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils';
|
||||
import { AlertsClient } from '../../lib/create_alerts_client.ts/create_alerts_client';
|
||||
import { getGroupByTermsAgg } from './get_group_by_terms_agg';
|
||||
|
@ -25,7 +24,7 @@ export async function getLatestEntitiesAlerts({
|
|||
}: {
|
||||
alertsClient: AlertsClient;
|
||||
identityFieldsPerEntityType: IdentityFieldsPerEntityType;
|
||||
}): Promise<Array<{ [key: string]: any; alertsCount?: number; [ENTITY_TYPE]: string }>> {
|
||||
}): Promise<Array<{ [key: string]: any; alertsCount?: number; entityType: string }>> {
|
||||
if (identityFieldsPerEntityType.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
@ -54,7 +53,7 @@ export async function getLatestEntitiesAlerts({
|
|||
|
||||
return buckets.map((bucket: Bucket) => ({
|
||||
alertsCount: bucket.doc_count,
|
||||
[ENTITY_TYPE]: entityType,
|
||||
entityType,
|
||||
...bucket.key,
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
|
|||
import * as t from 'io-ts';
|
||||
import { orderBy } from 'lodash';
|
||||
import { joinByKey } from '@kbn/observability-utils/array/join_by_key';
|
||||
import { entityColumnIdsRt, Entity } from '../../../common/entities';
|
||||
import { entityColumnIdsRt, InventoryEntity } from '../../../common/entities';
|
||||
import { createInventoryServerRoute } from '../create_inventory_server_route';
|
||||
import { getEntityTypes } from './get_entity_types';
|
||||
import { getLatestEntities } from './get_latest_entities';
|
||||
|
@ -61,7 +61,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({
|
|||
logger,
|
||||
plugins,
|
||||
request,
|
||||
}): Promise<{ entities: Entity[] }> => {
|
||||
}): Promise<{ entities: InventoryEntity[] }> => {
|
||||
const coreContext = await context.core;
|
||||
const inventoryEsClient = createObservabilityEsClient({
|
||||
client: coreContext.elasticsearch.client.asCurrentUser,
|
||||
|
@ -90,16 +90,16 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({
|
|||
});
|
||||
|
||||
const joined = joinByKey(
|
||||
[...latestEntities, ...alerts],
|
||||
[...latestEntities, ...alerts] as InventoryEntity[],
|
||||
[...identityFieldsPerEntityType.values()].flat()
|
||||
).filter((entity) => entity['entity.id']) as Entity[];
|
||||
).filter((latestEntity) => latestEntity.entityId);
|
||||
|
||||
return {
|
||||
entities:
|
||||
sortField === 'alertsCount'
|
||||
? orderBy(
|
||||
joined,
|
||||
[(item: Entity) => item?.alertsCount === undefined, sortField],
|
||||
[(item: InventoryEntity) => item?.alertsCount === undefined, sortField],
|
||||
['asc', sortDirection] // push entities without alertsCount to the end
|
||||
)
|
||||
: joined,
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects';
|
||||
import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
|
||||
import { getBuiltinEntityDefinitionIdESQLWhereClause } from '../entities/query_helper';
|
||||
import { ENTITIES_LATEST_ALIAS } from '../../../common/entities';
|
||||
|
@ -18,14 +17,15 @@ export async function getHasData({
|
|||
logger: Logger;
|
||||
}) {
|
||||
try {
|
||||
const esqlResults = await inventoryEsClient.esql('get_has_data', {
|
||||
const esqlResults = await inventoryEsClient.esql<{ _count: number }>('get_has_data', {
|
||||
query: `FROM ${ENTITIES_LATEST_ALIAS}
|
||||
| ${getBuiltinEntityDefinitionIdESQLWhereClause()}
|
||||
| STATS _count = COUNT(*)
|
||||
| LIMIT 1`,
|
||||
});
|
||||
|
||||
const totalCount = esqlResultToPlainObjects(esqlResults)?.[0]._count ?? 0;
|
||||
const totalCount = esqlResults[0]._count;
|
||||
|
||||
return { hasData: totalCount > 0 };
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
|
|
|
@ -53,7 +53,6 @@
|
|||
"@kbn/spaces-plugin",
|
||||
"@kbn/cloud-plugin",
|
||||
"@kbn/storybook",
|
||||
"@kbn/zod",
|
||||
"@kbn/dashboard-plugin",
|
||||
"@kbn/deeplinks-analytics",
|
||||
"@kbn/controls-plugin",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue