mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[eem] _search endpoint / initial entity manager UI (#199609)](https://github.com/elastic/kibana/pull/199609) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Kevin Lacabane","email":"kevin.lacabane@elastic.co"},"sourceCommit":{"committedDate":"2024-11-22T15:12:04Z","message":"[eem] _search endpoint / initial entity manager UI (#199609)\n\n## Summary\r\n\r\n- create `_search` endpoint to discover entities with esql queries. It\r\ncurrently reads sources of the provided `type` from\r\n`kibana_entity_definitions` index. Run this query to insert a\r\ndefinition:\r\n```\r\nPOST kibana_entity_definitions/_doc\r\n{\r\n \"entity_type\": \"service\",\r\n \"index_patterns\": [\"remote_cluster:logs-*\"],\r\n \"metadata_fields\": [],\r\n \"identity_fields\": [\"service.name\"],\r\n \"filters\": [],\r\n \"timestamp_field\": \"@timestamp\"\r\n}\r\n```\r\n\r\nBy default `_search` will look at data in the last 5m. The lookup period\r\ncan be overriden by providing `start`/`end` parameters in ISO format. It\r\nalso accepts a `limit` to specify the number of entities returned which\r\ndefaults to 10\r\n\r\n```\r\nPOST kbn:/internal/entities/v2/_search\r\n{\r\n \"type\": \"service\",\r\n \"start\": \"2024-11-19T20:40:00.000Z\",\r\n \"end\": \"2024-11-19T20:50:00.000Z\",\r\n \"limit\": 20\r\n}\r\n```\r\n\r\n- create `_search/preview` endpoint to preview output of entity sources\r\nwithout persisting them\r\n \r\n- create UI to preview results of an entity definition at\r\n`/app/entity_manager`. The application is living in its own plugin at\r\n`observability_solution/entity_manager_app`\r\n\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Milton Hultgren <miltonhultgren@gmail.com>","sha":"0b3f4fbd3cd60663289fc13f8f01e3f4c9131479","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport missing","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-entities"],"number":199609,"url":"https://github.com/elastic/kibana/pull/199609","mergeCommit":{"message":"[eem] _search endpoint / initial entity manager UI (#199609)\n\n## Summary\r\n\r\n- create `_search` endpoint to discover entities with esql queries. It\r\ncurrently reads sources of the provided `type` from\r\n`kibana_entity_definitions` index. Run this query to insert a\r\ndefinition:\r\n```\r\nPOST kibana_entity_definitions/_doc\r\n{\r\n \"entity_type\": \"service\",\r\n \"index_patterns\": [\"remote_cluster:logs-*\"],\r\n \"metadata_fields\": [],\r\n \"identity_fields\": [\"service.name\"],\r\n \"filters\": [],\r\n \"timestamp_field\": \"@timestamp\"\r\n}\r\n```\r\n\r\nBy default `_search` will look at data in the last 5m. The lookup period\r\ncan be overriden by providing `start`/`end` parameters in ISO format. It\r\nalso accepts a `limit` to specify the number of entities returned which\r\ndefaults to 10\r\n\r\n```\r\nPOST kbn:/internal/entities/v2/_search\r\n{\r\n \"type\": \"service\",\r\n \"start\": \"2024-11-19T20:40:00.000Z\",\r\n \"end\": \"2024-11-19T20:50:00.000Z\",\r\n \"limit\": 20\r\n}\r\n```\r\n\r\n- create `_search/preview` endpoint to preview output of entity sources\r\nwithout persisting them\r\n \r\n- create UI to preview results of an entity definition at\r\n`/app/entity_manager`. The application is living in its own plugin at\r\n`observability_solution/entity_manager_app`\r\n\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Milton Hultgren <miltonhultgren@gmail.com>","sha":"0b3f4fbd3cd60663289fc13f8f01e3f4c9131479"}},"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/199609","number":199609,"mergeCommit":{"message":"[eem] _search endpoint / initial entity manager UI (#199609)\n\n## Summary\r\n\r\n- create `_search` endpoint to discover entities with esql queries. It\r\ncurrently reads sources of the provided `type` from\r\n`kibana_entity_definitions` index. Run this query to insert a\r\ndefinition:\r\n```\r\nPOST kibana_entity_definitions/_doc\r\n{\r\n \"entity_type\": \"service\",\r\n \"index_patterns\": [\"remote_cluster:logs-*\"],\r\n \"metadata_fields\": [],\r\n \"identity_fields\": [\"service.name\"],\r\n \"filters\": [],\r\n \"timestamp_field\": \"@timestamp\"\r\n}\r\n```\r\n\r\nBy default `_search` will look at data in the last 5m. The lookup period\r\ncan be overriden by providing `start`/`end` parameters in ISO format. It\r\nalso accepts a `limit` to specify the number of entities returned which\r\ndefaults to 10\r\n\r\n```\r\nPOST kbn:/internal/entities/v2/_search\r\n{\r\n \"type\": \"service\",\r\n \"start\": \"2024-11-19T20:40:00.000Z\",\r\n \"end\": \"2024-11-19T20:50:00.000Z\",\r\n \"limit\": 20\r\n}\r\n```\r\n\r\n- create `_search/preview` endpoint to preview output of entity sources\r\nwithout persisting them\r\n \r\n- create UI to preview results of an entity definition at\r\n`/app/entity_manager`. The application is living in its own plugin at\r\n`observability_solution/entity_manager_app`\r\n\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Milton Hultgren <miltonhultgren@gmail.com>","sha":"0b3f4fbd3cd60663289fc13f8f01e3f4c9131479"}}]}] BACKPORT--> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
eb0abce6fe
commit
337ab20ad3
35 changed files with 1482 additions and 14 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -415,6 +415,7 @@ x-pack/plugins/enterprise_search @elastic/search-kibana
|
|||
x-pack/plugins/observability_solution/entities_data_access @elastic/obs-entities
|
||||
x-pack/packages/kbn-entities-schema @elastic/obs-entities
|
||||
x-pack/test/api_integration/apis/entity_manager/fixture_plugin @elastic/obs-entities
|
||||
x-pack/plugins/observability_solution/entity_manager_app @elastic/obs-entities
|
||||
x-pack/plugins/entity_manager @elastic/obs-entities
|
||||
examples/error_boundary @elastic/appex-sharedux
|
||||
packages/kbn-es @elastic/kibana-operations
|
||||
|
|
|
@ -579,6 +579,10 @@ security and spaces filtering.
|
|||
|This plugin provides access to observed entity data, such as information about hosts, pods, containers, services, and more.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/observability_solution/entity_manager_app/README.md[entityManagerApp]
|
||||
|This plugin provides a user interface to interact with the Entity Manager.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog]
|
||||
|The event log plugin provides a persistent history of alerting and action
|
||||
activities.
|
||||
|
|
|
@ -478,6 +478,7 @@
|
|||
"@kbn/entities-data-access-plugin": "link:x-pack/plugins/observability_solution/entities_data_access",
|
||||
"@kbn/entities-schema": "link:x-pack/packages/kbn-entities-schema",
|
||||
"@kbn/entity-manager-fixture-plugin": "link:x-pack/test/api_integration/apis/entity_manager/fixture_plugin",
|
||||
"@kbn/entityManager-app-plugin": "link:x-pack/plugins/observability_solution/entity_manager_app",
|
||||
"@kbn/entityManager-plugin": "link:x-pack/plugins/entity_manager",
|
||||
"@kbn/error-boundary-example-plugin": "link:examples/error_boundary",
|
||||
"@kbn/es-errors": "link:packages/kbn-es-errors",
|
||||
|
|
|
@ -42,6 +42,7 @@ pageLoadAssetSize:
|
|||
embeddableEnhanced: 22107
|
||||
enterpriseSearch: 66810
|
||||
entityManager: 17175
|
||||
entityManagerApp: 20378
|
||||
esql: 37000
|
||||
esqlDataGrid: 24582
|
||||
esUiShared: 326654
|
||||
|
|
|
@ -143,6 +143,7 @@ export const applicationUsageSchema = {
|
|||
enterpriseSearchSemanticSearch: commonSchema,
|
||||
enterpriseSearchVectorSearch: commonSchema,
|
||||
enterpriseSearchElasticsearch: commonSchema,
|
||||
entity_manager: commonSchema,
|
||||
appSearch: commonSchema,
|
||||
workplaceSearch: commonSchema,
|
||||
searchExperiences: commonSchema,
|
||||
|
|
|
@ -3015,6 +3015,137 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"entity_manager": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The application being tracked"
|
||||
}
|
||||
},
|
||||
"viewId": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "Always `main`"
|
||||
}
|
||||
},
|
||||
"clicks_total": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application since we started counting them"
|
||||
}
|
||||
},
|
||||
"clicks_7_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application over the last 7 days"
|
||||
}
|
||||
},
|
||||
"clicks_30_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application over the last 30 days"
|
||||
}
|
||||
},
|
||||
"clicks_90_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application over the last 90 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_total": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen since we started counting them."
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_7_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen over the last 7 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_30_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen over the last 30 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_90_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen over the last 90 days"
|
||||
}
|
||||
},
|
||||
"views": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The application being tracked"
|
||||
}
|
||||
},
|
||||
"viewId": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The application view being tracked"
|
||||
}
|
||||
},
|
||||
"clicks_total": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application sub view since we started counting them"
|
||||
}
|
||||
},
|
||||
"clicks_7_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the active application sub view over the last 7 days"
|
||||
}
|
||||
},
|
||||
"clicks_30_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the active application sub view over the last 30 days"
|
||||
}
|
||||
},
|
||||
"clicks_90_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the active application sub view over the last 90 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_total": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application sub view is active and on-screen since we started counting them."
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_7_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_30_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_90_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSearch": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
|
|
|
@ -824,6 +824,8 @@
|
|||
"@kbn/entities-schema/*": ["x-pack/packages/kbn-entities-schema/*"],
|
||||
"@kbn/entity-manager-fixture-plugin": ["x-pack/test/api_integration/apis/entity_manager/fixture_plugin"],
|
||||
"@kbn/entity-manager-fixture-plugin/*": ["x-pack/test/api_integration/apis/entity_manager/fixture_plugin/*"],
|
||||
"@kbn/entityManager-app-plugin": ["x-pack/plugins/observability_solution/entity_manager_app"],
|
||||
"@kbn/entityManager-app-plugin/*": ["x-pack/plugins/observability_solution/entity_manager_app/*"],
|
||||
"@kbn/entityManager-plugin": ["x-pack/plugins/entity_manager"],
|
||||
"@kbn/entityManager-plugin/*": ["x-pack/plugins/entity_manager/*"],
|
||||
"@kbn/error-boundary-example-plugin": ["examples/error_boundary"],
|
||||
|
|
|
@ -23,6 +23,13 @@ export interface MetadataRecord {
|
|||
[key: string]: string[] | MetadataRecord | string;
|
||||
}
|
||||
|
||||
export interface EntityV2 {
|
||||
'entity.id': string;
|
||||
'entity.last_seen_timestamp': string;
|
||||
'entity.type': string;
|
||||
[metadata: string]: any;
|
||||
}
|
||||
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||
|
||||
type Literal = z.infer<typeof literalSchema>;
|
||||
|
|
|
@ -8,9 +8,13 @@
|
|||
"plugin": {
|
||||
"id": "entityManager",
|
||||
"configPath": ["xpack", "entityManager"],
|
||||
"requiredPlugins": ["security", "encryptedSavedObjects", "licensing"],
|
||||
"browser": true,
|
||||
"server": true,
|
||||
"requiredPlugins": [
|
||||
"security",
|
||||
"encryptedSavedObjects",
|
||||
"licensing"
|
||||
],
|
||||
"requiredBundles": []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ export const plugin: PluginInitializer<
|
|||
return new Plugin(context);
|
||||
};
|
||||
|
||||
export { EntityClient } from './lib/entity_client';
|
||||
|
||||
export type { EntityManagerPublicPluginSetup, EntityManagerPublicPluginStart };
|
||||
export type EntityManagerAppId = 'entityManager';
|
||||
|
||||
|
|
|
@ -22,16 +22,14 @@ export class Plugin implements EntityManagerPluginClass {
|
|||
}
|
||||
|
||||
setup(core: CoreSetup) {
|
||||
const entityClient = new EntityClient(core);
|
||||
return {
|
||||
entityClient,
|
||||
entityClient: new EntityClient(core),
|
||||
};
|
||||
}
|
||||
|
||||
start(core: CoreStart) {
|
||||
const entityClient = new EntityClient(core);
|
||||
return {
|
||||
entityClient,
|
||||
entityClient: new EntityClient(core),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import type { EntityClient } from './lib/entity_client';
|
|||
export interface EntityManagerPublicPluginSetup {
|
||||
entityClient: EntityClient;
|
||||
}
|
||||
|
||||
export interface EntityManagerPublicPluginStart {
|
||||
entityClient: EntityClient;
|
||||
}
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export class AssetNotFoundError extends Error {
|
||||
constructor(ean: string) {
|
||||
super(`Asset with ean (${ean}) not found in the provided time range`);
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
this.name = 'AssetNotFoundError';
|
||||
export class UnknownEntityType extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'UnknownEntityType';
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EntityDefinition, EntityDefinitionUpdate } from '@kbn/entities-schema';
|
||||
import { EntityV2, EntityDefinition, EntityDefinitionUpdate } from '@kbn/entities-schema';
|
||||
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
|
||||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { Logger } from '@kbn/logging';
|
||||
|
@ -23,6 +23,9 @@ import { stopTransforms } from './entities/stop_transforms';
|
|||
import { deleteIndices } from './entities/delete_index';
|
||||
import { EntityDefinitionWithState } from './entities/types';
|
||||
import { EntityDefinitionUpdateConflict } from './entities/errors/entity_definition_update_conflict';
|
||||
import { EntitySource, getEntityInstancesQuery } from './queries';
|
||||
import { mergeEntitiesList, runESQLQuery } from './queries/utils';
|
||||
import { UnknownEntityType } from './entities/errors/unknown_entity_type';
|
||||
|
||||
export class EntityClient {
|
||||
constructor(
|
||||
|
@ -126,8 +129,6 @@ export class EntityClient {
|
|||
});
|
||||
|
||||
if (deleteData) {
|
||||
// delete data with current user as system user does not have
|
||||
// .entities privileges
|
||||
await deleteIndices(this.options.esClient, definition, this.options.logger);
|
||||
}
|
||||
}
|
||||
|
@ -170,4 +171,114 @@ export class EntityClient {
|
|||
this.options.logger.info(`Stopping transforms for definition [${definition.id}]`);
|
||||
return stopTransforms(this.options.esClient, definition, this.options.logger);
|
||||
}
|
||||
|
||||
async getEntitySources({ type }: { type: string }) {
|
||||
const result = await this.options.esClient.search<EntitySource>({
|
||||
index: 'kibana_entity_definitions',
|
||||
query: {
|
||||
bool: {
|
||||
must: {
|
||||
term: { entity_type: type },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return result.hits.hits.map((hit) => hit._source) as EntitySource[];
|
||||
}
|
||||
|
||||
async searchEntities({
|
||||
type,
|
||||
start,
|
||||
end,
|
||||
metadataFields = [],
|
||||
filters = [],
|
||||
limit = 10,
|
||||
}: {
|
||||
type: string;
|
||||
start: string;
|
||||
end: string;
|
||||
metadataFields?: string[];
|
||||
filters?: string[];
|
||||
limit?: number;
|
||||
}) {
|
||||
const sources = await this.getEntitySources({ type });
|
||||
if (sources.length === 0) {
|
||||
throw new UnknownEntityType(`No sources found for entity type [${type}]`);
|
||||
}
|
||||
|
||||
return this.searchEntitiesBySources({
|
||||
sources,
|
||||
start,
|
||||
end,
|
||||
metadataFields,
|
||||
filters,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
|
||||
async searchEntitiesBySources({
|
||||
sources,
|
||||
start,
|
||||
end,
|
||||
metadataFields = [],
|
||||
filters = [],
|
||||
limit = 10,
|
||||
}: {
|
||||
sources: EntitySource[];
|
||||
start: string;
|
||||
end: string;
|
||||
metadataFields?: string[];
|
||||
filters?: string[];
|
||||
limit?: number;
|
||||
}) {
|
||||
const entities = await Promise.all(
|
||||
sources.map(async (source) => {
|
||||
const mandatoryFields = [source.timestamp_field, ...source.identity_fields];
|
||||
const metaFields = [...metadataFields, ...source.metadata_fields];
|
||||
const { fields } = await this.options.esClient.fieldCaps({
|
||||
index: source.index_patterns,
|
||||
fields: [...mandatoryFields, ...metaFields],
|
||||
});
|
||||
|
||||
const sourceHasMandatoryFields = mandatoryFields.every((field) => !!fields[field]);
|
||||
if (!sourceHasMandatoryFields) {
|
||||
// we can't build entities without id fields so we ignore the source.
|
||||
// filters should likely behave similarly.
|
||||
this.options.logger.info(
|
||||
`Ignoring source for type [${source.type}] with index_patterns [${source.index_patterns}] because some mandatory fields [${mandatoryFields}] are not mapped`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// but metadata field not being available is fine
|
||||
const availableMetadataFields = metaFields.filter((field) => fields[field]);
|
||||
|
||||
const query = getEntityInstancesQuery({
|
||||
source: {
|
||||
...source,
|
||||
metadata_fields: availableMetadataFields,
|
||||
filters: [...source.filters, ...filters],
|
||||
},
|
||||
start,
|
||||
end,
|
||||
limit,
|
||||
});
|
||||
this.options.logger.debug(`Entity query: ${query}`);
|
||||
|
||||
const rawEntities = await runESQLQuery<EntityV2>({
|
||||
query,
|
||||
esClient: this.options.esClient,
|
||||
});
|
||||
|
||||
return rawEntities.map((entity) => {
|
||||
entity['entity.id'] = source.identity_fields.map((field) => entity[field]).join(':');
|
||||
entity['entity.type'] = source.type;
|
||||
return entity;
|
||||
});
|
||||
})
|
||||
).then((results) => results.flat());
|
||||
|
||||
return mergeEntitiesList(entities).slice(0, limit);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { getEntityInstancesQuery } from '.';
|
||||
|
||||
describe('getEntityInstancesQuery', () => {
|
||||
describe('getEntityInstancesQuery', () => {
|
||||
it('generates a valid esql query', () => {
|
||||
const query = getEntityInstancesQuery({
|
||||
source: {
|
||||
type: 'service',
|
||||
index_patterns: ['logs-*', 'metrics-*'],
|
||||
identity_fields: ['service.name'],
|
||||
metadata_fields: ['host.name'],
|
||||
filters: [],
|
||||
timestamp_field: 'custom_timestamp_field',
|
||||
},
|
||||
limit: 5,
|
||||
start: '2024-11-20T19:00:00.000Z',
|
||||
end: '2024-11-20T20:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(query).toEqual(
|
||||
'FROM logs-*,metrics-*|' +
|
||||
'WHERE custom_timestamp_field >= "2024-11-20T19:00:00.000Z"|' +
|
||||
'WHERE custom_timestamp_field <= "2024-11-20T20:00:00.000Z"|' +
|
||||
'WHERE service.name IS NOT NULL|' +
|
||||
'STATS entity.last_seen_timestamp=MAX(custom_timestamp_field),metadata.host.name=VALUES(host.name) BY service.name|' +
|
||||
'SORT entity.last_seen_timestamp DESC|' +
|
||||
'LIMIT 5'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
91
x-pack/plugins/entity_manager/server/lib/queries/index.ts
Normal file
91
x-pack/plugins/entity_manager/server/lib/queries/index.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
|
||||
export const entitySourceSchema = z.object({
|
||||
type: z.string(),
|
||||
timestamp_field: z.optional(z.string()).default('@timestamp'),
|
||||
index_patterns: z.array(z.string()),
|
||||
identity_fields: z.array(z.string()),
|
||||
metadata_fields: z.array(z.string()),
|
||||
filters: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type EntitySource = z.infer<typeof entitySourceSchema>;
|
||||
|
||||
const sourceCommand = ({ source }: { source: EntitySource }) => {
|
||||
let query = `FROM ${source.index_patterns}`;
|
||||
|
||||
const esMetadataFields = source.metadata_fields.filter((field) =>
|
||||
['_index', '_id'].includes(field)
|
||||
);
|
||||
if (esMetadataFields.length) {
|
||||
query += ` METADATA ${esMetadataFields.join(',')}`;
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
const filterCommands = ({
|
||||
source,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
source: EntitySource;
|
||||
start: string;
|
||||
end: string;
|
||||
}) => {
|
||||
const commands = [
|
||||
`WHERE ${source.timestamp_field} >= "${start}"`,
|
||||
`WHERE ${source.timestamp_field} <= "${end}"`,
|
||||
];
|
||||
|
||||
source.identity_fields.forEach((field) => {
|
||||
commands.push(`WHERE ${field} IS NOT NULL`);
|
||||
});
|
||||
|
||||
source.filters.forEach((filter) => {
|
||||
commands.push(`WHERE ${filter}`);
|
||||
});
|
||||
|
||||
return commands;
|
||||
};
|
||||
|
||||
const statsCommand = ({ source }: { source: EntitySource }) => {
|
||||
const aggs = [
|
||||
// default 'last_seen' attribute
|
||||
`entity.last_seen_timestamp=MAX(${source.timestamp_field})`,
|
||||
...source.metadata_fields
|
||||
.filter((field) => !source.identity_fields.some((idField) => idField === field))
|
||||
.map((field) => `metadata.${field}=VALUES(${field})`),
|
||||
];
|
||||
|
||||
return `STATS ${aggs.join(',')} BY ${source.identity_fields.join(',')}`;
|
||||
};
|
||||
|
||||
export function getEntityInstancesQuery({
|
||||
source,
|
||||
limit,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
source: EntitySource;
|
||||
limit: number;
|
||||
start: string;
|
||||
end: string;
|
||||
}): string {
|
||||
const commands = [
|
||||
sourceCommand({ source }),
|
||||
...filterCommands({ source, start, end }),
|
||||
statsCommand({ source }),
|
||||
`SORT entity.last_seen_timestamp DESC`,
|
||||
`LIMIT ${limit}`,
|
||||
];
|
||||
|
||||
return commands.join('|');
|
||||
}
|
135
x-pack/plugins/entity_manager/server/lib/queries/utils.test.ts
Normal file
135
x-pack/plugins/entity_manager/server/lib/queries/utils.test.ts
Normal file
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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 { mergeEntitiesList } from './utils';
|
||||
|
||||
describe('mergeEntitiesList', () => {
|
||||
describe('mergeEntitiesList', () => {
|
||||
it('merges entities on entity.id', () => {
|
||||
const entities = [
|
||||
{
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
},
|
||||
{
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedEntities = mergeEntitiesList(entities);
|
||||
expect(mergedEntities.length).toEqual(1);
|
||||
expect(mergedEntities[0]).toEqual({
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
});
|
||||
});
|
||||
|
||||
it('merges metadata fields', () => {
|
||||
const entities = [
|
||||
{
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
'metadata.host.name': 'host-1',
|
||||
'metadata.agent.name': 'agent-1',
|
||||
'metadata.service.environment': ['dev', 'staging'],
|
||||
'metadata.only_in_record_1': 'foo',
|
||||
},
|
||||
{
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
'metadata.host.name': ['host-2', 'host-3'],
|
||||
'metadata.agent.name': 'agent-2',
|
||||
'metadata.service.environment': 'prod',
|
||||
'metadata.only_in_record_2': 'bar',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedEntities = mergeEntitiesList(entities);
|
||||
expect(mergedEntities.length).toEqual(1);
|
||||
expect(mergedEntities[0]).toEqual({
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
'metadata.host.name': ['host-1', 'host-2', 'host-3'],
|
||||
'metadata.agent.name': ['agent-1', 'agent-2'],
|
||||
'metadata.service.environment': ['dev', 'staging', 'prod'],
|
||||
'metadata.only_in_record_1': 'foo',
|
||||
'metadata.only_in_record_2': 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
it('picks most recent timestamp when merging', () => {
|
||||
const entities = [
|
||||
{
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
'metadata.host.name': 'host-1',
|
||||
},
|
||||
{
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T20:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
'metadata.host.name': 'host-2',
|
||||
},
|
||||
{
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T16:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
'metadata.host.name': 'host-3',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedEntities = mergeEntitiesList(entities);
|
||||
expect(mergedEntities.length).toEqual(1);
|
||||
expect(mergedEntities[0]).toEqual({
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T20:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
'metadata.host.name': ['host-1', 'host-2', 'host-3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('deduplicates metadata values', () => {
|
||||
const entities = [
|
||||
{
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
'metadata.host.name': 'host-1',
|
||||
},
|
||||
{
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T20:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
'metadata.host.name': 'host-2',
|
||||
},
|
||||
{
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T16:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
'metadata.host.name': ['host-1', 'host-2'],
|
||||
},
|
||||
];
|
||||
|
||||
const mergedEntities = mergeEntitiesList(entities);
|
||||
expect(mergedEntities.length).toEqual(1);
|
||||
expect(mergedEntities[0]).toEqual({
|
||||
'entity.id': 'foo',
|
||||
'entity.last_seen_timestamp': '2024-11-20T20:00:00.000Z',
|
||||
'entity.type': 'service',
|
||||
'metadata.host.name': ['host-1', 'host-2'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
90
x-pack/plugins/entity_manager/server/lib/queries/utils.ts
Normal file
90
x-pack/plugins/entity_manager/server/lib/queries/utils.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { EntityV2 } from '@kbn/entities-schema';
|
||||
import { ESQLSearchResponse } from '@kbn/es-types';
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
function mergeEntities(entity1: EntityV2, entity2: EntityV2): EntityV2 {
|
||||
const merged: EntityV2 = {
|
||||
...entity1,
|
||||
'entity.last_seen_timestamp': new Date(
|
||||
Math.max(
|
||||
Date.parse(entity1['entity.last_seen_timestamp']),
|
||||
Date.parse(entity2['entity.last_seen_timestamp'])
|
||||
)
|
||||
).toISOString(),
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(entity2).filter(([_key]) =>
|
||||
_key.startsWith('metadata.')
|
||||
)) {
|
||||
if (merged[key]) {
|
||||
merged[key] = uniq([
|
||||
...(Array.isArray(merged[key]) ? merged[key] : [merged[key]]),
|
||||
...(Array.isArray(value) ? value : [value]),
|
||||
]);
|
||||
} else {
|
||||
merged[key] = value;
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function mergeEntitiesList(entities: EntityV2[]): EntityV2[] {
|
||||
const instances: { [key: string]: EntityV2 } = {};
|
||||
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const entity = entities[i];
|
||||
const id = entity['entity.id'];
|
||||
|
||||
if (instances[id]) {
|
||||
instances[id] = mergeEntities(instances[id], entity);
|
||||
} else {
|
||||
instances[id] = entity;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(instances);
|
||||
}
|
||||
|
||||
export async function runESQLQuery<T>({
|
||||
esClient,
|
||||
query,
|
||||
}: {
|
||||
esClient: ElasticsearchClient;
|
||||
query: string;
|
||||
}): Promise<T[]> {
|
||||
const esqlResponse = (await esClient.esql.query(
|
||||
{
|
||||
query,
|
||||
format: 'json',
|
||||
},
|
||||
{ querystring: { drop_null_columns: true } }
|
||||
)) as unknown as ESQLSearchResponse;
|
||||
|
||||
const documents = esqlResponse.values.map((row) =>
|
||||
row.reduce<Record<string, any>>((acc, value, index) => {
|
||||
const column = esqlResponse.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;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
) as T[];
|
||||
|
||||
return documents;
|
||||
}
|
|
@ -10,6 +10,7 @@ import { deleteEntityDefinitionRoute } from './delete';
|
|||
import { getEntityDefinitionRoute } from './get';
|
||||
import { resetEntityDefinitionRoute } from './reset';
|
||||
import { updateEntityDefinitionRoute } from './update';
|
||||
import { searchEntitiesRoute, searchEntitiesPreviewRoute } from '../v2/search';
|
||||
|
||||
export const entitiesRoutes = {
|
||||
...createEntityDefinitionRoute,
|
||||
|
@ -17,4 +18,6 @@ export const entitiesRoutes = {
|
|||
...getEntityDefinitionRoute,
|
||||
...resetEntityDefinitionRoute,
|
||||
...updateEntityDefinitionRoute,
|
||||
...searchEntitiesRoute,
|
||||
...searchEntitiesPreviewRoute,
|
||||
};
|
||||
|
|
96
x-pack/plugins/entity_manager/server/routes/v2/search.ts
Normal file
96
x-pack/plugins/entity_manager/server/routes/v2/search.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import { z } from '@kbn/zod';
|
||||
import { createEntityManagerServerRoute } from '../create_entity_manager_server_route';
|
||||
import { entitySourceSchema } from '../../lib/queries';
|
||||
import { UnknownEntityType } from '../../lib/entities/errors/unknown_entity_type';
|
||||
|
||||
export const searchEntitiesRoute = createEntityManagerServerRoute({
|
||||
endpoint: 'POST /internal/entities/v2/_search',
|
||||
params: z.object({
|
||||
body: z.object({
|
||||
type: z.string(),
|
||||
metadata_fields: z.optional(z.array(z.string())).default([]),
|
||||
filters: z.optional(z.array(z.string())).default([]),
|
||||
start: z
|
||||
.optional(z.string())
|
||||
.default(() => moment().subtract(5, 'minutes').toISOString())
|
||||
.refine((val) => moment(val).isValid(), {
|
||||
message: 'start should be a date in ISO format',
|
||||
}),
|
||||
end: z
|
||||
.optional(z.string())
|
||||
.default(() => moment().toISOString())
|
||||
.refine((val) => moment(val).isValid(), {
|
||||
message: 'start should be a date in ISO format',
|
||||
}),
|
||||
limit: z.optional(z.number()).default(10),
|
||||
}),
|
||||
}),
|
||||
handler: async ({ request, response, params, logger, getScopedClient }) => {
|
||||
try {
|
||||
const { type, start, end, limit, filters, metadata_fields: metadataFields } = params.body;
|
||||
|
||||
const client = await getScopedClient({ request });
|
||||
const entities = await client.searchEntities({
|
||||
type,
|
||||
filters,
|
||||
metadataFields,
|
||||
start,
|
||||
end,
|
||||
limit,
|
||||
});
|
||||
|
||||
return response.ok({ body: { entities } });
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
|
||||
if (e instanceof UnknownEntityType) {
|
||||
return response.notFound({ body: e });
|
||||
}
|
||||
|
||||
return response.customError({ body: e, statusCode: 500 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const searchEntitiesPreviewRoute = createEntityManagerServerRoute({
|
||||
endpoint: 'POST /internal/entities/v2/_search/preview',
|
||||
params: z.object({
|
||||
body: z.object({
|
||||
sources: z.array(entitySourceSchema),
|
||||
start: z
|
||||
.optional(z.string())
|
||||
.default(() => moment().subtract(5, 'minutes').toISOString())
|
||||
.refine((val) => moment(val).isValid(), {
|
||||
message: 'start should be a date in ISO format',
|
||||
}),
|
||||
end: z
|
||||
.optional(z.string())
|
||||
.default(() => moment().toISOString())
|
||||
.refine((val) => moment(val).isValid(), {
|
||||
message: 'start should be a date in ISO format',
|
||||
}),
|
||||
limit: z.optional(z.number()).default(10),
|
||||
}),
|
||||
}),
|
||||
handler: async ({ request, response, params, logger, getScopedClient }) => {
|
||||
const { sources, start, end, limit } = params.body;
|
||||
|
||||
const client = await getScopedClient({ request });
|
||||
const entities = await client.searchEntitiesBySources({
|
||||
sources,
|
||||
start,
|
||||
end,
|
||||
limit,
|
||||
});
|
||||
|
||||
return response.ok({ body: { entities } });
|
||||
},
|
||||
});
|
|
@ -35,5 +35,6 @@
|
|||
"@kbn/encrypted-saved-objects-plugin",
|
||||
"@kbn/licensing-plugin",
|
||||
"@kbn/core-saved-objects-server",
|
||||
"@kbn/es-types",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# Entity Manager App Plugin
|
||||
|
||||
This plugin provides a user interface to interact with the Entity Manager.
|
|
@ -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.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: path.resolve(__dirname, '../../../..'),
|
||||
roots: ['<rootDir>/x-pack/plugins/observability_solution/entity_manager_app'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/observability_solution/entity_manager_app',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/observability_solution/entity_manager_app/{common,public,server}/**/*.{js,ts,tsx}',
|
||||
],
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/entityManager-app-plugin",
|
||||
"owner": "@elastic/obs-entities",
|
||||
"group": "observability",
|
||||
"visibility": "private",
|
||||
"description": "Entity manager plugin for entity assets (inventory, topology, etc)",
|
||||
"plugin": {
|
||||
"id": "entityManagerApp",
|
||||
"configPath": ["xpack", "entityManagerApp"],
|
||||
"browser": true,
|
||||
"server": false,
|
||||
"requiredPlugins": [
|
||||
"entityManager",
|
||||
"observabilityShared",
|
||||
"presentationUtil",
|
||||
"usageCollection",
|
||||
"licensing"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"cloud",
|
||||
"serverless"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"kibanaReact",
|
||||
"kibanaUtils"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { AppMountParameters, APP_WRAPPER_CLASS, CoreStart } from '@kbn/core/public';
|
||||
import { PerformanceContextProvider } from '@kbn/ebt-tools';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
||||
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
||||
import { EntityClient } from '@kbn/entityManager-plugin/public';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import { Router } from '@kbn/shared-ux-router';
|
||||
import { PluginContext } from './context/plugin_context';
|
||||
import { EntityManagerPluginStart } from './types';
|
||||
import { EntityManagerOverviewPage } from './pages/overview';
|
||||
|
||||
export function renderApp({
|
||||
core,
|
||||
plugins,
|
||||
appMountParameters,
|
||||
ObservabilityPageTemplate,
|
||||
usageCollection,
|
||||
isDev,
|
||||
kibanaVersion,
|
||||
isServerless,
|
||||
entityClient,
|
||||
}: {
|
||||
core: CoreStart;
|
||||
plugins: EntityManagerPluginStart;
|
||||
appMountParameters: AppMountParameters;
|
||||
ObservabilityPageTemplate: React.ComponentType<LazyObservabilityPageTemplateProps>;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
isDev?: boolean;
|
||||
kibanaVersion: string;
|
||||
isServerless?: boolean;
|
||||
entityClient: EntityClient;
|
||||
}) {
|
||||
const { element, history, theme$ } = appMountParameters;
|
||||
const isDarkMode = core.theme.getTheme().darkMode;
|
||||
|
||||
// ensure all divs are .kbnAppWrappers
|
||||
element.classList.add(APP_WRAPPER_CLASS);
|
||||
|
||||
const ApplicationUsageTrackingProvider =
|
||||
usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment;
|
||||
|
||||
const CloudProvider = plugins.cloud?.CloudContextProvider ?? React.Fragment;
|
||||
|
||||
ReactDOM.render(
|
||||
<KibanaRenderContextProvider {...core}>
|
||||
<ApplicationUsageTrackingProvider>
|
||||
<KibanaThemeProvider {...{ theme: { theme$ } }}>
|
||||
<CloudProvider>
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
...core,
|
||||
...plugins,
|
||||
storage: new Storage(localStorage),
|
||||
entityClient: new EntityClient(core),
|
||||
isDev,
|
||||
kibanaVersion,
|
||||
isServerless,
|
||||
}}
|
||||
>
|
||||
<PluginContext.Provider
|
||||
value={{
|
||||
isDev,
|
||||
isServerless,
|
||||
appMountParameters,
|
||||
ObservabilityPageTemplate,
|
||||
entityClient,
|
||||
}}
|
||||
>
|
||||
<Router history={history}>
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<RedirectAppLinks coreStart={core} data-test-subj="observabilityMainContainer">
|
||||
<PerformanceContextProvider>
|
||||
<EntityManagerOverviewPage />
|
||||
</PerformanceContextProvider>
|
||||
</RedirectAppLinks>
|
||||
</EuiThemeProvider>
|
||||
</Router>
|
||||
</PluginContext.Provider>
|
||||
</KibanaContextProvider>
|
||||
</CloudProvider>
|
||||
</KibanaThemeProvider>
|
||||
</ApplicationUsageTrackingProvider>
|
||||
</KibanaRenderContextProvider>,
|
||||
element
|
||||
);
|
||||
|
||||
return () => {
|
||||
ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { createContext } from 'react';
|
||||
import type { AppMountParameters } from '@kbn/core/public';
|
||||
import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public';
|
||||
import { EntityClient } from '@kbn/entityManager-plugin/public';
|
||||
|
||||
export interface PluginContextValue {
|
||||
isDev?: boolean;
|
||||
isServerless?: boolean;
|
||||
appMountParameters?: AppMountParameters;
|
||||
ObservabilityPageTemplate: React.ComponentType<LazyObservabilityPageTemplateProps>;
|
||||
entityClient: EntityClient;
|
||||
}
|
||||
|
||||
export const PluginContext = createContext<PluginContextValue | null>(null);
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { CoreStart } from '@kbn/core/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { EntityClient } from '@kbn/entityManager-plugin/public';
|
||||
|
||||
export type StartServices<AdditionalServices extends object = {}> = CoreStart &
|
||||
AdditionalServices & {
|
||||
storage: Storage;
|
||||
kibanaVersion: string;
|
||||
entityClient: EntityClient;
|
||||
};
|
||||
const useTypedKibana = <AdditionalServices extends object = {}>() =>
|
||||
useKibana<StartServices<AdditionalServices>>();
|
||||
|
||||
export { useTypedKibana as useKibana };
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { useContext } from 'react';
|
||||
import { PluginContext } from '../context/plugin_context';
|
||||
import type { PluginContextValue } from '../context/plugin_context';
|
||||
|
||||
export function usePluginContext(): PluginContextValue {
|
||||
const context = useContext(PluginContext);
|
||||
if (!context) {
|
||||
throw new Error('Plugin context value is missing!');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { PluginInitializer, PluginInitializerContext } from '@kbn/core/public';
|
||||
import { Plugin } from './plugin';
|
||||
|
||||
export const plugin: PluginInitializer<{}, {}> = (context: PluginInitializerContext) => {
|
||||
return new Plugin(context);
|
||||
};
|
|
@ -0,0 +1,347 @@
|
|||
/*
|
||||
* 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 React, { useState } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiCallOut,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { EntityV2 } from '@kbn/entities-schema';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
|
||||
function EntitySourceForm({
|
||||
source,
|
||||
index,
|
||||
onFieldChange,
|
||||
}: {
|
||||
source: any;
|
||||
index: number;
|
||||
onFieldChange: Function;
|
||||
}) {
|
||||
const onArrayFieldChange =
|
||||
(field: Exclude<keyof EntitySource, 'id'>) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.trim();
|
||||
if (!value) {
|
||||
onFieldChange(index, field, []);
|
||||
} else {
|
||||
onFieldChange(index, field, e.target.value.trim().split(','));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label="Index patterns (comma-separated)">
|
||||
<EuiFieldText
|
||||
data-test-subj="entityManagerFormIndexPatterns"
|
||||
name="index_patterns"
|
||||
defaultValue={source.index_patterns.join(',')}
|
||||
isInvalid={source.index_patterns.length === 0}
|
||||
onChange={onArrayFieldChange('index_patterns')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label="Identify fields (comma-separated field names)">
|
||||
<EuiFieldText
|
||||
data-test-subj="entityManagerFormIdentityFields"
|
||||
name="identity_fields"
|
||||
defaultValue={source.identity_fields.join(',')}
|
||||
isInvalid={source.identity_fields.length === 0}
|
||||
onChange={onArrayFieldChange('identity_fields')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label="Filters (comma-separated ESQL filters)">
|
||||
<EuiFieldText
|
||||
data-test-subj="entityManagerFormFilters"
|
||||
name="filters"
|
||||
defaultValue={source.filters.join(',')}
|
||||
onChange={onArrayFieldChange('filters')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label="Metadata (comma-separated field names)">
|
||||
<EuiFieldText
|
||||
data-test-subj="entityManagerFormMetadata"
|
||||
name="metadata"
|
||||
defaultValue={source.metadata_fields.join(',')}
|
||||
onChange={onArrayFieldChange('metadata_fields')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label="Timestamp field">
|
||||
<EuiFieldText
|
||||
data-test-subj="entityManagerFormTimestamp"
|
||||
name="timestamp_field"
|
||||
defaultValue={source.timestamp_field}
|
||||
onChange={(e) => onFieldChange(index, 'timestamp_field', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
interface EntitySource {
|
||||
id: string;
|
||||
index_patterns?: string[];
|
||||
identity_fields?: string[];
|
||||
metadata_fields?: string[];
|
||||
filters?: string[];
|
||||
timestamp_field?: string;
|
||||
}
|
||||
|
||||
const newEntitySource = ({
|
||||
indexPatterns = [],
|
||||
identityFields = [],
|
||||
metadataFields = [],
|
||||
filters = [],
|
||||
timestampField = '@timestamp',
|
||||
}: {
|
||||
indexPatterns?: string[];
|
||||
identityFields?: string[];
|
||||
metadataFields?: string[];
|
||||
filters?: string[];
|
||||
timestampField?: string;
|
||||
}) => ({
|
||||
id: uuid(),
|
||||
index_patterns: indexPatterns,
|
||||
identity_fields: identityFields,
|
||||
metadata_fields: metadataFields,
|
||||
timestamp_field: timestampField,
|
||||
filters,
|
||||
});
|
||||
|
||||
export function EntityManagerOverviewPage() {
|
||||
const { ObservabilityPageTemplate, entityClient } = usePluginContext();
|
||||
const [previewEntities, setPreviewEntities] = useState<EntityV2[]>([]);
|
||||
const [isSearchingEntities, setIsSearchingEntities] = useState(false);
|
||||
const [previewError, setPreviewError] = useState(null);
|
||||
const [formErrors, setFormErrors] = useState<string[]>([]);
|
||||
const [entityType, setEntityType] = useState('service');
|
||||
const [entitySources, setEntitySources] = useState([
|
||||
newEntitySource({
|
||||
indexPatterns: ['remote_cluster:logs-*'],
|
||||
identityFields: ['service.name'],
|
||||
}),
|
||||
]);
|
||||
|
||||
const searchEntities = async () => {
|
||||
if (
|
||||
!entitySources.some(
|
||||
(source) => source.identity_fields.length > 0 && source.index_patterns.length > 0
|
||||
)
|
||||
) {
|
||||
setFormErrors(['No valid source found']);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSearchingEntities(true);
|
||||
setFormErrors([]);
|
||||
setPreviewError(null);
|
||||
|
||||
try {
|
||||
const { entities } = await entityClient.repositoryClient(
|
||||
'POST /internal/entities/v2/_search/preview',
|
||||
{
|
||||
params: {
|
||||
body: {
|
||||
sources: entitySources
|
||||
.filter(
|
||||
(source) => source.index_patterns.length > 0 && source.identity_fields.length > 0
|
||||
)
|
||||
.map((source) => ({ ...source, type: entityType })),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setPreviewEntities(entities);
|
||||
} catch (err) {
|
||||
setPreviewError(err.body?.message);
|
||||
} finally {
|
||||
setIsSearchingEntities(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ObservabilityPageTemplate
|
||||
data-test-subj="entitiesPage"
|
||||
pageHeader={{
|
||||
bottomBorder: true,
|
||||
pageTitle: 'Entity Manager',
|
||||
}}
|
||||
>
|
||||
<EuiForm component="form" isInvalid={formErrors.length > 0} error={formErrors}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h2>Entity type</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow>
|
||||
<EuiFieldText
|
||||
data-test-subj="entityManagerFormType"
|
||||
name="type"
|
||||
defaultValue={entityType}
|
||||
placeholder="host, service, user..."
|
||||
onChange={(e) => {
|
||||
setEntityType(e.target.value.trim());
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="s">
|
||||
<h2>Entity sources</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="entityManagerFormAddSource"
|
||||
iconType="plusInCircle"
|
||||
onClick={() => setEntitySources([...entitySources, newEntitySource({})])}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{entitySources.map((source, i) => (
|
||||
<div key={source.id}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxs">
|
||||
<h4>Source {i + 1}</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{entitySources.length > 1 ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="entityManagerFormRemoveSource"
|
||||
color={'danger'}
|
||||
iconType={'minusInCircle'}
|
||||
onClick={() => {
|
||||
entitySources.splice(i, 1);
|
||||
setEntitySources(entitySources.map((_source) => ({ ..._source })));
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EntitySourceForm
|
||||
source={source}
|
||||
index={i}
|
||||
onFieldChange={(
|
||||
index: number,
|
||||
field: Exclude<keyof EntitySource, 'id'>,
|
||||
value: any
|
||||
) => {
|
||||
entitySources[index][field] = value;
|
||||
setEntitySources([...entitySources]);
|
||||
}}
|
||||
/>
|
||||
{i === entitySources.length - 1 ? (
|
||||
<EuiSpacer size="m" />
|
||||
) : (
|
||||
<EuiHorizontalRule margin="m" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<EuiFormRow>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
data-test-subj="entityManagerFormPreview"
|
||||
isDisabled={isSearchingEntities}
|
||||
onClick={searchEntities}
|
||||
>
|
||||
Preview
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton data-test-subj="entityManagerFormCreate" isDisabled={true} color="primary">
|
||||
Create
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{previewError ? (
|
||||
<EuiCallOut title="Error previewing entity definition" color="danger" iconType="error">
|
||||
<p>{previewError}</p>
|
||||
</EuiCallOut>
|
||||
) : null}
|
||||
|
||||
<EuiBasicTable
|
||||
loading={isSearchingEntities}
|
||||
tableCaption={'Preview entities'}
|
||||
items={previewEntities}
|
||||
columns={[
|
||||
{
|
||||
field: 'entity.id',
|
||||
name: 'entity.id',
|
||||
},
|
||||
{
|
||||
field: 'entity.type',
|
||||
name: 'entity.type',
|
||||
},
|
||||
{
|
||||
field: 'entity.last_seen_timestamp',
|
||||
name: 'entity.last_seen_timestamp',
|
||||
},
|
||||
...Array.from(new Set(entitySources.flatMap((source) => source.identity_fields))).map(
|
||||
(field) => ({
|
||||
field,
|
||||
name: field,
|
||||
})
|
||||
),
|
||||
...Array.from(new Set(entitySources.flatMap((source) => source.metadata_fields))).map(
|
||||
(field) => ({
|
||||
field: `metadata.${field}`,
|
||||
name: `metadata.${field}`,
|
||||
})
|
||||
),
|
||||
]}
|
||||
/>
|
||||
</ObservabilityPageTemplate>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { BehaviorSubject } from 'rxjs';
|
||||
import {
|
||||
App,
|
||||
AppMountParameters,
|
||||
AppStatus,
|
||||
AppUpdater,
|
||||
CoreSetup,
|
||||
DEFAULT_APP_CATEGORIES,
|
||||
PluginInitializerContext,
|
||||
} from '@kbn/core/public';
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { EntityClient } from '@kbn/entityManager-plugin/public';
|
||||
|
||||
import {
|
||||
EntityManagerAppPluginClass,
|
||||
EntityManagerPluginStart,
|
||||
EntityManagerPluginSetup,
|
||||
} from './types';
|
||||
|
||||
export class Plugin implements EntityManagerAppPluginClass {
|
||||
public logger: Logger;
|
||||
private readonly appUpdater$ = new BehaviorSubject<AppUpdater>(() => ({}));
|
||||
|
||||
constructor(private readonly context: PluginInitializerContext<{}>) {
|
||||
this.logger = context.logger.get();
|
||||
}
|
||||
|
||||
setup(core: CoreSetup<EntityManagerPluginStart, {}>, pluginSetup: EntityManagerPluginSetup) {
|
||||
const kibanaVersion = this.context.env.packageInfo.version;
|
||||
|
||||
const mount = async (params: AppMountParameters<unknown>) => {
|
||||
const { renderApp } = await import('./application');
|
||||
const [coreStart, pluginsStart] = await core.getStartServices();
|
||||
|
||||
return renderApp({
|
||||
appMountParameters: params,
|
||||
core: coreStart,
|
||||
isDev: this.context.env.mode.dev,
|
||||
kibanaVersion,
|
||||
usageCollection: pluginSetup.usageCollection,
|
||||
ObservabilityPageTemplate: pluginsStart.observabilityShared.navigation.PageTemplate,
|
||||
plugins: pluginsStart,
|
||||
isServerless: !!pluginsStart.serverless,
|
||||
entityClient: new EntityClient(core),
|
||||
});
|
||||
};
|
||||
|
||||
const appUpdater$ = this.appUpdater$;
|
||||
const app: App = {
|
||||
id: 'entity_manager',
|
||||
title: 'Entity Manager',
|
||||
order: 8002,
|
||||
updater$: appUpdater$,
|
||||
euiIconType: 'logoObservability',
|
||||
appRoute: '/app/entity_manager',
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
mount,
|
||||
visibleIn: [],
|
||||
keywords: ['observability', 'monitor', 'entities'],
|
||||
status: AppStatus.inaccessible,
|
||||
};
|
||||
|
||||
core.application.register(app);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
start() {
|
||||
return {};
|
||||
}
|
||||
|
||||
stop() {}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EntityManagerOverviewPage } from './pages/overview';
|
||||
|
||||
interface RouteDef {
|
||||
[key: string]: {
|
||||
handler: () => React.ReactElement;
|
||||
params: Record<string, string>;
|
||||
exact: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function getRoutes(): RouteDef {
|
||||
return {
|
||||
'/app/entity_manager': {
|
||||
handler: () => <EntityManagerOverviewPage />,
|
||||
params: {},
|
||||
exact: true,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { Plugin as PluginClass } from '@kbn/core/public';
|
||||
import { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
|
||||
import { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public';
|
||||
import {
|
||||
ObservabilitySharedPluginSetup,
|
||||
ObservabilitySharedPluginStart,
|
||||
} from '@kbn/observability-shared-plugin/public';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import { EntityManagerPublicPluginSetup } from '@kbn/entityManager-plugin/public/types';
|
||||
|
||||
export interface EntityManagerPluginSetup {
|
||||
observabilityShared: ObservabilitySharedPluginSetup;
|
||||
serverless?: ServerlessPluginSetup;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
entityManager: EntityManagerPublicPluginSetup;
|
||||
}
|
||||
|
||||
export interface EntityManagerPluginStart {
|
||||
presentationUtil: PresentationUtilPluginStart;
|
||||
cloud?: CloudStart;
|
||||
serverless?: ServerlessPluginStart;
|
||||
observabilityShared: ObservabilitySharedPluginStart;
|
||||
}
|
||||
|
||||
export type EntityManagerAppPluginClass = PluginClass<{}, {}>;
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"../../../../typings/**/*",
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"types/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/logging",
|
||||
"@kbn/ebt-tools",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/kibana-utils-plugin",
|
||||
"@kbn/observability-shared-plugin",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/react-kibana-context-theme",
|
||||
"@kbn/shared-ux-link-redirect-app",
|
||||
"@kbn/usage-collection-plugin",
|
||||
"@kbn/shared-ux-router",
|
||||
"@kbn/presentation-util-plugin",
|
||||
"@kbn/cloud-plugin",
|
||||
"@kbn/serverless",
|
||||
"@kbn/entityManager-plugin",
|
||||
"@kbn/entities-schema",
|
||||
]
|
||||
}
|
|
@ -5424,6 +5424,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/entityManager-app-plugin@link:x-pack/plugins/observability_solution/entity_manager_app":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/entityManager-plugin@link:x-pack/plugins/entity_manager":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue