mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Merge branch 'feature-integrations-manager' of github.com:elastic/kibana into feature-ingest
This commit is contained in:
commit
c7aa2ed5e8
101 changed files with 5461 additions and 9 deletions
5
docs/epm/index.asciidoc
Normal file
5
docs/epm/index.asciidoc
Normal file
|
@ -0,0 +1,5 @@
|
|||
[role="xpack"]
|
||||
[[epm]]
|
||||
== Elastic Package Manager
|
||||
|
||||
These are the docs for the Elastic Package Manager (EPM).
|
|
@ -131,6 +131,8 @@
|
|||
"@types/react-grid-layout": "^0.16.7",
|
||||
"@types/recompose": "^0.30.5",
|
||||
"@use-it/interval": "^0.1.3",
|
||||
"@types/tar": "^4.0.0",
|
||||
"@types/yauzl": "^2.9.1",
|
||||
"JSONStream": "1.3.5",
|
||||
"abortcontroller-polyfill": "^1.3.0",
|
||||
"angular": "^1.7.8",
|
||||
|
@ -422,6 +424,7 @@
|
|||
"listr": "^0.14.1",
|
||||
"load-grunt-config": "^3.0.1",
|
||||
"mocha": "6.2.1",
|
||||
"mock-http-server": "1.3.0",
|
||||
"multistream": "^2.1.1",
|
||||
"murmurhash3js": "3.0.1",
|
||||
"mutation-observer": "^1.0.3",
|
||||
|
|
|
@ -289,6 +289,22 @@
|
|||
'@types/lodash.clonedeep',
|
||||
],
|
||||
},
|
||||
{
|
||||
groupSlug: 'tar',
|
||||
groupName: 'tar related packages',
|
||||
packageNames: [
|
||||
'tar',
|
||||
'@types/tar',
|
||||
],
|
||||
},
|
||||
{
|
||||
groupSlug: 'yauzl',
|
||||
groupName: 'yauzl related packages',
|
||||
packageNames: [
|
||||
'yauzl',
|
||||
'@types/yauzl',
|
||||
],
|
||||
},
|
||||
{
|
||||
groupSlug: 'bluebird',
|
||||
groupName: 'bluebird related packages',
|
||||
|
@ -681,6 +697,14 @@
|
|||
'@types/git-url-parse',
|
||||
],
|
||||
},
|
||||
{
|
||||
groupSlug: 'js-search',
|
||||
groupName: 'js-search related packages',
|
||||
packageNames: [
|
||||
'js-search',
|
||||
'@types/js-search',
|
||||
],
|
||||
},
|
||||
{
|
||||
groupSlug: 'jsdom',
|
||||
groupName: 'jsdom related packages',
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"xpack.canvas": "legacy/plugins/canvas",
|
||||
"xpack.crossClusterReplication": "legacy/plugins/cross_cluster_replication",
|
||||
"xpack.dashboardMode": "legacy/plugins/dashboard_mode",
|
||||
"xpack.epm":"legacy/plugins/integrations_manager",
|
||||
"xpack.features": "plugins/features",
|
||||
"xpack.fileUpload": "legacy/plugins/file_upload",
|
||||
"xpack.fleet": "legacy/plugins/fleet",
|
||||
|
|
|
@ -42,6 +42,7 @@ import { snapshotRestore } from './legacy/plugins/snapshot_restore';
|
|||
import { transform } from './legacy/plugins/transform';
|
||||
import { actions } from './legacy/plugins/actions';
|
||||
import { alerting } from './legacy/plugins/alerting';
|
||||
import { epm } from './legacy/plugins/integrations_manager';
|
||||
import { lens } from './legacy/plugins/lens';
|
||||
import { ingest } from './legacy/plugins/ingest';
|
||||
import { fleet } from './legacy/plugins/fleet';
|
||||
|
@ -89,5 +90,6 @@ module.exports = function (kibana) {
|
|||
alerting(kibana),
|
||||
ingest(kibana),
|
||||
fleet(kibana),
|
||||
epm(kibana),
|
||||
];
|
||||
};
|
||||
|
|
53
x-pack/legacy/plugins/integrations_manager/README.md
Normal file
53
x-pack/legacy/plugins/integrations_manager/README.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Integrations Manager
|
||||
|
||||
## Development
|
||||
### Branch
|
||||
We're using a long-running feature branch [`feature-integrations-manager`](https://github.com/elastic/kibana/tree/feature-integrations-manager).
|
||||
|
||||
<details>
|
||||
<summary>Keeping up to date with upstream kibana</summary>
|
||||
|
||||
```bash
|
||||
## checkout feature branch to your fork
|
||||
git checkout -B feature-integrations-manager origin/feature-integrations-manager
|
||||
|
||||
## make sure your feature branch is current with upstream feature branch
|
||||
git pull upstream feature-integrations-manager
|
||||
|
||||
## pull in changes from upstream master
|
||||
git pull upstream master
|
||||
|
||||
## push changes to your remote
|
||||
git push origin
|
||||
|
||||
# Open a **DRAFT PR**. Normal PRs will re-notify authors of commits already merged
|
||||
# Draft PR will trigger CI run. Once CI is green ...
|
||||
|
||||
## push your changes to upstream feature branch
|
||||
git push upstream
|
||||
```
|
||||
</details>
|
||||
|
||||
### Feature development
|
||||
In your own fork of `elastic/kibana`, create a feature branch based on `feature-integrations-manager`.
|
||||
|
||||
```
|
||||
git checkout -b 1234-feature-description feature-integrations-manager
|
||||
# ... git commits for feature
|
||||
open https://github.com/elastic/kibana/compare/feature-integrations-manager...yourgithubname:1234-feature-description
|
||||
```
|
||||
|
||||
See https://github.com/elastic/kibana/pull/37950 for an example.
|
||||
|
||||
### Getting started
|
||||
See the Kibana docs for [how to set up your dev environment](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#setting-up-your-development-environment), [run Elasticsearch](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#running-elasticsearch), and [start Kibana](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#running-kibana).
|
||||
|
||||
One common workflow is:
|
||||
1. `yarn es snapshot`
|
||||
1. In another shell: `yarn start --no-base-path`
|
||||
|
||||
### Plugin architecture
|
||||
Follows the `common`, `server`, `public` structure from the [Architecture Style Guide
|
||||
](https://github.com/elastic/kibana/blob/master/style_guides/architecture_style_guide.md#file-and-folder-structure).
|
||||
|
||||
We use New Platform approach (structure, APIs, etc) where possible. There's a `kibana.json` manifest, and the server uses the `server/{index,plugin}.ts` approach from [`MIGRATION.md`](https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md#architecture). The client code we author is using New Platform shape & APIs, but the Manager deals with external systems which are at their own stages of migration.
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"title": "Asset",
|
||||
"type": "object",
|
||||
"description": "Item installed for Kibana (e.g. dashboard, visualization), Elasticsearch (e.g. ingest pipeline, ILM policy), or a Kibana plugin (e.g. ML job)",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "./asset_type.v1.json"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"type"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"type": "string",
|
||||
"title": "AssetType",
|
||||
"description": "Types of assets which can be installed/removed",
|
||||
"enum": [
|
||||
"index-template",
|
||||
"ingest-pipeline",
|
||||
"ilm-policy",
|
||||
"rollup-job",
|
||||
"ml-job",
|
||||
"data-frame-transform"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"title": "Datasource",
|
||||
"type": "object",
|
||||
"description": "A logical grouping of places where data is coming from, such as \"Production\", \"Staging\", \"Production East-1\", \"Metrics Cluster\", etc. -- these groupings are user-defined. We store information collected from the user about this logical grouping such as a name and any other information we need about it to generate the associated config. A package defines its own data source templates that can use user-provided values to generate the data source config. A single data source will typically enable users to collect both logs and metrics. A data source can be in multiple policies at the same time. A datasource can have multiple streams.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"package": {
|
||||
"$ref": "./package.v1.json"
|
||||
},
|
||||
"streams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "./stream.v1.json"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"read_alias": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"package",
|
||||
"streams"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"title": "Input",
|
||||
"type": "object",
|
||||
"description": "Where the data comes from",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"log",
|
||||
"metric/system",
|
||||
"metric/docker",
|
||||
"etc"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"type": "object",
|
||||
"example": "{paths: \"/var/log/*.log\"} or {metricsets: [\"container\", \"cpu\"]} or {username: \"elastic\", password: \"changeme\"}",
|
||||
"description": "Mix of configurable and required properties still TBD. Object for now might become string"
|
||||
},
|
||||
"ingest_pipelines": {
|
||||
"type": "array",
|
||||
"description": "Need a distinction for \"main\" ingest pipeline. Should be handled during install. Likely by package/manifest format",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"index_template": {
|
||||
"type": "string"
|
||||
},
|
||||
"ilm_policy": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"description": "",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "contents from fields.yml"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"config"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"title": "Output",
|
||||
"type": "object",
|
||||
"description": "Where to send the data",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "\"default\" or \"infosec1\""
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"elasticsearch",
|
||||
"something",
|
||||
"else"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"api_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"index_name": {
|
||||
"type": "string",
|
||||
"example": "metrics-mysql-prod_west-access",
|
||||
"description": "unique alias with write index"
|
||||
},
|
||||
"ingest_pipeline": {
|
||||
"type": "string",
|
||||
"example": "metrics-mysql-prod_west-access"
|
||||
},
|
||||
"config": {
|
||||
"type": "object",
|
||||
"description": "contains everything not otherwise specified (e.g. TLS, etc)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"type"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"title": "Package",
|
||||
"type": "object",
|
||||
"description": "A group of items related to a data ingestion source (e.g. MySQL, nginx, AWS). Can include Kibana assets, ES assets, data source configuration templates, manual install steps, etc.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "coredns"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "1.0.1, 1.3.1"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "CoreDNS logs and metrics integration.\nThe CoreDNS integrations allows to gather logs and metrics from the CoreDNS DNS server to get better insights.\n"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "CoreDNS"
|
||||
},
|
||||
"assets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "./asset.v1.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"version",
|
||||
"assets"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"title": "Policy",
|
||||
"type": "object",
|
||||
"description": "The entire config for the Beats agent, including all assigned data source config outputs along with agent-wide configuration values",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"use_case": {
|
||||
"type": "string"
|
||||
},
|
||||
"datasources": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"$ref": "./datasource.v1.json"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"active",
|
||||
"inactive"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"status"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"title": "Stream",
|
||||
"type": "object",
|
||||
"description": "A combination of an input type, the required config, an output, and any processors",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"input": {
|
||||
"$ref": "./input.v1.json"
|
||||
},
|
||||
"config": {
|
||||
"type": "object",
|
||||
"example": "{paths: \"/var/log/*.log\"} or {metricsets: [\"container\", \"cpu\"]} or {username: \"elastic\", password: \"changeme\"}"
|
||||
},
|
||||
"output": {
|
||||
"$ref": "./output.v1.json"
|
||||
},
|
||||
"processors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"input",
|
||||
"output"
|
||||
]
|
||||
}
|
160
x-pack/legacy/plugins/integrations_manager/common/api_specs/ingest/openapi.json
Executable file
160
x-pack/legacy/plugins/integrations_manager/common/api_specs/ingest/openapi.json
Executable file
|
@ -0,0 +1,160 @@
|
|||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Ingest",
|
||||
"version": "0.2",
|
||||
"description": "Strawman API for fka Ingest Plugin",
|
||||
"license": {
|
||||
"url": "https://raw.githubusercontent.com/elastic/elasticsearch/master/licenses/ELASTIC-LICENSE.txt",
|
||||
"name": "Elastic"
|
||||
},
|
||||
"contact": {
|
||||
"name": "Elastic Observability Team"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://localhost:5601/api/epm",
|
||||
"description": "EPM API Root"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/policies": {
|
||||
"get": {
|
||||
"summary": "Get policies",
|
||||
"tags": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "./models/policy.v1.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "query",
|
||||
"name": "datasource"
|
||||
},
|
||||
{
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "query",
|
||||
"name": "use_case"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/pageIndexParam"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/pageSizeParam"
|
||||
}
|
||||
],
|
||||
"description": "Return one or many polices based on the given filter",
|
||||
"operationId": "getPolicies"
|
||||
},
|
||||
"parameters": []
|
||||
},
|
||||
"/datasources": {
|
||||
"get": {
|
||||
"summary": "Get datasources",
|
||||
"tags": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "./models/datasource.v1.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "query",
|
||||
"name": "policy_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/pageIndexParam"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/pageSizeParam"
|
||||
}
|
||||
],
|
||||
"description": "Return datasources",
|
||||
"operationId": "getDatasources"
|
||||
}
|
||||
},
|
||||
"/policy/{policy_id}": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "policy_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"summary": "Get policy by id",
|
||||
"tags": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "./models/policy.v1.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Get Policy by id",
|
||||
"operationId": "getPolicyById"
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"parameters": {
|
||||
"pageSizeParam": {
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 50
|
||||
}
|
||||
},
|
||||
"pageIndexParam": {
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The entire config for the Beats agent, including all assigned data source config outputs
|
||||
* along with agent-wide configuration values
|
||||
*/
|
||||
export interface Policy {
|
||||
datasources?: Datasource[];
|
||||
description?: string;
|
||||
id: string;
|
||||
name?: string;
|
||||
status: Status;
|
||||
use_case?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A logical grouping of places where data is coming from, such as "Production", "Staging",
|
||||
* "Production East-1", "Metrics Cluster", etc. -- these groupings are user-defined. We
|
||||
* store information collected from the user about this logical grouping such as a name and
|
||||
* any other information we need about it to generate the associated config. A package
|
||||
* defines its own data source templates that can use user-provided values to generate the
|
||||
* data source config. A single data source will typically enable users to collect both logs
|
||||
* and metrics. A data source can be in multiple policies at the same time. A datasource can
|
||||
* have multiple streams.
|
||||
*/
|
||||
export interface Datasource {
|
||||
id?: string;
|
||||
name: string;
|
||||
package: Package;
|
||||
read_alias?: string;
|
||||
streams: Stream[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A group of items related to a data ingestion source (e.g. MySQL, nginx, AWS). Can include
|
||||
* Kibana assets, ES assets, data source configuration templates, manual install steps, etc.
|
||||
*/
|
||||
export interface Package {
|
||||
assets: Asset[];
|
||||
description?: string;
|
||||
name: string;
|
||||
title?: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item installed for Kibana (e.g. dashboard, visualization), Elasticsearch (e.g. ingest
|
||||
* pipeline, ILM policy), or a Kibana plugin (e.g. ML job)
|
||||
*/
|
||||
export interface Asset {
|
||||
id: string;
|
||||
type: AssetType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Types of assets which can be installed/removed
|
||||
*/
|
||||
export enum AssetType {
|
||||
DataFrameTransform = 'data-frame-transform',
|
||||
IlmPolicy = 'ilm-policy',
|
||||
IndexTemplate = 'index-template',
|
||||
IngestPipeline = 'ingest-pipeline',
|
||||
MlJob = 'ml-job',
|
||||
RollupJob = 'rollup-job',
|
||||
}
|
||||
|
||||
/**
|
||||
* A combination of an input type, the required config, an output, and any processors
|
||||
*/
|
||||
export interface Stream {
|
||||
config?: { [key: string]: any };
|
||||
id: string;
|
||||
input: Input;
|
||||
output: Output;
|
||||
processors?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the data comes from
|
||||
*/
|
||||
export interface Input {
|
||||
/**
|
||||
* Mix of configurable and required properties still TBD. Object for now might become string
|
||||
*/
|
||||
config: { [key: string]: any };
|
||||
fields?: Array<{ [key: string]: any }>;
|
||||
id?: string;
|
||||
ilm_policy?: string;
|
||||
index_template?: string;
|
||||
/**
|
||||
* Need a distinction for "main" ingest pipeline. Should be handled during install. Likely
|
||||
* by package/manifest format
|
||||
*/
|
||||
ingest_pipelines?: string[];
|
||||
type: InputType;
|
||||
}
|
||||
|
||||
export enum InputType {
|
||||
Etc = 'etc',
|
||||
Log = 'log',
|
||||
MetricDocker = 'metric/docker',
|
||||
MetricSystem = 'metric/system',
|
||||
}
|
||||
|
||||
/**
|
||||
* Where to send the data
|
||||
*/
|
||||
export interface Output {
|
||||
api_token?: string;
|
||||
/**
|
||||
* contains everything not otherwise specified (e.g. TLS, etc)
|
||||
*/
|
||||
config?: { [key: string]: any };
|
||||
id: string;
|
||||
/**
|
||||
* unique alias with write index
|
||||
*/
|
||||
index_name?: string;
|
||||
ingest_pipeline?: string;
|
||||
name: string;
|
||||
type: OutputType;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export enum OutputType {
|
||||
Elasticsearch = 'elasticsearch',
|
||||
Else = 'else',
|
||||
Something = 'something',
|
||||
}
|
||||
|
||||
export enum Status {
|
||||
Active = 'active',
|
||||
Inactive = 'inactive',
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import manifest from '../kibana.json';
|
||||
|
||||
export const PLUGIN = {
|
||||
ID: manifest.id,
|
||||
TITLE: i18n.translate('xpack.epm.pluginTitle', {
|
||||
defaultMessage: 'Elastic Package Manager',
|
||||
}),
|
||||
DESCRIPTION: 'Install and manage your Elastic data ingest packages',
|
||||
ICON: 'merge',
|
||||
CONFIG_PREFIX: 'xpack.epm',
|
||||
};
|
||||
|
||||
export const SAVED_OBJECT_TYPE = 'epm';
|
41
x-pack/legacy/plugins/integrations_manager/common/routes.ts
Normal file
41
x-pack/legacy/plugins/integrations_manager/common/routes.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { PLUGIN } from './constants';
|
||||
import { CategoryId } from './types';
|
||||
|
||||
export const API_ROOT = `/api/${PLUGIN.ID}`;
|
||||
export const API_LIST_PATTERN = `${API_ROOT}/list`;
|
||||
export const API_INFO_PATTERN = `${API_ROOT}/package/{pkgkey}`;
|
||||
export const API_INSTALL_PATTERN = `${API_ROOT}/install/{pkgkey}`;
|
||||
export const API_DELETE_PATTERN = `${API_ROOT}/delete/{pkgkey}`;
|
||||
export const API_CATEGORIES_PATTERN = `${API_ROOT}/categories`;
|
||||
|
||||
export interface ListParams {
|
||||
category?: CategoryId;
|
||||
}
|
||||
|
||||
export function getCategoriesPath() {
|
||||
return API_CATEGORIES_PATTERN;
|
||||
}
|
||||
|
||||
export function getListPath() {
|
||||
return API_LIST_PATTERN;
|
||||
}
|
||||
|
||||
export function getInfoPath(pkgkey: string) {
|
||||
return API_INFO_PATTERN.replace('{pkgkey}', pkgkey);
|
||||
}
|
||||
|
||||
export function getFilePath(filePath: string) {
|
||||
return `${API_ROOT}${filePath}`;
|
||||
}
|
||||
export function getInstallPath(pkgkey: string) {
|
||||
return API_INSTALL_PATTERN.replace('{pkgkey}', pkgkey).replace(/\/$/, ''); // trim trailing slash
|
||||
}
|
||||
|
||||
export function getRemovePath(pkgkey: string) {
|
||||
return API_DELETE_PATTERN.replace('{pkgkey}', pkgkey).replace(/\/$/, ''); // trim trailing slash
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// Calling Object.entries(PackagesGroupedByStatus) gave `status: string`
|
||||
// which causes a "string is not assignable to type InstallationStatus` error
|
||||
// see https://github.com/Microsoft/TypeScript/issues/20322
|
||||
// and https://github.com/Microsoft/TypeScript/pull/12253#issuecomment-263132208
|
||||
// and https://github.com/Microsoft/TypeScript/issues/21826#issuecomment-479851685
|
||||
export const entries = Object.entries as <T>(o: T) => Array<[keyof T, T[keyof T]]>;
|
109
x-pack/legacy/plugins/integrations_manager/common/types.ts
Normal file
109
x-pack/legacy/plugins/integrations_manager/common/types.ts
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObject, SavedObjectAttributes, SavedObjectReference } from 'src/core/server';
|
||||
|
||||
export { Request, ResponseToolkit, Server, ServerRoute } from 'hapi';
|
||||
|
||||
export type InstallationStatus = Installed['status'] | NotInstalled['status'];
|
||||
|
||||
export type AssetType = KibanaAssetType | ElasticsearchAssetType;
|
||||
|
||||
export type KibanaAssetType = 'dashboard' | 'visualization' | 'search' | 'index-pattern';
|
||||
|
||||
export type ElasticsearchAssetType = 'ingest-pipeline' | 'index-template' | 'ilm-policy';
|
||||
|
||||
// Registry's response types
|
||||
// from /search
|
||||
// https://github.com/elastic/integrations-registry/blob/master/docs/api/search.json
|
||||
export type RegistryList = RegistryListItem[];
|
||||
export interface RegistryListItem {
|
||||
description: string;
|
||||
download: string;
|
||||
icon: string;
|
||||
name: string;
|
||||
version: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface ScreenshotItem {
|
||||
src: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// from /package/{name}
|
||||
// https://github.com/elastic/integrations-registry/blob/master/docs/api/package.json
|
||||
export type ServiceName = 'kibana' | 'elasticsearch';
|
||||
export type RequirementVersion = string;
|
||||
|
||||
export interface ServiceRequirements {
|
||||
version: RequirementVersionRange;
|
||||
}
|
||||
|
||||
export interface RequirementVersionRange {
|
||||
min: RequirementVersion;
|
||||
max: RequirementVersion;
|
||||
}
|
||||
|
||||
// from /categories
|
||||
// https://github.com/elastic/integrations-registry/blob/master/docs/api/categories.json
|
||||
export type CategorySummaryList = CategorySummaryItem[];
|
||||
export type CategoryId = string;
|
||||
export interface CategorySummaryItem {
|
||||
id: CategoryId;
|
||||
title: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export type RequirementsByServiceName = Record<ServiceName, ServiceRequirements>;
|
||||
export interface AssetParts {
|
||||
pkgkey: string;
|
||||
service: ServiceName;
|
||||
type: AssetType;
|
||||
file: string;
|
||||
}
|
||||
export type AssetTypeToParts = Record<AssetType, AssetParts[]>;
|
||||
export type AssetsGroupedByServiceByType = Record<ServiceName, AssetTypeToParts>;
|
||||
export interface RegistryPackage {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
readme?: string;
|
||||
icon: string;
|
||||
requirement: RequirementsByServiceName;
|
||||
title?: string;
|
||||
screenshots?: ScreenshotItem[];
|
||||
assets: string[];
|
||||
}
|
||||
|
||||
// Managers public HTTP response types
|
||||
export type PackageList = PackageListItem[];
|
||||
// add title here until it's a part of registry response
|
||||
export type PackageListItem = Installable<Required<RegistryListItem>>;
|
||||
export type PackagesGroupedByStatus = Record<InstallationStatus, PackageList>;
|
||||
|
||||
// add title here until it's a part of registry response
|
||||
export type PackageInfo = Installable<
|
||||
Required<RegistryPackage> & { assets: AssetsGroupedByServiceByType }
|
||||
>;
|
||||
|
||||
export type Installation = SavedObject<InstallationAttributes>;
|
||||
export interface InstallationAttributes extends SavedObjectAttributes {
|
||||
installed: AssetReference[];
|
||||
}
|
||||
|
||||
export type Installable<T> = Installed<T> | NotInstalled<T>;
|
||||
|
||||
export type Installed<T = {}> = T & {
|
||||
status: 'installed';
|
||||
savedObject: Installation;
|
||||
};
|
||||
|
||||
export type NotInstalled<T = {}> = T & {
|
||||
status: 'not_installed';
|
||||
};
|
||||
|
||||
export type AssetReference = Pick<SavedObjectReference, 'id' | 'type'>;
|
53
x-pack/legacy/plugins/integrations_manager/index.ts
Normal file
53
x-pack/legacy/plugins/integrations_manager/index.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import JoiNamespace from 'joi';
|
||||
import { Legacy } from 'kibana';
|
||||
import { LegacyPluginInitializer, LegacyPluginOptions } from 'src/legacy/types';
|
||||
import { PLUGIN } from './common/constants';
|
||||
import manifest from './kibana.json';
|
||||
import { getConfigSchema } from './server/config';
|
||||
import { Plugin, createSetupShim } from './server/plugin';
|
||||
import { mappings, savedObjectSchemas } from './server/saved_objects';
|
||||
|
||||
const ROOT = `plugins/${PLUGIN.ID}`;
|
||||
|
||||
const pluginOptions: LegacyPluginOptions = {
|
||||
id: PLUGIN.ID,
|
||||
require: manifest.requiredPlugins,
|
||||
version: manifest.version,
|
||||
kibanaVersion: manifest.kibanaVersion,
|
||||
uiExports: {
|
||||
app: {
|
||||
title: PLUGIN.TITLE,
|
||||
description: PLUGIN.TITLE,
|
||||
main: `${ROOT}/index`,
|
||||
euiIconType: PLUGIN.ICON,
|
||||
order: 8100,
|
||||
},
|
||||
// This defines what shows up in the registry found at /app/kibana#/home and /app/kibana#/home/feature_directory
|
||||
home: [`${ROOT}/register_feature`],
|
||||
mappings,
|
||||
savedObjectSchemas,
|
||||
},
|
||||
configPrefix: PLUGIN.CONFIG_PREFIX,
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
config(Joi: typeof JoiNamespace) {
|
||||
return getConfigSchema(Joi);
|
||||
},
|
||||
deprecations: undefined,
|
||||
preInit: undefined,
|
||||
init(server: Legacy.Server) {
|
||||
const { initializerContext, coreSetup, pluginsSetup } = createSetupShim(server);
|
||||
const plugin = new Plugin(initializerContext);
|
||||
plugin.setup(coreSetup, pluginsSetup);
|
||||
},
|
||||
postInit: undefined,
|
||||
isEnabled: false,
|
||||
};
|
||||
|
||||
export const epm: LegacyPluginInitializer = kibana => new kibana.Plugin(pluginOptions);
|
9
x-pack/legacy/plugins/integrations_manager/kibana.json
Normal file
9
x-pack/legacy/plugins/integrations_manager/kibana.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "epm",
|
||||
"version": "0.0.2",
|
||||
"kibanaVersion": "kibana",
|
||||
"optionalPlugins": ["feature_catalogue"],
|
||||
"requiredPlugins": ["kibana", "elasticsearch", "xpack_main"],
|
||||
"server": true,
|
||||
"ui": true
|
||||
}
|
11
x-pack/legacy/plugins/integrations_manager/package.json
Normal file
11
x-pack/legacy/plugins/integrations_manager/package.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"author": "Elastic",
|
||||
"name": "epm",
|
||||
"version": "8.0.0",
|
||||
"private": true,
|
||||
"license": "Elastic-License",
|
||||
"dependencies": {
|
||||
"react-markdown": "^4.2.2"
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiNotificationBadge,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { entries } from '../../common/type_utils';
|
||||
import { AssetsGroupedByServiceByType, KibanaAssetType } from '../../common/types';
|
||||
import { AssetIcons, AssetTitleMap, ServiceIcons, ServiceTitleMap } from '../constants';
|
||||
import { useCore } from '../hooks/use_core';
|
||||
|
||||
export function AssetAccordion({ assets }: { assets: AssetsGroupedByServiceByType }) {
|
||||
const { theme } = useCore();
|
||||
|
||||
const FlexGroup = styled(EuiFlexGroup)`
|
||||
margin: ${theme.eui.ruleMargins.marginSmall};
|
||||
`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{entries(assets).map(([service, typeToParts], assetIndex) => {
|
||||
return (
|
||||
<Fragment key={service}>
|
||||
<FlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={ServiceIcons[service]} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiTitle key={service}>
|
||||
<EuiText>
|
||||
<h4>{ServiceTitleMap[service]} Assets</h4>
|
||||
</EuiText>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</FlexGroup>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
|
||||
{entries(typeToParts).map(([type, parts], typeIndex, typeEntries) => {
|
||||
let iconType = null;
|
||||
if (type in AssetIcons) {
|
||||
// only kibana assets have icons
|
||||
iconType = AssetIcons[type as KibanaAssetType];
|
||||
}
|
||||
// @types/styled-components@3 does yet support `defaultProps`, which EuiAccordion uses
|
||||
// Ref: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/31903
|
||||
// we're a major version behind; nearly 2
|
||||
return (
|
||||
<Fragment key={type}>
|
||||
<EuiAccordion
|
||||
style={{ margin: theme.eui.euiFormControlPadding }}
|
||||
id={type}
|
||||
buttonContent={
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{iconType ? <EuiIcon type={iconType} size="s" /> : ''}
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiText color="secondary">{AssetTitleMap[type]}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
paddingSize="m"
|
||||
extraAction={
|
||||
<EuiNotificationBadge color="subdued" size="m">
|
||||
{parts.length}
|
||||
</EuiNotificationBadge>
|
||||
}
|
||||
>
|
||||
<EuiText>
|
||||
<span role="img" aria-label="woman shrugging">
|
||||
🤷
|
||||
</span>
|
||||
</EuiText>
|
||||
</EuiAccordion>
|
||||
{typeIndex < typeEntries.length - 1 ? <EuiHorizontalRule margin="none" /> : ''}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
<EuiSpacer size="l" />
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import {
|
||||
EuiFacetButton,
|
||||
EuiFacetGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { entries } from '../../common/type_utils';
|
||||
import {
|
||||
DisplayedAssets,
|
||||
AssetIcons,
|
||||
AssetTitleMap,
|
||||
ServiceIcons,
|
||||
ServiceTitleMap,
|
||||
} from '../constants';
|
||||
import {
|
||||
AssetsGroupedByServiceByType,
|
||||
AssetTypeToParts,
|
||||
KibanaAssetType,
|
||||
} from '../../common/types';
|
||||
import { useCore } from '../hooks/use_core';
|
||||
|
||||
export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByType }) {
|
||||
const { theme } = useCore();
|
||||
const FirstHeaderRow = styled(EuiFlexGroup)`
|
||||
padding: 0 0 ${theme.eui.paddingSizes.m} 0;
|
||||
`;
|
||||
|
||||
const HeaderRow = styled(EuiFlexGroup)`
|
||||
padding: ${theme.eui.paddingSizes.m} 0;
|
||||
`;
|
||||
|
||||
const FacetGroup = styled(EuiFacetGroup)`
|
||||
flex-grow: 0;
|
||||
`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{entries(assets).map(([service, typeToParts], index) => {
|
||||
const Header = index === 0 ? FirstHeaderRow : HeaderRow;
|
||||
// filter out assets we are not going to display
|
||||
const filteredTypes: AssetTypeToParts = entries(typeToParts).reduce(
|
||||
(acc: any, [asset, value]) => {
|
||||
if (DisplayedAssets[service].includes(asset)) acc[asset] = value;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
return (
|
||||
<Fragment key={service}>
|
||||
<Header gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={ServiceIcons[service]} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiTitle key={service} size="xs">
|
||||
<EuiText>
|
||||
<h4>{ServiceTitleMap[service]} Assets</h4>
|
||||
</EuiText>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</Header>
|
||||
|
||||
<FacetGroup>
|
||||
{entries(filteredTypes).map(([type, parts]) => {
|
||||
let iconType = null;
|
||||
if (type in AssetIcons) {
|
||||
// only kibana assets have icons
|
||||
iconType = AssetIcons[type as KibanaAssetType];
|
||||
}
|
||||
const iconNode = iconType ? <EuiIcon type={iconType} size="s" /> : '';
|
||||
const FacetButton = styled(EuiFacetButton)`
|
||||
padding: '${theme.eui.paddingSizes.xs} 0';
|
||||
height: 'unset';
|
||||
`;
|
||||
|
||||
return (
|
||||
<FacetButton
|
||||
key={type}
|
||||
quantity={parts.length}
|
||||
icon={iconNode}
|
||||
// https://github.com/elastic/eui/issues/2216
|
||||
buttonRef={() => {}}
|
||||
>
|
||||
<EuiTextColor color="subdued">{AssetTitleMap[type]}</EuiTextColor>
|
||||
</FacetButton>
|
||||
);
|
||||
})}
|
||||
</FacetGroup>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useState, useRef, useLayoutEffect, Fragment, useCallback } from 'react';
|
||||
import { EuiButtonEmpty, EuiSpacer, EuiButton, EuiHorizontalRule } from '@elastic/eui';
|
||||
import euiStyled from '../../../../common/eui_styled_components';
|
||||
|
||||
const BottomFade = euiStyled.div`
|
||||
width: 100%;
|
||||
background: ${props =>
|
||||
`linear-gradient(${props.theme.eui.euiColorEmptyShade}00 0%, ${props.theme.eui.euiColorEmptyShade} 100%)`};
|
||||
margin-top: -${props => parseInt(props.theme.eui.spacerSizes.xl, 10) * 2}px;
|
||||
height: ${props => parseInt(props.theme.eui.spacerSizes.xl, 10) * 2}px;
|
||||
position: absolute;
|
||||
`;
|
||||
const ContentCollapseContainer = euiStyled.div`
|
||||
position: relative;
|
||||
`;
|
||||
const CollapseButtonContainer = euiStyled.div`
|
||||
display: inline-block;
|
||||
background-color: ${props => props.theme.eui.euiColorGhost};
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: ${props => parseInt(props.theme.eui.euiButtonHeight, 10) / 2}px;
|
||||
`;
|
||||
const CollapseButtonTop = euiStyled(EuiButtonEmpty)`
|
||||
float: right;
|
||||
`;
|
||||
|
||||
const CollapseButton = ({
|
||||
open,
|
||||
toggleCollapse,
|
||||
}: {
|
||||
open: boolean;
|
||||
toggleCollapse: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiHorizontalRule />
|
||||
<CollapseButtonContainer>
|
||||
<EuiButton onClick={toggleCollapse} iconType={`arrow${open ? 'Up' : 'Down'}`}>
|
||||
{open ? 'Collapse' : 'Read more'}
|
||||
</EuiButton>
|
||||
</CollapseButtonContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ContentCollapse = ({ children }: { children: React.ReactNode }) => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [height, setHeight] = useState<number | string>('auto');
|
||||
const [collapsible, setCollapsible] = useState<boolean>(true);
|
||||
const contentEl = useRef<HTMLDivElement>(null);
|
||||
const collapsedHeight = 360;
|
||||
|
||||
// if content is too small, don't collapse
|
||||
useLayoutEffect(
|
||||
() =>
|
||||
contentEl.current && contentEl.current.clientHeight < collapsedHeight
|
||||
? setCollapsible(false)
|
||||
: setHeight(collapsedHeight),
|
||||
[]
|
||||
);
|
||||
|
||||
const clickOpen = useCallback(() => {
|
||||
setOpen(!open);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{collapsible ? (
|
||||
<ContentCollapseContainer>
|
||||
<div
|
||||
ref={contentEl}
|
||||
style={{ height: `${open ? 'auto' : `${height}px`}`, overflow: 'hidden' }}
|
||||
>
|
||||
{open && (
|
||||
<CollapseButtonTop onClick={clickOpen} iconType={`arrow${open ? 'Up' : 'Down'}`}>
|
||||
Collapse
|
||||
</CollapseButtonTop>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
{!open && <BottomFade />}
|
||||
<CollapseButton open={open} toggleCollapse={clickOpen} />
|
||||
</ContentCollapseContainer>
|
||||
) : (
|
||||
<div>{children}</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiIcon, EuiPanel, IconType } from '@elastic/eui';
|
||||
import { useCore } from '../hooks/use_core';
|
||||
|
||||
export function IconPanel({ iconType }: { iconType: IconType }) {
|
||||
const { theme } = useCore();
|
||||
const Panel = styled(EuiPanel)`
|
||||
/* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */
|
||||
&&& {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
padding: ${theme.eui.spacerSizes.xl};
|
||||
svg {
|
||||
height: ${theme.eui.euiKeyPadMenuSize};
|
||||
width: ${theme.eui.euiKeyPadMenuSize};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<Panel>
|
||||
<EuiIcon type={iconType} size="original" />
|
||||
</Panel>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiCard, EuiIcon, ICON_TYPES } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { useLinks } from '../hooks';
|
||||
import { PackageListItem, PackageInfo } from '../../common/types';
|
||||
|
||||
export interface BadgeProps {
|
||||
showInstalledBadge?: boolean;
|
||||
}
|
||||
|
||||
type PackageCardProps = (PackageListItem | PackageInfo) & BadgeProps;
|
||||
|
||||
// adding the `href` causes EuiCard to use a `a` instead of a `button`
|
||||
// `a` tags use `euiLinkColor` which results in blueish Badge text
|
||||
const Card = styled(EuiCard)`
|
||||
color: inherit;
|
||||
`;
|
||||
|
||||
export function PackageCard({
|
||||
description,
|
||||
name,
|
||||
title,
|
||||
version,
|
||||
icon: iconUrl,
|
||||
showInstalledBadge,
|
||||
status,
|
||||
}: PackageCardProps) {
|
||||
const { toDetailView } = useLinks();
|
||||
const url = toDetailView({ name, version });
|
||||
|
||||
// try to find a logo in EUI
|
||||
const iconType = ICON_TYPES.find(key => key.toLowerCase() === `logo${name}`);
|
||||
|
||||
let optionalIcon;
|
||||
if (iconType) {
|
||||
optionalIcon = <EuiIcon type={iconType} size="l" />;
|
||||
} else if (iconUrl) {
|
||||
// skipping b/c images from registry are Not Good
|
||||
// https://github.com/elastic/integrations-registry/issues/45
|
||||
// optionalIcon = (
|
||||
// <img
|
||||
// width="24"
|
||||
// height="24"
|
||||
// src={`http://package-registry.app.elstc.co${iconUrl}`}
|
||||
// alt={`${name} icon`}
|
||||
// />
|
||||
// );
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
betaBadgeLabel={showInstalledBadge && status === 'installed' ? 'Installed' : ''}
|
||||
layout="horizontal"
|
||||
title={title}
|
||||
description={description}
|
||||
icon={optionalIcon}
|
||||
href={url}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment, ReactNode } from 'react';
|
||||
import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { PackageList } from '../../common/types';
|
||||
import { PackageCard, BadgeProps } from './package_card';
|
||||
|
||||
type ListProps = {
|
||||
controls?: ReactNode;
|
||||
title: string;
|
||||
list: PackageList;
|
||||
} & BadgeProps;
|
||||
|
||||
export function PackageListGrid({ controls, title, list, showInstalledBadge }: ListProps) {
|
||||
const controlsContent = <ControlsColumn title={title} controls={controls} />;
|
||||
const gridContent = <GridColumn list={list} showInstalledBadge={showInstalledBadge} />;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={1}>{controlsContent}</EuiFlexItem>
|
||||
<EuiFlexItem grow={3}>{gridContent}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
interface ControlsColumnProps {
|
||||
controls: ReactNode;
|
||||
title: string;
|
||||
}
|
||||
|
||||
function ControlsColumn({ controls, title }: ControlsColumnProps) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiText>
|
||||
<h2>{title}</h2>
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={2}>{controls}</EuiFlexItem>
|
||||
<EuiFlexItem grow={1} />
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
type GridColumnProps = {
|
||||
list: PackageList;
|
||||
} & BadgeProps;
|
||||
|
||||
function GridColumn({ list, showInstalledBadge }: GridColumnProps) {
|
||||
return (
|
||||
<EuiFlexGrid gutterSize="l" columns={3}>
|
||||
{list.map(item => (
|
||||
<EuiFlexItem key={`${item.name}-${item.version}`}>
|
||||
<PackageCard {...item} />
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGrid>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTextColor, EuiTitle } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { entries } from '../../common/type_utils';
|
||||
import { RequirementsByServiceName } from '../../common/types';
|
||||
import { ServiceTitleMap } from '../constants';
|
||||
import { useCore } from '../hooks/use_core';
|
||||
import { VersionBadge } from './version_badge';
|
||||
|
||||
export interface RequirementsProps {
|
||||
requirements: RequirementsByServiceName;
|
||||
}
|
||||
|
||||
export function Requirements(props: RequirementsProps) {
|
||||
const { requirements } = props;
|
||||
const { theme } = useCore();
|
||||
|
||||
const Text = styled.span`
|
||||
padding-bottom: ${theme.eui.paddingSizes.m};
|
||||
`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="xs">
|
||||
<Text>Compatibility</Text>
|
||||
</EuiTitle>
|
||||
{entries(requirements).map(([service, requirement]) => (
|
||||
<EuiFlexGroup key={service}>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiTextColor color="subdued" key={service}>
|
||||
{ServiceTitleMap[service]}:
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div>
|
||||
<VersionBadge version={requirement.version.min} />
|
||||
<span>{' - '}</span>
|
||||
<VersionBadge version={requirement.version.max} />
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
import { RequirementVersion } from '../../common/types';
|
||||
|
||||
export function VersionBadge({ version }: { version: RequirementVersion }) {
|
||||
return <EuiBadge color="hollow">v{version}</EuiBadge>;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { IconType } from '@elastic/eui';
|
||||
import { KibanaAssetType, AssetType, ServiceName } from '../common/types';
|
||||
|
||||
// TODO: figure out how to allow only corresponding asset types (KibanaAssetType, ElasticsearchAssetType)
|
||||
export const DisplayedAssets: Record<ServiceName, AssetType[]> = {
|
||||
kibana: ['index-pattern', 'visualization', 'search', 'dashboard'],
|
||||
elasticsearch: ['index-template', 'ingest-pipeline', 'ilm-policy'],
|
||||
};
|
||||
|
||||
export const AssetTitleMap: Record<AssetType, string> = {
|
||||
dashboard: 'Dashboard',
|
||||
'ilm-policy': 'ILM Policy',
|
||||
'ingest-pipeline': 'Ingest Pipeline',
|
||||
'index-pattern': 'Index Pattern',
|
||||
'index-template': 'Index Template',
|
||||
search: 'Saved Search',
|
||||
visualization: 'Visualization',
|
||||
};
|
||||
|
||||
export const ServiceTitleMap: Record<ServiceName, string> = {
|
||||
elasticsearch: 'Elasticsearch',
|
||||
kibana: 'Kibana',
|
||||
};
|
||||
|
||||
export const AssetIcons: Record<KibanaAssetType, IconType> = {
|
||||
dashboard: 'dashboardApp',
|
||||
'index-pattern': 'indexPatternApp',
|
||||
search: 'searchProfilerApp',
|
||||
visualization: 'visualizeApp',
|
||||
};
|
||||
|
||||
export const ServiceIcons: Record<ServiceName, IconType> = {
|
||||
elasticsearch: 'logoElasticsearch',
|
||||
kibana: 'logoKibana',
|
||||
};
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { createContext } from 'react';
|
||||
import { PluginCore } from '../plugin';
|
||||
|
||||
const CoreContext = createContext<PluginCore>({} as PluginCore);
|
||||
const CoreProvider: React.SFC<{ core: PluginCore }> = props => {
|
||||
const { core, ...restProps } = props;
|
||||
return <CoreContext.Provider value={core} {...restProps} />;
|
||||
};
|
||||
|
||||
export { CoreContext, CoreProvider };
|
81
x-pack/legacy/plugins/integrations_manager/public/data.ts
Normal file
81
x-pack/legacy/plugins/integrations_manager/public/data.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { HttpHandler } from 'src/core/public';
|
||||
import {
|
||||
getCategoriesPath,
|
||||
getInfoPath,
|
||||
getInstallPath,
|
||||
getListPath,
|
||||
getRemovePath,
|
||||
ListParams,
|
||||
getFilePath,
|
||||
} from '../common/routes';
|
||||
import {
|
||||
CategorySummaryList,
|
||||
PackageInfo,
|
||||
PackageList,
|
||||
PackagesGroupedByStatus,
|
||||
} from '../common/types';
|
||||
|
||||
const defaultClient: HttpHandler = (path, options?) => fetch(path, options).then(res => res.json());
|
||||
|
||||
let _fetch: HttpHandler = defaultClient;
|
||||
|
||||
export function setClient(client: HttpHandler): void {
|
||||
_fetch = client;
|
||||
}
|
||||
|
||||
export async function getCategories(): Promise<CategorySummaryList> {
|
||||
const path = getCategoriesPath();
|
||||
return _fetch(path);
|
||||
}
|
||||
|
||||
export async function getPackages(params?: ListParams): Promise<PackageList> {
|
||||
const path = getListPath();
|
||||
const options = params ? { query: { ...params } } : undefined;
|
||||
return _fetch(path, options);
|
||||
}
|
||||
|
||||
export async function getPackagesGroupedByStatus() {
|
||||
const path = getListPath();
|
||||
const list: PackageList = await _fetch(path);
|
||||
const initialValue: PackagesGroupedByStatus = {
|
||||
installed: [],
|
||||
not_installed: [],
|
||||
};
|
||||
|
||||
const groupedByStatus = list.reduce((grouped, item) => {
|
||||
if (!grouped[item.status]) {
|
||||
grouped[item.status] = [];
|
||||
}
|
||||
grouped[item.status].push(item);
|
||||
|
||||
return grouped;
|
||||
}, initialValue);
|
||||
|
||||
return groupedByStatus;
|
||||
}
|
||||
|
||||
export async function getPackageInfoByKey(pkgkey: string): Promise<PackageInfo> {
|
||||
const path = getInfoPath(pkgkey);
|
||||
return _fetch(path);
|
||||
}
|
||||
|
||||
export async function installPackage(pkgkey: string): Promise<PackageInfo> {
|
||||
const path = getInstallPath(pkgkey);
|
||||
return _fetch(path);
|
||||
}
|
||||
|
||||
export async function removePackage(pkgkey: string): Promise<PackageInfo> {
|
||||
const path = getRemovePath(pkgkey);
|
||||
return _fetch(path);
|
||||
}
|
||||
|
||||
export async function getFileByPath(filePath: string): Promise<string> {
|
||||
const path = getFilePath(filePath);
|
||||
return _fetch(path);
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { useBreadcrumbs } from './use_breadcrumbs';
|
||||
export { useCore } from './use_core';
|
||||
export { useLinks } from './use_links';
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ChromeBreadcrumb } from 'src/core/public';
|
||||
import { useCore } from '.';
|
||||
|
||||
export function useBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]) {
|
||||
const { chrome } = useCore();
|
||||
return chrome.setBreadcrumbs(newBreadcrumbs);
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { CoreContext } from '../contexts/core';
|
||||
|
||||
export function useCore() {
|
||||
return useContext(CoreContext);
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { PLUGIN } from '../../common/constants';
|
||||
import { getFilePath, getInfoPath } from '../../common/routes';
|
||||
import { patterns } from '../routes';
|
||||
import { useCore } from '.';
|
||||
import { DetailViewPanelName } from '..';
|
||||
|
||||
// TODO: get this from server/packages/handlers.ts (move elsewhere?)
|
||||
// seems like part of the name@version change
|
||||
interface DetailParams {
|
||||
name: string;
|
||||
version: string;
|
||||
panel?: DetailViewPanelName;
|
||||
}
|
||||
|
||||
const removeRelativePath = (relativePath: string): string =>
|
||||
new URL(relativePath, 'http://example.com').pathname;
|
||||
|
||||
export function useLinks() {
|
||||
const { http } = useCore();
|
||||
function appRoot(path: string) {
|
||||
// include '#' because we're using HashRouter
|
||||
return http.basePath.prepend(patterns.APP_ROOT + '#' + path);
|
||||
}
|
||||
|
||||
return {
|
||||
toAssets: (path: string) => http.basePath.prepend(`/plugins/${PLUGIN.ID}/assets/${path}`),
|
||||
toImage: (path: string) => http.basePath.prepend(getFilePath(path)),
|
||||
toRelativeImage: ({
|
||||
path,
|
||||
packageName,
|
||||
version,
|
||||
}: {
|
||||
path: string;
|
||||
packageName: string;
|
||||
version: string;
|
||||
}) => {
|
||||
const imagePath = removeRelativePath(path);
|
||||
const pkgkey = `${packageName}-${version}`;
|
||||
const filePath = `${getInfoPath(pkgkey)}/${imagePath}`;
|
||||
return http.basePath.prepend(filePath);
|
||||
},
|
||||
toListView: () => appRoot(patterns.LIST_VIEW),
|
||||
toDetailView: ({ name, version, panel }: DetailParams) => {
|
||||
// panel is optional, but `generatePath` won't accept `path: undefined`
|
||||
// so use this to pass `{ pkgkey }` or `{ pkgkey, panel }`
|
||||
const params = Object.assign({ pkgkey: `${name}-${version}` }, panel ? { panel } : {});
|
||||
return appRoot(generatePath(patterns.DETAIL_VIEW, params));
|
||||
},
|
||||
};
|
||||
}
|
48
x-pack/legacy/plugins/integrations_manager/public/index.ts
Normal file
48
x-pack/legacy/plugins/integrations_manager/public/index.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import euiLight from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import euiDark from '@elastic/eui/dist/eui_theme_dark.json';
|
||||
import chrome from 'ui/chrome';
|
||||
import { npSetup, npStart } from 'ui/new_platform';
|
||||
import { Plugin, PluginInitializerContext } from './plugin';
|
||||
import { routes } from './routes';
|
||||
|
||||
// create './types' later and move there?
|
||||
export type DetailViewPanelName = 'overview' | 'assets' | 'data-sources';
|
||||
|
||||
const REACT_APP_ROOT_ID = 'epm__root';
|
||||
const template = `<div id="${REACT_APP_ROOT_ID}" style="flex-grow: 1; display: flex; flex-direction: column"></div>`;
|
||||
const getRootEl = () => document.getElementById(REACT_APP_ROOT_ID);
|
||||
|
||||
export const plugin = (initializerContext: PluginInitializerContext = {}) =>
|
||||
new Plugin(initializerContext);
|
||||
|
||||
const epmPlugin = plugin();
|
||||
epmPlugin.setup(npSetup.core);
|
||||
|
||||
// @ts-ignore
|
||||
chrome.setRootTemplate(template);
|
||||
|
||||
waitForElement(getRootEl).then(element => {
|
||||
const isDarkMode = npStart.core.uiSettings.get('theme:darkMode');
|
||||
epmPlugin.start({
|
||||
...npStart.core,
|
||||
routes,
|
||||
theme: { eui: isDarkMode ? euiDark : euiLight },
|
||||
renderTo: element,
|
||||
});
|
||||
});
|
||||
|
||||
function waitForElement(fn: () => HTMLElement | null): Promise<HTMLElement> {
|
||||
return new Promise(resolve => {
|
||||
const element = fn();
|
||||
if (element) {
|
||||
resolve(element);
|
||||
} else {
|
||||
setTimeout(() => resolve(waitForElement(fn)), 10);
|
||||
}
|
||||
});
|
||||
}
|
64
x-pack/legacy/plugins/integrations_manager/public/plugin.tsx
Normal file
64
x-pack/legacy/plugins/integrations_manager/public/plugin.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { HashRouter, Switch } from 'react-router-dom';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
import euiLight from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { ChromeStart, CoreSetup, HttpStart, I18nStart } from 'src/core/public';
|
||||
import { CoreProvider } from './contexts/core';
|
||||
import { setClient } from './data';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface PluginInitializerContext {}
|
||||
|
||||
export type PluginSetup = ReturnType<Plugin['setup']>;
|
||||
export type PluginStart = ReturnType<Plugin['start']>;
|
||||
|
||||
export interface PluginTheme {
|
||||
eui: typeof euiLight;
|
||||
}
|
||||
|
||||
export interface PluginCore {
|
||||
chrome: ChromeStart;
|
||||
http: HttpStart;
|
||||
i18n: I18nStart;
|
||||
routes: JSX.Element[];
|
||||
theme: PluginTheme;
|
||||
renderTo: HTMLElement;
|
||||
}
|
||||
|
||||
export class Plugin {
|
||||
constructor(initializerContext: PluginInitializerContext) {}
|
||||
// called when plugin is setting up during Kibana's startup sequence
|
||||
public setup(core: CoreSetup) {
|
||||
setClient(core.http.fetch);
|
||||
}
|
||||
// called after all plugins are set up
|
||||
public start(core: PluginCore) {
|
||||
ReactDOM.render(<App core={core} />, core.renderTo);
|
||||
}
|
||||
}
|
||||
|
||||
function App(props: { core: PluginCore }) {
|
||||
const { i18n, routes } = props.core;
|
||||
|
||||
return (
|
||||
<EuiErrorBoundary>
|
||||
<CoreProvider core={props.core}>
|
||||
<i18n.Context>
|
||||
<ThemeProvider theme={props.core.theme}>
|
||||
<HashRouter>
|
||||
<Switch>{routes}</Switch>
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
</i18n.Context>
|
||||
</CoreProvider>
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { npSetup } from 'ui/new_platform';
|
||||
import { FeatureCatalogueCategory } from '../../../../../src/plugins/feature_catalogue/public';
|
||||
import { PLUGIN } from '../common/constants';
|
||||
import { patterns } from './routes';
|
||||
|
||||
// This defines what shows up in the registry found at /app/kibana#/home and /app/kibana#/home/feature_directory
|
||||
if (npSetup.plugins.feature_catalogue) {
|
||||
npSetup.plugins.feature_catalogue.register({
|
||||
id: PLUGIN.ID,
|
||||
title: PLUGIN.TITLE,
|
||||
description: PLUGIN.DESCRIPTION,
|
||||
icon: PLUGIN.ICON,
|
||||
path: patterns.APP_ROOT,
|
||||
showOnHomePage: true,
|
||||
category: FeatureCatalogueCategory.DATA,
|
||||
});
|
||||
}
|
34
x-pack/legacy/plugins/integrations_manager/public/routes.tsx
Normal file
34
x-pack/legacy/plugins/integrations_manager/public/routes.tsx
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { Detail, DetailProps } from './screens/detail';
|
||||
import { Home } from './screens/home';
|
||||
import { PLUGIN } from '../common/constants';
|
||||
|
||||
// patterns are used by React Router and are relative to `APP_ROOT`
|
||||
export const patterns = {
|
||||
APP_ROOT: `/app/${PLUGIN.ID}`,
|
||||
LIST_VIEW: '/',
|
||||
DETAIL_VIEW: '/detail/:pkgkey/:panel?',
|
||||
};
|
||||
|
||||
export const routes = [
|
||||
<Route key="home" path={patterns.LIST_VIEW} exact={true} component={Home} />,
|
||||
<Route
|
||||
key="detail"
|
||||
path={patterns.DETAIL_VIEW}
|
||||
exact={true}
|
||||
render={(props: DetailMatch) => <Detail {...props.match.params} />}
|
||||
/>,
|
||||
];
|
||||
|
||||
interface DetailMatch {
|
||||
match: {
|
||||
params: DetailProps;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { SideNavLinks } from './side_nav_links';
|
||||
import { PackageInfo } from '../../../common/types';
|
||||
import { AssetAccordion } from '../../components/asset_accordion';
|
||||
import { AssetsFacetGroup } from '../../components/assets_facet_group';
|
||||
import { Requirements } from '../../components/requirements';
|
||||
import { CenterColumn, LeftColumn, RightColumn } from './layout';
|
||||
import { OverviewPanel } from './overview_panel';
|
||||
import { DEFAULT_PANEL, DetailProps } from '.';
|
||||
|
||||
type ContentProps = PackageInfo & Pick<DetailProps, 'panel'> & { hasIconPanel: boolean };
|
||||
export function Content(props: ContentProps) {
|
||||
const { hasIconPanel, name, panel, version } = props;
|
||||
const SideNavColumn = hasIconPanel
|
||||
? styled(LeftColumn)`
|
||||
/* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */
|
||||
&&& {
|
||||
margin-top: 77px;
|
||||
}
|
||||
`
|
||||
: LeftColumn;
|
||||
|
||||
// fixes IE11 problem with nested flex items
|
||||
const ContentFlexGroup = styled(EuiFlexGroup)`
|
||||
flex: 0 0 auto !important;
|
||||
`;
|
||||
return (
|
||||
<ContentFlexGroup>
|
||||
<SideNavColumn>
|
||||
<SideNavLinks name={name} version={version} active={panel || DEFAULT_PANEL} />
|
||||
</SideNavColumn>
|
||||
<CenterColumn>
|
||||
<ContentPanel {...props} />
|
||||
</CenterColumn>
|
||||
<RightColumn>
|
||||
<RightColumnContent {...props} />
|
||||
</RightColumn>
|
||||
</ContentFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
type ContentPanelProps = PackageInfo & Pick<DetailProps, 'panel'>;
|
||||
export function ContentPanel(props: ContentPanelProps) {
|
||||
const { assets, panel } = props;
|
||||
switch (panel) {
|
||||
case 'assets':
|
||||
return <AssetAccordion assets={assets} />;
|
||||
case 'data-sources':
|
||||
return (
|
||||
<EuiTitle size="xs">
|
||||
<span>Data Sources</span>
|
||||
</EuiTitle>
|
||||
);
|
||||
case 'overview':
|
||||
default:
|
||||
return <OverviewPanel {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
type RightColumnContentProps = PackageInfo & Pick<DetailProps, 'panel'>;
|
||||
function RightColumnContent(props: RightColumnContentProps) {
|
||||
const { assets, requirement, panel } = props;
|
||||
switch (panel) {
|
||||
case 'overview':
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<Requirements requirements={requirement} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssetsFacetGroup assets={assets} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
default:
|
||||
return <EuiSpacer />;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiButtonEmptyProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPage,
|
||||
EuiTitle,
|
||||
IconType,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
// TODO: either
|
||||
// * import from a shared point
|
||||
// * duplicate inside our folder
|
||||
import { useTrackedPromise } from '../../../../infra/public/utils/use_tracked_promise';
|
||||
import { installPackage } from '../../data';
|
||||
import { PLUGIN } from '../../../common/constants';
|
||||
import { PackageInfo } from '../../../common/types';
|
||||
import { VersionBadge } from '../../components/version_badge';
|
||||
import { IconPanel } from '../../components/icon_panel';
|
||||
import { useBreadcrumbs, useCore, useLinks } from '../../hooks';
|
||||
import { CenterColumn, LeftColumn, RightColumn } from './layout';
|
||||
|
||||
type HeaderProps = PackageInfo & { iconType?: IconType };
|
||||
|
||||
export function Header(props: HeaderProps) {
|
||||
const { iconType, title, version } = props;
|
||||
const { theme } = useCore();
|
||||
const { toListView } = useLinks();
|
||||
useBreadcrumbs([{ text: PLUGIN.TITLE, href: toListView() }, { text: title }]);
|
||||
|
||||
const FullWidthNavRow = styled(EuiPage)`
|
||||
/* no left padding so link is against column left edge */
|
||||
padding-left: 0;
|
||||
`;
|
||||
|
||||
const Text = styled.span`
|
||||
margin-right: ${theme.eui.spacerSizes.xl};
|
||||
`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<FullWidthNavRow>
|
||||
<NavButtonBack />
|
||||
</FullWidthNavRow>
|
||||
<EuiFlexGroup>
|
||||
{iconType ? (
|
||||
<LeftColumn>
|
||||
<IconPanel iconType={iconType} />
|
||||
</LeftColumn>
|
||||
) : null}
|
||||
<CenterColumn>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<Text>{title}</Text>
|
||||
<VersionBadge version={version} />
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</CenterColumn>
|
||||
<RightColumn>
|
||||
<EuiFlexGroup direction="column" alignItems="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<InstallationButton {...props} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</RightColumn>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function NavButtonBack() {
|
||||
const { toListView } = useLinks();
|
||||
const { theme } = useCore();
|
||||
const ButtonEmpty = styled(EuiButtonEmpty).attrs<EuiButtonEmptyProps>({
|
||||
href: toListView(),
|
||||
})`
|
||||
margin-right: ${theme.eui.spacerSizes.xl};
|
||||
`;
|
||||
|
||||
return (
|
||||
<ButtonEmpty iconType="arrowLeft" size="xs" flush="left">
|
||||
Browse Packages
|
||||
</ButtonEmpty>
|
||||
);
|
||||
}
|
||||
|
||||
const installedButton = (
|
||||
<EuiButtonEmpty iconType="check" disabled>
|
||||
This package is installed
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
function InstallationButton(props: PackageInfo) {
|
||||
const [isInstalled, setInstalled] = useState<boolean>(props.status === 'installed');
|
||||
const [installationRequest, attemptInstallation] = useTrackedPromise(
|
||||
{
|
||||
createPromise: async () => {
|
||||
const pkgkey = `${props.name}-${props.version}`;
|
||||
return installPackage(pkgkey);
|
||||
},
|
||||
onResolve: (value: PackageInfo) => setInstalled(value.status === 'installed'),
|
||||
},
|
||||
[props.name, props.version, installPackage]
|
||||
);
|
||||
const isLoading = installationRequest.state === 'pending';
|
||||
const handleClickInstall = useCallback(attemptInstallation, [attemptInstallation]);
|
||||
|
||||
const installButton = (
|
||||
<EuiButton isLoading={isLoading} fill={true} onClick={handleClickInstall}>
|
||||
{isLoading ? 'Installing' : 'Install package'}
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
return isInstalled ? installedButton : installButton;
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import { EuiPage, EuiPageBody, EuiPageWidthProps, ICON_TYPES } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { PackageInfo } from '../../../common/types';
|
||||
import { DetailViewPanelName } from '../../';
|
||||
import { getPackageInfoByKey } from '../../data';
|
||||
import { useCore } from '../../hooks/use_core';
|
||||
import { Header } from './header';
|
||||
import { Content } from './content';
|
||||
|
||||
export const DEFAULT_PANEL: DetailViewPanelName = 'overview';
|
||||
|
||||
export interface DetailProps {
|
||||
pkgkey: string;
|
||||
panel?: DetailViewPanelName;
|
||||
}
|
||||
|
||||
export function Detail({ pkgkey, panel = DEFAULT_PANEL }: DetailProps) {
|
||||
const [info, setInfo] = useState<PackageInfo | null>(null);
|
||||
useEffect(() => {
|
||||
getPackageInfoByKey(pkgkey).then(response => {
|
||||
const { title } = response;
|
||||
setInfo({ ...response, title });
|
||||
});
|
||||
}, [pkgkey]);
|
||||
|
||||
// don't have designs for loading/empty states
|
||||
if (!info) return null;
|
||||
|
||||
return <DetailLayout restrictWidth={1200} {...info} panel={panel} />;
|
||||
}
|
||||
|
||||
type LayoutProps = PackageInfo & Pick<DetailProps, 'panel'> & EuiPageWidthProps;
|
||||
export function DetailLayout(props: LayoutProps) {
|
||||
const { name, restrictWidth } = props;
|
||||
const { theme } = useCore();
|
||||
const iconType = ICON_TYPES.find(key => key.toLowerCase() === `logo${name}`);
|
||||
|
||||
const FullWidthHeader = styled(EuiPage)`
|
||||
border-bottom: ${theme.eui.euiBorderThin}
|
||||
padding-bottom: ${theme.eui.paddingSizes.xl};
|
||||
`;
|
||||
|
||||
const paddingSizeTop: number = parseInt(theme.eui.paddingSizes.xl, 10) * 1.25;
|
||||
const FullWidthContent = styled(EuiPage)`
|
||||
background-color: ${theme.eui.euiColorEmptyShade};
|
||||
padding-top: ${paddingSizeTop}px;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<FullWidthHeader>
|
||||
<EuiPageBody restrictWidth={restrictWidth}>
|
||||
<Header iconType={iconType} {...props} />
|
||||
</EuiPageBody>
|
||||
</FullWidthHeader>
|
||||
<FullWidthContent>
|
||||
<EuiPageBody restrictWidth={restrictWidth}>
|
||||
<Content hasIconPanel={!!iconType} {...props} />
|
||||
</EuiPageBody>
|
||||
</FullWidthContent>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent, ReactNode } from 'react';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
interface ColumnProps {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const LeftColumn: FunctionComponent<ColumnProps> = ({ children, ...rest }) => {
|
||||
return (
|
||||
<EuiFlexItem grow={2} {...rest}>
|
||||
{children}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const CenterColumn: FunctionComponent<ColumnProps> = ({ children, ...rest }) => {
|
||||
return (
|
||||
<EuiFlexItem grow={7} {...rest}>
|
||||
{children}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const RightColumn: FunctionComponent<ColumnProps> = ({ children, ...rest }) => {
|
||||
return (
|
||||
<EuiFlexItem grow={3} {...rest}>
|
||||
{children}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiText,
|
||||
EuiCodeBlock,
|
||||
EuiTableHeaderCell,
|
||||
EuiTableRow,
|
||||
EuiTableRowCell,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
/** prevents links to the new pages from accessing `window.opener` */
|
||||
const REL_NOOPENER = 'noopener';
|
||||
|
||||
/** prevents search engine manipulation by noting the linked document is not trusted or endorsed by us */
|
||||
const REL_NOFOLLOW = 'nofollow';
|
||||
|
||||
/** prevents the browser from sending the current address as referrer via the Referer HTTP header */
|
||||
const REL_NOREFERRER = 'noreferrer';
|
||||
|
||||
export const markdownRenderers = {
|
||||
root: ({ children }: { children: React.ReactNode[] }) => (
|
||||
<EuiText grow={true}>{children}</EuiText>
|
||||
),
|
||||
table: ({ children }: { children: React.ReactNode[] }) => (
|
||||
<table className="euiTable euiTable--responsive" style={{ tableLayout: 'auto' }}>
|
||||
{children}
|
||||
</table>
|
||||
),
|
||||
tableRow: ({ children }: { children: React.ReactNode[] }) => (
|
||||
<EuiTableRow>{children}</EuiTableRow>
|
||||
),
|
||||
tableCell: ({ isHeader, children }: { isHeader: boolean; children: React.ReactNode[] }) => {
|
||||
return isHeader ? (
|
||||
<EuiTableHeaderCell>{children}</EuiTableHeaderCell>
|
||||
) : (
|
||||
<EuiTableRowCell>{children}</EuiTableRowCell>
|
||||
);
|
||||
},
|
||||
// the headings used in markdown don't match our page so mapping them to the appropriate one
|
||||
heading: ({ level, children }: { level: number; children: React.ReactNode[] }) => {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return <h3>{children}</h3>;
|
||||
case 2:
|
||||
return <h4>{children}</h4>;
|
||||
case 3:
|
||||
return <h5>{children}</h5>;
|
||||
default:
|
||||
return <h6>{children}</h6>;
|
||||
}
|
||||
},
|
||||
link: ({ children, href }: { children: React.ReactNode[]; href?: string }) => (
|
||||
<EuiLink href={href} target="_blank" rel={`${REL_NOOPENER} ${REL_NOFOLLOW} ${REL_NOREFERRER}`}>
|
||||
{children}
|
||||
</EuiLink>
|
||||
),
|
||||
code: ({ language, value }: { language: string; value: string }) => {
|
||||
return (
|
||||
<EuiCodeBlock language={language} isCopyable>
|
||||
{value}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { PackageInfo } from '../../../common/types';
|
||||
import { Screenshots } from './screenshots';
|
||||
import { Readme } from './readme';
|
||||
|
||||
export function OverviewPanel(props: PackageInfo) {
|
||||
const { screenshots, readme, name, version } = props;
|
||||
return (
|
||||
<Fragment>
|
||||
{readme && <Readme readmePath={readme} packageName={name} version={version} />}
|
||||
<EuiSpacer size="xl" />
|
||||
{screenshots && <Screenshots images={screenshots} />}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useEffect, useState, Fragment } from 'react';
|
||||
import { EuiLoadingContent, EuiText } from '@elastic/eui';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { getFileByPath } from '../../data';
|
||||
import { markdownRenderers } from './markdown_renderers';
|
||||
import { useLinks } from '../../hooks';
|
||||
import { ContentCollapse } from '../../components/content_collapse';
|
||||
|
||||
export function Readme({
|
||||
readmePath,
|
||||
packageName,
|
||||
version,
|
||||
}: {
|
||||
readmePath: string;
|
||||
packageName: string;
|
||||
version: string;
|
||||
}) {
|
||||
const [markdown, setMarkdown] = useState<string | undefined>(undefined);
|
||||
const { toRelativeImage } = useLinks();
|
||||
const handleImageUri = React.useCallback(
|
||||
(uri: string) => {
|
||||
const isRelative =
|
||||
uri.indexOf('http://') === 0 || uri.indexOf('https://') === 0 ? false : true;
|
||||
const fullUri = isRelative ? toRelativeImage({ packageName, version, path: uri }) : uri;
|
||||
return fullUri;
|
||||
},
|
||||
[toRelativeImage, packageName, version]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getFileByPath(readmePath).then(res => {
|
||||
setMarkdown(res);
|
||||
});
|
||||
}, [readmePath]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{markdown !== undefined ? (
|
||||
<ContentCollapse>
|
||||
<ReactMarkdown
|
||||
transformImageUri={handleImageUri}
|
||||
renderers={markdownRenderers}
|
||||
source={markdown}
|
||||
/>
|
||||
</ContentCollapse>
|
||||
) : (
|
||||
<EuiText>
|
||||
{/* simulates a long page of text loading */}
|
||||
<p>
|
||||
<EuiLoadingContent lines={5} />
|
||||
</p>
|
||||
<p>
|
||||
<EuiLoadingContent lines={6} />
|
||||
</p>
|
||||
<p>
|
||||
<EuiLoadingContent lines={4} />
|
||||
</p>
|
||||
</EuiText>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiSpacer, EuiText, EuiTitle, EuiImage, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { ScreenshotItem } from '../../../common/types';
|
||||
import { useLinks, useCore } from '../../hooks';
|
||||
|
||||
interface ScreenshotProps {
|
||||
images: ScreenshotItem[];
|
||||
}
|
||||
|
||||
export function Screenshots(props: ScreenshotProps) {
|
||||
const { theme } = useCore();
|
||||
const { toImage } = useLinks();
|
||||
const { images } = props;
|
||||
|
||||
// for now, just get first image
|
||||
const image = images[0];
|
||||
const hasCaption = image.title ? true : false;
|
||||
|
||||
const horizontalPadding: number = parseInt(theme.eui.paddingSizes.xl, 10) * 2;
|
||||
const verticalPadding: number = parseInt(theme.eui.paddingSizes.xl, 10) * 1.75;
|
||||
const padding = hasCaption
|
||||
? `${theme.eui.paddingSizes.xl} ${horizontalPadding}px ${verticalPadding}px`
|
||||
: `${verticalPadding}px ${horizontalPadding}px`;
|
||||
|
||||
const ScreenshotsContainer = styled(EuiFlexGroup)`
|
||||
background: linear-gradient(360deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%),
|
||||
${theme.eui.euiColorPrimary};
|
||||
padding: ${padding};
|
||||
flex: 0 0 auto;
|
||||
border-radius: ${theme.eui.euiBorderRadius};
|
||||
`;
|
||||
// fixes ie11 problems with nested flex items
|
||||
const NestedEuiFlexItem = styled(EuiFlexItem)`
|
||||
flex: 0 0 auto !important;
|
||||
`;
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>Screenshots</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<ScreenshotsContainer gutterSize="none" direction="column" alignItems="center">
|
||||
{hasCaption && (
|
||||
<NestedEuiFlexItem>
|
||||
<EuiText color="ghost" aria-label="screenshot image caption">
|
||||
{image.title}
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
</NestedEuiFlexItem>
|
||||
)}
|
||||
<NestedEuiFlexItem>
|
||||
{/* By default EuiImage sets width to 100% and Figure to 22.5rem for size=l images,
|
||||
set image to same width. Will need to update if size changes.
|
||||
*/}
|
||||
<EuiImage
|
||||
url={toImage(image.src)}
|
||||
alt="screenshot image preview"
|
||||
size="l"
|
||||
allowFullScreen
|
||||
style={{ width: '22.5rem', maxWidth: '100%' }}
|
||||
/>
|
||||
</NestedEuiFlexItem>
|
||||
</ScreenshotsContainer>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiButtonEmpty, EuiButtonEmptyProps } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { PackageInfo } from '../../../common/types';
|
||||
import { entries } from '../../../common/type_utils';
|
||||
import { DetailViewPanelName } from '../../';
|
||||
import { useLinks } from '../../hooks';
|
||||
|
||||
export type NavLinkProps = Pick<PackageInfo, 'name' | 'version'> & {
|
||||
active: DetailViewPanelName;
|
||||
};
|
||||
|
||||
const PanelDisplayNames: Record<DetailViewPanelName, string> = {
|
||||
overview: 'Overview',
|
||||
assets: 'Assets',
|
||||
'data-sources': 'Data Sources',
|
||||
};
|
||||
|
||||
export function SideNavLinks({ name, version, active }: NavLinkProps) {
|
||||
const { toDetailView } = useLinks();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{entries(PanelDisplayNames).map(([panel, display]) => {
|
||||
const Link = styled(EuiButtonEmpty).attrs<EuiButtonEmptyProps>({
|
||||
href: toDetailView({ name, version, panel }),
|
||||
})`
|
||||
font-weight: ${p =>
|
||||
active === panel
|
||||
? p.theme.eui.euiFontWeightSemiBold
|
||||
: p.theme.eui.euiFontWeightRegular};
|
||||
`;
|
||||
return (
|
||||
<div key={panel}>
|
||||
<Link>{display}</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiFacetButton, EuiFacetGroup } from '@elastic/eui';
|
||||
import { CategorySummaryItem, CategorySummaryList } from '../../../common/types';
|
||||
|
||||
export function CategoryFacets({
|
||||
categories,
|
||||
selectedCategory,
|
||||
onCategoryChange,
|
||||
}: {
|
||||
categories: CategorySummaryList;
|
||||
selectedCategory: string;
|
||||
onCategoryChange: (category: CategorySummaryItem) => unknown;
|
||||
}) {
|
||||
const controls = (
|
||||
<EuiFacetGroup>
|
||||
{categories.map(category => (
|
||||
<EuiFacetButton
|
||||
isSelected={category.id === selectedCategory}
|
||||
key={category.id}
|
||||
id={category.id}
|
||||
quantity={category.count}
|
||||
onClick={() => onCategoryChange(category)}
|
||||
>
|
||||
{category.title}
|
||||
</EuiFacetButton>
|
||||
))}
|
||||
</EuiFacetGroup>
|
||||
);
|
||||
|
||||
return controls;
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
// @ts-ignore (elastic/eui#1557) & (elastic/eui#1262) EuiImage is not exported yet
|
||||
EuiImage,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageWidthProps,
|
||||
// @ts-ignore
|
||||
EuiSearchBar,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { useCore, useLinks } from '../../hooks';
|
||||
|
||||
export type HeaderProps = EuiPageWidthProps & {
|
||||
onSearch: (userInput: string) => unknown;
|
||||
};
|
||||
|
||||
const Page = styled(EuiPage)`
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export function Header({ restrictWidth, onSearch }: HeaderProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const searchBar = (
|
||||
<EuiSearchBar
|
||||
query={searchTerm}
|
||||
key="search-input"
|
||||
box={{
|
||||
placeholder: 'Find a new package, or one you already use.',
|
||||
incremental: true,
|
||||
}}
|
||||
onChange={({ queryText: userInput }: { queryText: string }) => {
|
||||
setSearchTerm(userInput);
|
||||
onSearch(userInput);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const left = (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<HeroCopy />
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={4}>{searchBar}</EuiFlexItem>
|
||||
<EuiFlexItem grow={2} />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
const right = <HeroImage />;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<EuiPageBody restrictWidth={restrictWidth}>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={1}>{left}</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>{right}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageBody>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroCopy() {
|
||||
const { theme } = useCore();
|
||||
const Subtitle = styled(EuiText)`
|
||||
color: ${theme.eui.euiColorDarkShade};
|
||||
`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="l">
|
||||
<h1>Add Your Data</h1>
|
||||
</EuiTitle>
|
||||
<Subtitle>Some creative copy about packages goes here.</Subtitle>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroImage() {
|
||||
const { toAssets } = useLinks();
|
||||
const FlexGroup = styled(EuiFlexGroup)`
|
||||
margin-bottom: -2px; // puts image directly on EuiHorizontalRule
|
||||
`;
|
||||
return (
|
||||
<FlexGroup gutterSize="none" justifyContent="flexEnd">
|
||||
<EuiImage
|
||||
alt="Illustration of computer"
|
||||
url={toAssets('illustration_kibana_getting_started@2x.png')}
|
||||
/>
|
||||
</FlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { CategorySummaryList, PackageList } from '../../../common/types';
|
||||
import { getCategories, getPackages } from '../../data';
|
||||
import { LocalSearch, fieldsToSearch, searchIdField } from './search_packages';
|
||||
|
||||
export function useCategories() {
|
||||
const [categories, setCategories] = useState<CategorySummaryList>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getCategories().then(setCategories);
|
||||
}, []);
|
||||
|
||||
return [categories, setCategories] as [typeof categories, typeof setCategories];
|
||||
}
|
||||
|
||||
export function useCategoryPackages(selectedCategory: string) {
|
||||
const [categoryPackages, setCategoryPackages] = useState<PackageList>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getPackages({ category: selectedCategory }).then(setCategoryPackages);
|
||||
}, [selectedCategory]);
|
||||
|
||||
return [categoryPackages, setCategoryPackages] as [
|
||||
typeof categoryPackages,
|
||||
typeof setCategoryPackages
|
||||
];
|
||||
}
|
||||
|
||||
export function useAllPackages(selectedCategory: string, categoryPackages: PackageList) {
|
||||
const [allPackages, setAllPackages] = useState<PackageList>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedCategory) setAllPackages(categoryPackages);
|
||||
}, [selectedCategory, categoryPackages]);
|
||||
|
||||
return [allPackages, setAllPackages] as [typeof allPackages, typeof setAllPackages];
|
||||
}
|
||||
|
||||
export function useLocalSearch(allPackages: PackageList) {
|
||||
const localSearchRef = useRef<LocalSearch | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!allPackages.length) return;
|
||||
|
||||
const localSearch = new LocalSearch(searchIdField);
|
||||
fieldsToSearch.forEach(field => localSearch.addIndex(field));
|
||||
localSearch.addDocuments(allPackages);
|
||||
localSearchRef.current = localSearch;
|
||||
}, [allPackages]);
|
||||
|
||||
return localSearchRef;
|
||||
}
|
||||
|
||||
export function useInstalledPackages(allPackages: PackageList) {
|
||||
const [installedPackages, setInstalledPackages] = useState<PackageList>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setInstalledPackages(allPackages.filter(({ status }) => status === 'installed'));
|
||||
}, [allPackages]);
|
||||
|
||||
return [installedPackages, setInstalledPackages] as [
|
||||
typeof installedPackages,
|
||||
typeof setInstalledPackages
|
||||
];
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { EuiHorizontalRule, EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { PLUGIN } from '../../../common/constants';
|
||||
import { CategorySummaryItem, PackageList } from '../../../common/types';
|
||||
import { PackageListGrid } from '../../components/package_list_grid';
|
||||
import { useBreadcrumbs, useLinks } from '../../hooks';
|
||||
import { CategoryFacets } from './category_facets';
|
||||
import { Header } from './header';
|
||||
import {
|
||||
useCategories,
|
||||
useCategoryPackages,
|
||||
useAllPackages,
|
||||
useLocalSearch,
|
||||
useInstalledPackages,
|
||||
} from './hooks';
|
||||
import { SearchPackages } from './search_packages';
|
||||
|
||||
export const FullBleedPage = styled(EuiPage)`
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export function Home() {
|
||||
const maxContentWidth = 1200;
|
||||
const { toListView } = useLinks();
|
||||
useBreadcrumbs([{ text: PLUGIN.TITLE, href: toListView() }]);
|
||||
|
||||
const state = useHomeState();
|
||||
const body = state.searchTerm ? (
|
||||
<SearchPackages
|
||||
searchTerm={state.searchTerm}
|
||||
localSearchRef={state.localSearchRef}
|
||||
allPackages={state.allPackages}
|
||||
/>
|
||||
) : (
|
||||
<Fragment>
|
||||
{state.installedPackages.length ? (
|
||||
<Fragment>
|
||||
<InstalledPackages list={state.installedPackages} />
|
||||
<EuiHorizontalRule margin="xxl" />
|
||||
</Fragment>
|
||||
) : null}
|
||||
<AvailablePackages {...state} />
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Header restrictWidth={maxContentWidth} onSearch={state.setSearchTerm} />
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<FullBleedPage>
|
||||
<EuiPageBody restrictWidth={maxContentWidth}>{body}</EuiPageBody>
|
||||
</FullBleedPage>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
type HomeState = ReturnType<typeof useHomeState>;
|
||||
|
||||
export function useHomeState() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('');
|
||||
const [categories, setCategories] = useCategories();
|
||||
const [categoryPackages, setCategoryPackages] = useCategoryPackages(selectedCategory);
|
||||
const [allPackages, setAllPackages] = useAllPackages(selectedCategory, categoryPackages);
|
||||
const localSearchRef = useLocalSearch(allPackages);
|
||||
const [installedPackages, setInstalledPackages] = useInstalledPackages(allPackages);
|
||||
|
||||
return {
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
selectedCategory,
|
||||
setSelectedCategory,
|
||||
categories,
|
||||
setCategories,
|
||||
allPackages,
|
||||
setAllPackages,
|
||||
installedPackages,
|
||||
localSearchRef,
|
||||
setInstalledPackages,
|
||||
categoryPackages,
|
||||
setCategoryPackages,
|
||||
};
|
||||
}
|
||||
|
||||
function InstalledPackages({ list }: { list: PackageList }) {
|
||||
const title = 'Your Packages';
|
||||
|
||||
return <PackageListGrid title={title} list={list} />;
|
||||
}
|
||||
|
||||
function AvailablePackages({
|
||||
allPackages,
|
||||
categories,
|
||||
categoryPackages,
|
||||
selectedCategory,
|
||||
setSelectedCategory,
|
||||
}: HomeState) {
|
||||
const title = 'Available Packages';
|
||||
const noFilter = {
|
||||
id: '',
|
||||
title: 'All',
|
||||
count: allPackages.length,
|
||||
};
|
||||
|
||||
const controls = (
|
||||
<CategoryFacets
|
||||
categories={[noFilter, ...categories]}
|
||||
selectedCategory={selectedCategory}
|
||||
onCategoryChange={({ id }: CategorySummaryItem) => setSelectedCategory(id)}
|
||||
/>
|
||||
);
|
||||
|
||||
return <PackageListGrid title={title} controls={controls} list={categoryPackages} />;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Search as LocalSearch } from 'js-search';
|
||||
import { PackageList, PackageListItem } from '../../../common/types';
|
||||
import { SearchResults } from './search_results';
|
||||
|
||||
export { LocalSearch };
|
||||
export type SearchField = keyof PackageListItem;
|
||||
export const searchIdField: SearchField = 'name';
|
||||
export const fieldsToSearch: SearchField[] = ['description', 'name', 'title'];
|
||||
|
||||
interface SearchPackagesProps {
|
||||
searchTerm: string;
|
||||
localSearchRef: React.MutableRefObject<LocalSearch | null>;
|
||||
allPackages: PackageList;
|
||||
}
|
||||
|
||||
export function SearchPackages({ searchTerm, localSearchRef, allPackages }: SearchPackagesProps) {
|
||||
// this means the search index hasn't been built yet.
|
||||
// i.e. the intial fetch of all packages hasn't finished
|
||||
if (!localSearchRef.current) return <div>Still fetching matches. Try again in a moment.</div>;
|
||||
|
||||
const matches = localSearchRef.current.search(searchTerm) as PackageList;
|
||||
const matchingIds = matches.map(match => match[searchIdField]);
|
||||
const filtered = allPackages.filter(item => matchingIds.includes(item[searchIdField]));
|
||||
|
||||
return <SearchResults term={searchTerm} results={filtered} />;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { PackageList } from '../../../common/types';
|
||||
import { PackageListGrid } from '../../components/package_list_grid';
|
||||
|
||||
interface SearchResultsProps {
|
||||
term: string;
|
||||
results: PackageList;
|
||||
}
|
||||
|
||||
export function SearchResults({ term, results }: SearchResultsProps) {
|
||||
const title = 'Search results';
|
||||
return (
|
||||
<PackageListGrid
|
||||
title={title}
|
||||
list={results}
|
||||
showInstalledBadge={true}
|
||||
controls={
|
||||
<EuiTitle>
|
||||
<EuiText>
|
||||
{results.length} results for "{term}"
|
||||
</EuiText>
|
||||
</EuiTitle>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
55
x-pack/legacy/plugins/integrations_manager/server/config.ts
Normal file
55
x-pack/legacy/plugins/integrations_manager/server/config.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import JoiNamespace from 'joi';
|
||||
|
||||
export const config = {
|
||||
schema: schema.object({
|
||||
enabled: schema.boolean(),
|
||||
registryUrl: schema.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
export type EPMConfigSchema = TypeOf<typeof config.schema>;
|
||||
|
||||
// This is needed for the legacy plugin / NP shim setup in ../index
|
||||
// It has been moved here to keep the two config schemas as close together as possible.
|
||||
// Once we've moved to NP, the Joi version will disappear, so we're not trying to generate
|
||||
// these two config schemas from a common definition or from each other.
|
||||
|
||||
export const getConfigSchema = (Joi: typeof JoiNamespace) => {
|
||||
const EPMConfigSchema = Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
registryUrl: Joi.string()
|
||||
.uri()
|
||||
.default(),
|
||||
}).default();
|
||||
|
||||
return EPMConfigSchema;
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
enabled: true,
|
||||
registryUrl: 'http://package-registry.app.elstc.co',
|
||||
};
|
||||
|
||||
// As of 2019, this is a Singleton because of the way JavaScript modules are specified.
|
||||
// Every module that imports this file will have access to the same object.
|
||||
|
||||
// This is meant to be only updated from the config$ Observable's subscription
|
||||
// (see the Plugin class constructor in server/plugin.ts) but this is not enforced.
|
||||
|
||||
let _config: EPMConfigSchema = DEFAULT_CONFIG;
|
||||
|
||||
export const epmConfigStore = {
|
||||
updateConfig(newConfig: EPMConfigSchema) {
|
||||
_config = Object.assign({}, _config, newConfig);
|
||||
},
|
||||
getConfig() {
|
||||
return _config;
|
||||
},
|
||||
};
|
36
x-pack/legacy/plugins/integrations_manager/server/feature.ts
Normal file
36
x-pack/legacy/plugins/integrations_manager/server/feature.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { PLUGIN } from '../common/constants';
|
||||
import { Feature } from '../../../../plugins/features/server';
|
||||
|
||||
export const feature: Feature = {
|
||||
id: PLUGIN.ID,
|
||||
name: PLUGIN.TITLE,
|
||||
icon: PLUGIN.ICON,
|
||||
navLinkId: PLUGIN.ID,
|
||||
app: [PLUGIN.ID, 'kibana'],
|
||||
catalogue: [PLUGIN.ID],
|
||||
privileges: {
|
||||
all: {
|
||||
api: [PLUGIN.ID],
|
||||
catalogue: [PLUGIN.ID],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['show', 'save'],
|
||||
},
|
||||
read: {
|
||||
api: [PLUGIN.ID],
|
||||
catalogue: [PLUGIN.ID],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
};
|
13
x-pack/legacy/plugins/integrations_manager/server/index.ts
Normal file
13
x-pack/legacy/plugins/integrations_manager/server/index.ts
Normal file
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { Plugin, EPMPluginInitializerContext } from './plugin';
|
||||
|
||||
// Kibana NP needs config to be exported from here, see https://github.com/elastic/kibana/pull/45299/files#r323254805
|
||||
export { config } from './config';
|
||||
|
||||
export function plugin(initializerContext: EPMPluginInitializerContext) {
|
||||
return new Plugin(initializerContext);
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { rewriteIngestPipeline } from './ingest_pipelines';
|
||||
import { readFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
test('a json-format pipeline with pipeline references is correctly rewritten', () => {
|
||||
const inputStandard = readFileSync(
|
||||
path.join(__dirname, '/tests/ingest_pipelines/real_input_standard.json'),
|
||||
'utf-8'
|
||||
);
|
||||
const inputBeats = readFileSync(
|
||||
path.join(__dirname, '/tests/ingest_pipelines/real_input_beats.json'),
|
||||
'utf-8'
|
||||
);
|
||||
const output = readFileSync(
|
||||
path.join(__dirname, '/tests/ingest_pipelines/real_output.json')
|
||||
).toString('utf-8');
|
||||
|
||||
const substitutions = [
|
||||
{
|
||||
source: 'pipeline-json',
|
||||
target: 'new-pipeline-json',
|
||||
templateFunction: 'IngestPipeline',
|
||||
},
|
||||
{
|
||||
source: 'pipeline-plaintext',
|
||||
target: 'new-pipeline-plaintext',
|
||||
templateFunction: 'IngestPipeline',
|
||||
},
|
||||
];
|
||||
expect(rewriteIngestPipeline(inputStandard, substitutions)).toBe(output);
|
||||
expect(rewriteIngestPipeline(inputBeats, substitutions)).toBe(output);
|
||||
});
|
||||
|
||||
test('a yml-format pipeline with pipeline references is correctly rewritten', () => {
|
||||
const inputStandard = readFileSync(
|
||||
path.join(__dirname, '/tests/ingest_pipelines/real_input_standard.yml')
|
||||
).toString('utf-8');
|
||||
const inputBeats = readFileSync(
|
||||
path.join(__dirname, '/tests/ingest_pipelines/real_input_beats.yml')
|
||||
).toString('utf-8');
|
||||
const output = readFileSync(
|
||||
path.join(__dirname, '/tests/ingest_pipelines/real_output.yml')
|
||||
).toString('utf-8');
|
||||
|
||||
const substitutions = [
|
||||
{
|
||||
source: 'pipeline-json',
|
||||
target: 'new-pipeline-json',
|
||||
templateFunction: 'IngestPipeline',
|
||||
},
|
||||
{
|
||||
source: 'pipeline-plaintext',
|
||||
target: 'new-pipeline-plaintext',
|
||||
templateFunction: 'IngestPipeline',
|
||||
},
|
||||
];
|
||||
expect(rewriteIngestPipeline(inputStandard, substitutions)).toBe(output);
|
||||
expect(rewriteIngestPipeline(inputBeats, substitutions)).toBe(output);
|
||||
});
|
||||
|
||||
test('a json-format pipeline with no pipeline references stays unchanged', () => {
|
||||
const input = readFileSync(
|
||||
path.join(__dirname, '/tests/ingest_pipelines/no_replacement.json')
|
||||
).toString('utf-8');
|
||||
|
||||
const substitutions = [
|
||||
{
|
||||
source: 'pipeline-json',
|
||||
target: 'new-pipeline-json',
|
||||
templateFunction: 'IngestPipeline',
|
||||
},
|
||||
{
|
||||
source: 'pipeline-plaintext',
|
||||
target: 'new-pipeline-plaintext',
|
||||
templateFunction: 'IngestPipeline',
|
||||
},
|
||||
];
|
||||
expect(rewriteIngestPipeline(input, substitutions)).toBe(input);
|
||||
});
|
||||
|
||||
test('a yml-format pipeline with no pipeline references stays unchanged', () => {
|
||||
const input = readFileSync(
|
||||
path.join(__dirname, '/tests/ingest_pipelines/no_replacement.yml')
|
||||
).toString('utf-8');
|
||||
|
||||
const substitutions = [
|
||||
{
|
||||
source: 'pipeline-json',
|
||||
target: 'new-pipeline-json',
|
||||
templateFunction: 'IngestPipeline',
|
||||
},
|
||||
{
|
||||
source: 'pipeline-plaintext',
|
||||
target: 'new-pipeline-plaintext',
|
||||
templateFunction: 'IngestPipeline',
|
||||
},
|
||||
];
|
||||
expect(rewriteIngestPipeline(input, substitutions)).toBe(input);
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export function rewriteIngestPipeline(
|
||||
pipeline: string,
|
||||
substitutions: Array<{
|
||||
source: string;
|
||||
target: string;
|
||||
templateFunction: string;
|
||||
}>
|
||||
): string {
|
||||
substitutions.forEach(sub => {
|
||||
const { source, target, templateFunction } = sub;
|
||||
// This fakes the use of the golang text/template expression {{SomeTemplateFunction 'some-param'}}
|
||||
// cf. https://github.com/elastic/beats/blob/master/filebeat/fileset/fileset.go#L294
|
||||
|
||||
// "Standard style" uses '{{' and '}}' as delimiters
|
||||
const matchStandardStyle = `{{\\s?${templateFunction}\\s+['"]${source}['"]\\s?}}`;
|
||||
// "Beats style" uses '{<' and '>}' as delimiters because this is current practice in the beats project
|
||||
const matchBeatsStyle = `{<\\s?${templateFunction}\\s+['"]${source}['"]\\s?>}`;
|
||||
|
||||
const regexStandardStyle = new RegExp(matchStandardStyle);
|
||||
const regexBeatsStyle = new RegExp(matchBeatsStyle);
|
||||
pipeline = pipeline.replace(regexStandardStyle, target).replace(regexBeatsStyle, target);
|
||||
});
|
||||
return pipeline;
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getTemplate } from './template';
|
||||
|
||||
test('get template', () => {
|
||||
const pattern = 'logs-nginx-access-abcd-*';
|
||||
const template = getTemplate(pattern);
|
||||
expect(template.index_patterns).toStrictEqual([pattern]);
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* getTemplate retrieves the default template but overwrites the index pattern with the given value.
|
||||
*
|
||||
* @param indexPattern String with the index pattern
|
||||
*/
|
||||
export function getTemplate(indexPattern: string): Template {
|
||||
const template = getBaseTemplate();
|
||||
template.index_patterns = [indexPattern];
|
||||
return template;
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
order: number;
|
||||
index_patterns: string[];
|
||||
settings: object;
|
||||
mappings: object;
|
||||
aliases: object;
|
||||
}
|
||||
|
||||
function getBaseTemplate(): Template {
|
||||
return {
|
||||
// We need to decide which order we use for the templates
|
||||
order: 1,
|
||||
// To be completed with the correct index patterns
|
||||
index_patterns: [],
|
||||
settings: {
|
||||
index: {
|
||||
// ILM Policy must be added here
|
||||
lifecycle: {
|
||||
name: 'logs-default',
|
||||
rollover_alias: 'logs-nginx-access-abcd',
|
||||
},
|
||||
// What should be our default for the compression?
|
||||
codec: 'best_compression',
|
||||
// W
|
||||
mapping: {
|
||||
total_fields: {
|
||||
limit: '10000',
|
||||
},
|
||||
},
|
||||
// This is the default from Beats? So far seems to be a good value
|
||||
refresh_interval: '5s',
|
||||
// Default in the stack now, still good to have it in
|
||||
number_of_shards: '1',
|
||||
// All the default fields which should be queried have to be added here.
|
||||
// So far we add all keyword and text fields here.
|
||||
query: {
|
||||
default_field: ['message'],
|
||||
},
|
||||
// We are setting 30 because it can be devided by several numbers. Useful when shrinking.
|
||||
number_of_routing_shards: '30',
|
||||
},
|
||||
},
|
||||
mappings: {
|
||||
// To be filled with interesting information about this specific index
|
||||
_meta: {
|
||||
package: 'foo',
|
||||
},
|
||||
// All the dynamic field mappings
|
||||
dynamic_templates: [
|
||||
// This makes sure all mappings are keywords by default
|
||||
{
|
||||
strings_as_keyword: {
|
||||
mapping: {
|
||||
ignore_above: 1024,
|
||||
type: 'keyword',
|
||||
},
|
||||
match_mapping_type: 'string',
|
||||
},
|
||||
},
|
||||
// Example of a dynamic template
|
||||
{
|
||||
labels: {
|
||||
path_match: 'labels.*',
|
||||
mapping: {
|
||||
type: 'keyword',
|
||||
},
|
||||
match_mapping_type: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
// As we define fields ahead, we don't need any automatic field detection
|
||||
// This makes sure all the fields are mapped to keyword by default to prevent mapping conflicts
|
||||
date_detection: false,
|
||||
// All the properties we know from the fields.yml file
|
||||
properties: {
|
||||
container: {
|
||||
properties: {
|
||||
image: {
|
||||
properties: {
|
||||
name: {
|
||||
ignore_above: 1024,
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// To be filled with the aliases that we need
|
||||
aliases: {},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
{
|
||||
"description": "Pipeline for normalizing Kubernetes coredns logs",
|
||||
"processors": [
|
||||
{
|
||||
"pipeline": {
|
||||
"if": "ctx.message.charAt(0) == (char)(\"{\")",
|
||||
"name": "{{IngestPipeline 'pipeline-json' }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pipeline": {
|
||||
"if": "ctx.message.charAt(0) != (char)(\"{\")",
|
||||
"name": "{{IngestPipeline 'pipeline-plaintext' }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "ctx.event.created = ctx['@timestamp']; ctx['@timestamp'] = ctx['timestamp']; ctx.remove('timestamp');",
|
||||
"ignore_failure" : true
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "ctx['source'] = new HashMap(); if (ctx.temp.source.charAt(0) == (char)(\"[\")) { def p = ctx.temp.source.indexOf (']'); def l = ctx.temp.source.length(); ctx.source.address = ctx.temp.source.substring(1, p); ctx.source.port = ctx.temp.source.substring(p+2, l);} else { def p = ctx.temp.source.indexOf (':'); def l = ctx.temp.source.length(); ctx.source.address = ctx.temp.source.substring(0, p); ctx.source.port = ctx.temp.source.substring(p+1, l);} ctx.remove('temp');",
|
||||
"if": "ctx.temp?.source != null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"set": {
|
||||
"field": "source.ip",
|
||||
"value": "{{source.address}}",
|
||||
"if": "ctx.source?.address != null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "source.port",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.duration",
|
||||
"type": "double"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.query.size",
|
||||
"type": "long"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.response.size",
|
||||
"type": "long"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.dnssec_ok",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"uppercase": {
|
||||
"field": "coredns.response.flags"
|
||||
}
|
||||
},
|
||||
{
|
||||
"split": {
|
||||
"field": "coredns.response.flags",
|
||||
"separator": ","
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "ctx.event.duration = Math.round(ctx.coredns.duration * params.scale)",
|
||||
"params": {
|
||||
"scale": 1000000000
|
||||
},
|
||||
"if": "ctx.coredns?.duration != null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"remove": {
|
||||
"field": "coredns.duration",
|
||||
"ignore_missing": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"on_failure" : [{
|
||||
"set" : {
|
||||
"field" : "error.message",
|
||||
"value" : "{{ _ingest.on_failure_message }}"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"description": "Pipeline for dissecting message field in JSON logs",
|
||||
"processors": [
|
||||
{
|
||||
"json" : {
|
||||
"field" : "message",
|
||||
"target_field" : "json"
|
||||
}
|
||||
},
|
||||
{
|
||||
"dissect": {
|
||||
"field": "json.message",
|
||||
"pattern": "%{timestamp} [%{log.level}] %{temp.source} - %{coredns.id} \"%{coredns.query.type} %{coredns.query.class} %{coredns.query.name} %{network.transport} %{coredns.query.size} %{coredns.dnssec_ok} %{bufsize}\" %{coredns.response.code} %{coredns.response.flags} %{coredns.response.size} %{coredns.duration}s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"remove": {
|
||||
"field": ["message"],
|
||||
"ignore_failure" : true
|
||||
}
|
||||
},
|
||||
{
|
||||
"rename": {
|
||||
"field": "json.message",
|
||||
"target_field": "message",
|
||||
"ignore_failure" : true
|
||||
}
|
||||
},
|
||||
{
|
||||
"rename": {
|
||||
"field": "json.kubernetes",
|
||||
"target_field": "kubernetes",
|
||||
"ignore_failure" : true
|
||||
}
|
||||
},
|
||||
{
|
||||
"remove": {
|
||||
"field": ["json", "bufsize"],
|
||||
"ignore_failure" : true
|
||||
}
|
||||
}
|
||||
],
|
||||
"on_failure" : [{
|
||||
"set" : {
|
||||
"field" : "error.message",
|
||||
"value" : "{{ _ingest.on_failure_message }}"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
description: Pipeline for Cisco IOS logs.
|
||||
|
||||
processors:
|
||||
# IP Geolocation Lookup
|
||||
- geoip:
|
||||
field: source.ip
|
||||
target_field: source.geo
|
||||
ignore_missing: true
|
||||
- geoip:
|
||||
field: destination.ip
|
||||
target_field: destination.geo
|
||||
ignore_missing: true
|
||||
|
||||
# IP Autonomous System (AS) Lookup
|
||||
- geoip:
|
||||
database_file: GeoLite2-ASN.mmdb
|
||||
field: source.ip
|
||||
target_field: source.as
|
||||
properties:
|
||||
- asn
|
||||
- organization_name
|
||||
ignore_missing: true
|
||||
- geoip:
|
||||
database_file: GeoLite2-ASN.mmdb
|
||||
field: destination.ip
|
||||
target_field: destination.as
|
||||
properties:
|
||||
- asn
|
||||
- organization_name
|
||||
ignore_missing: true
|
||||
- rename:
|
||||
field: source.as.asn
|
||||
target_field: source.as.number
|
||||
ignore_missing: true
|
||||
- rename:
|
||||
field: source.as.organization_name
|
||||
target_field: source.as.organization.name
|
||||
ignore_missing: true
|
||||
- rename:
|
||||
field: destination.as.asn
|
||||
target_field: destination.as.number
|
||||
ignore_missing: true
|
||||
- rename:
|
||||
field: destination.as.organization_name
|
||||
target_field: destination.as.organization.name
|
||||
ignore_missing: true
|
||||
|
||||
on_failure:
|
||||
- set:
|
||||
field: error.message
|
||||
value: '{{ _ingest.on_failure_message }}'
|
|
@ -0,0 +1,101 @@
|
|||
{
|
||||
"description": "Pipeline for normalizing Kubernetes coredns logs",
|
||||
"processors": [
|
||||
{
|
||||
"pipeline": {
|
||||
"if": "ctx.message.charAt(0) == (char)(\"{\")",
|
||||
"name": "{<IngestPipeline 'pipeline-json' >}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pipeline": {
|
||||
"if": "ctx.message.charAt(0) != (char)(\"{\")",
|
||||
"name": "{<IngestPipeline 'pipeline-plaintext' >}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "ctx.event.created = ctx['@timestamp']; ctx['@timestamp'] = ctx['timestamp']; ctx.remove('timestamp');",
|
||||
"ignore_failure" : true
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "ctx['source'] = new HashMap(); if (ctx.temp.source.charAt(0) == (char)(\"[\")) { def p = ctx.temp.source.indexOf (']'); def l = ctx.temp.source.length(); ctx.source.address = ctx.temp.source.substring(1, p); ctx.source.port = ctx.temp.source.substring(p+2, l);} else { def p = ctx.temp.source.indexOf (':'); def l = ctx.temp.source.length(); ctx.source.address = ctx.temp.source.substring(0, p); ctx.source.port = ctx.temp.source.substring(p+1, l);} ctx.remove('temp');",
|
||||
"if": "ctx.temp?.source != null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"set": {
|
||||
"field": "source.ip",
|
||||
"value": "{{source.address}}",
|
||||
"if": "ctx.source?.address != null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "source.port",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.duration",
|
||||
"type": "double"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.query.size",
|
||||
"type": "long"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.response.size",
|
||||
"type": "long"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.dnssec_ok",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"uppercase": {
|
||||
"field": "coredns.response.flags"
|
||||
}
|
||||
},
|
||||
{
|
||||
"split": {
|
||||
"field": "coredns.response.flags",
|
||||
"separator": ","
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "ctx.event.duration = Math.round(ctx.coredns.duration * params.scale)",
|
||||
"params": {
|
||||
"scale": 1000000000
|
||||
},
|
||||
"if": "ctx.coredns?.duration != null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"remove": {
|
||||
"field": "coredns.duration",
|
||||
"ignore_missing": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"on_failure" : [{
|
||||
"set" : {
|
||||
"field" : "error.message",
|
||||
"value" : "{{ _ingest.on_failure_message }}"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
description: Pipeline for normalizing Kubernetes CoreDNS logs.
|
||||
processors:
|
||||
- pipeline:
|
||||
if: ctx.message.charAt(0) == (char)("{")
|
||||
name: '{<IngestPipeline "pipeline-json" >}'
|
||||
- pipeline:
|
||||
if: ctx.message.charAt(0) != (char)("{")
|
||||
name: '{<IngestPipeline "pipeline-plaintext" >}'
|
||||
- script:
|
||||
lang: painless
|
||||
source: >
|
||||
ctx.event.created = ctx['@timestamp'];
|
||||
ctx['@timestamp'] = ctx['timestamp'];
|
||||
ctx.remove('timestamp');
|
||||
ignore_failure: true
|
||||
- script:
|
||||
lang: painless
|
||||
if: ctx.temp?.source != null
|
||||
source: >
|
||||
ctx['source'] = new HashMap();
|
||||
if (ctx.temp.source.charAt(0) == (char)("[")) {
|
||||
def p = ctx.temp.source.indexOf (']');
|
||||
def l = ctx.temp.source.length();
|
||||
ctx.source.address = ctx.temp.source.substring(1, p);
|
||||
ctx.source.port = ctx.temp.source.substring(p+2, l);
|
||||
} else {
|
||||
def p = ctx.temp.source.indexOf(':');
|
||||
def l = ctx.temp.source.length();
|
||||
ctx.source.address = ctx.temp.source.substring(0, p);
|
||||
ctx.source.port = ctx.temp.source.substring(p+1, l);
|
||||
}
|
||||
ctx.remove('temp');
|
||||
- set:
|
||||
field: source.ip
|
||||
value: "{{source.address}}"
|
||||
if: ctx.source?.address != null
|
||||
- convert:
|
||||
field: source.port
|
||||
type: integer
|
||||
- convert:
|
||||
field: coredns.duration
|
||||
type: double
|
||||
- convert:
|
||||
field: coredns.query.size
|
||||
type: long
|
||||
- convert:
|
||||
field: coredns.response.size
|
||||
type: long
|
||||
- convert:
|
||||
field: coredns.dnssec_ok
|
||||
type: boolean
|
||||
- uppercase:
|
||||
field: dns.header_flags
|
||||
- split:
|
||||
field: dns.header_flags
|
||||
separator: ","
|
||||
- append:
|
||||
if: ctx.coredns?.dnssec_ok
|
||||
field: dns.header_flags
|
||||
value: DO
|
||||
- script:
|
||||
lang: painless
|
||||
source: ctx.event.duration = Math.round(ctx.coredns.duration * params.scale);
|
||||
params:
|
||||
scale: 1000000000
|
||||
if: ctx.coredns?.duration != null
|
||||
- remove:
|
||||
field:
|
||||
- coredns.duration
|
||||
ignore_missing: true
|
||||
# The following copies values from dns namespace (ECS) to the coredns
|
||||
# namespace to avoid introducing breaking change. This should be removed
|
||||
# for 8.0.0. Additionally coredns.dnssec_ok can be removed.
|
||||
- set:
|
||||
if: ctx.dns?.id != null
|
||||
field: coredns.id
|
||||
value: '{{dns.id}}'
|
||||
- set:
|
||||
if: ctx.dns?.question?.class != null
|
||||
field: coredns.query.class
|
||||
value: '{{dns.question.class}}'
|
||||
- set:
|
||||
if: ctx.dns?.question?.name != null
|
||||
field: coredns.query.name
|
||||
value: '{{dns.question.name}}'
|
||||
- set:
|
||||
if: ctx.dns?.question?.type != null
|
||||
field: coredns.query.type
|
||||
value: '{{dns.question.type}}'
|
||||
- set:
|
||||
if: ctx.dns?.response_code != null
|
||||
field: coredns.response.code
|
||||
value: '{{dns.response_code}}'
|
||||
- script:
|
||||
if: ctx.dns?.header_flags != null
|
||||
lang: painless
|
||||
source: >
|
||||
ctx.coredns.response.flags = ctx.dns.header_flags;
|
||||
# Right trim the trailing dot from domain names.
|
||||
- script:
|
||||
if: ctx.dns?.question?.name != null
|
||||
lang: painless
|
||||
source: >
|
||||
def q = ctx.dns.question.name;
|
||||
def end = q.length() - 1;
|
||||
if (q.charAt(end) == (char) '.') {
|
||||
ctx.dns.question.name = q.substring(0, end);
|
||||
}
|
||||
on_failure:
|
||||
- set:
|
||||
field: error.message
|
||||
value: "{{ _ingest.on_failure_message }}"
|
|
@ -0,0 +1,101 @@
|
|||
{
|
||||
"description": "Pipeline for normalizing Kubernetes coredns logs",
|
||||
"processors": [
|
||||
{
|
||||
"pipeline": {
|
||||
"if": "ctx.message.charAt(0) == (char)(\"{\")",
|
||||
"name": "{{IngestPipeline 'pipeline-json' }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pipeline": {
|
||||
"if": "ctx.message.charAt(0) != (char)(\"{\")",
|
||||
"name": "{{IngestPipeline 'pipeline-plaintext' }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "ctx.event.created = ctx['@timestamp']; ctx['@timestamp'] = ctx['timestamp']; ctx.remove('timestamp');",
|
||||
"ignore_failure" : true
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "ctx['source'] = new HashMap(); if (ctx.temp.source.charAt(0) == (char)(\"[\")) { def p = ctx.temp.source.indexOf (']'); def l = ctx.temp.source.length(); ctx.source.address = ctx.temp.source.substring(1, p); ctx.source.port = ctx.temp.source.substring(p+2, l);} else { def p = ctx.temp.source.indexOf (':'); def l = ctx.temp.source.length(); ctx.source.address = ctx.temp.source.substring(0, p); ctx.source.port = ctx.temp.source.substring(p+1, l);} ctx.remove('temp');",
|
||||
"if": "ctx.temp?.source != null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"set": {
|
||||
"field": "source.ip",
|
||||
"value": "{{source.address}}",
|
||||
"if": "ctx.source?.address != null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "source.port",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.duration",
|
||||
"type": "double"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.query.size",
|
||||
"type": "long"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.response.size",
|
||||
"type": "long"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.dnssec_ok",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"uppercase": {
|
||||
"field": "coredns.response.flags"
|
||||
}
|
||||
},
|
||||
{
|
||||
"split": {
|
||||
"field": "coredns.response.flags",
|
||||
"separator": ","
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "ctx.event.duration = Math.round(ctx.coredns.duration * params.scale)",
|
||||
"params": {
|
||||
"scale": 1000000000
|
||||
},
|
||||
"if": "ctx.coredns?.duration != null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"remove": {
|
||||
"field": "coredns.duration",
|
||||
"ignore_missing": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"on_failure" : [{
|
||||
"set" : {
|
||||
"field" : "error.message",
|
||||
"value" : "{{ _ingest.on_failure_message }}"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
description: Pipeline for normalizing Kubernetes CoreDNS logs.
|
||||
processors:
|
||||
- pipeline:
|
||||
if: ctx.message.charAt(0) == (char)("{")
|
||||
name: '{{IngestPipeline "pipeline-json" }}'
|
||||
- pipeline:
|
||||
if: ctx.message.charAt(0) != (char)("{")
|
||||
name: '{{IngestPipeline "pipeline-plaintext" }}'
|
||||
- script:
|
||||
lang: painless
|
||||
source: >
|
||||
ctx.event.created = ctx['@timestamp'];
|
||||
ctx['@timestamp'] = ctx['timestamp'];
|
||||
ctx.remove('timestamp');
|
||||
ignore_failure: true
|
||||
- script:
|
||||
lang: painless
|
||||
if: ctx.temp?.source != null
|
||||
source: >
|
||||
ctx['source'] = new HashMap();
|
||||
if (ctx.temp.source.charAt(0) == (char)("[")) {
|
||||
def p = ctx.temp.source.indexOf (']');
|
||||
def l = ctx.temp.source.length();
|
||||
ctx.source.address = ctx.temp.source.substring(1, p);
|
||||
ctx.source.port = ctx.temp.source.substring(p+2, l);
|
||||
} else {
|
||||
def p = ctx.temp.source.indexOf(':');
|
||||
def l = ctx.temp.source.length();
|
||||
ctx.source.address = ctx.temp.source.substring(0, p);
|
||||
ctx.source.port = ctx.temp.source.substring(p+1, l);
|
||||
}
|
||||
ctx.remove('temp');
|
||||
- set:
|
||||
field: source.ip
|
||||
value: "{{source.address}}"
|
||||
if: ctx.source?.address != null
|
||||
- convert:
|
||||
field: source.port
|
||||
type: integer
|
||||
- convert:
|
||||
field: coredns.duration
|
||||
type: double
|
||||
- convert:
|
||||
field: coredns.query.size
|
||||
type: long
|
||||
- convert:
|
||||
field: coredns.response.size
|
||||
type: long
|
||||
- convert:
|
||||
field: coredns.dnssec_ok
|
||||
type: boolean
|
||||
- uppercase:
|
||||
field: dns.header_flags
|
||||
- split:
|
||||
field: dns.header_flags
|
||||
separator: ","
|
||||
- append:
|
||||
if: ctx.coredns?.dnssec_ok
|
||||
field: dns.header_flags
|
||||
value: DO
|
||||
- script:
|
||||
lang: painless
|
||||
source: ctx.event.duration = Math.round(ctx.coredns.duration * params.scale);
|
||||
params:
|
||||
scale: 1000000000
|
||||
if: ctx.coredns?.duration != null
|
||||
- remove:
|
||||
field:
|
||||
- coredns.duration
|
||||
ignore_missing: true
|
||||
# The following copies values from dns namespace (ECS) to the coredns
|
||||
# namespace to avoid introducing breaking change. This should be removed
|
||||
# for 8.0.0. Additionally coredns.dnssec_ok can be removed.
|
||||
- set:
|
||||
if: ctx.dns?.id != null
|
||||
field: coredns.id
|
||||
value: '{{dns.id}}'
|
||||
- set:
|
||||
if: ctx.dns?.question?.class != null
|
||||
field: coredns.query.class
|
||||
value: '{{dns.question.class}}'
|
||||
- set:
|
||||
if: ctx.dns?.question?.name != null
|
||||
field: coredns.query.name
|
||||
value: '{{dns.question.name}}'
|
||||
- set:
|
||||
if: ctx.dns?.question?.type != null
|
||||
field: coredns.query.type
|
||||
value: '{{dns.question.type}}'
|
||||
- set:
|
||||
if: ctx.dns?.response_code != null
|
||||
field: coredns.response.code
|
||||
value: '{{dns.response_code}}'
|
||||
- script:
|
||||
if: ctx.dns?.header_flags != null
|
||||
lang: painless
|
||||
source: >
|
||||
ctx.coredns.response.flags = ctx.dns.header_flags;
|
||||
# Right trim the trailing dot from domain names.
|
||||
- script:
|
||||
if: ctx.dns?.question?.name != null
|
||||
lang: painless
|
||||
source: >
|
||||
def q = ctx.dns.question.name;
|
||||
def end = q.length() - 1;
|
||||
if (q.charAt(end) == (char) '.') {
|
||||
ctx.dns.question.name = q.substring(0, end);
|
||||
}
|
||||
on_failure:
|
||||
- set:
|
||||
field: error.message
|
||||
value: "{{ _ingest.on_failure_message }}"
|
|
@ -0,0 +1,101 @@
|
|||
{
|
||||
"description": "Pipeline for normalizing Kubernetes coredns logs",
|
||||
"processors": [
|
||||
{
|
||||
"pipeline": {
|
||||
"if": "ctx.message.charAt(0) == (char)(\"{\")",
|
||||
"name": "new-pipeline-json"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pipeline": {
|
||||
"if": "ctx.message.charAt(0) != (char)(\"{\")",
|
||||
"name": "new-pipeline-plaintext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "ctx.event.created = ctx['@timestamp']; ctx['@timestamp'] = ctx['timestamp']; ctx.remove('timestamp');",
|
||||
"ignore_failure" : true
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "ctx['source'] = new HashMap(); if (ctx.temp.source.charAt(0) == (char)(\"[\")) { def p = ctx.temp.source.indexOf (']'); def l = ctx.temp.source.length(); ctx.source.address = ctx.temp.source.substring(1, p); ctx.source.port = ctx.temp.source.substring(p+2, l);} else { def p = ctx.temp.source.indexOf (':'); def l = ctx.temp.source.length(); ctx.source.address = ctx.temp.source.substring(0, p); ctx.source.port = ctx.temp.source.substring(p+1, l);} ctx.remove('temp');",
|
||||
"if": "ctx.temp?.source != null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"set": {
|
||||
"field": "source.ip",
|
||||
"value": "{{source.address}}",
|
||||
"if": "ctx.source?.address != null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "source.port",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.duration",
|
||||
"type": "double"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.query.size",
|
||||
"type": "long"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.response.size",
|
||||
"type": "long"
|
||||
}
|
||||
},
|
||||
{
|
||||
"convert" : {
|
||||
"field" : "coredns.dnssec_ok",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"uppercase": {
|
||||
"field": "coredns.response.flags"
|
||||
}
|
||||
},
|
||||
{
|
||||
"split": {
|
||||
"field": "coredns.response.flags",
|
||||
"separator": ","
|
||||
}
|
||||
},
|
||||
{
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "ctx.event.duration = Math.round(ctx.coredns.duration * params.scale)",
|
||||
"params": {
|
||||
"scale": 1000000000
|
||||
},
|
||||
"if": "ctx.coredns?.duration != null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"remove": {
|
||||
"field": "coredns.duration",
|
||||
"ignore_missing": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"on_failure" : [{
|
||||
"set" : {
|
||||
"field" : "error.message",
|
||||
"value" : "{{ _ingest.on_failure_message }}"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
description: Pipeline for normalizing Kubernetes CoreDNS logs.
|
||||
processors:
|
||||
- pipeline:
|
||||
if: ctx.message.charAt(0) == (char)("{")
|
||||
name: 'new-pipeline-json'
|
||||
- pipeline:
|
||||
if: ctx.message.charAt(0) != (char)("{")
|
||||
name: 'new-pipeline-plaintext'
|
||||
- script:
|
||||
lang: painless
|
||||
source: >
|
||||
ctx.event.created = ctx['@timestamp'];
|
||||
ctx['@timestamp'] = ctx['timestamp'];
|
||||
ctx.remove('timestamp');
|
||||
ignore_failure: true
|
||||
- script:
|
||||
lang: painless
|
||||
if: ctx.temp?.source != null
|
||||
source: >
|
||||
ctx['source'] = new HashMap();
|
||||
if (ctx.temp.source.charAt(0) == (char)("[")) {
|
||||
def p = ctx.temp.source.indexOf (']');
|
||||
def l = ctx.temp.source.length();
|
||||
ctx.source.address = ctx.temp.source.substring(1, p);
|
||||
ctx.source.port = ctx.temp.source.substring(p+2, l);
|
||||
} else {
|
||||
def p = ctx.temp.source.indexOf(':');
|
||||
def l = ctx.temp.source.length();
|
||||
ctx.source.address = ctx.temp.source.substring(0, p);
|
||||
ctx.source.port = ctx.temp.source.substring(p+1, l);
|
||||
}
|
||||
ctx.remove('temp');
|
||||
- set:
|
||||
field: source.ip
|
||||
value: "{{source.address}}"
|
||||
if: ctx.source?.address != null
|
||||
- convert:
|
||||
field: source.port
|
||||
type: integer
|
||||
- convert:
|
||||
field: coredns.duration
|
||||
type: double
|
||||
- convert:
|
||||
field: coredns.query.size
|
||||
type: long
|
||||
- convert:
|
||||
field: coredns.response.size
|
||||
type: long
|
||||
- convert:
|
||||
field: coredns.dnssec_ok
|
||||
type: boolean
|
||||
- uppercase:
|
||||
field: dns.header_flags
|
||||
- split:
|
||||
field: dns.header_flags
|
||||
separator: ","
|
||||
- append:
|
||||
if: ctx.coredns?.dnssec_ok
|
||||
field: dns.header_flags
|
||||
value: DO
|
||||
- script:
|
||||
lang: painless
|
||||
source: ctx.event.duration = Math.round(ctx.coredns.duration * params.scale);
|
||||
params:
|
||||
scale: 1000000000
|
||||
if: ctx.coredns?.duration != null
|
||||
- remove:
|
||||
field:
|
||||
- coredns.duration
|
||||
ignore_missing: true
|
||||
# The following copies values from dns namespace (ECS) to the coredns
|
||||
# namespace to avoid introducing breaking change. This should be removed
|
||||
# for 8.0.0. Additionally coredns.dnssec_ok can be removed.
|
||||
- set:
|
||||
if: ctx.dns?.id != null
|
||||
field: coredns.id
|
||||
value: '{{dns.id}}'
|
||||
- set:
|
||||
if: ctx.dns?.question?.class != null
|
||||
field: coredns.query.class
|
||||
value: '{{dns.question.class}}'
|
||||
- set:
|
||||
if: ctx.dns?.question?.name != null
|
||||
field: coredns.query.name
|
||||
value: '{{dns.question.name}}'
|
||||
- set:
|
||||
if: ctx.dns?.question?.type != null
|
||||
field: coredns.query.type
|
||||
value: '{{dns.question.type}}'
|
||||
- set:
|
||||
if: ctx.dns?.response_code != null
|
||||
field: coredns.response.code
|
||||
value: '{{dns.response_code}}'
|
||||
- script:
|
||||
if: ctx.dns?.header_flags != null
|
||||
lang: painless
|
||||
source: >
|
||||
ctx.coredns.response.flags = ctx.dns.header_flags;
|
||||
# Right trim the trailing dot from domain names.
|
||||
- script:
|
||||
if: ctx.dns?.question?.name != null
|
||||
lang: painless
|
||||
source: >
|
||||
def q = ctx.dns.question.name;
|
||||
def end = q.length() - 1;
|
||||
if (q.charAt(end) == (char) '.') {
|
||||
ctx.dns.question.name = q.substring(0, end);
|
||||
}
|
||||
on_failure:
|
||||
- set:
|
||||
field: error.message
|
||||
value: "{{ _ingest.on_failure_message }}"
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from 'src/core/server/';
|
||||
import { SAVED_OBJECT_TYPE } from '../../common/constants';
|
||||
import { InstallationAttributes } from '../../common/types';
|
||||
import * as Registry from '../registry';
|
||||
import { createInstallableFrom } from './index';
|
||||
|
||||
export { SearchParams, fetchFile as getFile } from '../registry';
|
||||
|
||||
function nameAsTitle(name: string) {
|
||||
return name.charAt(0).toUpperCase() + name.substr(1).toLowerCase();
|
||||
}
|
||||
|
||||
export async function getCategories() {
|
||||
return Registry.fetchCategories();
|
||||
}
|
||||
|
||||
export async function getPackages(
|
||||
options: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
} & Registry.SearchParams
|
||||
) {
|
||||
const { savedObjectsClient } = options;
|
||||
const registryItems = await Registry.fetchList({ category: options.category }).then(items => {
|
||||
return items.map(item =>
|
||||
Object.assign({}, item, { title: item.title || nameAsTitle(item.name) })
|
||||
);
|
||||
});
|
||||
const searchObjects = registryItems.map(({ name, version }) => ({
|
||||
type: SAVED_OBJECT_TYPE,
|
||||
id: `${name}-${version}`,
|
||||
}));
|
||||
const results = await savedObjectsClient.bulkGet<InstallationAttributes>(searchObjects);
|
||||
const savedObjects = results.saved_objects.filter(o => !o.error); // ignore errors for now
|
||||
const packageList = registryItems
|
||||
.map(item =>
|
||||
createInstallableFrom(
|
||||
item,
|
||||
savedObjects.find(({ id }) => id === `${item.name}-${item.version}`)
|
||||
)
|
||||
)
|
||||
.sort(sortByName);
|
||||
return packageList;
|
||||
}
|
||||
|
||||
export async function getPackageInfo(options: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
pkgkey: string;
|
||||
}) {
|
||||
const { savedObjectsClient, pkgkey } = options;
|
||||
const [item, savedObject] = await Promise.all([
|
||||
Registry.fetchInfo(pkgkey),
|
||||
getInstallationObject({ savedObjectsClient, pkgkey }),
|
||||
Registry.getArchiveInfo(pkgkey),
|
||||
] as const);
|
||||
// adding `as const` due to regression in TS 3.7.2
|
||||
// see https://github.com/microsoft/TypeScript/issues/34925#issuecomment-550021453
|
||||
// and https://github.com/microsoft/TypeScript/pull/33707#issuecomment-550718523
|
||||
|
||||
// add properties that aren't (or aren't yet) on Registry response
|
||||
const updated = {
|
||||
...item,
|
||||
title: item.title || nameAsTitle(item.name),
|
||||
assets: Registry.groupPathsByService(item.assets),
|
||||
};
|
||||
return createInstallableFrom(updated, savedObject);
|
||||
}
|
||||
|
||||
export async function getInstallationObject(options: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
pkgkey: string;
|
||||
}) {
|
||||
const { savedObjectsClient, pkgkey } = options;
|
||||
return savedObjectsClient
|
||||
.get<InstallationAttributes>(SAVED_OBJECT_TYPE, pkgkey)
|
||||
.catch(e => undefined);
|
||||
}
|
||||
|
||||
function sortByName(a: { name: string }, b: { name: string }) {
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
} else if (a.name < b.name) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObject, SavedObjectsBulkCreateObject } from 'src/core/server/';
|
||||
import * as Registry from '../registry';
|
||||
|
||||
type ArchiveAsset = Pick<SavedObject, 'attributes' | 'migrationVersion' | 'references'>;
|
||||
type SavedObjectToBe = Required<SavedObjectsBulkCreateObject>;
|
||||
|
||||
export async function getObjects(
|
||||
pkgkey: string,
|
||||
filter = (entry: Registry.ArchiveEntry): boolean => true
|
||||
): Promise<SavedObjectToBe[]> {
|
||||
// Create a Map b/c some values, especially index-patterns, are referenced multiple times
|
||||
const objects: Map<string, SavedObjectToBe> = new Map();
|
||||
|
||||
// Get paths which match the given filter
|
||||
const paths = await Registry.getArchiveInfo(pkgkey, filter);
|
||||
|
||||
// Get all objects which matched filter. Add them to the Map
|
||||
const rootObjects = paths.map(getObject);
|
||||
rootObjects.forEach(obj => objects.set(obj.id, obj));
|
||||
|
||||
// Each of those objects might have `references` property like [{id, type, name}]
|
||||
for (const object of rootObjects) {
|
||||
// For each of those objects, if they have references
|
||||
for (const reference of object.references) {
|
||||
// Get the referenced objects. Call same function with a new filter
|
||||
const referencedObjects = await getObjects(pkgkey, (entry: Registry.ArchiveEntry) => {
|
||||
// Skip anything we've already stored
|
||||
if (objects.has(reference.id)) return false;
|
||||
|
||||
// Is the archive entry the reference we want?
|
||||
const { type, file } = Registry.pathParts(entry.path);
|
||||
const isType = type === reference.type;
|
||||
const isJson = file === `${reference.id}.json`;
|
||||
|
||||
return isType && isJson;
|
||||
});
|
||||
|
||||
// Add referenced objects to the Map
|
||||
referencedObjects.forEach(ro => objects.set(ro.id, ro));
|
||||
}
|
||||
}
|
||||
|
||||
// return the array of unique objects
|
||||
return Array.from(objects.values());
|
||||
}
|
||||
|
||||
// the assets from the registry are malformed
|
||||
// https://github.com/elastic/integrations-registry/issues/42
|
||||
function ensureJsonValues(obj: SavedObjectToBe) {
|
||||
const { attributes } = obj;
|
||||
if (
|
||||
attributes.kibanaSavedObjectMeta &&
|
||||
typeof attributes.kibanaSavedObjectMeta.searchSourceJSON !== 'string'
|
||||
) {
|
||||
attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(
|
||||
attributes.kibanaSavedObjectMeta.searchSourceJSON
|
||||
);
|
||||
}
|
||||
|
||||
['optionsJSON', 'panelsJSON', 'uiStateJSON', 'visState']
|
||||
.filter(key => typeof attributes[key] !== 'string')
|
||||
.forEach(key => (attributes[key] = JSON.stringify(attributes[key])));
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function getObject(key: string) {
|
||||
const buffer = Registry.getAsset(key);
|
||||
|
||||
// cache values are buffers. convert to string / JSON
|
||||
const json = buffer.toString('utf8');
|
||||
// convert that to an object & address issues with the formatting of some parts
|
||||
const asset: ArchiveAsset = ensureJsonValues(JSON.parse(json));
|
||||
|
||||
const { type, file } = Registry.pathParts(key);
|
||||
const savedObject: SavedObjectToBe = {
|
||||
type,
|
||||
id: file.replace('.json', ''),
|
||||
attributes: asset.attributes,
|
||||
references: asset.references || [],
|
||||
migrationVersion: asset.migrationVersion || {},
|
||||
};
|
||||
|
||||
return savedObject;
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AssetType, Request, ResponseToolkit } from '../../common/types';
|
||||
import { API_ROOT } from '../../common/routes';
|
||||
import { PluginContext } from '../plugin';
|
||||
import { getClient } from '../saved_objects';
|
||||
import {
|
||||
SearchParams,
|
||||
getCategories,
|
||||
getClusterAccessor,
|
||||
getFile,
|
||||
getPackageInfo,
|
||||
getPackages,
|
||||
installPackage,
|
||||
removeInstallation,
|
||||
} from './index';
|
||||
|
||||
interface Extra extends ResponseToolkit {
|
||||
context: PluginContext;
|
||||
}
|
||||
|
||||
interface ListPackagesRequest extends Request {
|
||||
query: Request['query'] & SearchParams;
|
||||
}
|
||||
|
||||
interface PackageInfoRequest extends Request {
|
||||
params: {
|
||||
pkgkey: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface InstallDeletePackageRequest extends Request {
|
||||
params: {
|
||||
pkgkey: string;
|
||||
asset: AssetType;
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleGetCategories(req: Request, extra: Extra) {
|
||||
return getCategories();
|
||||
}
|
||||
|
||||
export async function handleGetList(req: ListPackagesRequest, extra: Extra) {
|
||||
const savedObjectsClient = getClient(req);
|
||||
return getPackages({
|
||||
savedObjectsClient,
|
||||
category: req.query.category,
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleGetInfo(req: PackageInfoRequest, extra: Extra) {
|
||||
const { pkgkey } = req.params;
|
||||
const savedObjectsClient = getClient(req);
|
||||
return getPackageInfo({ savedObjectsClient, pkgkey });
|
||||
}
|
||||
|
||||
export const handleGetFile = async (req: Request, extra: Extra) => {
|
||||
if (!req.url.path) throw new Error('path is required');
|
||||
const filePath = req.url.path.replace(API_ROOT, '');
|
||||
const registryResponse = await getFile(filePath);
|
||||
const epmResponse = extra.response(registryResponse.body);
|
||||
const proxiedHeaders = ['Content-Type'];
|
||||
proxiedHeaders.forEach(key => {
|
||||
const value = registryResponse.headers.get(key);
|
||||
if (value !== null) epmResponse.header(key, value);
|
||||
});
|
||||
|
||||
return epmResponse;
|
||||
};
|
||||
|
||||
export async function handleRequestInstall(req: InstallDeletePackageRequest, extra: Extra) {
|
||||
const { pkgkey } = req.params;
|
||||
const savedObjectsClient = getClient(req);
|
||||
return installPackage({
|
||||
savedObjectsClient,
|
||||
pkgkey,
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleRequestDelete(req: InstallDeletePackageRequest, extra: Extra) {
|
||||
const { pkgkey } = req.params;
|
||||
const savedObjectsClient = getClient(req);
|
||||
const callCluster = getClusterAccessor(extra.context.esClient, req);
|
||||
return removeInstallation({ savedObjectsClient, pkgkey, callCluster });
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { IClusterClient, ScopedClusterClient } from 'src/core/server/';
|
||||
import { AssetType, Installable, Installation, Request } from '../../common/types';
|
||||
|
||||
export * from './get';
|
||||
export * from './install';
|
||||
export * from './remove';
|
||||
export * from './handlers';
|
||||
|
||||
export type CallESAsCurrentUser = ScopedClusterClient['callAsCurrentUser'];
|
||||
|
||||
export const SAVED_OBJECT_TYPES = new Set<AssetType>([
|
||||
'dashboard',
|
||||
'ilm-policy',
|
||||
'index-pattern',
|
||||
'index-template',
|
||||
'ingest-pipeline',
|
||||
'search',
|
||||
'visualization',
|
||||
]);
|
||||
|
||||
export function getClusterAccessor(esClient: IClusterClient, req: Request) {
|
||||
return esClient.asScoped(req).callAsCurrentUser;
|
||||
}
|
||||
|
||||
export function createInstallableFrom<T>(from: T, savedObject?: Installation): Installable<T> {
|
||||
return savedObject
|
||||
? {
|
||||
...from,
|
||||
status: 'installed',
|
||||
savedObject,
|
||||
}
|
||||
: {
|
||||
...from,
|
||||
status: 'not_installed',
|
||||
};
|
||||
}
|
||||
|
||||
export function assetUsesObjects(assetType: AssetType) {
|
||||
return SAVED_OBJECT_TYPES.has(assetType);
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObject, SavedObjectsClientContract } from 'src/core/server/';
|
||||
import { SAVED_OBJECT_TYPE } from '../../common/constants';
|
||||
import { AssetReference, AssetType, InstallationAttributes } from '../../common/types';
|
||||
import * as Registry from '../registry';
|
||||
import { getInstallationObject, getPackageInfo } from './index';
|
||||
import { getObjects } from './get_objects';
|
||||
|
||||
export async function installPackage(options: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
pkgkey: string;
|
||||
}) {
|
||||
const { savedObjectsClient, pkgkey } = options;
|
||||
|
||||
const toSave = await installAssets({
|
||||
savedObjectsClient,
|
||||
pkgkey,
|
||||
});
|
||||
|
||||
if (toSave.length) {
|
||||
// Save those references in the integration manager's state saved object
|
||||
await saveInstallationReferences({
|
||||
savedObjectsClient,
|
||||
pkgkey,
|
||||
toSave,
|
||||
});
|
||||
}
|
||||
|
||||
return getPackageInfo({ savedObjectsClient, pkgkey });
|
||||
}
|
||||
|
||||
// the function which how to install each of the various asset types
|
||||
// TODO: make it an exhaustive list
|
||||
// e.g. switch statement with cases for each enum key returning `never` for default case
|
||||
export async function installAssets(options: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
pkgkey: string;
|
||||
}) {
|
||||
const { savedObjectsClient, pkgkey } = options;
|
||||
|
||||
// Only install certain Kibana assets during package installation.
|
||||
// All other asset types need special handling
|
||||
const typesToInstall: AssetType[] = ['visualization', 'dashboard', 'search'];
|
||||
|
||||
const installationPromises = typesToInstall.map(async assetType =>
|
||||
installKibanaSavedObjects({ savedObjectsClient, pkgkey, assetType })
|
||||
);
|
||||
|
||||
// installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][]
|
||||
// call .flat to flatten into one dimensional array
|
||||
return Promise.all(installationPromises).then(results => results.flat());
|
||||
}
|
||||
|
||||
export async function saveInstallationReferences(options: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
pkgkey: string;
|
||||
toSave: AssetReference[];
|
||||
}) {
|
||||
const { savedObjectsClient, pkgkey, toSave } = options;
|
||||
const savedObject = await getInstallationObject({ savedObjectsClient, pkgkey });
|
||||
const savedRefs = savedObject && savedObject.attributes.installed;
|
||||
const mergeRefsReducer = (current: AssetReference[], pending: AssetReference) => {
|
||||
const hasRef = current.find(c => c.id === pending.id && c.type === pending.type);
|
||||
if (!hasRef) current.push(pending);
|
||||
return current;
|
||||
};
|
||||
|
||||
const toInstall = toSave.reduce(mergeRefsReducer, savedRefs || []);
|
||||
|
||||
return savedObjectsClient.create<InstallationAttributes>(
|
||||
SAVED_OBJECT_TYPE,
|
||||
{ installed: toInstall },
|
||||
{ id: pkgkey, overwrite: true }
|
||||
);
|
||||
}
|
||||
|
||||
async function installKibanaSavedObjects({
|
||||
savedObjectsClient,
|
||||
pkgkey,
|
||||
assetType,
|
||||
}: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
pkgkey: string;
|
||||
assetType: AssetType;
|
||||
}) {
|
||||
const isSameType = ({ path }: Registry.ArchiveEntry) =>
|
||||
assetType === Registry.pathParts(path).type;
|
||||
|
||||
const toBeSavedObjects = await getObjects(pkgkey, isSameType);
|
||||
if (toBeSavedObjects.length === 0) {
|
||||
return [];
|
||||
} else {
|
||||
const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, {
|
||||
overwrite: true,
|
||||
});
|
||||
const createdObjects = createResults.saved_objects;
|
||||
const installed = createdObjects.map(toAssetReference);
|
||||
return installed;
|
||||
}
|
||||
}
|
||||
|
||||
function toAssetReference({ id, type }: SavedObject) {
|
||||
const reference: AssetReference = { id, type };
|
||||
|
||||
return reference;
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from 'src/core/server/';
|
||||
import { SAVED_OBJECT_TYPE } from '../../common/constants';
|
||||
import {
|
||||
getInstallationObject,
|
||||
assetUsesObjects,
|
||||
CallESAsCurrentUser,
|
||||
getPackageInfo,
|
||||
} from './index';
|
||||
import { AssetType } from '../../common/types';
|
||||
|
||||
export async function removeInstallation(options: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
pkgkey: string;
|
||||
callCluster: CallESAsCurrentUser;
|
||||
}) {
|
||||
const { savedObjectsClient, pkgkey, callCluster } = options;
|
||||
const installation = await getInstallationObject({ savedObjectsClient, pkgkey });
|
||||
const installedObjects = (installation && installation.attributes.installed) || [];
|
||||
|
||||
// Delete the manager saved object with references to the asset objects
|
||||
// could also update with [] or some other state
|
||||
await savedObjectsClient.delete(SAVED_OBJECT_TYPE, pkgkey);
|
||||
|
||||
// Delete the installed assets
|
||||
const deletePromises = installedObjects.map(async ({ id, type }) => {
|
||||
const assetType = type as AssetType;
|
||||
if (assetUsesObjects(assetType)) {
|
||||
savedObjectsClient.delete(assetType, id);
|
||||
} else if (assetType === 'ingest-pipeline') {
|
||||
deletePipeline(callCluster, id);
|
||||
}
|
||||
});
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
// successful delete's in SO client return {}. return something more useful
|
||||
const packageInfo = await getPackageInfo({ savedObjectsClient, pkgkey });
|
||||
|
||||
return packageInfo;
|
||||
}
|
||||
|
||||
async function deletePipeline(callCluster: CallESAsCurrentUser, id: string): Promise<void> {
|
||||
// '*' shouldn't ever appear here, but it still would delete all ingest pipelines
|
||||
if (id && id !== '*') {
|
||||
await callCluster('ingest.deletePipeline', { id });
|
||||
}
|
||||
}
|
76
x-pack/legacy/plugins/integrations_manager/server/plugin.ts
Normal file
76
x-pack/legacy/plugins/integrations_manager/server/plugin.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { CoreSetup, CoreStart, IClusterClient, PluginInitializerContext } from 'src/core/server';
|
||||
import { PLUGIN } from '../common/constants';
|
||||
import { Server } from '../common/types';
|
||||
import { EPMConfigSchema, epmConfigStore } from './config';
|
||||
import { feature } from './feature';
|
||||
import { fetchList } from './registry';
|
||||
import { routes } from './routes';
|
||||
import { PluginSetupContract } from '../../../../plugins/features/server';
|
||||
|
||||
export { createSetupShim } from './shim';
|
||||
|
||||
export type EPMPluginInitializerContext = Pick<PluginInitializerContext, 'config'>;
|
||||
|
||||
export interface EPMCoreSetup {
|
||||
elasticsearch: CoreSetup['elasticsearch'];
|
||||
hapiServer: Server;
|
||||
}
|
||||
|
||||
export type PluginSetup = ReturnType<Plugin['setup']>;
|
||||
export type PluginStart = ReturnType<Plugin['start']>;
|
||||
export interface PluginContext {
|
||||
esClient: IClusterClient;
|
||||
}
|
||||
|
||||
export interface PluginsSetup {
|
||||
features: PluginSetupContract;
|
||||
}
|
||||
|
||||
export class Plugin {
|
||||
public config$: Observable<EPMConfigSchema>;
|
||||
|
||||
constructor(initializerContext: EPMPluginInitializerContext) {
|
||||
this.config$ = initializerContext.config.create<EPMConfigSchema>();
|
||||
this.config$.subscribe(configValue => {
|
||||
epmConfigStore.updateConfig(configValue);
|
||||
});
|
||||
}
|
||||
public setup(core: EPMCoreSetup, plugins: PluginsSetup) {
|
||||
const { elasticsearch, hapiServer } = core;
|
||||
const pluginContext: PluginContext = {
|
||||
esClient: elasticsearch.createClient(PLUGIN.ID),
|
||||
};
|
||||
|
||||
// make pluginContext entries available to handlers via h.context
|
||||
// https://github.com/hapijs/hapi/blob/master/API.md#route.options.bind
|
||||
// aligns closely with approach proposed in handler RFC
|
||||
// https://github.com/epixa/kibana/blob/rfc-handlers/rfcs/text/0003_handler_interface.md
|
||||
const routesWithContext = routes.map(function injectRouteContext(route) {
|
||||
// merge route.options.bind, defined or otherwise, into pluginContext
|
||||
// routes can add extra values or override pluginContext values (e.g. spies, etc)
|
||||
if (!route.options) route.options = {};
|
||||
route.options.bind = Object.assign({}, pluginContext, route.options.bind);
|
||||
|
||||
return route;
|
||||
});
|
||||
|
||||
// map routes to handlers
|
||||
hapiServer.route(routesWithContext);
|
||||
|
||||
// register the plugin
|
||||
plugins.features.registerFeature(feature);
|
||||
|
||||
// the JS API for other consumers
|
||||
return {
|
||||
getList: fetchList,
|
||||
};
|
||||
}
|
||||
public start(core: CoreStart) {}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
const cache: Map<string, Buffer> = new Map();
|
||||
export const cacheGet = (key: string) => cache.get(key);
|
||||
export const cacheSet = (key: string, value: Buffer) => cache.set(key, value);
|
||||
export const cacheHas = (key: string) => cache.has(key);
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import tar from 'tar';
|
||||
import { bufferToStream, streamToBuffer } from './streams';
|
||||
|
||||
export interface ArchiveEntry {
|
||||
path: string;
|
||||
buffer?: Buffer;
|
||||
}
|
||||
|
||||
export async function untarBuffer(
|
||||
buffer: Buffer,
|
||||
filter = (entry: ArchiveEntry): boolean => true,
|
||||
onEntry = (entry: ArchiveEntry): void => {}
|
||||
): Promise<void> {
|
||||
const deflatedStream = bufferToStream(buffer);
|
||||
// use tar.list vs .extract to avoid writing to disk
|
||||
const inflateStream = tar.list().on('entry', (entry: tar.FileStat) => {
|
||||
const path = entry.header.path || '';
|
||||
if (!filter({ path })) return;
|
||||
streamToBuffer(entry).then(entryBuffer => onEntry({ buffer: entryBuffer, path }));
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
inflateStream.on('end', resolve).on('error', reject);
|
||||
deflatedStream.pipe(inflateStream);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { pathParts } from './index';
|
||||
import { AssetParts } from '../../common/types';
|
||||
|
||||
const testPaths = [
|
||||
{
|
||||
path: 'foo-1.1.0/service/type/file.yml',
|
||||
assetParts: {
|
||||
file: 'file.yml',
|
||||
path: 'foo-1.1.0/service/type/file.yml',
|
||||
pkgkey: 'foo-1.1.0',
|
||||
service: 'service',
|
||||
type: 'type',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'iptables-1.0.4/kibana/visualization/683402b0-1f29-11e9-8ec4-cf5d91a864b3-ecs.json',
|
||||
assetParts: {
|
||||
file: '683402b0-1f29-11e9-8ec4-cf5d91a864b3-ecs.json',
|
||||
path: 'iptables-1.0.4/kibana/visualization/683402b0-1f29-11e9-8ec4-cf5d91a864b3-ecs.json',
|
||||
pkgkey: 'iptables-1.0.4',
|
||||
service: 'kibana',
|
||||
type: 'visualization',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test('testPathParts', () => {
|
||||
for (const value of testPaths) {
|
||||
expect(pathParts(value.path)).toStrictEqual(value.assetParts as AssetParts);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { URL } from 'url';
|
||||
import { Response } from 'node-fetch';
|
||||
import {
|
||||
AssetsGroupedByServiceByType,
|
||||
AssetParts,
|
||||
CategoryId,
|
||||
CategorySummaryList,
|
||||
RegistryList,
|
||||
RegistryPackage,
|
||||
} from '../../common/types';
|
||||
import { cacheGet, cacheSet } from './cache';
|
||||
import { ArchiveEntry, untarBuffer } from './extract';
|
||||
import { fetchUrl, getResponseStream, getResponse } from './requests';
|
||||
import { streamToBuffer } from './streams';
|
||||
import { epmConfigStore } from '../config';
|
||||
|
||||
export { ArchiveEntry } from './extract';
|
||||
|
||||
export interface SearchParams {
|
||||
category?: CategoryId;
|
||||
}
|
||||
|
||||
export async function fetchList(params?: SearchParams): Promise<RegistryList> {
|
||||
const { registryUrl } = epmConfigStore.getConfig();
|
||||
const url = new URL(`${registryUrl}/search`);
|
||||
if (params && params.category) {
|
||||
url.searchParams.set('category', params.category);
|
||||
}
|
||||
|
||||
return fetchUrl(url.toString()).then(JSON.parse);
|
||||
}
|
||||
|
||||
export async function fetchInfo(key: string): Promise<RegistryPackage> {
|
||||
const { registryUrl } = epmConfigStore.getConfig();
|
||||
return fetchUrl(`${registryUrl}/package/${key}`).then(JSON.parse);
|
||||
}
|
||||
|
||||
export async function fetchFile(filePath: string): Promise<Response> {
|
||||
const { registryUrl } = epmConfigStore.getConfig();
|
||||
return getResponse(`${registryUrl}${filePath}`);
|
||||
}
|
||||
|
||||
export async function fetchCategories(): Promise<CategorySummaryList> {
|
||||
const { registryUrl } = epmConfigStore.getConfig();
|
||||
return fetchUrl(`${registryUrl}/categories`).then(JSON.parse);
|
||||
}
|
||||
|
||||
export async function getArchiveInfo(
|
||||
pkgkey: string,
|
||||
filter = (entry: ArchiveEntry): boolean => true
|
||||
): Promise<string[]> {
|
||||
// assume .tar.gz for now. add support for .zip if/when we need it
|
||||
const key = `${pkgkey}.tar.gz`;
|
||||
const paths: string[] = [];
|
||||
const onEntry = (entry: ArchiveEntry) => {
|
||||
const { path, buffer } = entry;
|
||||
const { file } = pathParts(path);
|
||||
if (!file) return;
|
||||
if (buffer) {
|
||||
cacheSet(path, buffer);
|
||||
paths.push(path);
|
||||
}
|
||||
};
|
||||
|
||||
await extract(key, filter, onEntry);
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
export function pathParts(path: string): AssetParts {
|
||||
const [pkgkey, service, type, file] = path.split('/');
|
||||
const parts = { pkgkey, service, type, file, path } as AssetParts;
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
async function extract(
|
||||
key: string,
|
||||
filter = (entry: ArchiveEntry): boolean => true,
|
||||
onEntry: (entry: ArchiveEntry) => void
|
||||
) {
|
||||
const archiveBuffer = await getOrFetchArchiveBuffer(key);
|
||||
|
||||
return untarBuffer(archiveBuffer, filter, onEntry);
|
||||
}
|
||||
|
||||
async function getOrFetchArchiveBuffer(key: string): Promise<Buffer> {
|
||||
let buffer = cacheGet(key);
|
||||
if (!buffer) {
|
||||
buffer = await fetchArchiveBuffer(key);
|
||||
cacheSet(key, buffer);
|
||||
}
|
||||
|
||||
if (buffer) {
|
||||
return buffer;
|
||||
} else {
|
||||
throw new Error(`no archive buffer for ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchArchiveBuffer(key: string): Promise<Buffer> {
|
||||
const { registryUrl } = epmConfigStore.getConfig();
|
||||
return getResponseStream(`${registryUrl}/package/${key}`).then(streamToBuffer);
|
||||
}
|
||||
|
||||
export function getAsset(key: string) {
|
||||
const buffer = cacheGet(key);
|
||||
if (buffer === undefined) throw new Error(`Cannot find asset ${key}`);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export function groupPathsByService(paths: string[]): AssetsGroupedByServiceByType {
|
||||
// ASK: best way, if any, to avoid `any`?
|
||||
const assets = paths.reduce((map: any, path) => {
|
||||
const parts = pathParts(path.replace(/^\/package\//, ''));
|
||||
if (!map[parts.service]) map[parts.service] = {};
|
||||
if (!map[parts.service][parts.type]) map[parts.service][parts.type] = [];
|
||||
map[parts.service][parts.type].push(parts);
|
||||
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
kibana: assets.kibana,
|
||||
elasticsearch: assets.elasticsearch,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { streamToString } from './streams';
|
||||
|
||||
export async function getResponse(url: string): Promise<Response> {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
return response;
|
||||
} else {
|
||||
throw new Boom(response.statusText, { statusCode: response.status });
|
||||
}
|
||||
} catch (e) {
|
||||
throw Boom.boomify(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getResponseStream(url: string): Promise<NodeJS.ReadableStream> {
|
||||
const res = await getResponse(url);
|
||||
return res.body;
|
||||
}
|
||||
|
||||
export async function fetchUrl(url: string): Promise<string> {
|
||||
return getResponseStream(url).then(streamToString);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { PassThrough } from 'stream';
|
||||
|
||||
export function bufferToStream(buffer: Buffer): PassThrough {
|
||||
const stream = new PassThrough();
|
||||
stream.end(buffer);
|
||||
return stream;
|
||||
}
|
||||
|
||||
export function streamToString(stream: NodeJS.ReadableStream): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const body: string[] = [];
|
||||
stream.on('data', (chunk: string) => body.push(chunk));
|
||||
stream.on('end', () => resolve(body.join('')));
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
export function streamToBuffer(stream: NodeJS.ReadableStream): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on('data', chunk => chunks.push(Buffer.from(chunk)));
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
49
x-pack/legacy/plugins/integrations_manager/server/routes.ts
Normal file
49
x-pack/legacy/plugins/integrations_manager/server/routes.ts
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { PLUGIN } from '../common/constants';
|
||||
import { ServerRoute } from '../common/types';
|
||||
import * as CommonRoutes from '../common/routes';
|
||||
import * as Packages from './packages/handlers';
|
||||
|
||||
// Manager public API paths
|
||||
export const routes: ServerRoute[] = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: CommonRoutes.API_CATEGORIES_PATTERN,
|
||||
options: { tags: [`access:${PLUGIN.ID}`], json: { space: 2 } },
|
||||
handler: Packages.handleGetCategories,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: CommonRoutes.API_LIST_PATTERN,
|
||||
options: { tags: [`access:${PLUGIN.ID}`], json: { space: 2 } },
|
||||
handler: Packages.handleGetList,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: `${CommonRoutes.API_INFO_PATTERN}/{filePath*}`,
|
||||
options: { tags: [`access:${PLUGIN.ID}`] },
|
||||
handler: Packages.handleGetFile,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: CommonRoutes.API_INFO_PATTERN,
|
||||
options: { tags: [`access:${PLUGIN.ID}`], json: { space: 2 } },
|
||||
handler: Packages.handleGetInfo,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: CommonRoutes.API_INSTALL_PATTERN,
|
||||
options: { tags: [`access:${PLUGIN.ID}`], json: { space: 2 } },
|
||||
handler: Packages.handleRequestInstall,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: CommonRoutes.API_DELETE_PATTERN,
|
||||
options: { tags: [`access:${PLUGIN.ID}`], json: { space: 2 } },
|
||||
handler: Packages.handleRequestDelete,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SAVED_OBJECT_TYPE } from '../common/constants';
|
||||
import { Request } from '../common/types';
|
||||
|
||||
export const getClient = (req: Request) => req.getSavedObjectsClient();
|
||||
|
||||
export const mappings = {
|
||||
[SAVED_OBJECT_TYPE]: {
|
||||
properties: {
|
||||
installed: {
|
||||
type: 'nested',
|
||||
properties: {
|
||||
id: { type: 'keyword' },
|
||||
type: { type: 'keyword' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const savedObjectSchemas = {
|
||||
[SAVED_OBJECT_TYPE]: {
|
||||
isNamespaceAgnostic: true,
|
||||
},
|
||||
};
|
42
x-pack/legacy/plugins/integrations_manager/server/shim.ts
Normal file
42
x-pack/legacy/plugins/integrations_manager/server/shim.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Legacy } from 'kibana';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { PLUGIN } from '../common/constants';
|
||||
import { EPMCoreSetup, EPMPluginInitializerContext, PluginsSetup } from './plugin';
|
||||
|
||||
// yes, any. See https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts#L49-L58
|
||||
// for a way around it, but this is Legacy Platform and I'm not sure these hoops are worth jumping through.
|
||||
export const createSetupShim = (server: any) => {
|
||||
const newPlatform: Legacy.Server['newPlatform'] = server.newPlatform;
|
||||
const npSetup = newPlatform.setup;
|
||||
const getConfig$ = () =>
|
||||
new BehaviorSubject(server.config().get(PLUGIN.CONFIG_PREFIX)).asObservable();
|
||||
|
||||
const initializerContext: EPMPluginInitializerContext = {
|
||||
config: {
|
||||
create: getConfig$,
|
||||
createIfExists: getConfig$,
|
||||
},
|
||||
};
|
||||
|
||||
const coreSetup: EPMCoreSetup = {
|
||||
elasticsearch: npSetup.core.elasticsearch,
|
||||
hapiServer: newPlatform.__internals.hapiServer,
|
||||
};
|
||||
|
||||
const pluginsSetup = {
|
||||
// @ts-ignore: New Platform not typed
|
||||
features: npSetup.plugins.features as PluginsSetup['features'],
|
||||
};
|
||||
|
||||
return {
|
||||
initializerContext,
|
||||
coreSetup,
|
||||
pluginsSetup,
|
||||
};
|
||||
};
|
|
@ -69,6 +69,7 @@
|
|||
"@types/history": "^4.7.3",
|
||||
"@types/jest": "^24.0.18",
|
||||
"@types/joi": "^13.4.2",
|
||||
"@types/js-search": "^1.4.0",
|
||||
"@types/js-yaml": "^3.11.1",
|
||||
"@types/jsdom": "^12.2.4",
|
||||
"@types/json-stable-stringify": "^1.0.32",
|
||||
|
@ -264,6 +265,7 @@
|
|||
"isbinaryfile": "4.0.2",
|
||||
"joi": "^13.5.2",
|
||||
"jquery": "^3.4.1",
|
||||
"js-search": "^1.4.3",
|
||||
"js-yaml": "3.13.1",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
|
|
|
@ -38,4 +38,5 @@ require('@kbn/test').runTestsCli([
|
|||
require.resolve('../test/ui_capabilities/security_only/config'),
|
||||
require.resolve('../test/ui_capabilities/spaces_only/config'),
|
||||
require.resolve('../test/upgrade_assistant_integration/config'),
|
||||
require.resolve('../test/epm_api_integration/config'),
|
||||
]);
|
||||
|
|
|
@ -112,6 +112,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
'apm',
|
||||
'canvas',
|
||||
'infrastructure',
|
||||
'epm',
|
||||
'logs',
|
||||
'maps',
|
||||
'uptime',
|
||||
|
|
|
@ -38,6 +38,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
apm: ['all', 'read'],
|
||||
siem: ['all', 'read'],
|
||||
fleet: ['all', 'read'],
|
||||
epm: ['all', 'read'],
|
||||
},
|
||||
global: ['all', 'read'],
|
||||
space: ['all', 'read'],
|
||||
|
|
147
x-pack/test/epm_api_integration/apis/file.ts
Normal file
147
x-pack/test/epm_api_integration/apis/file.ts
Normal file
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import ServerMock from 'mock-http-server';
|
||||
import { FtrProviderContext } from '../../api_integration/ftr_provider_context';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
describe('package file', () => {
|
||||
const server = new ServerMock({ host: 'localhost', port: 6666 });
|
||||
beforeEach(() => {
|
||||
server.start(() => {});
|
||||
});
|
||||
afterEach(() => {
|
||||
server.stop(() => {});
|
||||
});
|
||||
it('fetches a .png screenshot image', async () => {
|
||||
server.on({
|
||||
method: 'GET',
|
||||
path: '/package/auditd-2.0.4/img/screenshots/auditbeat-file-integrity-dashboard.png',
|
||||
reply: {
|
||||
headers: { 'content-type': 'image/png' },
|
||||
},
|
||||
});
|
||||
|
||||
const supertest = getService('supertest');
|
||||
await supertest
|
||||
.get('/api/epm/package/auditd-2.0.4/img/screenshots/auditbeat-file-integrity-dashboard.png')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect('Content-Type', 'image/png')
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('fetches an .svg icon image', async () => {
|
||||
server.on({
|
||||
method: 'GET',
|
||||
path: '/package/auditd-2.0.4/img/icon.svg',
|
||||
reply: {
|
||||
headers: { 'content-type': 'image/svg' },
|
||||
},
|
||||
});
|
||||
|
||||
const supertest = getService('supertest');
|
||||
await supertest
|
||||
.get('/api/epm/package/auditd-2.0.4/img/icon.svg')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect('Content-Type', 'image/svg');
|
||||
});
|
||||
|
||||
it('fetches an auditbeat .conf rule file', async () => {
|
||||
server.on({
|
||||
method: 'GET',
|
||||
path: '/package/auditd-2.0.4/auditbeat/rules/sample-rules-linux-32bit.conf',
|
||||
});
|
||||
|
||||
const supertest = getService('supertest');
|
||||
await supertest
|
||||
.get('/api/epm/package/auditd-2.0.4/auditbeat/rules/sample-rules-linux-32bit.conf')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('fetches an auditbeat .yml config file', async () => {
|
||||
server.on({
|
||||
method: 'GET',
|
||||
path: '/package/auditd-2.0.4/auditbeat/config/config.yml',
|
||||
reply: {
|
||||
headers: { 'content-type': 'text/yaml; charset=UTF-8' },
|
||||
},
|
||||
});
|
||||
|
||||
const supertest = getService('supertest');
|
||||
await supertest
|
||||
.get('/api/epm/package/auditd-2.0.4/auditbeat/config/config.yml')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect('Content-Type', 'text/yaml; charset=UTF-8')
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('fetches a .json kibana visualization file', async () => {
|
||||
server.on({
|
||||
method: 'GET',
|
||||
path:
|
||||
'/package/auditd-2.0.4/kibana/visualization/b21e0c70-c252-11e7-8692-232bd1143e8a-ecs.json',
|
||||
});
|
||||
|
||||
const supertest = getService('supertest');
|
||||
await supertest
|
||||
.get(
|
||||
'/api/epm/package/auditd-2.0.4/kibana/visualization/b21e0c70-c252-11e7-8692-232bd1143e8a-ecs.json'
|
||||
)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('fetches a .json kibana dashboard file', async () => {
|
||||
server.on({
|
||||
method: 'GET',
|
||||
path:
|
||||
'/package/auditd-2.0.4/kibana/dashboard/7de391b0-c1ca-11e7-8995-936807a28b16-ecs.json',
|
||||
});
|
||||
|
||||
const supertest = getService('supertest');
|
||||
await supertest
|
||||
.get(
|
||||
'/api/epm/package/auditd-2.0.4/kibana/dashboard/7de391b0-c1ca-11e7-8995-936807a28b16-ecs.json'
|
||||
)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('fetches an .json index pattern file', async () => {
|
||||
server.on({
|
||||
method: 'GET',
|
||||
path: '/package/auditd-2.0.4/kibana/index-pattern/auditbeat-*.json',
|
||||
});
|
||||
|
||||
const supertest = getService('supertest');
|
||||
await supertest
|
||||
.get('/api/epm/package/auditd-2.0.4/kibana/index-pattern/auditbeat-*.json')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('fetches a .json search file', async () => {
|
||||
server.on({
|
||||
method: 'GET',
|
||||
path: '/package/auditd-2.0.4/kibana/search/0f10c430-c1c3-11e7-8995-936807a28b16-ecs.json',
|
||||
});
|
||||
|
||||
const supertest = getService('supertest');
|
||||
await supertest
|
||||
.get(
|
||||
'/api/epm/package/auditd-2.0.4/kibana/search/0f10c430-c1c3-11e7-8995-936807a28b16-ecs.json'
|
||||
)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
}
|
14
x-pack/test/epm_api_integration/apis/index.js
Normal file
14
x-pack/test/epm_api_integration/apis/index.js
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export default function ({ loadTestFile }) {
|
||||
describe('EPM Endpoints', function () {
|
||||
this.tags('ciGroup7');
|
||||
loadTestFile(require.resolve('./list'));
|
||||
loadTestFile(require.resolve('./file'));
|
||||
loadTestFile(require.resolve('./template'));
|
||||
});
|
||||
}
|
125
x-pack/test/epm_api_integration/apis/list.ts
Normal file
125
x-pack/test/epm_api_integration/apis/list.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import ServerMock from 'mock-http-server';
|
||||
import { FtrProviderContext } from '../../api_integration/ftr_provider_context';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
describe('list', () => {
|
||||
const server = new ServerMock({ host: 'localhost', port: 6666 });
|
||||
beforeEach(() => {
|
||||
server.start(() => {});
|
||||
});
|
||||
afterEach(() => {
|
||||
server.stop(() => {});
|
||||
});
|
||||
it('lists all packages from the registry', async () => {
|
||||
const searchResponse = [
|
||||
{
|
||||
description: 'First integration package',
|
||||
download: '/package/first-1.0.1.tar.gz',
|
||||
name: 'first',
|
||||
title: 'First',
|
||||
type: 'integration',
|
||||
version: '1.0.1',
|
||||
},
|
||||
{
|
||||
description: 'Second integration package',
|
||||
download: '/package/second-2.0.4.tar.gz',
|
||||
icons: [
|
||||
{
|
||||
src: '/package/second-2.0.4/img/icon.svg',
|
||||
type: 'image/svg+xml',
|
||||
},
|
||||
],
|
||||
name: 'second',
|
||||
title: 'Second',
|
||||
type: 'integration',
|
||||
version: '2.0.4',
|
||||
},
|
||||
];
|
||||
server.on({
|
||||
method: 'GET',
|
||||
path: '/search',
|
||||
reply: {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(searchResponse),
|
||||
},
|
||||
});
|
||||
|
||||
const supertest = getService('supertest');
|
||||
const fetchPackageList = async () => {
|
||||
const response = await supertest
|
||||
.get('/api/epm/list')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200);
|
||||
return response.body;
|
||||
};
|
||||
|
||||
const listResponse = await fetchPackageList();
|
||||
|
||||
expect(listResponse.length).to.be(2);
|
||||
expect(listResponse[0]).to.eql({ ...searchResponse[0], status: 'not_installed' });
|
||||
expect(listResponse[1]).to.eql({ ...searchResponse[1], status: 'not_installed' });
|
||||
});
|
||||
|
||||
it('sorts the packages even if the registry sends them unsorted', async () => {
|
||||
const searchResponse = [
|
||||
{
|
||||
description: 'BBB integration package',
|
||||
download: '/package/bbb-1.0.1.tar.gz',
|
||||
name: 'bbb',
|
||||
title: 'BBB',
|
||||
type: 'integration',
|
||||
version: '1.0.1',
|
||||
},
|
||||
{
|
||||
description: 'CCC integration package',
|
||||
download: '/package/ccc-2.0.4.tar.gz',
|
||||
name: 'ccc',
|
||||
title: 'CCC',
|
||||
type: 'integration',
|
||||
version: '2.0.4',
|
||||
},
|
||||
{
|
||||
description: 'AAA integration package',
|
||||
download: '/package/aaa-0.0.1.tar.gz',
|
||||
name: 'aaa',
|
||||
title: 'AAA',
|
||||
type: 'integration',
|
||||
version: '0.0.1',
|
||||
},
|
||||
];
|
||||
server.on({
|
||||
method: 'GET',
|
||||
path: '/search',
|
||||
reply: {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(searchResponse),
|
||||
},
|
||||
});
|
||||
|
||||
const supertest = getService('supertest');
|
||||
const fetchPackageList = async () => {
|
||||
const response = await supertest
|
||||
.get('/api/epm/list')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200);
|
||||
return response.body;
|
||||
};
|
||||
|
||||
const listResponse = await fetchPackageList();
|
||||
|
||||
expect(listResponse.length).to.be(3);
|
||||
expect(listResponse[0].name).to.eql('aaa');
|
||||
expect(listResponse[1].name).to.eql('bbb');
|
||||
expect(listResponse[2].name).to.eql('ccc');
|
||||
});
|
||||
});
|
||||
}
|
9
x-pack/test/epm_api_integration/apis/mock_http_server.d.ts
vendored
Normal file
9
x-pack/test/epm_api_integration/apis/mock_http_server.d.ts
vendored
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// No types for mock-http-server available, but we don't need them.
|
||||
|
||||
declare module 'mock-http-server';
|
29
x-pack/test/epm_api_integration/apis/template.ts
Normal file
29
x-pack/test/epm_api_integration/apis/template.ts
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../api_integration/ftr_provider_context';
|
||||
import { getTemplate } from '../../../legacy/plugins/integrations_manager/server/lib/template/template';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
// This test was inspired by https://github.com/elastic/kibana/blob/master/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js
|
||||
describe('load template', async () => {
|
||||
const indexPattern = 'foo';
|
||||
const templateName = 'bar';
|
||||
const es = getService('es');
|
||||
|
||||
const body = getTemplate(indexPattern);
|
||||
|
||||
const response = await es.indices.putTemplate({
|
||||
name: templateName,
|
||||
body,
|
||||
});
|
||||
expect(response).to.eql({ acknowledged: true });
|
||||
|
||||
const indexTemplate = await es.indices.getTemplate({ name: templateName });
|
||||
expect(indexTemplate[templateName].index_patterns).to.eql([indexPattern]);
|
||||
});
|
||||
}
|
35
x-pack/test/epm_api_integration/config.ts
Normal file
35
x-pack/test/epm_api_integration/config.ts
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
|
||||
|
||||
export default async function({ readConfigFile }: FtrConfigProviderContext) {
|
||||
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
|
||||
|
||||
return {
|
||||
testFiles: [require.resolve('./apis')],
|
||||
servers: xPackAPITestsConfig.get('servers'),
|
||||
services: {
|
||||
supertest: xPackAPITestsConfig.get('services.supertest'),
|
||||
es: xPackAPITestsConfig.get('services.es'),
|
||||
},
|
||||
junit: {
|
||||
reportName: 'X-Pack EPM API Integration Tests',
|
||||
},
|
||||
|
||||
esTestCluster: {
|
||||
...xPackAPITestsConfig.get('esTestCluster'),
|
||||
},
|
||||
|
||||
kbnTestServer: {
|
||||
...xPackAPITestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
|
||||
'--xpack.epm.registryUrl=http://localhost:6666',
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue