[Infrastructure UI] Implement inventory views CRUD endpoints (#154900)

## 📓  Summary

Part of #152617 
Closes #155158 

This PR implements the CRUD endpoints for the inventory views.
Following the approach used for the LogViews service, it exposes a
client that abstracts all the logic concerned to the `inventory-view`
saved objects.

It also follows the guideline provided for [Versioning
interfaces](https://docs.elastic.dev/kibana-dev-docs/versioning-interfaces)
and [Versioning HTTP
APIs](https://docs.elastic.dev/kibana-dev-docs/versioning-http-apis),
preparing for the serverless.

## 🤓 Tips for the reviewer
You can open the Kibana dev tools and play with the following snippet to
test the create APIs, or you can perform the same requests with your
preferred client:
```
// Get all
GET kbn:/api/infra/inventory_views

// Create one
POST kbn:/api/infra/inventory_views
{
  "attributes": {
    "name": "My inventory view"
  }
}

// Get one
GET kbn:/api/infra/inventory_views/<switch-with-id>

// Update one
PUT kbn:/api/infra/inventory_views/<switch-with-id>
{
  "attributes": {
    "name": "My inventory view 2"
  }
}

// Delete one
DELETE kbn:/api/infra/inventory_views/<switch-with-id>
```

## 👣 Next steps
- Replicate the same logic for the metrics explorer saved object
- Create a client-side abstraction to consume the service
- Update the existing react custom hooks to consume the endpoint

---------

Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marco Antonio Ghiani 2023-04-24 11:54:43 +02:00 committed by GitHub
parent f6e037794a
commit 6ac4e1919c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1855 additions and 4 deletions

View file

@ -7,7 +7,7 @@
*/
export type { IndexPatternType } from './src/index_pattern_rt';
export type { NonEmptyStringBrand } from './src/non_empty_string_rt';
export type { NonEmptyString, NonEmptyStringBrand } from './src/non_empty_string_rt';
export { deepExactRt } from './src/deep_exact_rt';
export { indexPatternRt } from './src/index_pattern_rt';

View file

@ -14,3 +14,9 @@ export * from './log_alerts';
export * from './snapshot_api';
export * from './host_details';
export * from './infra';
/**
* Exporting versioned APIs types
*/
export * from './latest';
export * as inventoryViewsV1 from './inventory_views/v1';

View file

@ -0,0 +1,66 @@
/*
* 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 { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
import { either } from 'fp-ts/Either';
export const INVENTORY_VIEW_URL = '/api/infra/inventory_views';
export const INVENTORY_VIEW_URL_ENTITY = `${INVENTORY_VIEW_URL}/{inventoryViewId}`;
export const getInventoryViewUrl = (inventoryViewId?: string) =>
[INVENTORY_VIEW_URL, inventoryViewId].filter(Boolean).join('/');
const inventoryViewIdRT = new rt.Type<string, string, unknown>(
'InventoryViewId',
rt.string.is,
(u, c) =>
either.chain(rt.string.validate(u, c), (id) => {
return id === '0'
? rt.failure(u, c, `The inventory view with id ${id} is not configurable.`)
: rt.success(id);
}),
String
);
export const inventoryViewRequestParamsRT = rt.type({
inventoryViewId: inventoryViewIdRT,
});
export type InventoryViewRequestParams = rt.TypeOf<typeof inventoryViewRequestParamsRT>;
export const inventoryViewRequestQueryRT = rt.partial({
sourceId: rt.string,
});
export type InventoryViewRequestQuery = rt.TypeOf<typeof inventoryViewRequestQueryRT>;
const inventoryViewAttributesResponseRT = rt.intersection([
rt.strict({
name: nonEmptyStringRt,
isDefault: rt.boolean,
isStatic: rt.boolean,
}),
rt.UnknownRecord,
]);
const inventoryViewResponseRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
attributes: inventoryViewAttributesResponseRT,
}),
rt.partial({
updatedAt: rt.number,
version: rt.string,
}),
])
);
export const inventoryViewResponsePayloadRT = rt.type({
data: inventoryViewResponseRT,
});
export type GetInventoryViewResponsePayload = rt.TypeOf<typeof inventoryViewResponsePayloadRT>;

View file

@ -0,0 +1,29 @@
/*
* 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 { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
export const createInventoryViewAttributesRequestPayloadRT = rt.intersection([
rt.type({
name: nonEmptyStringRt,
}),
rt.UnknownRecord,
rt.exact(rt.partial({ isDefault: rt.undefined, isStatic: rt.undefined })),
]);
export type CreateInventoryViewAttributesRequestPayload = rt.TypeOf<
typeof createInventoryViewAttributesRequestPayloadRT
>;
export const createInventoryViewRequestPayloadRT = rt.type({
attributes: createInventoryViewAttributesRequestPayloadRT,
});
export type CreateInventoryViewRequestPayload = rt.TypeOf<
typeof createInventoryViewRequestPayloadRT
>;

View file

@ -0,0 +1,34 @@
/*
* 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 { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
export const findInventoryViewAttributesResponseRT = rt.strict({
name: nonEmptyStringRt,
isDefault: rt.boolean,
isStatic: rt.boolean,
});
const findInventoryViewResponseRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
attributes: findInventoryViewAttributesResponseRT,
}),
rt.partial({
updatedAt: rt.number,
version: rt.string,
}),
])
);
export const findInventoryViewResponsePayloadRT = rt.type({
data: rt.array(findInventoryViewResponseRT),
});
export type FindInventoryViewResponsePayload = rt.TypeOf<typeof findInventoryViewResponsePayloadRT>;

View file

@ -0,0 +1,14 @@
/*
* 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 * as rt from 'io-ts';
export const getInventoryViewRequestParamsRT = rt.type({
inventoryViewId: rt.string,
});
export type GetInventoryViewRequestParams = rt.TypeOf<typeof getInventoryViewRequestParamsRT>;

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './common';
export * from './get_inventory_view';
export * from './find_inventory_view';
export * from './create_inventory_view';
export * from './update_inventory_view';

View file

@ -0,0 +1,29 @@
/*
* 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 { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
export const updateInventoryViewAttributesRequestPayloadRT = rt.intersection([
rt.type({
name: nonEmptyStringRt,
}),
rt.UnknownRecord,
rt.exact(rt.partial({ isDefault: rt.undefined, isStatic: rt.undefined })),
]);
export type UpdateInventoryViewAttributesRequestPayload = rt.TypeOf<
typeof updateInventoryViewAttributesRequestPayloadRT
>;
export const updateInventoryViewRequestPayloadRT = rt.type({
attributes: updateInventoryViewAttributesRequestPayloadRT,
});
export type UpdateInventoryViewRequestPayload = rt.TypeOf<
typeof updateInventoryViewRequestPayloadRT
>;

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './inventory_views/v1';

View file

@ -0,0 +1,52 @@
/*
* 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 { i18n } from '@kbn/i18n';
import type { NonEmptyString } from '@kbn/io-ts-utils';
import type { InventoryViewAttributes } from './types';
export const staticInventoryViewId = '0';
export const staticInventoryViewAttributes: InventoryViewAttributes = {
name: i18n.translate('xpack.infra.savedView.defaultViewNameHosts', {
defaultMessage: 'Default view',
}) as NonEmptyString,
isDefault: false,
isStatic: true,
metric: {
type: 'cpu',
},
groupBy: [],
nodeType: 'host',
view: 'map',
customOptions: [],
boundsOverride: {
max: 1,
min: 0,
},
autoBounds: true,
accountId: '',
region: '',
customMetrics: [],
legend: {
palette: 'cool',
steps: 10,
reverseColors: false,
},
source: 'default',
sort: {
by: 'name',
direction: 'desc',
},
timelineOpen: false,
filterQuery: {
kind: 'kuery',
expression: '',
},
time: Date.now(),
autoReload: false,
};

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './defaults';
export * from './types';

View file

@ -0,0 +1,24 @@
/*
* 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 { staticInventoryViewAttributes } from './defaults';
import type { InventoryView, InventoryViewAttributes } from './types';
export const createInventoryViewMock = (
id: string,
attributes: InventoryViewAttributes,
updatedAt?: number,
version?: string
): InventoryView => ({
id,
attributes: {
...staticInventoryViewAttributes,
...attributes,
},
updatedAt,
version,
});

View file

@ -0,0 +1,35 @@
/*
* 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 { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
export const inventoryViewAttributesRT = rt.intersection([
rt.strict({
name: nonEmptyStringRt,
isDefault: rt.boolean,
isStatic: rt.boolean,
}),
rt.UnknownRecord,
]);
export type InventoryViewAttributes = rt.TypeOf<typeof inventoryViewAttributesRT>;
export const inventoryViewRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
attributes: inventoryViewAttributesRT,
}),
rt.partial({
updatedAt: rt.number,
version: rt.string,
}),
])
);
export type InventoryView = rt.TypeOf<typeof inventoryViewRT>;

View file

@ -0,0 +1,51 @@
/*
* 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 { i18n } from '@kbn/i18n';
import type { NonEmptyString } from '@kbn/io-ts-utils';
import type { MetricsExplorerViewAttributes } from './types';
export const staticMetricsExplorerViewId = 'static';
export const staticMetricsExplorerViewAttributes: MetricsExplorerViewAttributes = {
name: i18n.translate('xpack.infra.savedView.defaultViewNameHosts', {
defaultMessage: 'Default view',
}) as NonEmptyString,
isDefault: false,
isStatic: true,
options: {
aggregation: 'avg',
metrics: [
{
aggregation: 'avg',
field: 'system.cpu.total.norm.pct',
color: 'color0',
},
{
aggregation: 'avg',
field: 'kubernetes.pod.cpu.usage.node.pct',
color: 'color1',
},
{
aggregation: 'avg',
field: 'docker.cpu.total.pct',
color: 'color2',
},
],
source: 'default',
},
chartOptions: {
type: 'line',
yAxisMode: 'fromZero',
stack: false,
},
currentTimerange: {
from: 'now-1h',
to: 'now',
interval: '>=10s',
},
};

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './types';

View file

@ -0,0 +1,24 @@
/*
* 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 { staticMetricsExplorerViewAttributes } from './defaults';
import type { MetricsExplorerView, MetricsExplorerViewAttributes } from './types';
export const createmetricsExplorerViewMock = (
id: string,
attributes: MetricsExplorerViewAttributes,
updatedAt?: number,
version?: string
): MetricsExplorerView => ({
id,
attributes: {
...staticMetricsExplorerViewAttributes,
...attributes,
},
updatedAt,
version,
});

View file

@ -0,0 +1,35 @@
/*
* 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 { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
export const metricsExplorerViewAttributesRT = rt.intersection([
rt.strict({
name: nonEmptyStringRt,
isDefault: rt.boolean,
isStatic: rt.boolean,
}),
rt.UnknownRecord,
]);
export type MetricsExplorerViewAttributes = rt.TypeOf<typeof metricsExplorerViewAttributesRT>;
export const metricsExplorerViewRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
attributes: metricsExplorerViewAttributesRT,
}),
rt.partial({
updatedAt: rt.number,
version: rt.string,
}),
])
);
export type MetricsExplorerView = rt.TypeOf<typeof metricsExplorerViewRT>;

View file

@ -8,6 +8,7 @@
import { InfraBackendLibs } from './lib/infra_types';
import { initGetHostsAnomaliesRoute, initGetK8sAnomaliesRoute } from './routes/infra_ml';
import { initInventoryMetaRoute } from './routes/inventory_metadata';
import { initInventoryViewRoutes } from './routes/inventory_views';
import { initIpToHostName } from './routes/ip_to_hostname';
import { initGetLogAlertsChartPreviewDataRoute } from './routes/log_alerts';
import {
@ -61,6 +62,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
initMetricsAPIRoute(libs);
initMetadataRoute(libs);
initInventoryMetaRoute(libs);
initInventoryViewRoutes(libs);
initGetLogAlertsChartPreviewDataRoute(libs);
initProcessListRoute(libs);
initOverviewRoute(libs);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { createInventoryViewsServiceStartMock } from './services/inventory_views/inventory_views_service.mock';
import {
createLogViewsServiceSetupMock,
createLogViewsServiceStartMock,
@ -23,6 +24,7 @@ const createInfraSetupMock = () => {
const createInfraStartMock = () => {
const infraStartMock: jest.Mocked<InfraPluginStart> = {
getMetricIndices: jest.fn(),
inventoryViews: createInventoryViewsServiceStartMock(),
logViews: createLogViewsServiceStartMock(),
};
return infraStartMock;

View file

@ -20,8 +20,6 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants';
import { defaultLogViewsStaticConfig } from '../common/log_views';
import { publicConfigKeys } from '../common/plugin_config_types';
import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view';
import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view';
import { configDeprecations, getInfraDeprecationsFactory } from './deprecations';
import { LOGS_FEATURE, METRICS_FEATURE } from './features';
import { initInfraServer } from './infra_server';
@ -43,7 +41,12 @@ import { InfraBackendLibs, InfraDomainLibs } from './lib/infra_types';
import { makeGetMetricIndices } from './lib/metrics/make_get_metric_indices';
import { infraSourceConfigurationSavedObjectType, InfraSources } from './lib/sources';
import { InfraSourceStatus } from './lib/source_status';
import { logViewSavedObjectType } from './saved_objects';
import {
inventoryViewSavedObjectType,
logViewSavedObjectType,
metricsExplorerViewSavedObjectType,
} from './saved_objects';
import { InventoryViewsService } from './services/inventory_views';
import { LogEntriesService } from './services/log_entries';
import { LogViewsService } from './services/log_views';
import { RulesService } from './services/rules';
@ -117,6 +120,7 @@ export class InfraServerPlugin
private logsRules: RulesService;
private metricsRules: RulesService;
private inventoryViews: InventoryViewsService;
private logViews: LogViewsService;
constructor(context: PluginInitializerContext<InfraConfig>) {
@ -134,6 +138,7 @@ export class InfraServerPlugin
this.logger.get('metricsRules')
);
this.inventoryViews = new InventoryViewsService(this.logger.get('inventoryViews'));
this.logViews = new LogViewsService(this.logger.get('logViews'));
}
@ -148,6 +153,7 @@ export class InfraServerPlugin
sources,
}
);
const inventoryViews = this.inventoryViews.setup();
const logViews = this.logViews.setup();
// register saved object types
@ -229,11 +235,17 @@ export class InfraServerPlugin
return {
defineInternalSourceConfiguration: sources.defineInternalSourceConfiguration.bind(sources),
inventoryViews,
logViews,
} as InfraPluginSetup;
}
start(core: CoreStart, plugins: InfraServerPluginStartDeps) {
const inventoryViews = this.inventoryViews.start({
infraSources: this.libs.sources,
savedObjects: core.savedObjects,
});
const logViews = this.logViews.start({
infraSources: this.libs.sources,
savedObjects: core.savedObjects,
@ -247,6 +259,7 @@ export class InfraServerPlugin
});
return {
inventoryViews,
logViews,
getMetricIndices: makeGetMetricIndices(this.libs.sources),
};

View file

@ -0,0 +1,350 @@
# Inventory Views CRUD api
## Find all: `GET /api/infra/inventory_views`
Retrieves all inventory views in a reduced version.
### Request
- **Method**: GET
- **Path**: /api/infra/inventory_views
- **Query params**:
- `sourceId` _(optional)_: Specify a source id related to the inventory views. Default value: `default`.
### Response
```json
GET /api/infra/inventory_views
Status code: 200
{
"data": [
{
"id": "static",
"attributes": {
"name": "Default view",
"isDefault": false,
"isStatic": true
}
},
{
"id": "927ad6a0-da0c-11ed-9487-41e9b90f96b9",
"version": "WzQwMiwxXQ==",
"updatedAt": 1681398305034,
"attributes": {
"name": "Ad-hoc",
"isDefault": true,
"isStatic": false
}
},
{
"id": "c301ef20-da0c-11ed-aac0-77131228e6f1",
"version": "WzQxMCwxXQ==",
"updatedAt": 1681398386450,
"attributes": {
"name": "Custom",
"isDefault": false,
"isStatic": false
}
}
]
}
```
## Get one: `GET /api/infra/inventory_views/{inventoryViewId}`
Retrieves a single inventory view by ID
### Request
- **Method**: GET
- **Path**: /api/infra/inventory_views/{inventoryViewId}
- **Query params**:
- `sourceId` _(optional)_: Specify a source id related to the inventory view. Default value: `default`.
### Response
```json
GET /api/infra/inventory_views/927ad6a0-da0c-11ed-9487-41e9b90f96b9
Status code: 200
{
"data": {
"id": "927ad6a0-da0c-11ed-9487-41e9b90f96b9",
"version": "WzQwMiwxXQ==",
"updatedAt": 1681398305034,
"attributes": {
"name": "Ad-hoc",
"isDefault": true,
"isStatic": false,
"metric": {
"type": "cpu"
},
"sort": {
"by": "name",
"direction": "desc"
},
"groupBy": [],
"nodeType": "host",
"view": "map",
"customOptions": [],
"customMetrics": [],
"boundsOverride": {
"max": 1,
"min": 0
},
"autoBounds": true,
"accountId": "",
"region": "",
"autoReload": false,
"filterQuery": {
"expression": "",
"kind": "kuery"
},
"legend": {
"palette": "cool",
"reverseColors": false,
"steps": 10
},
"timelineOpen": false
}
}
}
```
```json
GET /api/infra/inventory_views/random-id
Status code: 404
{
"statusCode": 404,
"error": "Not Found",
"message": "Saved object [inventory-view/random-id] not found"
}
```
## Create one: `POST /api/infra/inventory_views`
Creates a new inventory view.
### Request
- **Method**: POST
- **Path**: /api/infra/inventory_views
- **Request body**:
```json
{
"attributes": {
"name": "View name",
"metric": {
"type": "cpu"
},
"sort": {
"by": "name",
"direction": "desc"
},
//...
}
}
```
### Response
```json
POST /api/infra/inventory_views
Status code: 201
{
"data": {
"id": "927ad6a0-da0c-11ed-9487-41e9b90f96b9",
"version": "WzQwMiwxXQ==",
"updatedAt": 1681398305034,
"attributes": {
"name": "View name",
"isDefault": false,
"isStatic": false,
"metric": {
"type": "cpu"
},
"sort": {
"by": "name",
"direction": "desc"
},
"groupBy": [],
"nodeType": "host",
"view": "map",
"customOptions": [],
"customMetrics": [],
"boundsOverride": {
"max": 1,
"min": 0
},
"autoBounds": true,
"accountId": "",
"region": "",
"autoReload": false,
"filterQuery": {
"expression": "",
"kind": "kuery"
},
"legend": {
"palette": "cool",
"reverseColors": false,
"steps": 10
},
"timelineOpen": false
}
}
}
```
Send in the payload a `name` attribute already held by another view:
```json
POST /api/infra/inventory_views
Status code: 409
{
"statusCode": 409,
"error": "Conflict",
"message": "A view with that name already exists."
}
```
## Update one: `PUT /api/infra/inventory_views/{inventoryViewId}`
Updates an inventory view.
Any attribute can be updated except for `isDefault` and `isStatic`, which are derived by the source configuration preference set by the user.
### Request
- **Method**: PUT
- **Path**: /api/infra/inventory_views/{inventoryViewId}
- **Query params**:
- `sourceId` _(optional)_: Specify a source id related to the inventory view. Default value: `default`.
- **Request body**:
```json
{
"attributes": {
"name": "View name",
"metric": {
"type": "cpu"
},
"sort": {
"by": "name",
"direction": "desc"
},
//...
}
}
```
### Response
```json
PUT /api/infra/inventory_views/927ad6a0-da0c-11ed-9487-41e9b90f96b9
Status code: 200
{
"data": {
"id": "927ad6a0-da0c-11ed-9487-41e9b90f96b9",
"version": "WzQwMiwxXQ==",
"updatedAt": 1681398305034,
"attributes": {
"name": "View name",
"isDefault": false,
"isStatic": false,
"metric": {
"type": "cpu"
},
"sort": {
"by": "name",
"direction": "desc"
},
"groupBy": [],
"nodeType": "host",
"view": "map",
"customOptions": [],
"customMetrics": [],
"boundsOverride": {
"max": 1,
"min": 0
},
"autoBounds": true,
"accountId": "",
"region": "",
"autoReload": false,
"filterQuery": {
"expression": "",
"kind": "kuery"
},
"legend": {
"palette": "cool",
"reverseColors": false,
"steps": 10
},
"timelineOpen": false
}
}
}
```
```json
PUT /api/infra/inventory_views/random-id
Status code: 404
{
"statusCode": 404,
"error": "Not Found",
"message": "Saved object [inventory-view/random-id] not found"
}
```
Send in the payload a `name` attribute already held by another view:
```json
PUT /api/infra/inventory_views/927ad6a0-da0c-11ed-9487-41e9b90f96b9
Status code: 409
{
"statusCode": 409,
"error": "Conflict",
"message": "A view with that name already exists."
}
```
## Delete one: `DELETE /api/infra/inventory_views/{inventoryViewId}`
Deletes an inventory view.
### Request
- **Method**: DELETE
- **Path**: /api/infra/inventory_views/{inventoryViewId}
### Response
```json
DELETE /api/infra/inventory_views/927ad6a0-da0c-11ed-9487-41e9b90f96b9
Status code: 204 No content
```
```json
DELETE /api/infra/inventory_views/random-id
Status code: 404
{
"statusCode": 404,
"error": "Not Found",
"message": "Saved object [inventory-view/random-id] not found"
}
```

View file

@ -0,0 +1,58 @@
/*
* 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 { isBoom } from '@hapi/boom';
import { createValidationFunction } from '../../../common/runtime_types';
import {
createInventoryViewRequestPayloadRT,
inventoryViewResponsePayloadRT,
INVENTORY_VIEW_URL,
} from '../../../common/http_api/latest';
import type { InfraBackendLibs } from '../../lib/infra_types';
export const initCreateInventoryViewRoute = ({
framework,
getStartServices,
}: Pick<InfraBackendLibs, 'framework' | 'getStartServices'>) => {
framework.registerRoute(
{
method: 'post',
path: INVENTORY_VIEW_URL,
validate: {
body: createValidationFunction(createInventoryViewRequestPayloadRT),
},
},
async (_requestContext, request, response) => {
const { body } = request;
const { inventoryViews } = (await getStartServices())[2];
const inventoryViewsClient = inventoryViews.getScopedClient(request);
try {
const inventoryView = await inventoryViewsClient.create(body.attributes);
return response.custom({
statusCode: 201,
body: inventoryViewResponsePayloadRT.encode({ data: inventoryView }),
});
} catch (error) {
if (isBoom(error)) {
return response.customError({
statusCode: error.output.statusCode,
body: { message: error.output.payload.message },
});
}
return response.customError({
statusCode: error.statusCode ?? 500,
body: {
message: error.message ?? 'An unexpected error occurred',
},
});
}
}
);
};

View file

@ -0,0 +1,54 @@
/*
* 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 { isBoom } from '@hapi/boom';
import { createValidationFunction } from '../../../common/runtime_types';
import {
inventoryViewRequestParamsRT,
INVENTORY_VIEW_URL_ENTITY,
} from '../../../common/http_api/latest';
import type { InfraBackendLibs } from '../../lib/infra_types';
export const initDeleteInventoryViewRoute = ({
framework,
getStartServices,
}: Pick<InfraBackendLibs, 'framework' | 'getStartServices'>) => {
framework.registerRoute(
{
method: 'delete',
path: INVENTORY_VIEW_URL_ENTITY,
validate: {
params: createValidationFunction(inventoryViewRequestParamsRT),
},
},
async (_requestContext, request, response) => {
const { params } = request;
const { inventoryViews } = (await getStartServices())[2];
const inventoryViewsClient = inventoryViews.getScopedClient(request);
try {
await inventoryViewsClient.delete(params.inventoryViewId);
return response.noContent();
} catch (error) {
if (isBoom(error)) {
return response.customError({
statusCode: error.output.statusCode,
body: { message: error.output.payload.message },
});
}
return response.customError({
statusCode: error.statusCode ?? 500,
body: {
message: error.message ?? 'An unexpected error occurred',
},
});
}
}
);
};

View file

@ -0,0 +1,49 @@
/*
* 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 { createValidationFunction } from '../../../common/runtime_types';
import {
findInventoryViewResponsePayloadRT,
inventoryViewRequestQueryRT,
INVENTORY_VIEW_URL,
} from '../../../common/http_api/latest';
import type { InfraBackendLibs } from '../../lib/infra_types';
export const initFindInventoryViewRoute = ({
framework,
getStartServices,
}: Pick<InfraBackendLibs, 'framework' | 'getStartServices'>) => {
framework.registerRoute(
{
method: 'get',
path: INVENTORY_VIEW_URL,
validate: {
query: createValidationFunction(inventoryViewRequestQueryRT),
},
},
async (_requestContext, request, response) => {
const { query } = request;
const { inventoryViews } = (await getStartServices())[2];
const inventoryViewsClient = inventoryViews.getScopedClient(request);
try {
const inventoryViewsList = await inventoryViewsClient.find(query);
return response.ok({
body: findInventoryViewResponsePayloadRT.encode({ data: inventoryViewsList }),
});
} catch (error) {
return response.customError({
statusCode: error.statusCode ?? 500,
body: {
message: error.message ?? 'An unexpected error occurred',
},
});
}
}
);
};

View file

@ -0,0 +1,59 @@
/*
* 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 { isBoom } from '@hapi/boom';
import { createValidationFunction } from '../../../common/runtime_types';
import {
inventoryViewResponsePayloadRT,
inventoryViewRequestQueryRT,
INVENTORY_VIEW_URL_ENTITY,
getInventoryViewRequestParamsRT,
} from '../../../common/http_api/latest';
import type { InfraBackendLibs } from '../../lib/infra_types';
export const initGetInventoryViewRoute = ({
framework,
getStartServices,
}: Pick<InfraBackendLibs, 'framework' | 'getStartServices'>) => {
framework.registerRoute(
{
method: 'get',
path: INVENTORY_VIEW_URL_ENTITY,
validate: {
params: createValidationFunction(getInventoryViewRequestParamsRT),
query: createValidationFunction(inventoryViewRequestQueryRT),
},
},
async (_requestContext, request, response) => {
const { params, query } = request;
const { inventoryViews } = (await getStartServices())[2];
const inventoryViewsClient = inventoryViews.getScopedClient(request);
try {
const inventoryView = await inventoryViewsClient.get(params.inventoryViewId, query);
return response.ok({
body: inventoryViewResponsePayloadRT.encode({ data: inventoryView }),
});
} catch (error) {
if (isBoom(error)) {
return response.customError({
statusCode: error.output.statusCode,
body: { message: error.output.payload.message },
});
}
return response.customError({
statusCode: error.statusCode ?? 500,
body: {
message: error.message ?? 'An unexpected error occurred',
},
});
}
}
);
};

View file

@ -0,0 +1,23 @@
/*
* 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 { InfraBackendLibs } from '../../lib/infra_types';
import { initCreateInventoryViewRoute } from './create_inventory_view';
import { initDeleteInventoryViewRoute } from './delete_inventory_view';
import { initFindInventoryViewRoute } from './find_inventory_view';
import { initGetInventoryViewRoute } from './get_inventory_view';
import { initUpdateInventoryViewRoute } from './update_inventory_view';
export const initInventoryViewRoutes = (
dependencies: Pick<InfraBackendLibs, 'framework' | 'getStartServices'>
) => {
initCreateInventoryViewRoute(dependencies);
initDeleteInventoryViewRoute(dependencies);
initFindInventoryViewRoute(dependencies);
initGetInventoryViewRoute(dependencies);
initUpdateInventoryViewRoute(dependencies);
};

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isBoom } from '@hapi/boom';
import { createValidationFunction } from '../../../common/runtime_types';
import {
inventoryViewRequestParamsRT,
inventoryViewRequestQueryRT,
inventoryViewResponsePayloadRT,
INVENTORY_VIEW_URL_ENTITY,
updateInventoryViewRequestPayloadRT,
} from '../../../common/http_api/latest';
import type { InfraBackendLibs } from '../../lib/infra_types';
export const initUpdateInventoryViewRoute = ({
framework,
getStartServices,
}: Pick<InfraBackendLibs, 'framework' | 'getStartServices'>) => {
framework.registerRoute(
{
method: 'put',
path: INVENTORY_VIEW_URL_ENTITY,
validate: {
params: createValidationFunction(inventoryViewRequestParamsRT),
query: createValidationFunction(inventoryViewRequestQueryRT),
body: createValidationFunction(updateInventoryViewRequestPayloadRT),
},
},
async (_requestContext, request, response) => {
const { body, params, query } = request;
const { inventoryViews } = (await getStartServices())[2];
const inventoryViewsClient = inventoryViews.getScopedClient(request);
try {
const inventoryView = await inventoryViewsClient.update(
params.inventoryViewId,
body.attributes,
query
);
return response.ok({
body: inventoryViewResponsePayloadRT.encode({ data: inventoryView }),
});
} catch (error) {
if (isBoom(error)) {
return response.customError({
statusCode: error.output.statusCode,
body: { message: error.output.payload.message },
});
}
return response.customError({
statusCode: error.statusCode ?? 500,
body: {
message: error.message ?? 'An unexpected error occurred',
},
});
}
}
);
};

View file

@ -5,4 +5,6 @@
* 2.0.
*/
export * from './inventory_view';
export * from './log_view';
export * from './metrics_explorer_view';

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export {
inventoryViewSavedObjectName,
inventoryViewSavedObjectType,
} from './inventory_view_saved_object';
export { inventoryViewSavedObjectRT } from './types';

View file

@ -0,0 +1,39 @@
/*
* 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 { fold } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import type { SavedObject, SavedObjectsType } from '@kbn/core/server';
import { inventoryViewSavedObjectRT } from './types';
export const inventoryViewSavedObjectName = 'inventory-view';
const getInventoryViewTitle = (savedObject: SavedObject<unknown>) =>
pipe(
inventoryViewSavedObjectRT.decode(savedObject),
fold(
() => `Inventory view [id=${savedObject.id}]`,
({ attributes: { name } }) => name
)
);
export const inventoryViewSavedObjectType: SavedObjectsType = {
name: inventoryViewSavedObjectName,
hidden: false,
namespaceType: 'single',
management: {
defaultSearchField: 'name',
displayName: 'inventory view',
getTitle: getInventoryViewTitle,
icon: 'metricsApp',
importableAndExportable: true,
},
mappings: {
dynamic: false,
properties: {},
},
};

View file

@ -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 { isoToEpochRt, nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
export const inventoryViewSavedObjectAttributesRT = rt.intersection([
rt.strict({
name: nonEmptyStringRt,
}),
rt.UnknownRecord,
]);
export const inventoryViewSavedObjectRT = rt.intersection([
rt.type({
id: rt.string,
attributes: inventoryViewSavedObjectAttributesRT,
}),
rt.partial({
version: rt.string,
updated_at: isoToEpochRt,
}),
]);

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export {
metricsExplorerViewSavedObjectName,
metricsExplorerViewSavedObjectType,
} from './metrics_explorer_view_saved_object';
export { metricsExplorerViewSavedObjectRT } from './types';

View file

@ -0,0 +1,39 @@
/*
* 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 { fold } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import type { SavedObject, SavedObjectsType } from '@kbn/core/server';
import { metricsExplorerViewSavedObjectRT } from './types';
export const metricsExplorerViewSavedObjectName = 'metrics-explorer-view';
const getMetricsExplorerViewTitle = (savedObject: SavedObject<unknown>) =>
pipe(
metricsExplorerViewSavedObjectRT.decode(savedObject),
fold(
() => `Metrics explorer view [id=${savedObject.id}]`,
({ attributes: { name } }) => name
)
);
export const metricsExplorerViewSavedObjectType: SavedObjectsType = {
name: metricsExplorerViewSavedObjectName,
hidden: false,
namespaceType: 'single',
management: {
defaultSearchField: 'name',
displayName: 'metrics explorer view',
getTitle: getMetricsExplorerViewTitle,
icon: 'metricsApp',
importableAndExportable: true,
},
mappings: {
dynamic: false,
properties: {},
},
};

View file

@ -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 { isoToEpochRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
import { metricsExplorerViewAttributesRT } from '../../../common/metrics_explorer_views';
export const metricsExplorerViewSavedObjectRT = rt.intersection([
rt.type({
id: rt.string,
attributes: metricsExplorerViewAttributesRT,
}),
rt.partial({
version: rt.string,
updated_at: isoToEpochRt,
}),
]);

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { InventoryViewsService } from './inventory_views_service';
export { InventoryViewsClient } from './inventory_views_client';
export type {
InventoryViewsServiceSetup,
InventoryViewsServiceStart,
InventoryViewsServiceStartDeps,
} from './types';

View file

@ -0,0 +1,16 @@
/*
* 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 { IInventoryViewsClient } from './types';
export const createInventoryViewsClientMock = (): jest.Mocked<IInventoryViewsClient> => ({
delete: jest.fn(),
find: jest.fn(),
get: jest.fn(),
create: jest.fn(),
update: jest.fn(),
});

View file

@ -0,0 +1,255 @@
/*
* 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 { loggerMock } from '@kbn/logging-mocks';
import { SavedObjectsClientContract } from '@kbn/core/server';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { InventoryViewAttributes } from '../../../common/inventory_views';
import { InfraSource } from '../../lib/sources';
import { createInfraSourcesMock } from '../../lib/sources/mocks';
import { inventoryViewSavedObjectName } from '../../saved_objects/inventory_view';
import { InventoryViewsClient } from './inventory_views_client';
import { createInventoryViewMock } from '../../../common/inventory_views/inventory_view.mock';
import {
CreateInventoryViewAttributesRequestPayload,
UpdateInventoryViewAttributesRequestPayload,
} from '../../../common/http_api/latest';
describe('InventoryViewsClient class', () => {
const mockFindInventoryList = (savedObjectsClient: jest.Mocked<SavedObjectsClientContract>) => {
const inventoryViewListMock = [
createInventoryViewMock('0', {
isDefault: true,
} as InventoryViewAttributes),
createInventoryViewMock('default_id', {
name: 'Default view 2',
isStatic: false,
} as InventoryViewAttributes),
createInventoryViewMock('custom_id', {
name: 'Custom',
isStatic: false,
} as InventoryViewAttributes),
];
savedObjectsClient.find.mockResolvedValue({
total: 2,
saved_objects: inventoryViewListMock.slice(1).map((view) => ({
...view,
type: inventoryViewSavedObjectName,
score: 0,
references: [],
})),
per_page: 1000,
page: 1,
});
return inventoryViewListMock;
};
describe('.find', () => {
it('resolves the list of existing inventory views', async () => {
const { inventoryViewsClient, infraSources, savedObjectsClient } =
createInventoryViewsClient();
infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration);
const inventoryViewListMock = mockFindInventoryList(savedObjectsClient);
const inventoryViewList = await inventoryViewsClient.find({});
expect(savedObjectsClient.find).toHaveBeenCalled();
expect(inventoryViewList).toEqual(inventoryViewListMock);
});
it('always resolves at least the static inventory view', async () => {
const { inventoryViewsClient, infraSources, savedObjectsClient } =
createInventoryViewsClient();
const inventoryViewListMock = [
createInventoryViewMock('0', {
isDefault: true,
} as InventoryViewAttributes),
];
infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration);
savedObjectsClient.find.mockResolvedValue({
total: 2,
saved_objects: [],
per_page: 1000,
page: 1,
});
const inventoryViewList = await inventoryViewsClient.find({});
expect(savedObjectsClient.find).toHaveBeenCalled();
expect(inventoryViewList).toEqual(inventoryViewListMock);
});
});
it('.get resolves the an inventory view by id', async () => {
const { inventoryViewsClient, infraSources, savedObjectsClient } = createInventoryViewsClient();
const inventoryViewMock = createInventoryViewMock('custom_id', {
name: 'Custom',
isDefault: false,
isStatic: false,
} as InventoryViewAttributes);
infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration);
savedObjectsClient.get.mockResolvedValue({
...inventoryViewMock,
type: inventoryViewSavedObjectName,
references: [],
});
const inventoryView = await inventoryViewsClient.get('custom_id', {});
expect(savedObjectsClient.get).toHaveBeenCalled();
expect(inventoryView).toEqual(inventoryViewMock);
});
describe('.create', () => {
it('generate a new inventory view', async () => {
const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient();
const inventoryViewMock = createInventoryViewMock('new_id', {
name: 'New view',
isStatic: false,
} as InventoryViewAttributes);
mockFindInventoryList(savedObjectsClient);
savedObjectsClient.create.mockResolvedValue({
...inventoryViewMock,
type: inventoryViewSavedObjectName,
references: [],
});
const inventoryView = await inventoryViewsClient.create({
name: 'New view',
} as CreateInventoryViewAttributesRequestPayload);
expect(savedObjectsClient.create).toHaveBeenCalled();
expect(inventoryView).toEqual(inventoryViewMock);
});
it('throws an error when a conflicting name is given', async () => {
const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient();
mockFindInventoryList(savedObjectsClient);
await expect(
async () =>
await inventoryViewsClient.create({
name: 'Custom',
} as CreateInventoryViewAttributesRequestPayload)
).rejects.toThrow('A view with that name already exists.');
});
});
describe('.update', () => {
it('update an existing inventory view by id', async () => {
const { inventoryViewsClient, infraSources, savedObjectsClient } =
createInventoryViewsClient();
const inventoryViews = mockFindInventoryList(savedObjectsClient);
const inventoryViewMock = {
...inventoryViews[1],
attributes: {
...inventoryViews[1].attributes,
name: 'New name',
},
};
infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration);
savedObjectsClient.update.mockResolvedValue({
...inventoryViewMock,
type: inventoryViewSavedObjectName,
references: [],
});
const inventoryView = await inventoryViewsClient.update(
'default_id',
{
name: 'New name',
} as UpdateInventoryViewAttributesRequestPayload,
{}
);
expect(savedObjectsClient.update).toHaveBeenCalled();
expect(inventoryView).toEqual(inventoryViewMock);
});
it('throws an error when a conflicting name is given', async () => {
const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient();
mockFindInventoryList(savedObjectsClient);
await expect(
async () =>
await inventoryViewsClient.update(
'default_id',
{
name: 'Custom',
} as UpdateInventoryViewAttributesRequestPayload,
{}
)
).rejects.toThrow('A view with that name already exists.');
});
});
it('.delete removes an inventory view by id', async () => {
const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient();
savedObjectsClient.delete.mockResolvedValue({});
const inventoryView = await inventoryViewsClient.delete('custom_id');
expect(savedObjectsClient.delete).toHaveBeenCalled();
expect(inventoryView).toEqual({});
});
});
const createInventoryViewsClient = () => {
const logger = loggerMock.create();
const savedObjectsClient = savedObjectsClientMock.create();
const infraSources = createInfraSourcesMock();
const inventoryViewsClient = new InventoryViewsClient(logger, savedObjectsClient, infraSources);
return {
infraSources,
inventoryViewsClient,
savedObjectsClient,
};
};
const basicTestSourceConfiguration: InfraSource = {
id: 'ID',
origin: 'stored',
configuration: {
name: 'NAME',
description: 'DESCRIPTION',
logIndices: {
type: 'index_pattern',
indexPatternId: 'INDEX_PATTERN_ID',
},
logColumns: [],
fields: {
message: [],
},
metricAlias: 'METRIC_ALIAS',
inventoryDefaultView: '0',
metricsExplorerDefaultView: 'METRICS_EXPLORER_DEFAULT_VIEW',
anomalyThreshold: 0,
},
};

View file

@ -0,0 +1,199 @@
/*
* 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 {
Logger,
SavedObject,
SavedObjectsClientContract,
SavedObjectsUpdateResponse,
} from '@kbn/core/server';
import Boom from '@hapi/boom';
import {
staticInventoryViewAttributes,
staticInventoryViewId,
} from '../../../common/inventory_views';
import type {
CreateInventoryViewAttributesRequestPayload,
InventoryViewRequestQuery,
} from '../../../common/http_api/latest';
import type { InventoryView, InventoryViewAttributes } from '../../../common/inventory_views';
import { decodeOrThrow } from '../../../common/runtime_types';
import type { IInfraSources } from '../../lib/sources';
import { inventoryViewSavedObjectName } from '../../saved_objects/inventory_view';
import { inventoryViewSavedObjectRT } from '../../saved_objects/inventory_view/types';
import type { IInventoryViewsClient } from './types';
export class InventoryViewsClient implements IInventoryViewsClient {
constructor(
private readonly logger: Logger,
private readonly savedObjectsClient: SavedObjectsClientContract,
private readonly infraSources: IInfraSources
) {}
static STATIC_VIEW_ID = '0';
public async find(query: InventoryViewRequestQuery): Promise<InventoryView[]> {
this.logger.debug('Trying to load inventory views ...');
const sourceId = query.sourceId ?? 'default';
const [sourceConfiguration, inventoryViewSavedObject] = await Promise.all([
this.infraSources.getSourceConfiguration(this.savedObjectsClient, sourceId),
this.savedObjectsClient.find({
type: inventoryViewSavedObjectName,
perPage: 1000, // Fetch 1 page by default with a max of 1000 results
}),
]);
const defaultView = InventoryViewsClient.createStaticView(
sourceConfiguration.configuration.inventoryDefaultView
);
const views = inventoryViewSavedObject.saved_objects.map((savedObject) =>
this.mapSavedObjectToInventoryView(
savedObject,
sourceConfiguration.configuration.inventoryDefaultView
)
);
const inventoryViews = [defaultView, ...views];
const sortedInventoryViews = this.moveDefaultViewOnTop(inventoryViews);
return sortedInventoryViews;
}
public async get(
inventoryViewId: string,
query: InventoryViewRequestQuery
): Promise<InventoryView> {
this.logger.debug(`Trying to load inventory view with id ${inventoryViewId} ...`);
const sourceId = query.sourceId ?? 'default';
// Handle the case where the requested resource is the static inventory view
if (inventoryViewId === InventoryViewsClient.STATIC_VIEW_ID) {
const sourceConfiguration = await this.infraSources.getSourceConfiguration(
this.savedObjectsClient,
sourceId
);
return InventoryViewsClient.createStaticView(
sourceConfiguration.configuration.inventoryDefaultView
);
}
const [sourceConfiguration, inventoryViewSavedObject] = await Promise.all([
this.infraSources.getSourceConfiguration(this.savedObjectsClient, sourceId),
this.savedObjectsClient.get(inventoryViewSavedObjectName, inventoryViewId),
]);
return this.mapSavedObjectToInventoryView(
inventoryViewSavedObject,
sourceConfiguration.configuration.inventoryDefaultView
);
}
public async create(
attributes: CreateInventoryViewAttributesRequestPayload
): Promise<InventoryView> {
this.logger.debug(`Trying to create inventory view ...`);
// Validate there is not a view with the same name
await this.assertNameConflict(attributes.name);
const inventoryViewSavedObject = await this.savedObjectsClient.create(
inventoryViewSavedObjectName,
attributes
);
return this.mapSavedObjectToInventoryView(inventoryViewSavedObject);
}
public async update(
inventoryViewId: string,
attributes: CreateInventoryViewAttributesRequestPayload,
query: InventoryViewRequestQuery
): Promise<InventoryView> {
this.logger.debug(`Trying to update inventory view with id "${inventoryViewId}"...`);
// Validate there is not a view with the same name
await this.assertNameConflict(attributes.name, [inventoryViewId]);
const sourceId = query.sourceId ?? 'default';
const [sourceConfiguration, inventoryViewSavedObject] = await Promise.all([
this.infraSources.getSourceConfiguration(this.savedObjectsClient, sourceId),
this.savedObjectsClient.update(inventoryViewSavedObjectName, inventoryViewId, attributes),
]);
return this.mapSavedObjectToInventoryView(
inventoryViewSavedObject,
sourceConfiguration.configuration.inventoryDefaultView
);
}
public delete(inventoryViewId: string): Promise<{}> {
this.logger.debug(`Trying to delete inventory view with id ${inventoryViewId} ...`);
return this.savedObjectsClient.delete(inventoryViewSavedObjectName, inventoryViewId);
}
private mapSavedObjectToInventoryView(
savedObject: SavedObject | SavedObjectsUpdateResponse,
defaultViewId?: string
) {
const inventoryViewSavedObject = decodeOrThrow(inventoryViewSavedObjectRT)(savedObject);
return {
id: inventoryViewSavedObject.id,
version: inventoryViewSavedObject.version,
updatedAt: inventoryViewSavedObject.updated_at,
attributes: {
...inventoryViewSavedObject.attributes,
isDefault: inventoryViewSavedObject.id === defaultViewId,
isStatic: false,
},
};
}
private moveDefaultViewOnTop(views: InventoryView[]) {
const defaultViewPosition = views.findIndex((view) => view.attributes.isDefault);
if (defaultViewPosition !== -1) {
const element = views.splice(defaultViewPosition, 1)[0];
views.unshift(element);
}
return views;
}
/**
* We want to control conflicting names on the views
*/
private async assertNameConflict(name: string, whitelist: string[] = []) {
const results = await this.savedObjectsClient.find<InventoryViewAttributes>({
type: inventoryViewSavedObjectName,
perPage: 1000,
});
const hasConflict = [InventoryViewsClient.createStaticView(), ...results.saved_objects].some(
(obj) => !whitelist.includes(obj.id) && obj.attributes.name === name
);
if (hasConflict) {
throw Boom.conflict('A view with that name already exists.');
}
}
private static createStaticView = (defaultViewId?: string): InventoryView => ({
id: staticInventoryViewId,
attributes: {
...staticInventoryViewAttributes,
isDefault: defaultViewId === InventoryViewsClient.STATIC_VIEW_ID,
},
});
}

View file

@ -0,0 +1,18 @@
/*
* 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 { createInventoryViewsClientMock } from './inventory_views_client.mock';
import type { InventoryViewsServiceSetup, InventoryViewsServiceStart } from './types';
export const createInventoryViewsServiceSetupMock =
(): jest.Mocked<InventoryViewsServiceSetup> => {};
export const createInventoryViewsServiceStartMock =
(): jest.Mocked<InventoryViewsServiceStart> => ({
getClient: jest.fn((_savedObjectsClient: any) => createInventoryViewsClientMock()),
getScopedClient: jest.fn((_request: any) => createInventoryViewsClientMock()),
});

View file

@ -0,0 +1,39 @@
/*
* 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 { KibanaRequest, Logger, SavedObjectsClientContract } from '@kbn/core/server';
import { InventoryViewsClient } from './inventory_views_client';
import type {
InventoryViewsServiceSetup,
InventoryViewsServiceStart,
InventoryViewsServiceStartDeps,
} from './types';
export class InventoryViewsService {
constructor(private readonly logger: Logger) {}
public setup(): InventoryViewsServiceSetup {}
public start({
infraSources,
savedObjects,
}: InventoryViewsServiceStartDeps): InventoryViewsServiceStart {
const { logger } = this;
return {
getClient(savedObjectsClient: SavedObjectsClientContract) {
return new InventoryViewsClient(logger, savedObjectsClient, infraSources);
},
getScopedClient(request: KibanaRequest) {
const savedObjectsClient = savedObjects.getScopedClient(request);
return this.getClient(savedObjectsClient);
},
};
}
}

View file

@ -0,0 +1,45 @@
/*
* 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 {
KibanaRequest,
SavedObjectsClientContract,
SavedObjectsServiceStart,
} from '@kbn/core/server';
import type {
CreateInventoryViewAttributesRequestPayload,
InventoryViewRequestQuery,
UpdateInventoryViewAttributesRequestPayload,
} from '../../../common/http_api/latest';
import type { InventoryView } from '../../../common/inventory_views';
import type { InfraSources } from '../../lib/sources';
export interface InventoryViewsServiceStartDeps {
infraSources: InfraSources;
savedObjects: SavedObjectsServiceStart;
}
export type InventoryViewsServiceSetup = void;
export interface InventoryViewsServiceStart {
getClient(savedObjectsClient: SavedObjectsClientContract): IInventoryViewsClient;
getScopedClient(request: KibanaRequest): IInventoryViewsClient;
}
export interface IInventoryViewsClient {
delete(inventoryViewId: string): Promise<{}>;
find(query: InventoryViewRequestQuery): Promise<InventoryView[]>;
get(inventoryViewId: string, query: InventoryViewRequestQuery): Promise<InventoryView>;
create(
inventoryViewAttributes: CreateInventoryViewAttributesRequestPayload
): Promise<InventoryView>;
update(
inventoryViewId: string,
inventoryViewAttributes: UpdateInventoryViewAttributesRequestPayload,
query: InventoryViewRequestQuery
): Promise<InventoryView>;
}

View file

@ -14,6 +14,7 @@ import type { SearchRequestHandlerContext } from '@kbn/data-plugin/server';
import type { MlPluginSetup } from '@kbn/ml-plugin/server';
import type { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration';
import { InfraServerPluginStartDeps } from './lib/adapters/framework';
import { InventoryViewsServiceStart } from './services/inventory_views';
import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views/types';
export type { InfraConfig } from '../common/plugin_config_types';
@ -30,6 +31,7 @@ export interface InfraPluginSetup {
}
export interface InfraPluginStart {
inventoryViews: InventoryViewsServiceStart;
logViews: LogViewsServiceStart;
getMetricIndices: (
savedObjectsClient: SavedObjectsClientContract,