From f93ec7988b13d5e690e6490ff443ebebc1bcd46d Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Wed, 18 Mar 2020 17:14:45 +0100 Subject: [PATCH 01/42] [EPM] Add mapping field types to index template generation v2 (#60266) * Add properties needed for index templates to Field * Add data type handling to template generation * Adjust tests * Update fields test snapshots * Remove duplicate fields from test file * Add test cases * Enhance processFields * move expand stage to expandFields * fix expandFields * add deduplication stage dedupFields * Use processField() to preprocess fields * Remove alias fields with invalid path * Remove obsolete code. * Fix documentation. * Add unit tests for getField() * Don't fail on invalid input for now. * Validate array fields. * Guard against invalid input. --- .../__snapshots__/template.test.ts.snap | 1599 ++++++++++- .../epm/elasticsearch/template/install.ts | 4 +- .../elasticsearch/template/template.test.ts | 31 +- .../epm/elasticsearch/template/template.ts | 101 +- .../fields/__snapshots__/field.test.ts.snap | 2421 ++++++++++++++++- .../server/services/epm/fields/field.test.ts | 53 +- .../server/services/epm/fields/field.ts | 136 +- .../server/services/epm/fields/tests/base.yml | 16 + .../services/epm/fields/tests/system.yml | 1625 +++++++++++ 9 files changed, 5914 insertions(+), 72 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/fields/tests/system.yml diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index ad4d636164d71e..0e239c24dd9cf8 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`tests loading fields.yml: base.yml 1`] = ` +exports[`tests loading base.yml: base.yml 1`] = ` { "order": 1, "index_patterns": [ @@ -47,10 +47,12 @@ exports[`tests loading fields.yml: base.yml 1`] = ` "user": { "properties": { "auid": { - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "euid": { - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -59,7 +61,10 @@ exports[`tests loading fields.yml: base.yml 1`] = ` "nested": { "properties": { "foo": { - "type": "keyword" + "type": "text" + }, + "bar": { + "type": "long" } } } @@ -68,7 +73,1593 @@ exports[`tests loading fields.yml: base.yml 1`] = ` "nested": { "properties": { "bar": { + "type": "keyword", + "ignore_above": 1024 + }, + "baz": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "myalias": { + "type": "alias", + "path": "user.euid" + }, + "validarray": { + "type": "integer" + } + } + }, + "aliases": {} +} +`; + +exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` +{ + "order": 1, + "index_patterns": [ + "foo-*" + ], + "settings": { + "index": { + "lifecycle": { + "name": "logs-default" + }, + "codec": "best_compression", + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "refresh_interval": "5s", + "number_of_shards": "1", + "query": { + "default_field": [ + "message" + ] + }, + "number_of_routing_shards": "30" + } + }, + "mappings": { + "_meta": { + "package": "foo" + }, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "date_detection": false, + "properties": { + "coredns": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "query": { + "properties": { + "size": { + "type": "long" + }, + "class": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "response": { + "properties": { + "code": { + "type": "keyword", + "ignore_above": 1024 + }, + "flags": { + "type": "keyword", + "ignore_above": 1024 + }, + "size": { + "type": "long" + } + } + }, + "dnssec_ok": { + "type": "boolean" + } + } + } + } + }, + "aliases": {} +} +`; + +exports[`tests loading system.yml: system.yml 1`] = ` +{ + "order": 1, + "index_patterns": [ + "whatsthis-*" + ], + "settings": { + "index": { + "lifecycle": { + "name": "metrics-default" + }, + "codec": "best_compression", + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "refresh_interval": "5s", + "number_of_shards": "1", + "query": { + "default_field": [ + "message" + ] + }, + "number_of_routing_shards": "30" + } + }, + "mappings": { + "_meta": { + "package": "foo" + }, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "date_detection": false, + "properties": { + "system": { + "properties": { + "core": { + "properties": { + "id": { + "type": "long" + }, + "user": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } + } + }, + "system": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } + } + }, + "nice": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } + } + }, + "idle": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } + } + }, + "iowait": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } + } + }, + "irq": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } + } + }, + "softirq": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } + } + }, + "steal": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } + } + } + } + }, + "cpu": { + "properties": { + "cores": { + "type": "long" + }, + "user": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "ticks": { + "type": "long" + } + } + }, + "system": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "ticks": { + "type": "long" + } + } + }, + "nice": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "ticks": { + "type": "long" + } + } + }, + "idle": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "ticks": { + "type": "long" + } + } + }, + "iowait": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "ticks": { + "type": "long" + } + } + }, + "irq": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "ticks": { + "type": "long" + } + } + }, + "softirq": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "ticks": { + "type": "long" + } + } + }, + "steal": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "ticks": { + "type": "long" + } + } + }, + "total": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + } + } + } + } + }, + "diskio": { + "properties": { + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "serial_number": { + "type": "keyword", + "ignore_above": 1024 + }, + "read": { + "properties": { + "count": { + "type": "long" + }, + "bytes": { + "type": "long" + }, + "time": { + "type": "long" + } + } + }, + "write": { + "properties": { + "count": { + "type": "long" + }, + "bytes": { + "type": "long" + }, + "time": { + "type": "long" + } + } + }, + "io": { + "properties": { + "time": { + "type": "long" + } + } + }, + "iostat": { + "properties": { + "read": { + "properties": { + "request": { + "properties": { + "merges_per_sec": { + "type": "float" + }, + "per_sec": { + "type": "float" + } + } + }, + "per_sec": { + "properties": { + "bytes": { + "type": "float" + } + } + }, + "await": { + "type": "float" + } + } + }, + "write": { + "properties": { + "request": { + "properties": { + "merges_per_sec": { + "type": "float" + }, + "per_sec": { + "type": "float" + } + } + }, + "per_sec": { + "properties": { + "bytes": { + "type": "float" + } + } + }, + "await": { + "type": "float" + } + } + }, + "request": { + "properties": { + "avg_size": { + "type": "float" + } + } + }, + "queue": { + "properties": { + "avg_size": { + "type": "float" + } + } + }, + "await": { + "type": "float" + }, + "service_time": { + "type": "float" + }, + "busy": { + "type": "float" + } + } + } + } + }, + "entropy": { + "properties": { + "available_bits": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "filesystem": { + "properties": { + "available": { + "type": "long" + }, + "device_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + }, + "mount_point": { + "type": "keyword", + "ignore_above": 1024 + }, + "files": { + "type": "long" + }, + "free": { + "type": "long" + }, + "free_files": { + "type": "long" + }, + "total": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + } + } + }, + "fsstat": { + "properties": { + "count": { + "type": "long" + }, + "total_files": { + "type": "long" + }, + "total_size": { + "properties": { + "free": { + "type": "long" + }, + "used": { + "type": "long" + }, + "total": { + "type": "long" + } + } + } + } + }, + "load": { + "properties": { + "1": { + "type": "scaled_float", + "scaling_factor": 100 + }, + "5": { + "type": "scaled_float", + "scaling_factor": 100 + }, + "15": { + "type": "scaled_float", + "scaling_factor": 100 + }, + "norm": { + "properties": { + "1": { + "type": "scaled_float", + "scaling_factor": 100 + }, + "5": { + "type": "scaled_float", + "scaling_factor": 100 + }, + "15": { + "type": "scaled_float", + "scaling_factor": 100 + } + } + }, + "cores": { + "type": "long" + } + } + }, + "memory": { + "properties": { + "total": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "free": { + "type": "long" + }, + "actual": { + "properties": { + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "free": { + "type": "long" + } + } + }, + "swap": { + "properties": { + "total": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "free": { + "type": "long" + }, + "out": { + "properties": { + "pages": { + "type": "long" + } + } + }, + "in": { + "properties": { + "pages": { + "type": "long" + } + } + }, + "readahead": { + "properties": { + "pages": { + "type": "long" + }, + "cached": { + "type": "long" + } + } + } + } + }, + "hugepages": { + "properties": { + "total": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "long" + } + } + }, + "free": { + "type": "long" + }, + "reserved": { + "type": "long" + }, + "surplus": { + "type": "long" + }, + "default_size": { + "type": "long" + }, + "swap": { + "properties": { + "out": { + "properties": { + "pages": { + "type": "long" + }, + "fallback": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "network": { + "properties": { + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "out": { + "properties": { + "bytes": { + "type": "long" + }, + "packets": { + "type": "long" + }, + "errors": { + "type": "long" + }, + "dropped": { + "type": "long" + } + } + }, + "in": { + "properties": { + "bytes": { + "type": "long" + }, + "packets": { + "type": "long" + }, + "errors": { + "type": "long" + }, + "dropped": { + "type": "long" + } + } + } + } + }, + "network_summary": { + "properties": { + "ip": { + "properties": { + "*": { + "type": "object" + } + } + }, + "tcp": { + "properties": { + "*": { + "type": "object" + } + } + }, + "udp": { + "properties": { + "*": { + "type": "object" + } + } + }, + "udp_lite": { + "properties": { + "*": { + "type": "object" + } + } + }, + "icmp": { + "properties": { + "*": { + "type": "object" + } + } + } + } + }, + "process": { + "properties": { + "state": { + "type": "keyword", + "ignore_above": 1024 + }, + "cmdline": { + "type": "keyword", + "ignore_above": 2048 + }, + "env": { + "type": "object" + }, + "cpu": { + "properties": { + "user": { + "properties": { + "ticks": { + "type": "long" + } + } + }, + "total": { + "properties": { + "value": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "ticks": { + "type": "long" + } + } + }, + "system": { + "properties": { + "ticks": { + "type": "long" + } + } + }, + "start_time": { + "type": "date" + } + } + }, + "memory": { + "properties": { + "size": { + "type": "long" + }, + "rss": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } + } + }, + "share": { + "type": "long" + } + } + }, + "fd": { + "properties": { + "open": { + "type": "long" + }, + "limit": { + "properties": { + "soft": { + "type": "long" + }, + "hard": { + "type": "long" + } + } + } + } + }, + "cgroup": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "cpu": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "cfs": { + "properties": { + "period": { + "properties": { + "us": { + "type": "long" + } + } + }, + "quota": { + "properties": { + "us": { + "type": "long" + } + } + }, + "shares": { + "type": "long" + } + } + }, + "rt": { + "properties": { + "period": { + "properties": { + "us": { + "type": "long" + } + } + }, + "runtime": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "stats": { + "properties": { + "periods": { + "type": "long" + }, + "throttled": { + "properties": { + "periods": { + "type": "long" + }, + "ns": { + "type": "long" + } + } + } + } + } + } + }, + "cpuacct": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "total": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "stats": { + "properties": { + "user": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "system": { + "properties": { + "ns": { + "type": "long" + } + } + } + } + }, + "percpu": { + "type": "object" + } + } + }, + "memory": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "mem": { + "properties": { + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "failures": { + "type": "long" + } + } + }, + "memsw": { + "properties": { + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "failures": { + "type": "long" + } + } + }, + "kmem": { + "properties": { + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "failures": { + "type": "long" + } + } + }, + "kmem_tcp": { + "properties": { + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "failures": { + "type": "long" + } + } + }, + "stats": { + "properties": { + "active_anon": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "active_file": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "cache": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "hierarchical_memory_limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "hierarchical_memsw_limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "inactive_anon": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "inactive_file": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "mapped_file": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "page_faults": { + "type": "long" + }, + "major_page_faults": { + "type": "long" + }, + "pages_in": { + "type": "long" + }, + "pages_out": { + "type": "long" + }, + "rss": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "rss_huge": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "swap": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "unevictable": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "blkio": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "total": { + "properties": { + "bytes": { + "type": "long" + }, + "ios": { + "type": "long" + } + } + } + } + } + } + }, + "summary": { + "properties": { + "total": { + "type": "long" + }, + "running": { + "type": "long" + }, + "idle": { + "type": "long" + }, + "sleeping": { + "type": "long" + }, + "stopped": { + "type": "long" + }, + "zombie": { + "type": "long" + }, + "dead": { + "type": "long" + }, + "unknown": { + "type": "long" + } + } + } + } + }, + "raid": { + "properties": { + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "level": { + "type": "keyword", + "ignore_above": 1024 + }, + "sync_action": { + "type": "keyword", + "ignore_above": 1024 + }, + "disks": { + "properties": { + "active": { + "type": "long" + }, + "total": { + "type": "long" + }, + "spare": { + "type": "long" + }, + "failed": { + "type": "long" + }, + "states": { + "properties": { + "*": { + "type": "object" + } + } + } + } + }, + "blocks": { + "properties": { + "total": { + "type": "long" + }, + "synced": { + "type": "long" + } + } + } + } + }, + "socket": { + "properties": { + "local": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "remote": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + }, + "host": { + "type": "keyword", + "ignore_above": 1024 + }, + "etld_plus_one": { + "type": "keyword", + "ignore_above": 1024 + }, + "host_error": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "process": { + "properties": { + "cmdline": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "user": { + "properties": {} + }, + "summary": { + "properties": { + "all": { + "properties": { + "count": { + "type": "long" + }, + "listening": { + "type": "long" + } + } + }, + "tcp": { + "properties": { + "memory": { + "type": "long" + }, + "all": { + "properties": { + "orphan": { + "type": "long" + }, + "count": { + "type": "long" + }, + "listening": { + "type": "long" + }, + "established": { + "type": "long" + }, + "close_wait": { + "type": "long" + }, + "time_wait": { + "type": "long" + }, + "syn_sent": { + "type": "long" + }, + "syn_recv": { + "type": "long" + }, + "fin_wait1": { + "type": "long" + }, + "fin_wait2": { + "type": "long" + }, + "last_ack": { + "type": "long" + }, + "closing": { + "type": "long" + } + } + } + } + }, + "udp": { + "properties": { + "memory": { + "type": "long" + }, + "all": { + "properties": { + "count": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "uptime": { + "properties": { + "duration": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "users": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "seat": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + }, + "service": { + "type": "keyword", + "ignore_above": 1024 + }, + "remote": { + "type": "boolean" + }, + "state": { + "type": "keyword", + "ignore_above": 1024 + }, + "scope": { + "type": "keyword", + "ignore_above": 1024 + }, + "leader": { + "type": "long" + }, + "remote_host": { + "type": "keyword", + "ignore_above": 1024 + } + } } } } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index 005bb78e458e30..de4ba25590c983 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -12,7 +12,7 @@ import { ElasticsearchAssetType, } from '../../../../types'; import { CallESAsCurrentUser } from '../../../../types'; -import { Field, loadFieldsFromYaml } from '../../fields/field'; +import { Field, loadFieldsFromYaml, processFields } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; import { generateMappings, generateTemplateName, getTemplate } from './template'; import * as Registry from '../../registry'; @@ -98,7 +98,7 @@ export async function installTemplate({ dataset: Dataset; packageVersion: string; }): Promise { - const mappings = generateMappings(fields); + const mappings = generateMappings(processFields(fields)); const templateName = generateTemplateName(dataset); let pipelineName; if (dataset.ingest_pipeline) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts index aa5be59b6a5cde..f4e13748641ed6 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts @@ -28,15 +28,38 @@ test('get template', () => { expect(template.index_patterns).toStrictEqual([`${templateName}-*`]); }); -test('tests loading fields.yml', () => { - // Load fields.yml file +test('tests loading base.yml', () => { const ymlPath = path.join(__dirname, '../../fields/tests/base.yml'); const fieldsYML = readFileSync(ymlPath, 'utf-8'); const fields: Field[] = safeLoad(fieldsYML); - processFields(fields); - const mappings = generateMappings(fields); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); const template = getTemplate('logs', 'foo', mappings); expect(template).toMatchSnapshot(path.basename(ymlPath)); }); + +test('tests loading coredns.logs.yml', () => { + const ymlPath = path.join(__dirname, '../../fields/tests/coredns.logs.yml'); + const fieldsYML = readFileSync(ymlPath, 'utf-8'); + const fields: Field[] = safeLoad(fieldsYML); + + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + const template = getTemplate('logs', 'foo', mappings); + + expect(template).toMatchSnapshot(path.basename(ymlPath)); +}); + +test('tests loading system.yml', () => { + const ymlPath = path.join(__dirname, '../../fields/tests/system.yml'); + const fieldsYML = readFileSync(ymlPath, 'utf-8'); + const fields: Field[] = safeLoad(fieldsYML); + + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + const template = getTemplate('metrics', 'whatsthis', mappings); + + expect(template).toMatchSnapshot(path.basename(ymlPath)); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index f075771e9808a3..71c9acc6c10da7 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -14,6 +14,10 @@ interface Properties { interface Mappings { properties: any; } + +const DEFAULT_SCALING_FACTOR = 1000; +const DEFAULT_IGNORE_ABOVE = 1024; + /** * getTemplate retrieves the default template but overwrites the index pattern with the given value. * @@ -33,31 +37,98 @@ export function getTemplate( } /** - * Generate mapping takes the given fields array and creates the Elasticsearch + * Generate mapping takes the given nested fields array and creates the Elasticsearch * mapping properties out of it. * + * This assumes that all fields with dotted.names have been expanded in a previous step. + * * @param fields */ export function generateMappings(fields: Field[]): Mappings { const props: Properties = {}; - fields.forEach(field => { - // Are there more fields inside this field? Build them recursively - if (field.fields && field.fields.length > 0) { - props[field.name] = generateMappings(field.fields); - return; - } + // TODO: this can happen when the fields property in fields.yml is present but empty + // Maybe validation should be moved to fields/field.ts + if (fields) { + fields.forEach(field => { + // If type is not defined, assume keyword + const type = field.type || 'keyword'; + + let fieldProps = getDefaultProperties(field); + + switch (type) { + case 'group': + fieldProps = generateMappings(field.fields!); + break; + case 'integer': + fieldProps.type = 'long'; + break; + case 'scaled_float': + fieldProps.type = 'scaled_float'; + fieldProps.scaling_factor = field.scaling_factor || DEFAULT_SCALING_FACTOR; + break; + case 'text': + fieldProps.type = 'text'; + if (field.analyzer) { + fieldProps.analyzer = field.analyzer; + } + if (field.search_analyzer) { + fieldProps.search_analyzer = field.search_analyzer; + } + break; + case 'keyword': + fieldProps.type = 'keyword'; + if (field.ignore_above) { + fieldProps.ignore_above = field.ignore_above; + } else { + fieldProps.ignore_above = DEFAULT_IGNORE_ABOVE; + } + break; + // TODO move handling of multi_fields here? + case 'object': + // TODO improve + fieldProps.type = 'object'; + break; + case 'array': + // this assumes array fields were validated in an earlier step + // adding an array field with no object_type would result in an error + // when the template is added to ES + if (field.object_type) { + fieldProps.type = field.object_type; + } + break; + case 'alias': + // this assumes alias fields were validated in an earlier step + // adding a path to a field that doesn't exist would result in an error + // when the template is added to ES. + fieldProps.type = 'alias'; + fieldProps.path = field.path; + break; + default: + fieldProps.type = type; + } + props[field.name] = fieldProps; + }); + } - // If not type is defined, take keyword - const type = field.type || 'keyword'; - // Only add keyword fields for now - // TODO: add support for other field types - if (type === 'keyword') { - props[field.name] = { type }; - } - }); return { properties: props }; } +function getDefaultProperties(field: Field): Properties { + const properties: Properties = {}; + + if (field.index) { + properties.index = field.index; + } + if (field.doc_values) { + properties.doc_values = field.doc_values; + } + if (field.copy_to) { + properties.copy_to = field.copy_to; + } + + return properties; +} + /** * Generates the template name out of the given information */ diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/__snapshots__/field.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/fields/__snapshots__/field.test.ts.snap index 76991bde77008b..5c402b896093a7 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/__snapshots__/field.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/__snapshots__/field.test.ts.snap @@ -23,7 +23,12 @@ exports[`tests loading fields.yml: base.yml 1`] = ` "type": "group", "fields": [ { - "name": "foo" + "name": "foo", + "type": "text" + }, + { + "name": "bar", + "type": "integer" } ] } @@ -35,8 +40,21 @@ exports[`tests loading fields.yml: base.yml 1`] = ` "fields": [ { "name": "bar" + }, + { + "name": "baz" } ] + }, + { + "name": "myalias", + "type": "alias", + "path": "user.euid" + }, + { + "name": "validarray", + "type": "array", + "object_type": "integer" } ] `; @@ -54,46 +72,2395 @@ exports[`tests loading fields.yml: coredns.logs.yml 1`] = ` "description": "id of the DNS transaction\\n" }, { - "name": "query.size", - "type": "integer", - "format": "bytes", - "description": "size of the DNS query\\n" + "name": "query", + "type": "group", + "fields": [ + { + "name": "size", + "type": "integer", + "format": "bytes", + "description": "size of the DNS query\\n" + }, + { + "name": "class", + "type": "keyword", + "description": "DNS query class\\n" + }, + { + "name": "name", + "type": "keyword", + "description": "DNS query name\\n" + }, + { + "name": "type", + "type": "keyword", + "description": "DNS query type\\n" + } + ] }, { - "name": "query.class", - "type": "keyword", - "description": "DNS query class\\n" + "name": "response", + "type": "group", + "fields": [ + { + "name": "code", + "type": "keyword", + "description": "DNS response code\\n" + }, + { + "name": "flags", + "type": "keyword", + "description": "DNS response flags\\n" + }, + { + "name": "size", + "type": "integer", + "format": "bytes", + "description": "size of the DNS response\\n" + } + ] }, { - "name": "query.name", - "type": "keyword", - "description": "DNS query name\\n" + "name": "dnssec_ok", + "type": "boolean", + "description": "dnssec flag\\n" + } + ] + } +] +`; + +exports[`tests loading fields.yml: system.yml 1`] = ` +[ + { + "name": "system", + "type": "group", + "fields": [ + { + "name": "core", + "type": "group", + "description": "\`system-core\` contains CPU metrics for a single core of a multi-core system.\\n", + "fields": [ + { + "name": "id", + "type": "long", + "description": "CPU Core number.\\n" + }, + { + "name": "user", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in user space.\\n" + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent in user space.\\n" + } + ] + }, + { + "name": "system", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in kernel space.\\n" + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent in kernel space.\\n" + } + ] + }, + { + "name": "nice", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent on low-priority processes.\\n" + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent on low-priority processes.\\n" + } + ] + }, + { + "name": "idle", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent idle.\\n" + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent idle.\\n" + } + ] + }, + { + "name": "iowait", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in wait (on disk).\\n" + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent in wait (on disk).\\n" + } + ] + }, + { + "name": "irq", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent servicing and handling hardware interrupts.\\n" + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent servicing and handling hardware interrupts.\\n" + } + ] + }, + { + "name": "softirq", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent servicing and handling software interrupts.\\n" + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent servicing and handling software interrupts.\\n" + } + ] + }, + { + "name": "steal", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in involuntary wait by the virtual CPU while the hypervisor was servicing another processor. Available only on Unix.\\n" + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent in involuntary wait by the virtual CPU while the hypervisor was servicing another processor. Available only on Unix.\\n" + } + ] + } + ] }, { - "name": "query.type", - "type": "keyword", - "description": "DNS query type\\n" + "name": "cpu", + "type": "group", + "description": "\`cpu\` contains local CPU stats.\\n", + "release": "ga", + "fields": [ + { + "name": "cores", + "type": "long", + "description": "The number of CPU cores present on the host. The non-normalized percentages will have a maximum value of \`100% * cores\`. The normalized percentages already take this value into account and have a maximum value of 100%.\\n" + }, + { + "name": "user", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in user space. On multi-core systems, you can have percentages that are greater than 100%. For example, if 3 cores are at 60% use, then the \`system.cpu.user.pct\` will be 180%.\\n" + }, + { + "name": "norm", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in user space.\\n" + } + ] + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent in user space.\\n" + } + ] + }, + { + "name": "system", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in kernel space.\\n" + }, + { + "name": "norm", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in kernel space.\\n" + } + ] + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent in kernel space.\\n" + } + ] + }, + { + "name": "nice", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent on low-priority processes.\\n" + }, + { + "name": "norm", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent on low-priority processes.\\n" + } + ] + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent on low-priority processes.\\n" + } + ] + }, + { + "name": "idle", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent idle.\\n" + }, + { + "name": "norm", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent idle.\\n" + } + ] + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent idle.\\n" + } + ] + }, + { + "name": "iowait", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in wait (on disk).\\n" + }, + { + "name": "norm", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in wait (on disk).\\n" + } + ] + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent in wait (on disk).\\n" + } + ] + }, + { + "name": "irq", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent servicing and handling hardware interrupts.\\n" + }, + { + "name": "norm", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent servicing and handling hardware interrupts.\\n" + } + ] + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent servicing and handling hardware interrupts.\\n" + } + ] + }, + { + "name": "softirq", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent servicing and handling software interrupts.\\n" + }, + { + "name": "norm", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent servicing and handling software interrupts.\\n" + } + ] + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent servicing and handling software interrupts.\\n" + } + ] + }, + { + "name": "steal", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in involuntary wait by the virtual CPU while the hypervisor was servicing another processor. Available only on Unix.\\n" + }, + { + "name": "norm", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in involuntary wait by the virtual CPU while the hypervisor was servicing another processor. Available only on Unix.\\n" + } + ] + }, + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time spent in involuntary wait by the virtual CPU while the hypervisor was servicing another processor. Available only on Unix.\\n" + } + ] + }, + { + "name": "total", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent in states other than Idle and IOWait.\\n" + }, + { + "name": "norm", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time in states other than Idle and IOWait, normalised by the number of cores.\\n" + } + ] + } + ] + } + ] }, { - "name": "response.code", - "type": "keyword", - "description": "DNS response code\\n" + "name": "diskio", + "type": "group", + "description": "\`disk\` contains disk IO metrics collected from the operating system.\\n", + "release": "ga", + "fields": [ + { + "name": "name", + "type": "keyword", + "example": "sda1", + "description": "The disk name.\\n" + }, + { + "name": "serial_number", + "type": "keyword", + "description": "The disk's serial number. This may not be provided by all operating systems.\\n" + }, + { + "name": "read", + "type": "group", + "fields": [ + { + "name": "count", + "type": "long", + "description": "The total number of reads completed successfully.\\n" + }, + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The total number of bytes read successfully. On Linux this is the number of sectors read multiplied by an assumed sector size of 512.\\n" + }, + { + "name": "time", + "type": "long", + "description": "The total number of milliseconds spent by all reads.\\n" + } + ] + }, + { + "name": "write", + "type": "group", + "fields": [ + { + "name": "count", + "type": "long", + "description": "The total number of writes completed successfully.\\n" + }, + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The total number of bytes written successfully. On Linux this is the number of sectors written multiplied by an assumed sector size of 512.\\n" + }, + { + "name": "time", + "type": "long", + "description": "The total number of milliseconds spent by all writes.\\n" + } + ] + }, + { + "name": "io", + "type": "group", + "fields": [ + { + "name": "time", + "type": "long", + "description": "The total number of of milliseconds spent doing I/Os.\\n" + } + ] + }, + { + "name": "iostat", + "type": "group", + "fields": [ + { + "name": "read", + "type": "group", + "fields": [ + { + "name": "request", + "type": "group", + "fields": [ + { + "name": "merges_per_sec", + "type": "float", + "description": "The number of read requests merged per second that were queued to the device.\\n" + }, + { + "name": "per_sec", + "type": "float", + "description": "The number of read requests that were issued to the device per second\\n" + } + ] + }, + { + "name": "per_sec", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "float", + "description": "The number of Bytes read from the device per second.\\n", + "format": "bytes" + } + ] + }, + { + "name": "await", + "type": "float", + "description": "The average time spent for read requests issued to the device to be served.\\n" + } + ] + }, + { + "name": "write", + "type": "group", + "fields": [ + { + "name": "request", + "type": "group", + "fields": [ + { + "name": "merges_per_sec", + "type": "float", + "description": "The number of write requests merged per second that were queued to the device.\\n" + }, + { + "name": "per_sec", + "type": "float", + "description": "The number of write requests that were issued to the device per second\\n" + } + ] + }, + { + "name": "per_sec", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "float", + "description": "The number of Bytes write from the device per second.\\n", + "format": "bytes" + } + ] + }, + { + "name": "await", + "type": "float", + "description": "The average time spent for write requests issued to the device to be served.\\n" + } + ] + }, + { + "name": "request", + "type": "group", + "fields": [ + { + "name": "avg_size", + "type": "float", + "description": "The average size (in bytes) of the requests that were issued to the device.\\n" + } + ] + }, + { + "name": "queue", + "type": "group", + "fields": [ + { + "name": "avg_size", + "type": "float", + "description": "The average queue length of the requests that were issued to the device.\\n" + } + ] + }, + { + "name": "await", + "type": "float", + "description": "The average time spent for requests issued to the device to be served.\\n" + }, + { + "name": "service_time", + "type": "float", + "description": "The average service time (in milliseconds) for I/O requests that were issued to the device.\\n" + }, + { + "name": "busy", + "type": "float", + "description": "Percentage of CPU time during which I/O requests were issued to the device (bandwidth utilization for the device). Device saturation occurs when this value is close to 100%.\\n" + } + ] + } + ] }, { - "name": "response.flags", - "type": "keyword", - "description": "DNS response flags\\n" + "name": "entropy", + "type": "group", + "description": "Available system entropy\\n", + "release": "ga", + "fields": [ + { + "name": "available_bits", + "type": "long", + "description": "The available bits of entropy\\n" + }, + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of available entropy, relative to the pool size of 4096\\n" + } + ] }, { - "name": "response.size", - "type": "integer", - "format": "bytes", - "description": "size of the DNS response\\n" + "name": "filesystem", + "type": "group", + "description": "\`filesystem\` contains local filesystem stats.\\n", + "release": "ga", + "fields": [ + { + "name": "available", + "type": "long", + "format": "bytes", + "description": "The disk space available to an unprivileged user in bytes.\\n" + }, + { + "name": "device_name", + "type": "keyword", + "description": "The disk name. For example: \`/dev/disk1\`\\n" + }, + { + "name": "type", + "type": "keyword", + "description": "The disk type. For example: \`ext4\`\\n" + }, + { + "name": "mount_point", + "type": "keyword", + "description": "The mounting point. For example: \`/\`\\n" + }, + { + "name": "files", + "type": "long", + "description": "The total number of file nodes in the file system.\\n" + }, + { + "name": "free", + "type": "long", + "format": "bytes", + "description": "The disk space available in bytes.\\n" + }, + { + "name": "free_files", + "type": "long", + "description": "The number of free file nodes in the file system.\\n" + }, + { + "name": "total", + "type": "long", + "format": "bytes", + "description": "The total disk space in bytes.\\n" + }, + { + "name": "used", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The used disk space in bytes.\\n" + }, + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of used disk space.\\n" + } + ] + } + ] }, { - "name": "dnssec_ok", - "type": "boolean", - "description": "dnssec flag\\n" + "name": "fsstat", + "type": "group", + "description": "\`system.fsstat\` contains filesystem metrics aggregated from all mounted filesystems.\\n", + "release": "ga", + "fields": [ + { + "name": "count", + "type": "long", + "description": "Number of file systems found." + }, + { + "name": "total_files", + "type": "long", + "description": "Total number of files." + }, + { + "name": "total_size", + "format": "bytes", + "type": "group", + "description": "Nested file system docs.", + "fields": [ + { + "name": "free", + "type": "long", + "format": "bytes", + "description": "Total free space.\\n" + }, + { + "name": "used", + "type": "long", + "format": "bytes", + "description": "Total used space.\\n" + }, + { + "name": "total", + "type": "long", + "format": "bytes", + "description": "Total space (used plus free).\\n" + } + ] + } + ] + }, + { + "name": "load", + "type": "group", + "description": "CPU load averages.\\n", + "release": "ga", + "fields": [ + { + "name": "1", + "type": "scaled_float", + "scaling_factor": 100, + "description": "Load average for the last minute.\\n" + }, + { + "name": "5", + "type": "scaled_float", + "scaling_factor": 100, + "description": "Load average for the last 5 minutes.\\n" + }, + { + "name": "15", + "type": "scaled_float", + "scaling_factor": 100, + "description": "Load average for the last 15 minutes.\\n" + }, + { + "name": "norm", + "type": "group", + "fields": [ + { + "name": "1", + "type": "scaled_float", + "scaling_factor": 100, + "description": "Load for the last minute divided by the number of cores.\\n" + }, + { + "name": "5", + "type": "scaled_float", + "scaling_factor": 100, + "description": "Load for the last 5 minutes divided by the number of cores.\\n" + }, + { + "name": "15", + "type": "scaled_float", + "scaling_factor": 100, + "description": "Load for the last 15 minutes divided by the number of cores.\\n" + } + ] + }, + { + "name": "cores", + "type": "long", + "description": "The number of CPU cores present on the host.\\n" + } + ] + }, + { + "name": "memory", + "type": "group", + "description": "\`memory\` contains local memory stats.\\n", + "release": "ga", + "fields": [ + { + "name": "total", + "type": "long", + "format": "bytes", + "description": "Total memory.\\n" + }, + { + "name": "used", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Used memory.\\n" + }, + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of used memory.\\n" + } + ] + }, + { + "name": "free", + "type": "long", + "format": "bytes", + "description": "The total amount of free memory in bytes. This value does not include memory consumed by system caches and buffers (see system.memory.actual.free).\\n" + }, + { + "name": "actual", + "type": "group", + "description": "Actual memory used and free.\\n", + "fields": [ + { + "name": "used", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Actual used memory in bytes. It represents the difference between the total and the available memory. The available memory depends on the OS. For more details, please check \`system.actual.free\`.\\n" + }, + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of actual used memory.\\n" + } + ] + }, + { + "name": "free", + "type": "long", + "format": "bytes", + "description": "Actual free memory in bytes. It is calculated based on the OS. On Linux it consists of the free memory plus caches and buffers. On OSX it is a sum of free memory and the inactive memory. On Windows, it is equal to \`system.memory.free\`.\\n" + } + ] + }, + { + "name": "swap", + "type": "group", + "prefix": "[float]", + "description": "This group contains statistics related to the swap memory usage on the system.", + "fields": [ + { + "name": "total", + "type": "long", + "format": "bytes", + "description": "Total swap memory.\\n" + }, + { + "name": "used", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Used swap memory.\\n" + }, + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of used swap memory.\\n" + } + ] + }, + { + "name": "free", + "type": "long", + "format": "bytes", + "description": "Available swap memory.\\n" + }, + { + "name": "out", + "type": "group", + "fields": [ + { + "name": "pages", + "type": "long", + "description": "count of pages swapped out" + } + ] + }, + { + "name": "in", + "type": "group", + "fields": [ + { + "name": "pages", + "type": "long", + "description": "count of pages swapped in" + } + ] + }, + { + "name": "readahead", + "type": "group", + "fields": [ + { + "name": "pages", + "type": "long", + "description": "swap readahead pages" + }, + { + "name": "cached", + "type": "long", + "description": "swap readahead cache hits" + } + ] + } + ] + }, + { + "name": "hugepages", + "type": "group", + "prefix": "[float]", + "description": "This group contains statistics related to huge pages usage on the system.", + "fields": [ + { + "name": "total", + "type": "long", + "format": "number", + "description": "Number of huge pages in the pool.\\n" + }, + { + "name": "used", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Memory used in allocated huge pages.\\n" + }, + { + "name": "pct", + "type": "long", + "format": "percent", + "description": "Percentage of huge pages used.\\n" + } + ] + }, + { + "name": "free", + "type": "long", + "format": "number", + "description": "Number of available huge pages in the pool.\\n" + }, + { + "name": "reserved", + "type": "long", + "format": "number", + "description": "Number of reserved but not allocated huge pages in the pool.\\n" + }, + { + "name": "surplus", + "type": "long", + "format": "number", + "description": "Number of overcommited huge pages.\\n" + }, + { + "name": "default_size", + "type": "long", + "format": "bytes", + "description": "Default size for huge pages.\\n" + }, + { + "name": "swap", + "type": "group", + "fields": [ + { + "name": "out", + "type": "group", + "description": "huge pages swapped out", + "fields": [ + { + "name": "pages", + "type": "long", + "description": "pages swapped out" + }, + { + "name": "fallback", + "type": "long", + "description": "Count of huge pages that must be split before swapout" + } + ] + } + ] + } + ] + } + ] + }, + { + "name": "network", + "type": "group", + "description": "\`network\` contains network IO metrics for a single network interface.\\n", + "release": "ga", + "fields": [ + { + "name": "name", + "type": "keyword", + "example": "eth0", + "description": "The network interface name.\\n" + }, + { + "name": "out", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The number of bytes sent.\\n" + }, + { + "name": "packets", + "type": "long", + "description": "The number of packets sent.\\n" + }, + { + "name": "errors", + "type": "long", + "description": "The number of errors while sending.\\n" + }, + { + "name": "dropped", + "type": "long", + "description": "The number of outgoing packets that were dropped. This value is always 0 on Darwin and BSD because it is not reported by the operating system.\\n" + } + ] + }, + { + "name": "in", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The number of bytes received.\\n" + }, + { + "name": "packets", + "type": "long", + "description": "The number or packets received.\\n" + }, + { + "name": "errors", + "type": "long", + "description": "The number of errors while receiving.\\n" + }, + { + "name": "dropped", + "type": "long", + "description": "The number of incoming packets that were dropped.\\n" + } + ] + } + ] + }, + { + "name": "network_summary", + "type": "group", + "release": "beta", + "description": "Metrics relating to global network activity\\n", + "fields": [ + { + "name": "ip", + "type": "group", + "fields": [ + { + "name": "*", + "type": "object", + "description": "IP counters\\n" + } + ] + }, + { + "name": "tcp", + "type": "group", + "fields": [ + { + "name": "*", + "type": "object", + "description": "TCP counters\\n" + } + ] + }, + { + "name": "udp", + "type": "group", + "fields": [ + { + "name": "*", + "type": "object", + "description": "UDP counters\\n" + } + ] + }, + { + "name": "udp_lite", + "type": "group", + "fields": [ + { + "name": "*", + "type": "object", + "description": "UDP Lite counters\\n" + } + ] + }, + { + "name": "icmp", + "type": "group", + "fields": [ + { + "name": "*", + "type": "object", + "description": "ICMP counters\\n" + } + ] + } + ] + }, + { + "name": "process", + "type": "group", + "description": "\`process\` contains process metadata, CPU metrics, and memory metrics.\\n", + "release": "ga", + "fields": [ + { + "name": "state", + "type": "keyword", + "description": "The process state. For example: \\"running\\".\\n" + }, + { + "name": "cmdline", + "type": "keyword", + "description": "The full command-line used to start the process, including the arguments separated by space.\\n", + "ignore_above": 2048 + }, + { + "name": "env", + "type": "object", + "object_type": "keyword", + "description": "The environment variables used to start the process. The data is available on FreeBSD, Linux, and OS X.\\n" + }, + { + "name": "cpu", + "type": "group", + "prefix": "[float]", + "description": "CPU-specific statistics per process.", + "fields": [ + { + "name": "user", + "type": "group", + "fields": [ + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time the process spent in user space.\\n" + } + ] + }, + { + "name": "total", + "type": "group", + "fields": [ + { + "name": "value", + "type": "long", + "description": "The value of CPU usage since starting the process.\\n" + }, + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent by the process since the last update. Its value is similar to the %CPU value of the process displayed by the top command on Unix systems.\\n" + }, + { + "name": "norm", + "type": "group", + "fields": [ + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of CPU time spent by the process since the last event. This value is normalized by the number of CPU cores and it ranges from 0 to 100%.\\n" + } + ] + }, + { + "name": "ticks", + "type": "long", + "description": "The total CPU time spent by the process.\\n" + } + ] + }, + { + "name": "system", + "type": "group", + "fields": [ + { + "name": "ticks", + "type": "long", + "description": "The amount of CPU time the process spent in kernel space.\\n" + } + ] + }, + { + "name": "start_time", + "type": "date", + "description": "The time when the process was started.\\n" + } + ] + }, + { + "name": "memory", + "type": "group", + "description": "Memory-specific statistics per process.", + "prefix": "[float]", + "fields": [ + { + "name": "size", + "type": "long", + "format": "bytes", + "description": "The total virtual memory the process has.\\n" + }, + { + "name": "rss", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The Resident Set Size. The amount of memory the process occupied in main memory (RAM).\\n" + }, + { + "name": "pct", + "type": "scaled_float", + "format": "percent", + "description": "The percentage of memory the process occupied in main memory (RAM).\\n" + } + ] + }, + { + "name": "share", + "type": "long", + "format": "bytes", + "description": "The shared memory the process uses.\\n" + } + ] + }, + { + "name": "fd", + "type": "group", + "description": "File descriptor usage metrics. This set of metrics is available for Linux and FreeBSD.\\n", + "prefix": "[float]", + "fields": [ + { + "name": "open", + "type": "long", + "description": "The number of file descriptors open by the process." + }, + { + "name": "limit", + "type": "group", + "fields": [ + { + "name": "soft", + "type": "long", + "description": "The soft limit on the number of file descriptors opened by the process. The soft limit can be changed by the process at any time.\\n" + }, + { + "name": "hard", + "type": "long", + "description": "The hard limit on the number of file descriptors opened by the process. The hard limit can only be raised by root.\\n" + } + ] + } + ] + }, + { + "name": "cgroup", + "type": "group", + "description": "Metrics and limits from the cgroup of which the task is a member. cgroup metrics are reported when the process has membership in a non-root cgroup. These metrics are only available on Linux.\\n", + "fields": [ + { + "name": "id", + "type": "keyword", + "description": "The ID common to all cgroups associated with this task. If there isn't a common ID used by all cgroups this field will be absent.\\n" + }, + { + "name": "path", + "type": "keyword", + "description": "The path to the cgroup relative to the cgroup subsystem's mountpoint. If there isn't a common path used by all cgroups this field will be absent.\\n" + }, + { + "name": "cpu", + "type": "group", + "description": "The cpu subsystem schedules CPU access for tasks in the cgroup. Access can be controlled by two separate schedulers, CFS and RT. CFS stands for completely fair scheduler which proportionally divides the CPU time between cgroups based on weight. RT stands for real time scheduler which sets a maximum amount of CPU time that processes in the cgroup can consume during a given period.\\n", + "fields": [ + { + "name": "id", + "type": "keyword", + "description": "ID of the cgroup." + }, + { + "name": "path", + "type": "keyword", + "description": "Path to the cgroup relative to the cgroup subsystem's mountpoint.\\n" + }, + { + "name": "cfs", + "type": "group", + "fields": [ + { + "name": "period", + "type": "group", + "fields": [ + { + "name": "us", + "type": "long", + "description": "Period of time in microseconds for how regularly a cgroup's access to CPU resources should be reallocated.\\n" + } + ] + }, + { + "name": "quota", + "type": "group", + "fields": [ + { + "name": "us", + "type": "long", + "description": "Total amount of time in microseconds for which all tasks in a cgroup can run during one period (as defined by cfs.period.us).\\n" + } + ] + }, + { + "name": "shares", + "type": "long", + "description": "An integer value that specifies a relative share of CPU time available to the tasks in a cgroup. The value specified in the cpu.shares file must be 2 or higher.\\n" + } + ] + }, + { + "name": "rt", + "type": "group", + "fields": [ + { + "name": "period", + "type": "group", + "fields": [ + { + "name": "us", + "type": "long", + "description": "Period of time in microseconds for how regularly a cgroup's access to CPU resources is reallocated.\\n" + } + ] + }, + { + "name": "runtime", + "type": "group", + "fields": [ + { + "name": "us", + "type": "long", + "description": "Period of time in microseconds for the longest continuous period in which the tasks in a cgroup have access to CPU resources.\\n" + } + ] + } + ] + }, + { + "name": "stats", + "type": "group", + "fields": [ + { + "name": "periods", + "type": "long", + "description": "Number of period intervals (as specified in cpu.cfs.period.us) that have elapsed.\\n" + }, + { + "name": "throttled", + "type": "group", + "fields": [ + { + "name": "periods", + "type": "long", + "description": "Number of times tasks in a cgroup have been throttled (that is, not allowed to run because they have exhausted all of the available time as specified by their quota).\\n" + }, + { + "name": "ns", + "type": "long", + "description": "The total time duration (in nanoseconds) for which tasks in a cgroup have been throttled.\\n" + } + ] + } + ] + } + ] + }, + { + "name": "cpuacct", + "type": "group", + "description": "CPU accounting metrics.", + "fields": [ + { + "name": "id", + "type": "keyword", + "description": "ID of the cgroup." + }, + { + "name": "path", + "type": "keyword", + "description": "Path to the cgroup relative to the cgroup subsystem's mountpoint.\\n" + }, + { + "name": "total", + "type": "group", + "fields": [ + { + "name": "ns", + "type": "long", + "description": "Total CPU time in nanoseconds consumed by all tasks in the cgroup.\\n" + } + ] + }, + { + "name": "stats", + "type": "group", + "fields": [ + { + "name": "user", + "type": "group", + "fields": [ + { + "name": "ns", + "type": "long", + "description": "CPU time consumed by tasks in user mode." + } + ] + }, + { + "name": "system", + "type": "group", + "fields": [ + { + "name": "ns", + "type": "long", + "description": "CPU time consumed by tasks in user (kernel) mode." + } + ] + } + ] + }, + { + "name": "percpu", + "type": "object", + "object_type": "long", + "description": "CPU time (in nanoseconds) consumed on each CPU by all tasks in this cgroup.\\n" + } + ] + }, + { + "name": "memory", + "type": "group", + "description": "Memory limits and metrics.", + "fields": [ + { + "name": "id", + "type": "keyword", + "description": "ID of the cgroup." + }, + { + "name": "path", + "type": "keyword", + "description": "Path to the cgroup relative to the cgroup subsystem's mountpoint.\\n" + }, + { + "name": "mem", + "type": "group", + "fields": [ + { + "name": "usage", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Total memory usage by processes in the cgroup (in bytes).\\n" + }, + { + "name": "max", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The maximum memory used by processes in the cgroup (in bytes).\\n" + } + ] + } + ] + }, + { + "name": "limit", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The maximum amount of user memory in bytes (including file cache) that tasks in the cgroup are allowed to use.\\n" + } + ] + }, + { + "name": "failures", + "type": "long", + "description": "The number of times that the memory limit (mem.limit.bytes) was reached.\\n" + } + ] + }, + { + "name": "memsw", + "type": "group", + "fields": [ + { + "name": "usage", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The sum of current memory usage plus swap space used by processes in the cgroup (in bytes).\\n" + }, + { + "name": "max", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The maximum amount of memory and swap space used by processes in the cgroup (in bytes).\\n" + } + ] + } + ] + }, + { + "name": "limit", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The maximum amount for the sum of memory and swap usage that tasks in the cgroup are allowed to use.\\n" + } + ] + }, + { + "name": "failures", + "type": "long", + "description": "The number of times that the memory plus swap space limit (memsw.limit.bytes) was reached.\\n" + } + ] + }, + { + "name": "kmem", + "type": "group", + "fields": [ + { + "name": "usage", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Total kernel memory usage by processes in the cgroup (in bytes).\\n" + }, + { + "name": "max", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The maximum kernel memory used by processes in the cgroup (in bytes).\\n" + } + ] + } + ] + }, + { + "name": "limit", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The maximum amount of kernel memory that tasks in the cgroup are allowed to use.\\n" + } + ] + }, + { + "name": "failures", + "type": "long", + "description": "The number of times that the memory limit (kmem.limit.bytes) was reached.\\n" + } + ] + }, + { + "name": "kmem_tcp", + "type": "group", + "fields": [ + { + "name": "usage", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Total memory usage for TCP buffers in bytes.\\n" + }, + { + "name": "max", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The maximum memory used for TCP buffers by processes in the cgroup (in bytes).\\n" + } + ] + } + ] + }, + { + "name": "limit", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "The maximum amount of memory for TCP buffers that tasks in the cgroup are allowed to use.\\n" + } + ] + }, + { + "name": "failures", + "type": "long", + "description": "The number of times that the memory limit (kmem_tcp.limit.bytes) was reached.\\n" + } + ] + }, + { + "name": "stats", + "type": "group", + "fields": [ + { + "name": "active_anon", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Anonymous and swap cache on active least-recently-used (LRU) list, including tmpfs (shmem), in bytes.\\n" + } + ] + }, + { + "name": "active_file", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "File-backed memory on active LRU list, in bytes." + } + ] + }, + { + "name": "cache", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Page cache, including tmpfs (shmem), in bytes." + } + ] + }, + { + "name": "hierarchical_memory_limit", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Memory limit for the hierarchy that contains the memory cgroup, in bytes.\\n" + } + ] + }, + { + "name": "hierarchical_memsw_limit", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Memory plus swap limit for the hierarchy that contains the memory cgroup, in bytes.\\n" + } + ] + }, + { + "name": "inactive_anon", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Anonymous and swap cache on inactive LRU list, including tmpfs (shmem), in bytes\\n" + } + ] + }, + { + "name": "inactive_file", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "File-backed memory on inactive LRU list, in bytes.\\n" + } + ] + }, + { + "name": "mapped_file", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Size of memory-mapped mapped files, including tmpfs (shmem), in bytes.\\n" + } + ] + }, + { + "name": "page_faults", + "type": "long", + "description": "Number of times that a process in the cgroup triggered a page fault.\\n" + }, + { + "name": "major_page_faults", + "type": "long", + "description": "Number of times that a process in the cgroup triggered a major fault. \\"Major\\" faults happen when the kernel actually has to read the data from disk.\\n" + }, + { + "name": "pages_in", + "type": "long", + "description": "Number of pages paged into memory. This is a counter.\\n" + }, + { + "name": "pages_out", + "type": "long", + "description": "Number of pages paged out of memory. This is a counter.\\n" + }, + { + "name": "rss", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Anonymous and swap cache (includes transparent hugepages), not including tmpfs (shmem), in bytes.\\n" + } + ] + }, + { + "name": "rss_huge", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Number of bytes of anonymous transparent hugepages.\\n" + } + ] + }, + { + "name": "swap", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Swap usage, in bytes.\\n" + } + ] + }, + { + "name": "unevictable", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Memory that cannot be reclaimed, in bytes.\\n" + } + ] + } + ] + } + ] + }, + { + "name": "blkio", + "type": "group", + "description": "Block IO metrics.", + "fields": [ + { + "name": "id", + "type": "keyword", + "description": "ID of the cgroup." + }, + { + "name": "path", + "type": "keyword", + "description": "Path to the cgroup relative to the cgroup subsystems mountpoint.\\n" + }, + { + "name": "total", + "type": "group", + "fields": [ + { + "name": "bytes", + "type": "long", + "format": "bytes", + "description": "Total number of bytes transferred to and from all block devices by processes in the cgroup.\\n" + }, + { + "name": "ios", + "type": "long", + "description": "Total number of I/O operations performed on all devices by processes in the cgroup as seen by the throttling policy.\\n" + } + ] + } + ] + } + ] + }, + { + "name": "summary", + "title": "Process Summary", + "type": "group", + "description": "Summary metrics for the processes running on the host.\\n", + "release": "ga", + "fields": [ + { + "name": "total", + "type": "long", + "description": "Total number of processes on this host.\\n" + }, + { + "name": "running", + "type": "long", + "description": "Number of running processes on this host.\\n" + }, + { + "name": "idle", + "type": "long", + "description": "Number of idle processes on this host.\\n" + }, + { + "name": "sleeping", + "type": "long", + "description": "Number of sleeping processes on this host.\\n" + }, + { + "name": "stopped", + "type": "long", + "description": "Number of stopped processes on this host.\\n" + }, + { + "name": "zombie", + "type": "long", + "description": "Number of zombie processes on this host.\\n" + }, + { + "name": "dead", + "type": "long", + "description": "Number of dead processes on this host. It's very unlikely that it will appear but in some special situations it may happen.\\n" + }, + { + "name": "unknown", + "type": "long", + "description": "Number of processes for which the state couldn't be retrieved or is unknown.\\n" + } + ] + } + ] + }, + { + "name": "raid", + "type": "group", + "description": "raid\\n", + "release": "ga", + "fields": [ + { + "name": "name", + "type": "keyword", + "description": "Name of the device.\\n" + }, + { + "name": "status", + "type": "keyword", + "description": "activity-state of the device.\\n" + }, + { + "name": "level", + "type": "keyword", + "description": "The raid level of the device\\n" + }, + { + "name": "sync_action", + "type": "keyword", + "description": "Current sync action, if the RAID array is redundant\\n" + }, + { + "name": "disks", + "type": "group", + "fields": [ + { + "name": "active", + "type": "long", + "description": "Number of active disks.\\n" + }, + { + "name": "total", + "type": "long", + "description": "Total number of disks the device consists of.\\n" + }, + { + "name": "spare", + "type": "long", + "description": "Number of spared disks.\\n" + }, + { + "name": "failed", + "type": "long", + "description": "Number of failed disks.\\n" + }, + { + "name": "states", + "type": "group", + "fields": [ + { + "name": "*", + "type": "object", + "object_type": "keyword", + "description": "map of raw disk states\\n" + } + ] + } + ] + }, + { + "name": "blocks", + "type": "group", + "fields": [ + { + "name": "total", + "type": "long", + "description": "Number of blocks the device holds, in 1024-byte blocks.\\n" + }, + { + "name": "synced", + "type": "long", + "description": "Number of blocks on the device that are in sync, in 1024-byte blocks.\\n" + } + ] + } + ] + }, + { + "name": "socket", + "type": "group", + "description": "TCP sockets that are active.\\n", + "release": "ga", + "fields": [ + { + "name": "local", + "type": "group", + "fields": [ + { + "name": "ip", + "type": "ip", + "example": "192.0.2.1 or 2001:0DB8:ABED:8536::1", + "description": "Local IP address. This can be an IPv4 or IPv6 address.\\n" + }, + { + "name": "port", + "type": "long", + "example": 22, + "description": "Local port.\\n" + } + ] + }, + { + "name": "remote", + "type": "group", + "fields": [ + { + "name": "ip", + "type": "ip", + "example": "192.0.2.1 or 2001:0DB8:ABED:8536::1", + "description": "Remote IP address. This can be an IPv4 or IPv6 address.\\n" + }, + { + "name": "port", + "type": "long", + "example": 22, + "description": "Remote port.\\n" + }, + { + "name": "host", + "type": "keyword", + "example": "76-211-117-36.nw.example.com.", + "description": "PTR record associated with the remote IP. It is obtained via reverse IP lookup.\\n" + }, + { + "name": "etld_plus_one", + "type": "keyword", + "example": "example.com.", + "description": "The effective top-level domain (eTLD) of the remote host plus one more label. For example, the eTLD+1 for \\"foo.bar.golang.org.\\" is \\"golang.org.\\". The data for determining the eTLD comes from an embedded copy of the data from http://publicsuffix.org.\\n" + }, + { + "name": "host_error", + "type": "keyword", + "description": "Error describing the cause of the reverse lookup failure.\\n" + } + ] + }, + { + "name": "process", + "type": "group", + "fields": [ + { + "name": "cmdline", + "type": "keyword", + "description": "Full command line\\n" + } + ] + }, + { + "name": "user", + "type": "group", + "fields": [] + }, + { + "name": "summary", + "title": "Socket summary", + "type": "group", + "description": "Summary metrics of open sockets in the host system\\n", + "release": "ga", + "fields": [ + { + "name": "all", + "type": "group", + "description": "All connections\\n", + "fields": [ + { + "name": "count", + "type": "integer", + "description": "All open connections\\n" + }, + { + "name": "listening", + "type": "integer", + "description": "All listening ports\\n" + } + ] + }, + { + "name": "tcp", + "type": "group", + "description": "All TCP connections\\n", + "fields": [ + { + "name": "memory", + "type": "integer", + "format": "bytes", + "description": "Memory used by TCP sockets in bytes, based on number of allocated pages and system page size. Corresponds to limits set in /proc/sys/net/ipv4/tcp_mem. Only available on Linux.\\n" + }, + { + "name": "all", + "type": "group", + "description": "All TCP connections\\n", + "fields": [ + { + "name": "orphan", + "type": "integer", + "description": "A count of all orphaned tcp sockets. Only available on Linux.\\n" + }, + { + "name": "count", + "type": "integer", + "description": "All open TCP connections\\n" + }, + { + "name": "listening", + "type": "integer", + "description": "All TCP listening ports\\n" + }, + { + "name": "established", + "type": "integer", + "description": "Number of established TCP connections\\n" + }, + { + "name": "close_wait", + "type": "integer", + "description": "Number of TCP connections in _close_wait_ state\\n" + }, + { + "name": "time_wait", + "type": "integer", + "description": "Number of TCP connections in _time_wait_ state\\n" + }, + { + "name": "syn_sent", + "type": "integer", + "description": "Number of TCP connections in _syn_sent_ state\\n" + }, + { + "name": "syn_recv", + "type": "integer", + "description": "Number of TCP connections in _syn_recv_ state\\n" + }, + { + "name": "fin_wait1", + "type": "integer", + "description": "Number of TCP connections in _fin_wait1_ state\\n" + }, + { + "name": "fin_wait2", + "type": "integer", + "description": "Number of TCP connections in _fin_wait2_ state\\n" + }, + { + "name": "last_ack", + "type": "integer", + "description": "Number of TCP connections in _last_ack_ state\\n" + }, + { + "name": "closing", + "type": "integer", + "description": "Number of TCP connections in _closing_ state\\n" + } + ] + } + ] + }, + { + "name": "udp", + "type": "group", + "description": "All UDP connections\\n", + "fields": [ + { + "name": "memory", + "type": "integer", + "format": "bytes", + "description": "Memory used by UDP sockets in bytes, based on number of allocated pages and system page size. Corresponds to limits set in /proc/sys/net/ipv4/udp_mem. Only available on Linux.\\n" + }, + { + "name": "all", + "type": "group", + "description": "All UDP connections\\n", + "fields": [ + { + "name": "count", + "type": "integer", + "description": "All open UDP connections\\n" + } + ] + } + ] + } + ] + } + ] + }, + { + "name": "uptime", + "type": "group", + "description": "\`uptime\` contains the operating system uptime metric.\\n", + "release": "ga", + "fields": [ + { + "name": "duration", + "type": "group", + "fields": [ + { + "name": "ms", + "type": "long", + "format": "duration", + "input_format": "milliseconds", + "description": "The OS uptime in milliseconds.\\n" + } + ] + } + ] + }, + { + "name": "users", + "type": "group", + "release": "beta", + "description": "Logged-in user session data\\n", + "fields": [ + { + "name": "id", + "type": "keyword", + "description": "The ID of the session\\n" + }, + { + "name": "seat", + "type": "keyword", + "description": "An associated logind seat\\n" + }, + { + "name": "path", + "type": "keyword", + "description": "The DBus object path of the session\\n" + }, + { + "name": "type", + "type": "keyword", + "description": "The type of the user session\\n" + }, + { + "name": "service", + "type": "keyword", + "description": "A session associated with the service\\n" + }, + { + "name": "remote", + "type": "boolean", + "description": "A bool indicating a remote session\\n" + }, + { + "name": "state", + "type": "keyword", + "description": "The current state of the session\\n" + }, + { + "name": "scope", + "type": "keyword", + "description": "The associated systemd scope\\n" + }, + { + "name": "leader", + "type": "long", + "description": "The root PID of the session\\n" + }, + { + "name": "remote_host", + "type": "keyword", + "description": "A remote host address for the session\\n" + } + ] } ] } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts index 3cdf011d9d0e32..929f2518ee748e 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts @@ -8,7 +8,7 @@ import { readFileSync } from 'fs'; import glob from 'glob'; import { safeLoad } from 'js-yaml'; import path from 'path'; -import { Field, processFields } from './field'; +import { Field, Fields, getField, processFields } from './field'; // Add our own serialiser to just do JSON.stringify expect.addSnapshotSerializer({ @@ -27,9 +27,56 @@ test('tests loading fields.yml', () => { for (const file of files) { const fieldsYML = readFileSync(file, 'utf-8'); const fields: Field[] = safeLoad(fieldsYML); - processFields(fields); + const processedFields = processFields(fields); // Check that content file and generated file are equal - expect(fields).toMatchSnapshot(path.basename(file)); + expect(processedFields).toMatchSnapshot(path.basename(file)); } }); + +describe('getField searches recursively for nested field in fields given an array of path parts', () => { + const searchFields: Fields = [ + { + name: '1', + fields: [ + { + name: '1-1', + }, + { + name: '1-2', + }, + ], + }, + { + name: '2', + fields: [ + { + name: '2-1', + }, + { + name: '2-2', + fields: [ + { + name: '2-2-1', + }, + { + name: '2-2-2', + }, + ], + }, + ], + }, + ]; + test('returns undefined when the field does not exist', () => { + expect(getField(searchFields, ['0'])).toBe(undefined); + }); + test('returns undefined if the field is not a leaf node', () => { + expect(getField(searchFields, ['1'])?.name).toBe(undefined); + }); + test('returns undefined searching for a nested field that does not exist', () => { + expect(getField(searchFields, ['1', '1-3'])?.name).toBe(undefined); + }); + test('returns nested field that is a leaf node', () => { + expect(getField(searchFields, ['2', '2-2', '2-2-1'])?.name).toBe('2-2-1'); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts index eb515f5652f36e..4a1a84baf6599e 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts @@ -21,6 +21,12 @@ export interface Field { required?: boolean; multi_fields?: Fields; doc_values?: boolean; + copy_to?: string; + analyzer?: string; + search_analyzer?: string; + ignore_above?: number; + object_type?: string; + scaling_factor?: number; // Kibana specific analyzed?: boolean; @@ -43,44 +49,140 @@ export interface Field { export type Fields = Field[]; /** - * ProcessFields takes the given fields read from yaml and expands it. + * expandFields takes the given fields read from yaml and expands them. * There are dotted fields in the field.yml like `foo.bar`. These should - * be stored as an object inside an object and is the main purpose of this - * preprocessing. + * be stored as an field within a 'group' field. * - * Note: This function modifies the passed field param. + * Note: This function modifies the passed fields array. */ -export function processFields(fields: Fields) { +export function expandFields(fields: Fields) { fields.forEach((field, key) => { const fieldName = field.name; - // If the field name contains a dot, it means we need to create sub objects + // If the field name contains a dot, it means we need to + // - take the first part of the name + // - create a field of type 'group' with this first part + // - put the original field, named with the rest of the original name in the fields property of the new group field if (fieldName.includes('.')) { // Split up the name by dots to extract first and other parts const nameParts = fieldName.split('.'); // Getting first part of the name for the new field - const newNameTop = nameParts[0]; - delete nameParts[0]; + const groupFieldName = nameParts[0]; // Put back together the parts again for the new field name - const newName = nameParts.length === 1 ? nameParts[0] : nameParts.slice(1).join('.'); + const restFieldName = nameParts.slice(1).join('.'); - field.name = newName; + // keep all properties of the original field, but give it the shortened name + field.name = restFieldName; - // Create the new field with the old field inside - const newField: Field = { - name: newNameTop, + // create a new field of type group with the original field in the fields array + const groupField: Field = { + name: groupFieldName, type: 'group', fields: [field], }; - // Replace the old field in the array - fields[key] = newField; - if (newField.fields) { - processFields(newField.fields); + // check child fields further down the tree + if (groupField.fields) { + expandFields(groupField.fields); } + // Replace the original field in the array with the new one + fields[key] = groupField; + } else { + // even if this field doesn't have dots to expand, its child fields further down the tree might + if (field.fields) { + expandFields(field.fields); + } + } + }); +} +/** + * dedupFields takes the given fields and merges sibling fields with the + * same name together. + * These can result from expandFields when the input contains dotted field + * names that share parts of their hierarchy. + */ +function dedupFields(fields: Fields): Fields { + const dedupedFields: Fields = []; + fields.forEach(field => { + const found = dedupedFields.find(f => { + return f.name === field.name; + }); + if (found) { + if (found.type === 'group' && field.type === 'group' && found.fields && field.fields) { + found.fields = dedupFields(found.fields.concat(field.fields)); + } else { + // only 'group' fields can be merged in this way + // XXX: don't abort on error for now + // see discussion in https://github.com/elastic/kibana/pull/59894 + // throw new Error( + // "Can't merge fields " + JSON.stringify(found) + ' and ' + JSON.stringify(field) + // ); + } + } else { + if (field.fields) { + field.fields = dedupFields(field.fields); + } + dedupedFields.push(field); } }); + return dedupedFields; +} + +/** validateFields takes the given fields and verifies: + * + * - all fields of type alias point to existing fields. + * - all fields of type array have a property object_type + * + * Invalid fields are silently removed. + */ + +function validateFields(fields: Fields, allFields: Fields): Fields { + const validatedFields: Fields = []; + + fields.forEach(field => { + if (field.type === 'alias') { + if (field.path && getField(allFields, field.path.split('.'))) { + validatedFields.push(field); + } + } else if (field.type === 'array') { + if (field.object_type) { + validatedFields.push(field); + } + } else { + validatedFields.push(field); + } + if (field.fields) { + field.fields = validateFields(field.fields, allFields); + } + }); + return validatedFields; +} + +export const getField = (fields: Fields, pathNames: string[]): Field | undefined => { + if (!pathNames.length) return undefined; + // get the first rest of path names + const [name, ...restPathNames] = pathNames; + for (const field of fields) { + if (field.name === name) { + // check field's fields, passing in the remaining path names + if (field.fields && field.fields.length > 0) { + return getField(field.fields, restPathNames); + } + // no nested fields to search, but still more names - not found + if (restPathNames.length) { + return undefined; + } + return field; + } + } + return undefined; +}; + +export function processFields(fields: Fields): Fields { + expandFields(fields); + const dedupedFields = dedupFields(fields); + return validateFields(dedupedFields, dedupedFields); } const isFields = (path: string) => { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/tests/base.yml b/x-pack/plugins/ingest_manager/server/services/epm/fields/tests/base.yml index 86b61245aa3b80..5a71c7dee54dc0 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/tests/base.yml +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/tests/base.yml @@ -4,4 +4,20 @@ - name: auid - name: euid - name: long.nested.foo + type: text +- name: long.nested.bar + type: integer - name: nested.bar +- name: nested.baz +- name: myalias + type: alias + path: user.euid +- name: invalidalias + type: alias + path: euid +- name: validarray + type: array + object_type: integer +- name: invalidarray + type: array + diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/tests/system.yml b/x-pack/plugins/ingest_manager/server/services/epm/fields/tests/system.yml new file mode 100644 index 00000000000000..609914616a6837 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/tests/system.yml @@ -0,0 +1,1625 @@ +- name: system.core + type: group + description: > + `system-core` contains CPU metrics for a single core of a multi-core system. + fields: + - name: id + type: long + description: > + CPU Core number. + + # Percentages + - name: user.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in user space. + + - name: user.ticks + type: long + description: > + The amount of CPU time spent in user space. + + - name: system.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in kernel space. + + - name: system.ticks + type: long + description: > + The amount of CPU time spent in kernel space. + + - name: nice.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent on low-priority processes. + + - name: nice.ticks + type: long + description: > + The amount of CPU time spent on low-priority processes. + + - name: idle.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent idle. + + - name: idle.ticks + type: long + description: > + The amount of CPU time spent idle. + + - name: iowait.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in wait (on disk). + + - name: iowait.ticks + type: long + description: > + The amount of CPU time spent in wait (on disk). + + - name: irq.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent servicing and handling hardware interrupts. + + - name: irq.ticks + type: long + description: > + The amount of CPU time spent servicing and handling hardware interrupts. + + - name: softirq.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent servicing and handling software interrupts. + + - name: softirq.ticks + type: long + description: > + The amount of CPU time spent servicing and handling software interrupts. + + - name: steal.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in involuntary wait by the virtual CPU while the hypervisor + was servicing another processor. + Available only on Unix. + + - name: steal.ticks + type: long + description: > + The amount of CPU time spent in involuntary wait by the virtual CPU while the hypervisor + was servicing another processor. + Available only on Unix. +- name: system.cpu + type: group + description: > + `cpu` contains local CPU stats. + release: ga + fields: + - name: cores + type: long + description: > + The number of CPU cores present on the host. The non-normalized + percentages will have a maximum value of `100% * cores`. The + normalized percentages already take this value into account and have + a maximum value of 100%. + + # Percentages + - name: user.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in user space. On multi-core systems, + you can have percentages that are greater than 100%. For example, if 3 + cores are at 60% use, then the `system.cpu.user.pct` will be 180%. + + - name: system.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in kernel space. + + - name: nice.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent on low-priority processes. + + - name: idle.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent idle. + + - name: iowait.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in wait (on disk). + + - name: irq.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent servicing and handling hardware interrupts. + + - name: softirq.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent servicing and handling software interrupts. + + - name: steal.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in involuntary wait by the virtual CPU while the hypervisor + was servicing another processor. + Available only on Unix. + + - name: total.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in states other than Idle and IOWait. + + # Normalized Percentages + - name: user.norm.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in user space. + + - name: system.norm.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in kernel space. + + - name: nice.norm.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent on low-priority processes. + + - name: idle.norm.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent idle. + + - name: iowait.norm.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in wait (on disk). + + - name: irq.norm.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent servicing and handling hardware interrupts. + + - name: softirq.norm.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent servicing and handling software interrupts. + + - name: steal.norm.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent in involuntary wait by the virtual CPU while the hypervisor + was servicing another processor. + Available only on Unix. + + - name: total.norm.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time in states other than Idle and IOWait, normalised by the number of cores. + + + # Ticks + - name: user.ticks + type: long + description: > + The amount of CPU time spent in user space. + + - name: system.ticks + type: long + description: > + The amount of CPU time spent in kernel space. + + - name: nice.ticks + type: long + description: > + The amount of CPU time spent on low-priority processes. + + - name: idle.ticks + type: long + description: > + The amount of CPU time spent idle. + + - name: iowait.ticks + type: long + description: > + The amount of CPU time spent in wait (on disk). + + - name: irq.ticks + type: long + description: > + The amount of CPU time spent servicing and handling hardware interrupts. + + - name: softirq.ticks + type: long + description: > + The amount of CPU time spent servicing and handling software interrupts. + + - name: steal.ticks + type: long + description: > + The amount of CPU time spent in involuntary wait by the virtual CPU while the hypervisor + was servicing another processor. + Available only on Unix. +- name: system.diskio + type: group + description: > + `disk` contains disk IO metrics collected from the operating system. + release: ga + fields: + - name: name + type: keyword + example: sda1 + description: > + The disk name. + + - name: serial_number + type: keyword + description: > + The disk's serial number. This may not be provided by all operating + systems. + + - name: read.count + type: long + description: > + The total number of reads completed successfully. + + - name: write.count + type: long + description: > + The total number of writes completed successfully. + + - name: read.bytes + type: long + format: bytes + description: > + The total number of bytes read successfully. On Linux this is + the number of sectors read multiplied by an assumed sector size of 512. + + - name: write.bytes + type: long + format: bytes + description: > + The total number of bytes written successfully. On Linux this is + the number of sectors written multiplied by an assumed sector size of + 512. + + - name: read.time + type: long + description: > + The total number of milliseconds spent by all reads. + + - name: write.time + type: long + description: > + The total number of milliseconds spent by all writes. + + - name: io.time + type: long + description: > + The total number of of milliseconds spent doing I/Os. + + - name: iostat.read.request.merges_per_sec + type: float + description: > + The number of read requests merged per second that were queued to the device. + + - name: iostat.write.request.merges_per_sec + type: float + description: > + The number of write requests merged per second that were queued to the device. + + - name: iostat.read.request.per_sec + type: float + description: > + The number of read requests that were issued to the device per second + + - name: iostat.write.request.per_sec + type: float + description: > + The number of write requests that were issued to the device per second + + - name: iostat.read.per_sec.bytes + type: float + description: > + The number of Bytes read from the device per second. + format: bytes + + - name: iostat.read.await + type: float + description: > + The average time spent for read requests issued to the device to be served. + + - name: iostat.write.per_sec.bytes + type: float + description: > + The number of Bytes write from the device per second. + format: bytes + + - name: iostat.write.await + type: float + description: > + The average time spent for write requests issued to the device to be served. + + - name: iostat.request.avg_size + type: float + description: > + The average size (in bytes) of the requests that were issued to the device. + + - name: iostat.queue.avg_size + type: float + description: > + The average queue length of the requests that were issued to the device. + + - name: iostat.await + type: float + description: > + The average time spent for requests issued to the device to be served. + + - name: iostat.service_time + type: float + description: > + The average service time (in milliseconds) for I/O requests that were issued to the device. + + - name: iostat.busy + type: float + description: > + Percentage of CPU time during which I/O requests were issued to the device (bandwidth utilization for the device). Device saturation occurs when this value is close to 100%. +- name: system.entropy + type: group + description: > + Available system entropy + release: ga + fields: + - name: available_bits + type: long + description: > + The available bits of entropy + - name: pct + type: scaled_float + format: percent + description: > + The percentage of available entropy, relative to the pool size of 4096 +- name: system.filesystem + type: group + description: > + `filesystem` contains local filesystem stats. + release: ga + fields: + - name: available + type: long + format: bytes + description: > + The disk space available to an unprivileged user in bytes. + - name: device_name + type: keyword + description: > + The disk name. For example: `/dev/disk1` + - name: type + type: keyword + description: > + The disk type. For example: `ext4` + - name: mount_point + type: keyword + description: > + The mounting point. For example: `/` + - name: files + type: long + description: > + The total number of file nodes in the file system. + - name: free + type: long + format: bytes + description: > + The disk space available in bytes. + - name: free_files + type: long + description: > + The number of free file nodes in the file system. + - name: total + type: long + format: bytes + description: > + The total disk space in bytes. + - name: used.bytes + type: long + format: bytes + description: > + The used disk space in bytes. + - name: used.pct + type: scaled_float + format: percent + description: > + The percentage of used disk space. +- name: system.fsstat + type: group + description: > + `system.fsstat` contains filesystem metrics aggregated from all mounted + filesystems. + release: ga + fields: + - name: count + type: long + description: Number of file systems found. + - name: total_files + type: long + description: Total number of files. + - name: total_size + format: bytes + type: group + description: Nested file system docs. + fields: + - name: free + type: long + format: bytes + description: > + Total free space. + - name: used + type: long + format: bytes + description: > + Total used space. + - name: total + type: long + format: bytes + description: > + Total space (used plus free). +- name: system.load + type: group + description: > + CPU load averages. + release: ga + fields: + - name: "1" + type: scaled_float + scaling_factor: 100 + description: > + Load average for the last minute. + - name: "5" + type: scaled_float + scaling_factor: 100 + description: > + Load average for the last 5 minutes. + - name: "15" + type: scaled_float + scaling_factor: 100 + description: > + Load average for the last 15 minutes. + + - name: "norm.1" + type: scaled_float + scaling_factor: 100 + description: > + Load for the last minute divided by the number of cores. + + - name: "norm.5" + type: scaled_float + scaling_factor: 100 + description: > + Load for the last 5 minutes divided by the number of cores. + + - name: "norm.15" + type: scaled_float + scaling_factor: 100 + description: > + Load for the last 15 minutes divided by the number of cores. + + - name: "cores" + type: long + description: > + The number of CPU cores present on the host. +- name: system.memory + type: group + description: > + `memory` contains local memory stats. + release: ga + fields: + - name: total + type: long + format: bytes + description: > + Total memory. + + - name: used.bytes + type: long + format: bytes + description: > + Used memory. + + - name: free + type: long + format: bytes + description: > + The total amount of free memory in bytes. This value does not include memory consumed by system caches and + buffers (see system.memory.actual.free). + + - name: used.pct + type: scaled_float + format: percent + description: > + The percentage of used memory. + + - name: actual + type: group + description: > + Actual memory used and free. + fields: + + - name: used.bytes + type: long + format: bytes + description: > + Actual used memory in bytes. It represents the difference between the total and the available memory. The + available memory depends on the OS. For more details, please check `system.actual.free`. + + - name: free + type: long + format: bytes + description: > + Actual free memory in bytes. It is calculated based on the OS. On Linux it consists of the free memory + plus caches and buffers. On OSX it is a sum of free memory and the inactive memory. On Windows, it is equal + to `system.memory.free`. + + - name: used.pct + type: scaled_float + format: percent + description: > + The percentage of actual used memory. + + - name: swap + type: group + prefix: "[float]" + description: This group contains statistics related to the swap memory usage on the system. + fields: + - name: total + type: long + format: bytes + description: > + Total swap memory. + + - name: used.bytes + type: long + format: bytes + description: > + Used swap memory. + + - name: free + type: long + format: bytes + description: > + Available swap memory. + + - name: out.pages + type: long + description: count of pages swapped out + + - name: in.pages + type: long + description: count of pages swapped in + + - name: readahead.pages + type: long + description: swap readahead pages + + - name: readahead.cached + type: long + description: swap readahead cache hits + + - name: used.pct + type: scaled_float + format: percent + description: > + The percentage of used swap memory. + + - name: hugepages + type: group + prefix: "[float]" + description: This group contains statistics related to huge pages usage on the system. + fields: + - name: total + type: long + format: number + description: > + Number of huge pages in the pool. + + - name: used.bytes + type: long + format: bytes + description: > + Memory used in allocated huge pages. + + - name: used.pct + type: long + format: percent + description: > + Percentage of huge pages used. + + - name: free + type: long + format: number + description: > + Number of available huge pages in the pool. + + - name: reserved + type: long + format: number + description: > + Number of reserved but not allocated huge pages in the pool. + + - name: surplus + type: long + format: number + description: > + Number of overcommited huge pages. + + - name: default_size + type: long + format: bytes + description: > + Default size for huge pages. + + - name: swap.out + type: group + description: huge pages swapped out + fields: + - name: pages + type: long + description: pages swapped out + - name: fallback + type: long + description: Count of huge pages that must be split before swapout +- name: system.network + type: group + description: > + `network` contains network IO metrics for a single network interface. + release: ga + fields: + - name: name + type: keyword + example: eth0 + description: > + The network interface name. + + - name: out.bytes + type: long + format: bytes + description: > + The number of bytes sent. + + - name: in.bytes + type: long + format: bytes + description: > + The number of bytes received. + + - name: out.packets + type: long + description: > + The number of packets sent. + + - name: in.packets + type: long + description: > + The number or packets received. + + - name: in.errors + type: long + description: > + The number of errors while receiving. + + - name: out.errors + type: long + description: > + The number of errors while sending. + + - name: in.dropped + type: long + description: > + The number of incoming packets that were dropped. + + - name: out.dropped + type: long + description: > + The number of outgoing packets that were dropped. This value is always + 0 on Darwin and BSD because it is not reported by the operating system. +- name: system.network_summary + type: group + release: beta + description: > + Metrics relating to global network activity + fields: + - name: ip.* + type: object + description: > + IP counters + - name: tcp.* + type: object + description: > + TCP counters + - name: udp.* + type: object + description: > + UDP counters + - name: udp_lite.* + type: object + description: > + UDP Lite counters + - name: icmp.* + type: object + description: > + ICMP counters +- name: system.process + type: group + description: > + `process` contains process metadata, CPU metrics, and memory metrics. + release: ga + fields: + - name: name + type: alias + path: process.name + migration: true + - name: state + type: keyword + description: > + The process state. For example: "running". + - name: pid + type: alias + path: process.pid + migration: true + - name: ppid + type: alias + path: process.ppid + migration: true + - name: pgid + type: alias + path: process.pgid + migration: true + - name: cmdline + type: keyword + description: > + The full command-line used to start the process, including the + arguments separated by space. + ignore_above: 2048 + - name: username + type: alias + path: user.name + migration: true + - name: cwd + type: alias + path: process.working_directory + migration: true + - name: env + type: object + object_type: keyword + description: > + The environment variables used to start the process. The data is + available on FreeBSD, Linux, and OS X. + - name: cpu + type: group + prefix: "[float]" + description: CPU-specific statistics per process. + fields: + - name: user.ticks + type: long + description: > + The amount of CPU time the process spent in user space. + - name: total.value + type: long + description: > + The value of CPU usage since starting the process. + - name: total.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent by the process since the last update. Its value is similar to the + %CPU value of the process displayed by the top command on Unix systems. + - name: total.norm.pct + type: scaled_float + format: percent + description: > + The percentage of CPU time spent by the process since the last event. + This value is normalized by the number of CPU cores and it ranges + from 0 to 100%. + - name: system.ticks + type: long + description: > + The amount of CPU time the process spent in kernel space. + - name: total.ticks + type: long + description: > + The total CPU time spent by the process. + - name: start_time + type: date + description: > + The time when the process was started. + - name: memory + type: group + description: Memory-specific statistics per process. + prefix: "[float]" + fields: + - name: size + type: long + format: bytes + description: > + The total virtual memory the process has. + - name: rss.bytes + type: long + format: bytes + description: > + The Resident Set Size. The amount of memory the process occupied in main memory (RAM). + - name: rss.pct + type: scaled_float + format: percent + description: > + The percentage of memory the process occupied in main memory (RAM). + - name: share + type: long + format: bytes + description: > + The shared memory the process uses. + - name: fd + type: group + description: > + File descriptor usage metrics. This set of metrics is available for + Linux and FreeBSD. + prefix: "[float]" + fields: + - name: open + type: long + description: The number of file descriptors open by the process. + - name: limit.soft + type: long + description: > + The soft limit on the number of file descriptors opened by the + process. The soft limit can be changed by the process at any time. + - name: limit.hard + type: long + description: > + The hard limit on the number of file descriptors opened by the + process. The hard limit can only be raised by root. + - name: cgroup + type: group + description: > + Metrics and limits from the cgroup of which the task is a member. + cgroup metrics are reported when the process has membership in a + non-root cgroup. These metrics are only available on Linux. + fields: + - name: id + type: keyword + description: > + The ID common to all cgroups associated with this task. + If there isn't a common ID used by all cgroups this field will be + absent. + + - name: path + type: keyword + description: > + The path to the cgroup relative to the cgroup subsystem's mountpoint. + If there isn't a common path used by all cgroups this field will be + absent. + + - name: cpu + type: group + description: > + The cpu subsystem schedules CPU access for tasks in the cgroup. + Access can be controlled by two separate schedulers, CFS and RT. + CFS stands for completely fair scheduler which proportionally + divides the CPU time between cgroups based on weight. RT stands for + real time scheduler which sets a maximum amount of CPU time that + processes in the cgroup can consume during a given period. + + fields: + - name: id + type: keyword + description: ID of the cgroup. + + - name: path + type: keyword + description: > + Path to the cgroup relative to the cgroup subsystem's + mountpoint. + + - name: cfs.period.us + type: long + description: > + Period of time in microseconds for how regularly a + cgroup's access to CPU resources should be reallocated. + + - name: cfs.quota.us + type: long + description: > + Total amount of time in microseconds for which all + tasks in a cgroup can run during one period (as defined by + cfs.period.us). + + - name: cfs.shares + type: long + description: > + An integer value that specifies a relative share of CPU time + available to the tasks in a cgroup. The value specified in the + cpu.shares file must be 2 or higher. + + - name: rt.period.us + type: long + description: > + Period of time in microseconds for how regularly a cgroup's + access to CPU resources is reallocated. + + - name: rt.runtime.us + type: long + description: > + Period of time in microseconds for the longest continuous period + in which the tasks in a cgroup have access to CPU resources. + + - name: stats.periods + type: long + description: > + Number of period intervals (as specified in cpu.cfs.period.us) + that have elapsed. + + - name: stats.throttled.periods + type: long + description: > + Number of times tasks in a cgroup have been throttled (that is, + not allowed to run because they have exhausted all of the + available time as specified by their quota). + + - name: stats.throttled.ns + type: long + description: > + The total time duration (in nanoseconds) for which tasks in a + cgroup have been throttled. + + - name: cpuacct + type: group + description: CPU accounting metrics. + fields: + - name: id + type: keyword + description: ID of the cgroup. + + - name: path + type: keyword + description: > + Path to the cgroup relative to the cgroup subsystem's + mountpoint. + + - name: total.ns + type: long + description: > + Total CPU time in nanoseconds consumed by all tasks in the + cgroup. + + - name: stats.user.ns + type: long + description: CPU time consumed by tasks in user mode. + + - name: stats.system.ns + type: long + description: CPU time consumed by tasks in user (kernel) mode. + + - name: percpu + type: object + object_type: long + description: > + CPU time (in nanoseconds) consumed on each CPU by all tasks in + this cgroup. + + - name: memory + type: group + description: Memory limits and metrics. + fields: + - name: id + type: keyword + description: ID of the cgroup. + + - name: path + type: keyword + description: > + Path to the cgroup relative to the cgroup subsystem's mountpoint. + + - name: mem.usage.bytes + type: long + format: bytes + description: > + Total memory usage by processes in the cgroup (in bytes). + + - name: mem.usage.max.bytes + type: long + format: bytes + description: > + The maximum memory used by processes in the cgroup (in bytes). + + - name: mem.limit.bytes + type: long + format: bytes + description: > + The maximum amount of user memory in bytes (including file + cache) that tasks in the cgroup are allowed to use. + + - name: mem.failures + type: long + description: > + The number of times that the memory limit (mem.limit.bytes) was + reached. + + - name: memsw.usage.bytes + type: long + format: bytes + description: > + The sum of current memory usage plus swap space used by + processes in the cgroup (in bytes). + + - name: memsw.usage.max.bytes + type: long + format: bytes + description: > + The maximum amount of memory and swap space used by processes in + the cgroup (in bytes). + + - name: memsw.limit.bytes + type: long + format: bytes + description: > + The maximum amount for the sum of memory and swap usage + that tasks in the cgroup are allowed to use. + + - name: memsw.failures + type: long + description: > + The number of times that the memory plus swap space limit + (memsw.limit.bytes) was reached. + + - name: kmem.usage.bytes + type: long + format: bytes + description: > + Total kernel memory usage by processes in the cgroup (in bytes). + + - name: kmem.usage.max.bytes + type: long + format: bytes + description: > + The maximum kernel memory used by processes in the cgroup (in + bytes). + + - name: kmem.limit.bytes + type: long + format: bytes + description: > + The maximum amount of kernel memory that tasks in the cgroup are + allowed to use. + + - name: kmem.failures + type: long + description: > + The number of times that the memory limit (kmem.limit.bytes) was + reached. + + - name: kmem_tcp.usage.bytes + type: long + format: bytes + description: > + Total memory usage for TCP buffers in bytes. + + - name: kmem_tcp.usage.max.bytes + type: long + format: bytes + description: > + The maximum memory used for TCP buffers by processes in the + cgroup (in bytes). + + - name: kmem_tcp.limit.bytes + type: long + format: bytes + description: > + The maximum amount of memory for TCP buffers that tasks in the + cgroup are allowed to use. + + - name: kmem_tcp.failures + type: long + description: > + The number of times that the memory limit (kmem_tcp.limit.bytes) + was reached. + + - name: stats.active_anon.bytes + type: long + format: bytes + description: > + Anonymous and swap cache on active least-recently-used (LRU) + list, including tmpfs (shmem), in bytes. + + - name: stats.active_file.bytes + type: long + format: bytes + description: File-backed memory on active LRU list, in bytes. + + - name: stats.cache.bytes + type: long + format: bytes + description: Page cache, including tmpfs (shmem), in bytes. + + - name: stats.hierarchical_memory_limit.bytes + type: long + format: bytes + description: > + Memory limit for the hierarchy that contains the memory cgroup, + in bytes. + + - name: stats.hierarchical_memsw_limit.bytes + type: long + format: bytes + description: > + Memory plus swap limit for the hierarchy that contains the + memory cgroup, in bytes. + + - name: stats.inactive_anon.bytes + type: long + format: bytes + description: > + Anonymous and swap cache on inactive LRU list, including tmpfs + (shmem), in bytes + + - name: stats.inactive_file.bytes + type: long + format: bytes + description: > + File-backed memory on inactive LRU list, in bytes. + + - name: stats.mapped_file.bytes + type: long + format: bytes + description: > + Size of memory-mapped mapped files, including tmpfs (shmem), + in bytes. + + - name: stats.page_faults + type: long + description: > + Number of times that a process in the cgroup triggered a page + fault. + + - name: stats.major_page_faults + type: long + description: > + Number of times that a process in the cgroup triggered a major + fault. "Major" faults happen when the kernel actually has to + read the data from disk. + + - name: stats.pages_in + type: long + description: > + Number of pages paged into memory. This is a counter. + + - name: stats.pages_out + type: long + description: > + Number of pages paged out of memory. This is a counter. + + - name: stats.rss.bytes + type: long + format: bytes + description: > + Anonymous and swap cache (includes transparent hugepages), not + including tmpfs (shmem), in bytes. + + - name: stats.rss_huge.bytes + type: long + format: bytes + description: > + Number of bytes of anonymous transparent hugepages. + + - name: stats.swap.bytes + type: long + format: bytes + description: > + Swap usage, in bytes. + + - name: stats.unevictable.bytes + type: long + format: bytes + description: > + Memory that cannot be reclaimed, in bytes. + + - name: blkio + type: group + description: Block IO metrics. + fields: + - name: id + type: keyword + description: ID of the cgroup. + + - name: path + type: keyword + description: > + Path to the cgroup relative to the cgroup subsystems mountpoint. + + - name: total.bytes + type: long + format: bytes + description: > + Total number of bytes transferred to and from all block devices + by processes in the cgroup. + + - name: total.ios + type: long + description: > + Total number of I/O operations performed on all devices + by processes in the cgroup as seen by the throttling policy. +- name: system.process.summary + title: Process Summary + type: group + description: > + Summary metrics for the processes running on the host. + release: ga + fields: + - name: total + type: long + description: > + Total number of processes on this host. + - name: running + type: long + description: > + Number of running processes on this host. + - name: idle + type: long + description: > + Number of idle processes on this host. + - name: sleeping + type: long + description: > + Number of sleeping processes on this host. + - name: stopped + type: long + description: > + Number of stopped processes on this host. + - name: zombie + type: long + description: > + Number of zombie processes on this host. + - name: dead + type: long + description: > + Number of dead processes on this host. It's very unlikely that it will appear but in some special situations it may happen. + - name: unknown + type: long + description: > + Number of processes for which the state couldn't be retrieved or is unknown. +- name: system.raid + type: group + description: > + raid + release: ga + fields: + - name: name + type: keyword + description: > + Name of the device. + - name: status + type: keyword + description: > + activity-state of the device. + - name: level + type: keyword + description: > + The raid level of the device + - name: sync_action + type: keyword + description: > + Current sync action, if the RAID array is redundant + - name: disks.active + type: long + description: > + Number of active disks. + - name: disks.total + type: long + description: > + Total number of disks the device consists of. + - name: disks.spare + type: long + description: > + Number of spared disks. + - name: disks.failed + type: long + description: > + Number of failed disks. + - name: disks.states.* + type: object + object_type: keyword + description: > + map of raw disk states + - name: blocks.total + type: long + description: > + Number of blocks the device holds, in 1024-byte blocks. + - name: blocks.synced + type: long + description: > + Number of blocks on the device that are in sync, in 1024-byte blocks. +- name: system.socket + type: group + description: > + TCP sockets that are active. + release: ga + fields: + - name: direction + type: alias + path: network.direction + migration: true + + - name: family + type: alias + path: network.type + migration: true + + - name: local.ip + type: ip + example: 192.0.2.1 or 2001:0DB8:ABED:8536::1 + description: > + Local IP address. This can be an IPv4 or IPv6 address. + + - name: local.port + type: long + example: 22 + description: > + Local port. + + - name: remote.ip + type: ip + example: 192.0.2.1 or 2001:0DB8:ABED:8536::1 + description: > + Remote IP address. This can be an IPv4 or IPv6 address. + + - name: remote.port + type: long + example: 22 + description: > + Remote port. + + - name: remote.host + type: keyword + example: 76-211-117-36.nw.example.com. + description: > + PTR record associated with the remote IP. It is obtained via reverse + IP lookup. + + - name: remote.etld_plus_one + type: keyword + example: example.com. + description: > + The effective top-level domain (eTLD) of the remote host plus one more + label. For example, the eTLD+1 for "foo.bar.golang.org." is "golang.org.". + The data for determining the eTLD comes from an embedded copy of the data + from http://publicsuffix.org. + + - name: remote.host_error + type: keyword + description: > + Error describing the cause of the reverse lookup failure. + + - name: process.pid + type: alias + path: process.pid + migration: true + + - name: process.command + type: alias + path: process.name + migration: true + + - name: process.cmdline + type: keyword + description: > + Full command line + + - name: process.exe + type: alias + path: process.executable + migration: true + + - name: user.id + type: alias + path: user.id + migration: true + + - name: user.name + type: alias + path: user.full_name + migration: true +- name: system.socket.summary + title: Socket summary + type: group + description: > + Summary metrics of open sockets in the host system + release: ga + fields: + - name: all + type: group + description: > + All connections + fields: + - name: count + type: integer + description: > + All open connections + - name: listening + type: integer + description: > + All listening ports + - name: tcp + type: group + description: > + All TCP connections + fields: + - name: memory + type: integer + format: bytes + description: > + Memory used by TCP sockets in bytes, based on number of allocated pages and system page size. Corresponds to limits set in /proc/sys/net/ipv4/tcp_mem. Only available on Linux. + - name: all + type: group + description: > + All TCP connections + fields: + - name: orphan + type: integer + description: > + A count of all orphaned tcp sockets. Only available on Linux. + - name: count + type: integer + description: > + All open TCP connections + - name: listening + type: integer + description: > + All TCP listening ports + - name: established + type: integer + description: > + Number of established TCP connections + - name: close_wait + type: integer + description: > + Number of TCP connections in _close_wait_ state + - name: time_wait + type: integer + description: > + Number of TCP connections in _time_wait_ state + - name: syn_sent + type: integer + description: > + Number of TCP connections in _syn_sent_ state + - name: syn_recv + type: integer + description: > + Number of TCP connections in _syn_recv_ state + - name: fin_wait1 + type: integer + description: > + Number of TCP connections in _fin_wait1_ state + - name: fin_wait2 + type: integer + description: > + Number of TCP connections in _fin_wait2_ state + - name: last_ack + type: integer + description: > + Number of TCP connections in _last_ack_ state + - name: closing + type: integer + description: > + Number of TCP connections in _closing_ state + - name: udp + type: group + description: > + All UDP connections + fields: + - name: memory + type: integer + format: bytes + description: > + Memory used by UDP sockets in bytes, based on number of allocated pages and system page size. Corresponds to limits set in /proc/sys/net/ipv4/udp_mem. Only available on Linux. + - name: all + type: group + description: > + All UDP connections + fields: + - name: count + type: integer + description: > + All open UDP connections +- name: system.uptime + type: group + description: > + `uptime` contains the operating system uptime metric. + release: ga + fields: + - name: duration.ms + type: long + format: duration + input_format: milliseconds + description: > + The OS uptime in milliseconds. +- name: system.users + type: group + release: beta + description: > + Logged-in user session data + fields: + - name: id + type: keyword + description: > + The ID of the session + - name: seat + type: keyword + description: > + An associated logind seat + - name: path + type: keyword + description: > + The DBus object path of the session + - name: type + type: keyword + description: > + The type of the user session + - name: service + type: keyword + description: > + A session associated with the service + - name: remote + type: boolean + description: > + A bool indicating a remote session + - name: state + type: keyword + description: > + The current state of the session + - name: scope + type: keyword + description: > + The associated systemd scope + - name: leader + type: long + description: > + The root PID of the session + - name: remote_host + type: keyword + description: > + A remote host address for the session + + + + + From 18aa8245b7a717202f1d47e1cfbbcd6a9b4b33fd Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 18 Mar 2020 09:48:10 -0700 Subject: [PATCH 02/42] Fixed errors which are happening if switch between alert types (#60453) --- .../application/sections/alert_form/alert_form.tsx | 2 ++ .../public/common/index_controls/index.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index aa56b565ef3246..1fa620c5394a12 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -155,6 +155,7 @@ export const AlertForm = ({ alert, canChangeTrigger = true, dispatch, errors }: onClick={() => { setAlertProperty('alertTypeId', item.id); setAlertTypeModel(item); + setAlertProperty('params', {}); if (alertTypesIndex && alertTypesIndex[item.id]) { setDefaultActionGroupId(alertTypesIndex[item.id].defaultActionGroupId); } @@ -194,6 +195,7 @@ export const AlertForm = ({ alert, canChangeTrigger = true, dispatch, errors }: onClick={() => { setAlertProperty('alertTypeId', null); setAlertTypeModel(null); + setAlertProperty('params', {}); }} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts index 32fb35d6adebb4..f5da3918bf1011 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts @@ -10,6 +10,7 @@ import { loadIndexPatterns, getMatchingIndicesForThresholdAlertType, getThresholdAlertTypeFields, + getSavedObjectsClient, } from '../lib/index_threshold_api'; export interface IOption { @@ -18,8 +19,12 @@ export interface IOption { } export const getIndexPatterns = async () => { - const indexPatternObjects = await loadIndexPatterns(); - return indexPatternObjects.map((indexPattern: any) => indexPattern.attributes.title); + // TODO: Implement a possibility to retrive index patterns different way to be able to expose this in consumer plugins + if (getSavedObjectsClient()) { + const indexPatternObjects = await loadIndexPatterns(); + return indexPatternObjects.map((indexPattern: any) => indexPattern.attributes.title); + } + return []; }; export const getIndexOptions = async ( From 3e10276b20471a7884dfc52f379d6a956b4488d3 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Wed, 18 Mar 2020 11:00:44 -0600 Subject: [PATCH 03/42] [SIEM][Detection Engine] Fixes bug with timeline templates not working ### Summary Fixes a bug with the timeline templates not working when specifying filters. * Creates a type safe mechanism for getting StringArrays or regular strings * AddsType Script function returns to functions in the helpers file * Adds unit tests for the effected areas of code and corner cases Before this fix you would get these toaster errors if you tried to use a template name such as `host.name` in the timeline filters: Screen Shot 2020-03-18 at 12 58 01 AM After this fix it will work for you. Testing: 1) Create a timeline template that has a host.name as both a query and a filter such as this. You can give the value of the host.name any value such as placeholder. Screen Shot 2020-03-18 at 12 56 04 AM 2) Create a signal that uses it and produces a lot of signals off of something such as all host names Screen Shot 2020-03-18 at 12 50 47 AM 3) Ensure you select your **Timeline template** you saved by using the drop down Screen Shot 2020-03-18 at 12 51 21 AM 4) Once your signals have run, go to the signals page and send one of the signals for your newly crated rule which has a host name to the timeline from "View in timeline" Screen Shot 2020-03-18 at 12 52 10 AM You should notice that your timeline has both the query and the filter set correctly such as this Screen Shot 2020-03-18 at 12 56 23 AM ### Other notes All the different fields you can choose from for templates are: ``` 'host.name', 'host.hostname', 'host.domain', 'host.id', 'host.ip', 'client.ip', 'destination.ip', 'server.ip', 'source.ip', 'network.community_id', 'user.name', 'process.name', ``` And it should not work with anything outside of those. You should be able to mix and match them into different filters and queries to have a multiples of them. ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../components/signals/helpers.test.ts | 273 ++++++++++++++++++ .../components/signals/helpers.ts | 73 +++-- 2 files changed, 328 insertions(+), 18 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts new file mode 100644 index 00000000000000..7e6778ca4fb4f1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts @@ -0,0 +1,273 @@ +/* + * 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 { + getStringArray, + replaceTemplateFieldFromQuery, + replaceTemplateFieldFromMatchFilters, + reformatDataProviderWithNewValue, +} from './helpers'; +import { mockEcsData } from '../../../../mock/mock_ecs'; +import { Filter } from '../../../../../../../../../src/plugins/data/public'; +import { DataProvider } from '../../../../components/timeline/data_providers/data_provider'; +import { mockDataProviders } from '../../../../components/timeline/data_providers/mock/mock_data_providers'; +import { cloneDeep } from 'lodash/fp'; + +describe('helpers', () => { + let mockEcsDataClone = cloneDeep(mockEcsData); + beforeEach(() => { + mockEcsDataClone = cloneDeep(mockEcsData); + }); + describe('getStringOrStringArray', () => { + test('it should correctly return a string array', () => { + const value = getStringArray('x', { + x: 'The nickname of the developer we all :heart:', + }); + expect(value).toEqual(['The nickname of the developer we all :heart:']); + }); + + test('it should correctly return a string array with a single element', () => { + const value = getStringArray('x', { + x: ['The nickname of the developer we all :heart:'], + }); + expect(value).toEqual(['The nickname of the developer we all :heart:']); + }); + + test('it should correctly return a string array with two elements of strings', () => { + const value = getStringArray('x', { + x: ['The nickname of the developer we all :heart:', 'We are all made of stars'], + }); + expect(value).toEqual([ + 'The nickname of the developer we all :heart:', + 'We are all made of stars', + ]); + }); + + test('it should correctly return a string array with deep elements', () => { + const value = getStringArray('x.y.z', { + x: { y: { z: 'zed' } }, + }); + expect(value).toEqual(['zed']); + }); + + test('it should correctly return a string array with a non-existent value', () => { + const value = getStringArray('non.existent', { + x: { y: { z: 'zed' } }, + }); + expect(value).toEqual([]); + }); + + test('it should trace an error if the value is not a string', () => { + const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; + const value = getStringArray('a', { a: 5 }, mockConsole); + expect(value).toEqual([]); + expect( + mockConsole.trace + ).toHaveBeenCalledWith( + 'Data type that is not a string or string array detected:', + 5, + 'when trying to access field:', + 'a', + 'from data object of:', + { a: 5 } + ); + }); + + test('it should trace an error if the value is an array of mixed values', () => { + const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; + const value = getStringArray('a', { a: ['hi', 5] }, mockConsole); + expect(value).toEqual([]); + expect( + mockConsole.trace + ).toHaveBeenCalledWith( + 'Data type that is not a string or string array detected:', + ['hi', 5], + 'when trying to access field:', + 'a', + 'from data object of:', + { a: ['hi', 5] } + ); + }); + }); + + describe('replaceTemplateFieldFromQuery', () => { + test('given an empty query string this returns an empty query string', () => { + const replacement = replaceTemplateFieldFromQuery('', mockEcsDataClone[0]); + expect(replacement).toEqual(''); + }); + + test('given a query string with spaces this returns an empty query string', () => { + const replacement = replaceTemplateFieldFromQuery(' ', mockEcsDataClone[0]); + expect(replacement).toEqual(''); + }); + + test('it should replace a query with a template value such as apache from a mock template', () => { + const replacement = replaceTemplateFieldFromQuery( + 'host.name: placeholdertext', + mockEcsDataClone[0] + ); + expect(replacement).toEqual('host.name: apache'); + }); + + test('it should replace a template field with an ECS value that is not an array', () => { + mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const replacement = replaceTemplateFieldFromQuery('host.name: *', mockEcsDataClone[0]); + expect(replacement).toEqual('host.name: *'); + }); + + test('it should NOT replace a query with a template value that is not part of the template fields array', () => { + const replacement = replaceTemplateFieldFromQuery( + 'user.id: placeholdertext', + mockEcsDataClone[0] + ); + expect(replacement).toEqual('user.id: placeholdertext'); + }); + }); + + describe('replaceTemplateFieldFromMatchFilters', () => { + test('given an empty query filter this will return an empty filter', () => { + const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]); + expect(replacement).toEqual([]); + }); + + test('given a query filter this will return that filter with the placeholder replaced', () => { + const filters: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'host.name', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Braden' }, + }, + query: { match_phrase: { 'host.name': 'Braden' } }, + }, + ]; + const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); + const expected: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'host.name', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'apache' }, + }, + query: { match_phrase: { 'host.name': 'apache' } }, + }, + ]; + expect(replacement).toEqual(expected); + }); + + test('given a query filter with a value not in the templateFields, this will NOT replace the placeholder value', () => { + const filters: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'user.id', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Evan' }, + }, + query: { match_phrase: { 'user.id': 'Evan' } }, + }, + ]; + const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); + const expected: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'user.id', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Evan' }, + }, + query: { match_phrase: { 'user.id': 'Evan' } }, + }, + ]; + expect(replacement).toEqual(expected); + }); + }); + + describe('reformatDataProviderWithNewValue', () => { + test('it should replace a query with a template value such as apache from a mock data provider', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = 'Braden'; + const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); + expect(replacement).toEqual({ + id: 'apache', + name: 'apache', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'apache', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + }); + }); + + test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { + mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = 'Braden'; + const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); + expect(replacement).toEqual({ + id: 'apache', + name: 'apache', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'apache', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + }); + }); + + test('it should NOT replace a query with a template value that is not part of a template such as user.id', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'user.id'; + mockDataProvider.id = 'my-id'; + mockDataProvider.name = 'Rebecca'; + mockDataProvider.queryMatch.value = 'Rebecca'; + const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); + expect(replacement).toEqual({ + id: 'my-id', + name: 'Rebecca', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'user.id', + value: 'Rebecca', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts index 715d98ed336949..e8c9c2e3cf6c99 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts @@ -17,6 +17,11 @@ interface FindValueToChangeInQuery { valueToChange: string; } +/** + * Fields that will be replaced with the template strings from a a saved timeline template. + * This is used for the signals detection engine feature when you save a timeline template + * and are the fields you can replace when creating a template. + */ const templateFields = [ 'host.name', 'host.hostname', @@ -32,6 +37,36 @@ const templateFields = [ 'process.name', ]; +/** + * This will return an unknown as a string array if it exists from an unknown data type and a string + * that represents the path within the data object the same as lodash's "get". If the value is non-existent + * we will return an empty array. If it is a non string value then this will log a trace to the console + * that it encountered an error and return an empty array. + * @param field string of the field to access + * @param data The unknown data that is typically a ECS value to get the value + * @param localConsole The local console which can be sent in to make this pure (for tests) or use the default console + */ +export const getStringArray = (field: string, data: unknown, localConsole = console): string[] => { + const value: unknown | undefined = get(field, data); + if (value == null) { + return []; + } else if (typeof value === 'string') { + return [value]; + } else if (Array.isArray(value) && value.every(element => typeof element === 'string')) { + return value; + } else { + localConsole.trace( + 'Data type that is not a string or string array detected:', + value, + 'when trying to access field:', + field, + 'from data object of:', + data + ); + return []; + } +}; + export const findValueToChangeInQuery = ( keuryNode: KueryNode, valueToChange: FindValueToChangeInQuery[] = [] @@ -66,31 +101,33 @@ export const findValueToChangeInQuery = ( ); }; -export const replaceTemplateFieldFromQuery = (query: string, ecsData: Ecs) => { +export const replaceTemplateFieldFromQuery = (query: string, ecsData: Ecs): string => { if (query.trim() !== '') { const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query)); return valueToChange.reduce((newQuery, vtc) => { - const newValue = get(vtc.field, ecsData); - if (newValue != null) { - return newQuery.replace(vtc.valueToChange, newValue); + const newValue = getStringArray(vtc.field, ecsData); + if (newValue.length) { + return newQuery.replace(vtc.valueToChange, newValue[0]); + } else { + return newQuery; } - return newQuery; }, query); + } else { + return ''; } - return ''; }; -export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: Ecs) => +export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: Ecs): Filter[] => filters.map(filter => { if ( filter.meta.type === 'phrase' && filter.meta.key != null && templateFields.includes(filter.meta.key) ) { - const newValue = get(filter.meta.key, ecsData); - if (newValue != null) { - filter.meta.params = { query: newValue }; - filter.query = { match_phrase: { [filter.meta.key]: newValue } }; + const newValue = getStringArray(filter.meta.key, ecsData); + if (newValue.length) { + filter.meta.params = { query: newValue[0] }; + filter.query = { match_phrase: { [filter.meta.key]: newValue[0] } }; } } return filter; @@ -101,11 +138,11 @@ export const reformatDataProviderWithNewValue = { if (templateFields.includes(dataProvider.queryMatch.field)) { - const newValue = get(dataProvider.queryMatch.field, ecsData); - if (newValue != null) { - dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue); - dataProvider.name = newValue; - dataProvider.queryMatch.value = newValue; + const newValue = getStringArray(dataProvider.queryMatch.field, ecsData); + if (newValue.length) { + dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]); + dataProvider.name = newValue[0]; + dataProvider.queryMatch.value = newValue[0]; dataProvider.queryMatch.displayField = undefined; dataProvider.queryMatch.displayValue = undefined; } @@ -116,8 +153,8 @@ export const reformatDataProviderWithNewValue = - dataProviders.map((dataProvider: DataProvider) => { +): DataProvider[] => + dataProviders.map(dataProvider => { const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData); if (newDataProvider.and != null && !isEmpty(newDataProvider.and)) { newDataProvider.and = newDataProvider.and.map(andDataProvider => From 696b19e67a2effe7d4f61064d1b9652bebc6c3dd Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 18 Mar 2020 10:09:58 -0700 Subject: [PATCH 04/42] skip flaky suite (#60535) --- .../apps/discover/feature_controls/discover_security.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 1dd069bb907d10..98ab4c1f15a546 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -28,7 +28,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - describe('security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/60535 + describe.skip('security', () => { before(async () => { await esArchiver.load('discover/feature_controls/security'); await esArchiver.loadIfNeeded('logstash_functional'); From 52dd5e0f7a2f46c949e258744c77f1cb55f8dc99 Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Wed, 18 Mar 2020 10:15:47 -0700 Subject: [PATCH 05/42] Branding fixes for dashboard, loader and space selector (#60073) --- .../public/chrome/ui/_loading_indicator.scss | 53 +++++----- src/core/server/rendering/views/styles.tsx | 7 +- .../exit_full_screen_button.test.js.snap | 44 ++++++-- .../exit_full_screen_button.test.tsx.snap | 95 ++++++++++++++---- .../_exit_full_screen_button.scss | 68 ++++--------- .../exit_full_screen_button.tsx | 45 +++++++-- .../screenshots/baseline/area_chart.png | Bin 45352 -> 55900 bytes .../screenshots/baseline/tsvb_dashboard.png | Bin 41606 -> 52911 bytes .../space_selector.test.tsx.snap | 2 +- .../public/space_selector/space_selector.tsx | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 12 files changed, 203 insertions(+), 115 deletions(-) diff --git a/src/core/public/chrome/ui/_loading_indicator.scss b/src/core/public/chrome/ui/_loading_indicator.scss index 80694347393ce9..026c23b93b040f 100644 --- a/src/core/public/chrome/ui/_loading_indicator.scss +++ b/src/core/public/chrome/ui/_loading_indicator.scss @@ -22,29 +22,34 @@ $kbnLoadingIndicatorColor2: tint($euiColorAccent, 60%); } } - .kbnLoadingIndicator__bar { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - z-index: $euiZLevel1 + 1; - visibility: visible; - display: block; - animation: kbn-animate-loading-indicator 2s linear infinite; - background-color: $kbnLoadingIndicatorColor2; - background-image: linear-gradient(to right, - $kbnLoadingIndicatorColor1 0%, - $kbnLoadingIndicatorColor1 50%, - $kbnLoadingIndicatorColor2 50%, - $kbnLoadingIndicatorColor2 100% - ); - background-repeat: repeat-x; - background-size: $kbnLoadingIndicatorBackgroundSize $kbnLoadingIndicatorBackgroundSize; - width: 200%; - } +.kbnLoadingIndicator__bar { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + z-index: $euiZLevel1 + 1; + visibility: visible; + display: block; + animation: kbn-animate-loading-indicator 2s linear infinite; + background-color: $kbnLoadingIndicatorColor2; + background-image: linear-gradient( + to right, + $kbnLoadingIndicatorColor1 0%, + $kbnLoadingIndicatorColor1 50%, + $kbnLoadingIndicatorColor2 50%, + $kbnLoadingIndicatorColor2 100% + ); + background-repeat: repeat-x; + background-size: $kbnLoadingIndicatorBackgroundSize $kbnLoadingIndicatorBackgroundSize; + width: 200%; +} - @keyframes kbn-animate-loading-indicator { - from { transform: translateX(0); } - to { transform: translateX(-$kbnLoadingIndicatorBackgroundSize); } +@keyframes kbn-animate-loading-indicator { + from { + transform: translateX(0); } + to { + transform: translateX(-$kbnLoadingIndicatorBackgroundSize); + } +} diff --git a/src/core/server/rendering/views/styles.tsx b/src/core/server/rendering/views/styles.tsx index 9ab9f2ad0d6b86..71b42e34641186 100644 --- a/src/core/server/rendering/views/styles.tsx +++ b/src/core/server/rendering/views/styles.tsx @@ -53,7 +53,7 @@ export const Styles: FunctionComponent = ({ darkMode }) => { .kbnWelcomeView { line-height: 1.5; - background-color: #FFF; + background-color: ${darkMode ? '#1D1E24' : '#FFF'}; height: 100%; display: -webkit-box; display: -webkit-flex; @@ -97,6 +97,7 @@ export const Styles: FunctionComponent = ({ darkMode }) => { line-height: 40px !important; height: 40px !important; color: #98a2b3; + color: ${darkMode ? '#98A2B3' : '#69707D'}; } .kbnLoaderWrap { @@ -128,7 +129,7 @@ export const Styles: FunctionComponent = ({ darkMode }) => { width: 32px; height: 4px; overflow: hidden; - background-color: #D3DAE6; + background-color: ${darkMode ? '#25262E' : '#F5F7FA'}; line-height: 1; } @@ -142,7 +143,7 @@ export const Styles: FunctionComponent = ({ darkMode }) => { left: 0; transform: scaleX(0) translateX(0%); animation: kbnProgress 1s cubic-bezier(.694, .0482, .335, 1) infinite; - background-color: #006DE4; + background-color: ${darkMode ? '#1BA9F5' : '#006DE4'}; } @keyframes kbnProgress { diff --git a/src/legacy/ui/public/exit_full_screen/__snapshots__/exit_full_screen_button.test.js.snap b/src/legacy/ui/public/exit_full_screen/__snapshots__/exit_full_screen_button.test.js.snap index 27226eb010ba25..365f3afdab3952 100644 --- a/src/legacy/ui/public/exit_full_screen/__snapshots__/exit_full_screen_button.test.js.snap +++ b/src/legacy/ui/public/exit_full_screen/__snapshots__/exit_full_screen_button.test.js.snap @@ -12,19 +12,45 @@ exports[`is rendered 1`] = ` diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap b/src/plugins/kibana_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap index 39bd66ff71c617..ee97a5acfd3d23 100644 --- a/src/plugins/kibana_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap +++ b/src/plugins/kibana_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap @@ -17,27 +17,88 @@ exports[`is rendered 1`] = ` diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/_exit_full_screen_button.scss b/src/plugins/kibana_react/public/exit_full_screen_button/_exit_full_screen_button.scss index e810fe0ccdba6f..a2e951cb5b775c 100644 --- a/src/plugins/kibana_react/public/exit_full_screen_button/_exit_full_screen_button.scss +++ b/src/plugins/kibana_react/public/exit_full_screen_button/_exit_full_screen_button.scss @@ -4,66 +4,40 @@ */ .dshExitFullScreenButton { - height: $euiSizeXXL; - left: 0; - bottom: 0; + @include euiBottomShadow; + + left: $euiSizeS; + bottom: $euiSizeS; position: fixed; display: block; padding: 0; border: none; background: none; z-index: 5; + background: $euiColorFullShade; + padding: $euiSizeXS; + border-radius: $euiBorderRadius; + text-align: left; - &:hover, - &:focus { - transition: all $euiAnimSpeedExtraSlow $euiAnimSlightResistance; - z-index: 10 !important; /* 1 */ + &:hover { + background: $euiColorFullShade; - .dshExitFullScreenButton__text { - transition: all $euiAnimSpeedNormal $euiAnimSlightResistance; - transform: translateX(-$euiSize); + .dshExitFullScreenButton__icon { + color: $euiColorEmptyShade; } } } -.dshExitFullScreenButton__logo { - display: block; - // Just darken the background for all themes because the logo is always white - background-color: shade($euiColorPrimary, 25%); - height: $euiSizeXXL; - - // These numbers are very specific to the Kibana logo size - width: 92px; - background-image: url('ui/assets/images/kibana.svg'); - background-position: 8px 5px; - background-size: 72px 30px; - background-repeat: no-repeat; - - z-index: $euiZLevel1; +.dshExitFullScreenButton__title { + line-height: 1.2; + color: $euiColorEmptyShade; } -/** - * 1. Calc made to allow caret in text to peek out / animate. - */ - .dshExitFullScreenButton__text { - background: $euiColorPrimary; - color: $euiColorEmptyShade; - line-height: $euiSizeXXL; - display: inline-block; - font-size: $euiFontSizeS; - height: $euiSizeXXL; - position: absolute; - left: calc(100% + #{$euiSize}); /* 1 */ - top: 0px; - bottom: 0px; - white-space: nowrap; - padding: 0px $euiSizeXS 0px $euiSizeM; - transition: all .2s ease; - transform: translateX(-100%); - z-index: -1; - - .euiIcon { - margin-left: $euiSizeXS; - } + line-height: 1.2; + color: makeHighContrastColor($euiColorMediumShade, $euiColorFullShade); +} + +.dshExitFullScreenButton__icon { + color: makeHighContrastColor($euiColorMediumShade, $euiColorFullShade); } diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx index 5ce508ec1ed5b2..97fc02ac64e124 100644 --- a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx +++ b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import React, { PureComponent } from 'react'; import { EuiScreenReaderOnly, keyCodes } from '@elastic/eui'; -import { EuiIcon } from '@elastic/eui'; +import { EuiIcon, EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; export interface ExitFullScreenButtonProps { onExitFullScreenMode: () => void; @@ -61,17 +61,40 @@ class ExitFullScreenButtonUi extends PureComponent { )} className="dshExitFullScreenButton" onClick={this.props.onExitFullScreenMode} + data-test-subj="exitFullScreenModeLogo" > - - - {i18n.translate('kibana-react.exitFullScreenButton.exitFullScreenModeButtonLabel', { - defaultMessage: 'Exit full screen', - })} - - + + + + + +
+ +

+ {i18n.translate( + 'kibana-react.exitFullScreenButton.exitFullScreenModeButtonTitle', + { + defaultMessage: 'Elastic Kibana', + } + )} +

+
+ +

+ {i18n.translate( + 'kibana-react.exitFullScreenButton.exitFullScreenModeButtonText', + { + defaultMessage: 'Exit full screen', + } + )} +

+
+
+
+ + + +
diff --git a/test/functional/screenshots/baseline/area_chart.png b/test/functional/screenshots/baseline/area_chart.png index 2c2d599139100dee69f25a8eb30dcd2b6764b659..1a381d61dd9f18de0a50e66c55993f8d592432e1 100644 GIT binary patch literal 55900 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 Z_8Krsy}o3Hfk#Ra?8koKM*#k}g>rq- zXmSyDOK{`#6GBy^Bv}+rX8{*_g=I!zEBj|A=noMixk>@D3TAzSMwe8Th+p${KC+Rd74)~V7iDUWu`82#ne>0|z#((fv-sT5L2=7$yN zx!3SDLn{f0mX=zDE2UC-zQ+FuEMx$9?qn zOVt7zb`$X*&0~4_Xx7ws2N;@)P;kS41qmGrD0H!9r}F2p#i8r(y|%_#MIxrX#UJ&e-RHLHvFOwoY#4nz+MLa+sm!e4P9ud zd@J_GM&?r;Nve)LW#RkbJiJ4Rct4Q|s1RGspKL9)8$r@?=XAOZrypsycG}U_*&?bi z-50BpUMdPPjH*_luS_5wZ9R*I&ndy_b#_4DNlUOR$^)bj;>JDoKHk3!GC;tbd&0QQu6^%2+HyI@axdQiIczl71v6kGV~tMY7eMR}2__4)D< zUn~esGQrKBfgI!LL%mar`c|tzA_@NXaZ;p4-N09VpzFm0sGHs1Uf>D?QyC_-VJYNs zl+Pdf1Ubbqn zk|Zqs{!pS3#e;ddlfQ-U^AL_d3zZH1jsEX&O830sT)R$oSaY#@yuIx!NMizQzZZcBZ5Hpo&0#ID)AwrII z;Ot~w0Jt4=*C8a%BC(R+=IvM-(Gkb*uX!7*N|KLh?1~Rv)1-Xht6&!TOs?21CXDKI~Q!8H$^ke{qjJ zCuHO+WNCd&81uLseDw7lPXnE)&7&Gl`bo$Fr@S&k_*N%0Df=pZaAHJ+$C6<3JitS^ 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 literal 45352 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 Ja*20A{|B2jPZ$6I diff --git a/test/functional/screenshots/baseline/tsvb_dashboard.png b/test/functional/screenshots/baseline/tsvb_dashboard.png index d703be89b7460c053d00b997407f60fc2fd1a2ff..f5ebccbcb96c6b8ed3795f0f60b120080b45d9c3 100644 GIT binary patch literal 52911 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

rfMo)K0=SUejuEouf&@qAxH^xdO=q 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 literal 41606 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 diff --git a/x-pack/plugins/spaces/public/space_selector/__snapshots__/space_selector.test.tsx.snap b/x-pack/plugins/spaces/public/space_selector/__snapshots__/space_selector.test.tsx.snap index e1cfafd8972348..a9fd636776a4fe 100644 --- a/x-pack/plugins/spaces/public/space_selector/__snapshots__/space_selector.test.tsx.snap +++ b/x-pack/plugins/spaces/public/space_selector/__snapshots__/space_selector.test.tsx.snap @@ -17,7 +17,7 @@ exports[`it renders without crashing 1`] = ` > { - + diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index eb208e67ccfec0..86a6f958cf2580 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2380,7 +2380,6 @@ "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "凡例を切り替える", "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "凡例を切り替える", "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}、トグルオプション", - "kibana-react.exitFullScreenButton.exitFullScreenModeButtonLabel": "全画面を終了", "kibana-react.exitFullScreenButton.fullScreenModeDescription": "ESC キーで全画面モードを終了します。", "newsfeed.emptyPrompt.noNewsText": "Kibanaインスタンスがインターネットにアクセスできない場合、管理者にこの機能を無効にするように依頼してください。そうでない場合は、ニュースを取り込み続けます。", "newsfeed.emptyPrompt.noNewsTitle": "ニュースがない場合", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f85714a5913ad2..c580eb533feb94 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2381,7 +2381,6 @@ "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "切换图例", "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "切换图例", "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}切换选项", - "kibana-react.exitFullScreenButton.exitFullScreenModeButtonLabel": "退出全屏", "kibana-react.exitFullScreenButton.fullScreenModeDescription": "在全屏模式下,按 ESC 键可退出。", "newsfeed.emptyPrompt.noNewsText": "如果您的 Kibana 实例没有 Internet 连接,请让您的管理员禁用此功能。否则,我们将不断尝试获取新闻。", "newsfeed.emptyPrompt.noNewsTitle": "无新闻?", From 64af78045bb68efb124c8a2042e62f750f258fe4 Mon Sep 17 00:00:00 2001 From: kqualters-elastic <56408403+kqualters-elastic@users.noreply.github.com> Date: Wed, 18 Mar 2020 13:18:35 -0400 Subject: [PATCH 06/42] [Endpoint] resolver v1 events (#59233) * Unifying the test index name for resolver and alerts * Endpoint isn't sending the agent field so check for it * Update resolver to use either legacy or ecs events * Use correct format for child events api * Adding string or array for category and type * Add return types to process event models * Create a common/models.ts for common event logic * Decrease resolver min height * Update types to match cli tool * Add a smoke test for resolver rendering nodes, remove unused selector * Add common/models/event * Internationalize some strings, address pr comments Co-authored-by: Jonathan Buttner --- .../plugins/endpoint/common/models/event.ts | 31 ++++ x-pack/plugins/endpoint/common/types.ts | 5 +- .../endpoint/store/alerts/selectors.ts | 12 -- .../view/alerts/details/overview/index.tsx | 174 ++++++++++-------- .../endpoint/view/alerts/resolver.tsx | 5 +- .../resolver/models/indexed_process_tree.ts | 19 +- .../resolver/models/process_event.ts | 79 +++++--- .../embeddables/resolver/store/actions.ts | 6 +- .../embeddables/resolver/store/data/action.ts | 4 +- .../resolver/store/data/selectors.ts | 10 +- .../embeddables/resolver/store/methods.ts | 4 +- .../embeddables/resolver/store/middleware.ts | 64 +++++-- .../public/embeddables/resolver/types.ts | 27 ++- .../embeddables/resolver/view/index.tsx | 4 +- .../embeddables/resolver/view/panel.tsx | 19 +- .../resolver/view/process_event_dot.tsx | 10 +- .../resolver/view/use_camera.test.tsx | 8 +- .../test/functional/apps/endpoint/alerts.ts | 9 +- 18 files changed, 308 insertions(+), 182 deletions(-) create mode 100644 x-pack/plugins/endpoint/common/models/event.ts diff --git a/x-pack/plugins/endpoint/common/models/event.ts b/x-pack/plugins/endpoint/common/models/event.ts new file mode 100644 index 00000000000000..650486f3c3858d --- /dev/null +++ b/x-pack/plugins/endpoint/common/models/event.ts @@ -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 { EndpointEvent, LegacyEndpointEvent } from '../types'; + +export function isLegacyEvent( + event: EndpointEvent | LegacyEndpointEvent +): event is LegacyEndpointEvent { + return (event as LegacyEndpointEvent).endgame !== undefined; +} + +export function eventTimestamp( + event: EndpointEvent | LegacyEndpointEvent +): string | undefined | number { + if (isLegacyEvent(event)) { + return event.endgame.timestamp_utc; + } else { + return event['@timestamp']; + } +} + +export function eventName(event: EndpointEvent | LegacyEndpointEvent): string { + if (isLegacyEvent(event)) { + return event.endgame.process_name ? event.endgame.process_name : ''; + } else { + return event.process.name; + } +} diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index e423de56bf817f..7e4cf3d700ec8b 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -311,8 +311,8 @@ export interface EndpointEvent { version: string; }; event: { - category: string; - type: string; + category: string | string[]; + type: string | string[]; id: string; kind: string; }; @@ -328,6 +328,7 @@ export interface EndpointEvent { name: string; parent?: { entity_id: string; + name?: string; }; }; } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts index 68731bb3f307f5..5e9b08c09c2c79 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts @@ -165,15 +165,3 @@ export const hasSelectedAlert: (state: AlertListState) => boolean = createSelect uiQueryParams, ({ selected_alert: selectedAlert }) => selectedAlert !== undefined ); - -/** - * Determine if the alert event is most likely compatible with LegacyEndpointEvent. - */ -export const selectedAlertIsLegacyEndpointEvent: ( - state: AlertListState -) => boolean = createSelector(selectedAlertDetailsData, function(event) { - if (event === undefined) { - return false; - } - return 'endgame' in event; -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx index 82a4bc00a43962..0ec5a855c8615d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { memo, useMemo } from 'react'; +import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -19,87 +20,104 @@ import * as selectors from '../../../../store/alerts/selectors'; import { MetadataPanel } from './metadata_panel'; import { FormattedDate } from '../../formatted_date'; import { AlertDetailResolver } from '../../resolver'; +import { ResolverEvent } from '../../../../../../../common/types'; import { TakeActionDropdown } from './take_action_dropdown'; -export const AlertDetailsOverview = memo(() => { - const alertDetailsData = useAlertListSelector(selectors.selectedAlertDetailsData); - if (alertDetailsData === undefined) { - return null; - } - const selectedAlertIsLegacyEndpointEvent = useAlertListSelector( - selectors.selectedAlertIsLegacyEndpointEvent - ); +export const AlertDetailsOverview = styled( + memo(() => { + const alertDetailsData = useAlertListSelector(selectors.selectedAlertDetailsData); + if (alertDetailsData === undefined) { + return null; + } - const tabs: EuiTabbedContentTab[] = useMemo(() => { - return [ - { - id: 'overviewMetadata', - 'data-test-subj': 'overviewMetadata', - name: i18n.translate( - 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.overview', - { - defaultMessage: 'Overview', - } - ), - content: ( - <> - - - - ), - }, - { - id: 'overviewResolver', - name: i18n.translate( - 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.resolver', - { - defaultMessage: 'Resolver', - } - ), - content: ( - <> - - {selectedAlertIsLegacyEndpointEvent && } - - ), - }, - ]; - }, [selectedAlertIsLegacyEndpointEvent]); + const tabs: EuiTabbedContentTab[] = useMemo(() => { + return [ + { + id: 'overviewMetadata', + 'data-test-subj': 'overviewMetadata', + name: i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.overview', + { + defaultMessage: 'Overview', + } + ), + content: ( + <> + + + + ), + }, + { + id: 'overviewResolver', + 'data-test-subj': 'overviewResolverTab', + name: i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.resolver', + { + defaultMessage: 'Resolver', + } + ), + content: ( + <> + + + + ), + }, + ]; + }, [alertDetailsData]); - return ( - <> -

- -

+ return ( + <> +
+ +

+ +

+
+ + +

+ , + }} + /> +

+
+ + + Endpoint Status:{' '} + + {' '} + + + + + {' '} -

-
- - -

- , - }} - /> -

-
- - - Endpoint Status: Online - - Alert Status: Open - - - -
- - - ); -}); + + + + + + + + ); + }) +)` + height: 100%; + width: 100%; +`; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx index 52ef480bbb930c..d18bc59a35f52e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx @@ -10,12 +10,12 @@ import { Provider } from 'react-redux'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { Resolver } from '../../../../embeddables/resolver/view'; import { EndpointPluginServices } from '../../../../plugin'; -import { LegacyEndpointEvent } from '../../../../../common/types'; +import { ResolverEvent } from '../../../../../common/types'; import { storeFactory } from '../../../../embeddables/resolver/store'; export const AlertDetailResolver = styled( React.memo( - ({ className, selectedEvent }: { className?: string; selectedEvent?: LegacyEndpointEvent }) => { + ({ className, selectedEvent }: { className?: string; selectedEvent?: ResolverEvent }) => { const context = useKibana(); const { store } = storeFactory(context); @@ -33,4 +33,5 @@ export const AlertDetailResolver = styled( width: 100%; display: flex; flex-grow: 1; + min-height: 500px; `; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts index 6892bf11ecff2e..c9a03f0a479682 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts @@ -6,15 +6,15 @@ import { uniquePidForProcess, uniqueParentPidForProcess } from './process_event'; import { IndexedProcessTree } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; import { levelOrder as baseLevelOrder } from '../lib/tree_sequencers'; /** * Create a new IndexedProcessTree from an array of ProcessEvents */ -export function factory(processes: LegacyEndpointEvent[]): IndexedProcessTree { - const idToChildren = new Map(); - const idToValue = new Map(); +export function factory(processes: ResolverEvent[]): IndexedProcessTree { + const idToChildren = new Map(); + const idToValue = new Map(); for (const process of processes) { idToValue.set(uniquePidForProcess(process), process); @@ -36,10 +36,7 @@ export function factory(processes: LegacyEndpointEvent[]): IndexedProcessTree { /** * Returns an array with any children `ProcessEvent`s of the passed in `process` */ -export function children( - tree: IndexedProcessTree, - process: LegacyEndpointEvent -): LegacyEndpointEvent[] { +export function children(tree: IndexedProcessTree, process: ResolverEvent): ResolverEvent[] { const id = uniquePidForProcess(process); const processChildren = tree.idToChildren.get(id); return processChildren === undefined ? [] : processChildren; @@ -50,8 +47,8 @@ export function children( */ export function parent( tree: IndexedProcessTree, - childProcess: LegacyEndpointEvent -): LegacyEndpointEvent | undefined { + childProcess: ResolverEvent +): ResolverEvent | undefined { const uniqueParentPid = uniqueParentPidForProcess(childProcess); if (uniqueParentPid === undefined) { return undefined; @@ -74,7 +71,7 @@ export function root(tree: IndexedProcessTree) { if (size(tree) === 0) { return null; } - let current: LegacyEndpointEvent = tree.idToProcess.values().next().value; + let current: ResolverEvent = tree.idToProcess.values().next().value; while (parent(tree, current) !== undefined) { current = parent(tree, current)!; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts index 876168d2ed96ac..a709d6caf46cbc 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts @@ -4,36 +4,65 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; +import * as event from '../../../../common/models/event'; +import { ResolverProcessType } from '../types'; /** * Returns true if the process's eventType is either 'processCreated' or 'processRan'. * Resolver will only render 'graphable' process events. */ -export function isGraphableProcess(passedEvent: LegacyEndpointEvent) { +export function isGraphableProcess(passedEvent: ResolverEvent) { return eventType(passedEvent) === 'processCreated' || eventType(passedEvent) === 'processRan'; } +function isValue(field: string | string[], value: string) { + if (field instanceof Array) { + return field.length === 1 && field[0] === value; + } else { + return field === value; + } +} + /** * Returns a custom event type for a process event based on the event's metadata. */ -export function eventType(passedEvent: LegacyEndpointEvent) { - const { - endgame: { event_type_full: type, event_subtype_full: subType }, - } = passedEvent; +export function eventType(passedEvent: ResolverEvent): ResolverProcessType { + if (event.isLegacyEvent(passedEvent)) { + const { + endgame: { event_type_full: type, event_subtype_full: subType }, + } = passedEvent; - if (type === 'process_event') { - if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') { - return 'processCreated'; - } else if (subType === 'already_running') { - return 'processRan'; - } else if (subType === 'termination_event') { - return 'processTerminated'; - } else { - return 'unknownProcessEvent'; + if (type === 'process_event') { + if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') { + return 'processCreated'; + } else if (subType === 'already_running') { + return 'processRan'; + } else if (subType === 'termination_event') { + return 'processTerminated'; + } else { + return 'unknownProcessEvent'; + } + } else if (type === 'alert_event') { + return 'processCausedAlert'; + } + } else { + const { + event: { type, category, kind }, + } = passedEvent; + if (isValue(category, 'process')) { + if (isValue(type, 'start') || isValue(type, 'change') || isValue(type, 'creation')) { + return 'processCreated'; + } else if (isValue(type, 'info')) { + return 'processRan'; + } else if (isValue(type, 'end')) { + return 'processTerminated'; + } else { + return 'unknownProcessEvent'; + } + } else if (kind === 'alert') { + return 'processCausedAlert'; } - } else if (type === 'alert_event') { - return 'processCausedAlert'; } return 'unknownEvent'; } @@ -41,13 +70,21 @@ export function eventType(passedEvent: LegacyEndpointEvent) { /** * Returns the process event's pid */ -export function uniquePidForProcess(event: LegacyEndpointEvent) { - return event.endgame.unique_pid; +export function uniquePidForProcess(passedEvent: ResolverEvent): string { + if (event.isLegacyEvent(passedEvent)) { + return String(passedEvent.endgame.unique_pid); + } else { + return passedEvent.process.entity_id; + } } /** * Returns the process event's parent pid */ -export function uniqueParentPidForProcess(event: LegacyEndpointEvent) { - return event.endgame.unique_ppid; +export function uniqueParentPidForProcess(passedEvent: ResolverEvent): string | undefined { + if (event.isLegacyEvent(passedEvent)) { + return String(passedEvent.endgame.unique_ppid); + } else { + return passedEvent.process.parent?.entity_id; + } } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts index ecba0ec404d44d..fec2078cc60c9a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts @@ -5,7 +5,7 @@ */ import { CameraAction } from './camera'; import { DataAction } from './data'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; /** * When the user wants to bring a process node front-and-center on the map. @@ -16,7 +16,7 @@ interface UserBroughtProcessIntoView { /** * Used to identify the process node that should be brought into view. */ - readonly process: LegacyEndpointEvent; + readonly process: ResolverEvent; /** * The time (since epoch in milliseconds) when the action was dispatched. */ @@ -33,7 +33,7 @@ interface UserChangedSelectedEvent { /** * Optional because they could have unselected the event. */ - selectedEvent?: LegacyEndpointEvent; + readonly selectedEvent?: ResolverEvent; }; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts index f34d7c08ce08cc..373afa89921dcd 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyEndpointEvent } from '../../../../../common/types'; +import { ResolverEvent } from '../../../../../common/types'; interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; readonly payload: { readonly data: { readonly result: { - readonly search_results: readonly LegacyEndpointEvent[]; + readonly search_results: readonly ResolverEvent[]; }; }; }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 304abbb06880b2..e8007f82e30c2f 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -14,7 +14,7 @@ import { ProcessWithWidthMetadata, Matrix3, } from '../../types'; -import { LegacyEndpointEvent } from '../../../../../common/types'; +import { ResolverEvent } from '../../../../../common/types'; import { Vector2 } from '../../types'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; import { isGraphableProcess } from '../../models/process_event'; @@ -112,7 +112,7 @@ export const graphableProcesses = createSelector( * */ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): ProcessWidths { - const widths = new Map(); + const widths = new Map(); if (size(indexedProcessTree) === 0) { return widths; @@ -313,13 +313,13 @@ function processPositions( indexedProcessTree: IndexedProcessTree, widths: ProcessWidths ): ProcessPositions { - const positions = new Map(); + const positions = new Map(); /** * This algorithm iterates the tree in level order. It keeps counters that are reset for each parent. * By keeping track of the last parent node, we can know when we are dealing with a new set of siblings and * reset the counters. */ - let lastProcessedParentNode: LegacyEndpointEvent | undefined; + let lastProcessedParentNode: ResolverEvent | undefined; /** * Nodes are positioned relative to their siblings. We walk this in level order, so we handle * children left -> right. @@ -424,7 +424,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( * Transform the positions of nodes and edges so they seem like they are on an isometric grid. */ const transformedEdgeLineSegments: EdgeLineSegment[] = []; - const transformedPositions = new Map(); + const transformedPositions = new Map(); for (const [processEvent, position] of positions) { transformedPositions.set(processEvent, applyMatrix3(position, isometricTransformMatrix)); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts index 9f06643626f500..f15307a6623880 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts @@ -7,7 +7,7 @@ import { animatePanning } from './camera/methods'; import { processNodePositionsAndEdgeLineSegments } from './selectors'; import { ResolverState } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; const animationDuration = 1000; @@ -17,7 +17,7 @@ const animationDuration = 1000; export function animateProcessIntoView( state: ResolverState, startTime: number, - process: LegacyEndpointEvent + process: ResolverEvent ): ResolverState { const { processNodePositions } = processNodePositionsAndEdgeLineSegments(state); const position = processNodePositions.get(process); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts index 900aece60618d3..23e4a4fe7d7edc 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts @@ -8,6 +8,8 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; import { EndpointPluginServices } from '../../../plugin'; import { ResolverState, ResolverAction } from '../types'; +import { ResolverEvent } from '../../../../common/types'; +import * as event from '../../../../common/models/event'; type MiddlewareFactory = ( context?: KibanaReactContextValue @@ -19,22 +21,54 @@ export const resolverMiddlewareFactory: MiddlewareFactory = context => { return api => next => async (action: ResolverAction) => { next(action); if (action.type === 'userChangedSelectedEvent') { - if (context?.services.http) { + /** + * concurrently fetches a process's details, its ancestors, and its related events. + */ + if (context?.services.http && action.payload.selectedEvent) { api.dispatch({ type: 'appRequestedResolverData' }); - const uniquePid = action.payload.selectedEvent?.endgame?.unique_pid; - const legacyEndpointID = action.payload.selectedEvent?.agent?.id; - const [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ - context.services.http.get(`/api/endpoint/resolver/${uniquePid}`, { - query: { legacyEndpointID }, - }), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`, { - query: { legacyEndpointID }, - }), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`, { - query: { legacyEndpointID }, - }), - ]); - const response = [...lifecycle, ...children, ...relatedEvents]; + let response = []; + let lifecycle: ResolverEvent[]; + let childEvents: ResolverEvent[]; + let relatedEvents: ResolverEvent[]; + let children = []; + const ancestors: ResolverEvent[] = []; + const maxAncestors = 5; + if (event.isLegacyEvent(action.payload.selectedEvent)) { + const uniquePid = action.payload.selectedEvent?.endgame?.unique_pid; + const legacyEndpointID = action.payload.selectedEvent?.agent?.id; + [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${uniquePid}`, { + query: { legacyEndpointID }, + }), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`, { + query: { legacyEndpointID }, + }), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`, { + query: { legacyEndpointID }, + }), + ]); + childEvents = children.length > 0 ? children.map((child: any) => child.lifecycle) : []; + } else { + const uniquePid = action.payload.selectedEvent.process.entity_id; + const ppid = action.payload.selectedEvent.process.parent?.entity_id; + async function getAncestors(pid: string | undefined) { + if (ancestors.length < maxAncestors && pid !== undefined) { + const parent = await context?.services.http.get(`/api/endpoint/resolver/${pid}`); + ancestors.push(parent.lifecycle[0]); + if (parent.lifecycle[0].process?.parent?.entity_id) { + await getAncestors(parent.lifecycle[0].process.parent.entity_id); + } + } + } + [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${uniquePid}`), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`), + getAncestors(ppid), + ]); + } + childEvents = children.length > 0 ? children.map((child: any) => child.lifecycle) : []; + response = [...lifecycle, ...childEvents, ...relatedEvents, ...ancestors]; api.dispatch({ type: 'serverReturnedResolverData', payload: { data: { result: { search_results: response } } }, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 4c2a1ea5ac21f2..4380d3ab989999 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -8,7 +8,7 @@ import { Store } from 'redux'; import { ResolverAction } from './store/actions'; export { ResolverAction } from './store/actions'; -import { LegacyEndpointEvent } from '../../../common/types'; +import { ResolverEvent } from '../../../common/types'; /** * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. @@ -115,7 +115,7 @@ export type CameraState = { * State for `data` reducer which handles receiving Resolver data from the backend. */ export interface DataState { - readonly results: readonly LegacyEndpointEvent[]; + readonly results: readonly ResolverEvent[]; isLoading: boolean; } @@ -184,21 +184,21 @@ export interface IndexedProcessTree { /** * Map of ID to a process's children */ - idToChildren: Map; + idToChildren: Map; /** * Map of ID to process */ - idToProcess: Map; + idToProcess: Map; } /** * A map of ProcessEvents (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees` */ -export type ProcessWidths = Map; +export type ProcessWidths = Map; /** * Map of ProcessEvents (representing process nodes) to their positions. Calculated by `processPositions` */ -export type ProcessPositions = Map; +export type ProcessPositions = Map; /** * An array of vectors2 forming an polyline. Used to connect process nodes in the graph. */ @@ -208,11 +208,11 @@ export type EdgeLineSegment = Vector2[]; * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. */ export type ProcessWithWidthMetadata = { - process: LegacyEndpointEvent; + process: ResolverEvent; width: number; } & ( | { - parent: LegacyEndpointEvent; + parent: ResolverEvent; parentWidth: number; isOnlyChild: boolean; firstChildWidth: number; @@ -275,4 +275,15 @@ export interface SideEffectSimulator { mock: jest.Mocked> & Pick; } +/** + * The internal types of process events used by resolver, mapped from v0 and v1 events. + */ +export type ResolverProcessType = + | 'processCreated' + | 'processRan' + | 'processTerminated' + | 'unknownProcessEvent' + | 'processCausedAlert' + | 'unknownEvent'; + export type ResolverStore = Store; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index 52a0872f269f5a..eab22f993d0a89 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -15,7 +15,7 @@ import { GraphControls } from './graph_controls'; import { ProcessEventDot } from './process_event_dot'; import { useCamera } from './use_camera'; import { ResolverAction } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; const StyledPanel = styled(Panel)` position: absolute; @@ -39,7 +39,7 @@ export const Resolver = styled( selectedEvent, }: { className?: string; - selectedEvent?: LegacyEndpointEvent; + selectedEvent?: ResolverEvent; }) { const { processNodePositions, edgeLineSegments } = useSelector( selectors.processNodePositionsAndEdgeLineSegments diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx index 84c299698bb32c..1250c1106b355d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx @@ -11,7 +11,8 @@ import euiVars from '@elastic/eui/dist/eui_theme_light.json'; import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { SideEffectContext } from './side_effect_context'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; +import * as event from '../../../../common/models/event'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as selectors from '../store/selectors'; @@ -38,7 +39,7 @@ export const Panel = memo(function Event({ className }: { className?: string }) interface ProcessTableView { name: string; timestamp?: Date; - event: LegacyEndpointEvent; + event: ResolverEvent; } const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments); @@ -48,14 +49,16 @@ export const Panel = memo(function Event({ className }: { className?: string }) () => [...processNodePositions.keys()].map(processEvent => { let dateTime; - if (processEvent.endgame.timestamp_utc) { - const date = new Date(processEvent.endgame.timestamp_utc); + const eventTime = event.eventTimestamp(processEvent); + const name = event.eventName(processEvent); + if (eventTime) { + const date = new Date(eventTime); if (isFinite(date.getTime())) { dateTime = date; } } return { - name: processEvent.endgame.process_name ? processEvent.endgame.process_name : '', + name, timestamp: dateTime, event: processEvent, }; @@ -115,9 +118,9 @@ export const Panel = memo(function Event({ className }: { className?: string }) }), dataType: 'date', sortable: true, - render(eventTimestamp?: Date) { - return eventTimestamp ? ( - formatter.format(eventTimestamp) + render(eventDate?: Date) { + return eventDate ? ( + formatter.format(eventDate) ) : ( {i18n.translate('xpack.endpoint.resolver.panel.tabel.row.timestampInvalidLabel', { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx index 034780c7ba14c8..2241df97291aed 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -8,7 +8,8 @@ import React from 'react'; import styled from 'styled-components'; import { applyMatrix3 } from '../lib/vector2'; import { Vector2, Matrix3 } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; +import * as eventModel from '../../../../common/models/event'; /** * A placeholder view for a process node. @@ -32,7 +33,7 @@ export const ProcessEventDot = styled( /** * An event which contains details about the process node. */ - event: LegacyEndpointEvent; + event: ResolverEvent; /** * projectionMatrix which can be used to convert `position` to screen coordinates. */ @@ -42,14 +43,13 @@ export const ProcessEventDot = styled( * Convert the position, which is in 'world' coordinates, to screen coordinates. */ const [left, top] = applyMatrix3(position, projectionMatrix); - const style = { left: (left - 20).toString() + 'px', top: (top - 20).toString() + 'px', }; return ( - - name: {event.endgame.process_name} + + name: {eventModel.eventName(event)}
x: {position[0]}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx index 711e4f9a5c537d..6e83fc19a922e2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx @@ -11,7 +11,7 @@ import { Provider } from 'react-redux'; import * as selectors from '../store/selectors'; import { storeFactory } from '../store'; import { Matrix3, ResolverAction, ResolverStore, SideEffectSimulator } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; import { SideEffectContext } from './side_effect_context'; import { applyMatrix3 } from '../lib/vector2'; import { sideEffectSimulator } from './side_effect_simulator'; @@ -133,9 +133,9 @@ describe('useCamera on an unpainted element', () => { expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled(); }); describe('when the camera begins animation', () => { - let process: LegacyEndpointEvent; + let process: ResolverEvent; beforeEach(() => { - const events: LegacyEndpointEvent[] = []; + const events: ResolverEvent[] = []; const numberOfEvents: number = Math.floor(Math.random() * 10 + 1); for (let index = 0; index < numberOfEvents; index++) { @@ -164,7 +164,7 @@ describe('useCamera on an unpainted element', () => { act(() => { store.dispatch(serverResponseAction); }); - const processes: LegacyEndpointEvent[] = [ + const processes: ResolverEvent[] = [ ...selectors .processNodePositionsAndEdgeLineSegments(store.getState()) .processNodePositions.keys(), diff --git a/x-pack/test/functional/apps/endpoint/alerts.ts b/x-pack/test/functional/apps/endpoint/alerts.ts index 1ce7eb41e66906..759574702c0f1b 100644 --- a/x-pack/test/functional/apps/endpoint/alerts.ts +++ b/x-pack/test/functional/apps/endpoint/alerts.ts @@ -18,8 +18,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.load('endpoint/alerts/api_feature'); await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/alerts'); }); - - it('loads in the browser', async () => { + it('loads the Alert List Page', async () => { await testSubjects.existOrFail('alertListPage'); }); it('contains the Alert List Page title', async () => { @@ -57,6 +56,12 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('loads the Alert List Flyout correctly', async () => { await testSubjects.existOrFail('alertDetailFlyout'); }); + + it('loads the resolver component and renders at least a single node', async () => { + await testSubjects.click('overviewResolverTab'); + await testSubjects.existOrFail('alertResolver'); + await testSubjects.existOrFail('resolverNode'); + }); }); after(async () => { From 4c9d95318e1694ff10d3c06836345c34f8e2c36b Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Wed, 18 Mar 2020 13:21:49 -0400 Subject: [PATCH 07/42] change index pattern id to be the same as index pattern title (#60436) --- .../server/services/epm/kibana/index_pattern/install.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index 1f111363604654..7aecc408e05fe0 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -95,10 +95,7 @@ export async function installIndexPatterns( // if this is an update because a package is being unisntalled (no pkgkey argument passed) and no other packages are installed, remove the index pattern if (!pkgkey && installedPackages.length === 0) { try { - await savedObjectsClient.delete( - INDEX_PATTERN_SAVED_OBJECT_TYPE, - `epm-ip-${indexPatternType}` - ); + await savedObjectsClient.delete(INDEX_PATTERN_SAVED_OBJECT_TYPE, `${indexPatternType}-*`); } catch (err) { // index pattern was probably deleted by the user already } @@ -111,7 +108,7 @@ export async function installIndexPatterns( const kibanaIndexPattern = createIndexPattern(indexPatternType, fields); // create or overwrite the index pattern await savedObjectsClient.create(INDEX_PATTERN_SAVED_OBJECT_TYPE, kibanaIndexPattern, { - id: `epm-ip-${indexPatternType}`, + id: `${indexPatternType}-*`, overwrite: true, }); }); From 923de46c7f175fb12c6a5e163db2e1ddb619053a Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 18 Mar 2020 18:26:39 +0100 Subject: [PATCH 08/42] Fix filter scope in bool query (#60488) --- .../console/server/lib/spec_definitions/js/query/dsl.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/plugins/console/server/lib/spec_definitions/js/query/dsl.js b/src/plugins/console/server/lib/spec_definitions/js/query/dsl.js index a5f0d15dee0e9b..16b952fe0fe4ff 100644 --- a/src/plugins/console/server/lib/spec_definitions/js/query/dsl.js +++ b/src/plugins/console/server/lib/spec_definitions/js/query/dsl.js @@ -281,9 +281,11 @@ export function queryDsl(api) { __scope_link: '.', }, ], - filter: { - __scope_link: 'GLOBAL.filter', - }, + filter: [ + { + __scope_link: 'GLOBAL.filter', + }, + ], minimum_should_match: 1, boost: 1.0, }, From 4fc89aeb0debe44632848c230f7a732aae6d5ff7 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 18 Mar 2020 11:42:08 -0600 Subject: [PATCH 09/42] [SIEM] [Cases] Shell scripts and unit tests (#60183) --- .../pages/case/components/all_cases/index.tsx | 7 +- .../case/components/bulk_actions/index.tsx | 1 + .../case/components/case_view/index.test.tsx | 60 ++++++++++++- .../pages/case/components/case_view/index.tsx | 2 +- .../components/confirm_delete_case/index.tsx | 1 + .../pages/case/components/create/index.tsx | 11 ++- .../components/property_actions/index.tsx | 14 ++- x-pack/plugins/case/server/scripts/README.md | 90 +++++++++++++++++++ .../server/scripts/check_env_variables.sh | 41 +++++++++ .../case/server/scripts/delete_cases.sh | 51 +++++++++++ .../case/server/scripts/delete_comment.sh | 39 ++++++++ .../plugins/case/server/scripts/find_cases.sh | 17 ++++ .../server/scripts/find_cases_by_filter.sh | 28 ++++++ .../case/server/scripts/find_cases_sort.sh | 24 +++++ .../scripts/generate_case_and_comment_data.sh | 30 +++++++ .../case/server/scripts/generate_case_data.sh | 16 ++++ .../plugins/case/server/scripts/get_case.sh | 38 ++++++++ .../case/server/scripts/get_case_comments.sh | 38 ++++++++ .../case/server/scripts/get_comment.sh | 40 +++++++++ .../case/server/scripts/get_reporters.sh | 23 +++++ .../plugins/case/server/scripts/get_status.sh | 23 +++++ .../plugins/case/server/scripts/get_tags.sh | 23 +++++ .../plugins/case/server/scripts/hard_reset.sh | 29 ++++++ .../server/scripts/mock/case/post_case.json | 8 ++ .../scripts/mock/case/post_case_v2.json | 8 ++ .../scripts/mock/comment/post_comment.json | 3 + .../scripts/mock/comment/post_comment_v2.json | 3 + .../case/server/scripts/patch_cases.sh | 24 +++++ .../case/server/scripts/patch_comment.sh | 26 ++++++ .../plugins/case/server/scripts/post_case.sh | 37 ++++++++ .../case/server/scripts/post_comment.sh | 57 ++++++++++++ 31 files changed, 801 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/case/server/scripts/README.md create mode 100755 x-pack/plugins/case/server/scripts/check_env_variables.sh create mode 100755 x-pack/plugins/case/server/scripts/delete_cases.sh create mode 100755 x-pack/plugins/case/server/scripts/delete_comment.sh create mode 100755 x-pack/plugins/case/server/scripts/find_cases.sh create mode 100755 x-pack/plugins/case/server/scripts/find_cases_by_filter.sh create mode 100755 x-pack/plugins/case/server/scripts/find_cases_sort.sh create mode 100755 x-pack/plugins/case/server/scripts/generate_case_and_comment_data.sh create mode 100755 x-pack/plugins/case/server/scripts/generate_case_data.sh create mode 100755 x-pack/plugins/case/server/scripts/get_case.sh create mode 100755 x-pack/plugins/case/server/scripts/get_case_comments.sh create mode 100755 x-pack/plugins/case/server/scripts/get_comment.sh create mode 100755 x-pack/plugins/case/server/scripts/get_reporters.sh create mode 100755 x-pack/plugins/case/server/scripts/get_status.sh create mode 100755 x-pack/plugins/case/server/scripts/get_tags.sh create mode 100755 x-pack/plugins/case/server/scripts/hard_reset.sh create mode 100644 x-pack/plugins/case/server/scripts/mock/case/post_case.json create mode 100644 x-pack/plugins/case/server/scripts/mock/case/post_case_v2.json create mode 100644 x-pack/plugins/case/server/scripts/mock/comment/post_comment.json create mode 100644 x-pack/plugins/case/server/scripts/mock/comment/post_comment_v2.json create mode 100755 x-pack/plugins/case/server/scripts/patch_cases.sh create mode 100755 x-pack/plugins/case/server/scripts/patch_comment.sh create mode 100755 x-pack/plugins/case/server/scripts/post_case.sh create mode 100755 x-pack/plugins/case/server/scripts/post_comment.sh diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 7b655999ace09c..9f836bd043c9dc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -231,10 +231,7 @@ export const AllCases = React.memo(() => { sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, }; const euiBasicTableSelectionProps = useMemo>( - () => ({ - selectable: (item: Case) => true, - onSelectionChange: setSelectedCases, - }), + () => ({ onSelectionChange: setSelectedCases }), [selectedCases] ); const isCasesLoading = useMemo( @@ -305,6 +302,7 @@ export const AllCases = React.memo(() => { {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} { { closePopover(); deleteCasesAction(selectedCaseIds); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx index 8754c0404d40ba..15d6cf7cf7317a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx @@ -7,16 +7,30 @@ import React from 'react'; import { mount } from 'enzyme'; import { CaseComponent } from './'; -import * as apiHook from '../../../../containers/case/use_update_case'; +import * as updateHook from '../../../../containers/case/use_update_case'; +import * as deleteHook from '../../../../containers/case/use_delete_cases'; import { caseProps, data } from './__mock__'; import { TestProviders } from '../../../../mock'; describe('CaseView ', () => { + const handleOnDeleteConfirm = jest.fn(); + const handleToggleModal = jest.fn(); + const dispatchResetIsDeleted = jest.fn(); const updateCaseProperty = jest.fn(); + /* eslint-disable no-console */ + // Silence until enzyme fixed to use ReactTestUtils.act() + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ beforeEach(() => { jest.resetAllMocks(); - jest.spyOn(apiHook, 'useUpdateCase').mockReturnValue({ + jest.spyOn(updateHook, 'useUpdateCase').mockReturnValue({ caseData: data, isLoading: false, isError: false, @@ -119,4 +133,46 @@ describe('CaseView ', () => { .prop('source') ).toEqual(data.comments[0].comment); }); + + it('toggle delete modal and cancel', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy(); + + wrapper + .find( + '[data-test-subj="case-view-actions"] button[data-test-subj="property-actions-ellipses"]' + ) + .first() + .simulate('click'); + wrapper.find('button[data-test-subj="property-actions-trash"]').simulate('click'); + expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); + wrapper.find('button[data-test-subj="confirmModalCancelButton"]').simulate('click'); + expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy(); + }); + + it('toggle delete modal and confirm', () => { + jest.spyOn(deleteHook, 'useDeleteCases').mockReturnValue({ + dispatchResetIsDeleted, + handleToggleModal, + handleOnDeleteConfirm, + isLoading: false, + isError: false, + isDeleted: false, + isDisplayConfirmDeleteModal: true, + }); + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); + wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); + expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([caseProps.caseId]); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 5ff542d2089054..82216e88a091e7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -216,7 +216,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => onChange={toggleStatusCase} /> - + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx index dff36a6dac5716..5755258b363885 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx @@ -32,6 +32,7 @@ const ConfirmDeleteCaseModalComp: React.FC = ({ buttonColor="danger" cancelButtonText={i18n.CANCEL} confirmButtonText={isPlural ? i18n.DELETE_CASES : i18n.DELETE_CASE} + data-test-subj="confirm-delete-case-modal" defaultFocusedButton="confirm" onCancel={onCancel} onConfirm={onConfirm} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index 3b9af8349437e8..20712c3c5a815d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -72,6 +72,10 @@ export const Create = React.memo(() => { } }, [form]); + const handleSetIsCancel = useCallback(() => { + setIsCancel(true); + }, [isCancel]); + if (caseData != null && caseData.id) { return ; } @@ -137,7 +141,12 @@ export const Create = React.memo(() => { responsive={false} > - setIsCancel(true)} iconType="cross"> + {i18n.CANCEL} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx index 7fe5b6f5f87940..01ccf3c510b604 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx @@ -13,9 +13,12 @@ export interface PropertyActionButtonProps { label: string; } +const ComponentId = 'property-actions'; + const PropertyActionButton = React.memo( ({ onClick, iconType, label }) => ( (({ propertyActio }, []); return ( - + (({ propertyActio isOpen={showActions} closePopover={onClosePopover} > - + {propertyActions.map((action, key) => ( Date: Wed, 18 Mar 2020 11:53:28 -0600 Subject: [PATCH 10/42] Fix "Notifications is not set" errors. (#60473) --- .../new_platform/new_platform.karma_mock.js | 18 ++++++---- .../public/new_platform/new_platform.test.ts | 34 ++++++++++++++++++- .../ui/public/new_platform/new_platform.ts | 18 ++++++---- src/plugins/data/public/plugin.ts | 2 ++ 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index ea84ba1ad28387..c58a7d2fbb5cde 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -21,13 +21,15 @@ import sinon from 'sinon'; import { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_field_formats'; import { METRIC_TYPE } from '@kbn/analytics'; import { + setFieldFormats, setIndexPatterns, - setQueryService, - setUiSettings, setInjectedMetadata, - setFieldFormats, - setSearchService, + setHttp, + setNotifications, setOverlays, + setQueryService, + setSearchService, + setUiSettings, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -477,11 +479,13 @@ export function __start__(coreStart) { // Services that need to be set in the legacy platform since the legacy data plugin // which previously provided them has been removed. + setHttp(npStart.core.http); + setNotifications(npStart.core.notifications); + setOverlays(npStart.core.overlays); setUiSettings(npStart.core.uiSettings); - setQueryService(npStart.plugins.data.query); - setIndexPatterns(npStart.plugins.data.indexPatterns); setFieldFormats(npStart.plugins.data.fieldFormats); + setIndexPatterns(npStart.plugins.data.indexPatterns); + setQueryService(npStart.plugins.data.query); setSearchService(npStart.plugins.data.search); setAggs(npStart.plugins.data.search.aggs); - setOverlays(npStart.core.overlays); } diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts index 498f05457bba98..dd41093f3a1f04 100644 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ b/src/legacy/ui/public/new_platform/new_platform.test.ts @@ -20,8 +20,19 @@ jest.mock('history'); import { setRootControllerMock, historyMock } from './new_platform.test.mocks'; -import { legacyAppRegister, __reset__, __setup__ } from './new_platform'; +import { + legacyAppRegister, + __reset__, + __setup__, + __start__, + PluginsSetup, + PluginsStart, +} from './new_platform'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import * as dataServices from '../../../../plugins/data/public/services'; +import { LegacyCoreSetup, LegacyCoreStart } from '../../../../core/public'; import { coreMock } from '../../../../core/public/mocks'; +import { npSetup, npStart } from './__mocks__'; describe('ui/new_platform', () => { describe('legacyAppRegister', () => { @@ -108,4 +119,25 @@ describe('ui/new_platform', () => { expect(unmountMock).toHaveBeenCalled(); }); }); + + describe('service getters', () => { + const services: Record = dataServices; + const getters = Object.keys(services).filter(k => k.substring(0, 3) === 'get'); + + getters.forEach(g => { + it(`sets a value for ${g}`, () => { + __reset__(); + __setup__( + (coreMock.createSetup() as unknown) as LegacyCoreSetup, + (npSetup.plugins as unknown) as PluginsSetup + ); + __start__( + (coreMock.createStart() as unknown) as LegacyCoreStart, + (npStart.plugins as unknown) as PluginsStart + ); + + expect(services[g]()).toBeDefined(); + }); + }); + }); }); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 07e17ad5622918..deb8387fee29c3 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -31,13 +31,15 @@ import { } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; import { + setFieldFormats, setIndexPatterns, - setQueryService, - setUiSettings, setInjectedMetadata, - setFieldFormats, - setSearchService, + setHttp, + setNotifications, setOverlays, + setQueryService, + setSearchService, + setUiSettings, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public'; @@ -141,12 +143,14 @@ export function __start__(coreStart: LegacyCoreStart, plugins: PluginsStart) { // Services that need to be set in the legacy platform since the legacy data plugin // which previously provided them has been removed. + setHttp(npStart.core.http); + setNotifications(npStart.core.notifications); + setOverlays(npStart.core.overlays); setUiSettings(npStart.core.uiSettings); - setQueryService(npStart.plugins.data.query); - setIndexPatterns(npStart.plugins.data.indexPatterns); setFieldFormats(npStart.plugins.data.fieldFormats); + setIndexPatterns(npStart.plugins.data.indexPatterns); + setQueryService(npStart.plugins.data.query); setSearchService(npStart.plugins.data.search); - setOverlays(npStart.core.overlays); } /** Flag used to ensure `legacyAppRegister` is only called once. */ diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index a01c1337122068..fc5dde94fa851f 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -39,6 +39,7 @@ import { createIndexPatternSelect } from './ui/index_pattern_select'; import { IndexPatternsService } from './index_patterns'; import { setFieldFormats, + setHttp, setIndexPatterns, setInjectedMetadata, setNotifications, @@ -128,6 +129,7 @@ export class DataPublicPlugin implements Plugin Date: Wed, 18 Mar 2020 12:06:54 -0600 Subject: [PATCH 11/42] [Maps] Blended layer that switches between documents and clusters (#57879) * [Maps] Blended layer that switches between documents and clusters * change layer type when scalingType changes * getSource * use cluster source when count exceeds value * ensure doc source stays in editor * start creating cluster style * pass all parts of style descriptor * get toggling between sources working * derive cluster style from document style * remove references to METRIC_TYPE * fix import * start typescripting blended_vector_layer * more typescript work * last of the TS errors * add migration to convert useTopTerm to scalingType * clean up * remove MapSavedObject work since its in a seperate PR now * fix EsSearchSource update editor jest test * fix map_selector jest test * move mutable state out of BlendedVectorLayer * one more change for removing mutable BlendedVectorLayer state * integrate newly merged MapSavedObjectAttributes type * review feedback * use data request for fetching feature count * add functional test * fix functional test * review feedback Co-authored-by: Elastic Machine --- .../common/data_request_descriptor_types.d.ts | 46 +++ .../plugins/maps/common/descriptor_types.d.ts | 13 +- .../common/migrations/scaling_type.test.ts | 74 +++++ .../maps/common/migrations/scaling_type.ts | 43 +++ x-pack/legacy/plugins/maps/migrations.js | 6 +- .../maps/public/actions/map_actions.d.ts | 18 ++ .../maps/public/actions/map_actions.js | 3 +- .../filter_editor/filter_editor.js | 28 -- .../connected_components/layer_panel/index.js | 3 +- .../connected_components/layer_panel/view.js | 4 +- .../public/layers/blended_vector_layer.ts | 261 ++++++++++++++++++ .../maps/public/layers/heatmap_layer.js | 10 +- .../plugins/maps/public/layers/layer.d.ts | 18 +- .../plugins/maps/public/layers/layer.js | 38 ++- .../es_geo_grid_source.d.ts | 15 +- .../es_geo_grid_source/es_geo_grid_source.js | 7 +- .../es_pew_pew_source/es_pew_pew_source.js | 6 +- .../update_source_editor.test.js.snap | 159 +++++++++-- .../es_search_source/es_search_source.d.ts | 2 +- .../es_search_source/es_search_source.js | 66 ++++- .../es_search_source/es_search_source.test.ts | 3 +- .../es_search_source/update_source_editor.js | 144 +++++++--- .../update_source_editor.test.js | 9 +- .../maps/public/layers/sources/es_source.d.ts | 21 +- .../maps/public/layers/sources/es_source.js | 30 +- .../public/layers/sources/es_term_source.js | 8 +- .../maps/public/layers/sources/source.d.ts | 4 + .../public/layers/sources/vector_source.d.ts | 8 +- .../public/layers/sources/vector_source.js | 4 - .../vector/components/vector_style_editor.js | 2 +- .../properties/dynamic_color_property.test.js | 2 +- .../properties/dynamic_style_property.d.ts | 7 +- .../properties/dynamic_style_property.js | 4 +- .../layers/styles/vector/vector_style.d.ts | 23 ++ .../layers/styles/vector/vector_style.js | 8 +- .../styles/vector/vector_style_defaults.js | 2 +- .../plugins/maps/public/layers/tile_layer.js | 2 +- .../maps/public/layers/tile_layer.test.ts | 8 + .../util/{data_request.js => data_request.ts} | 23 +- .../maps/public/layers/vector_layer.d.ts | 23 ++ .../maps/public/layers/vector_layer.js | 210 ++++++++------ .../maps/public/layers/vector_tile_layer.js | 8 +- .../maps/public/selectors/map_selectors.js | 3 + .../public/selectors/map_selectors.test.js | 1 + x-pack/plugins/maps/common/constants.ts | 7 + x-pack/plugins/maps/public/reducers/map.js | 13 +- .../apps/maps/blended_vector_layer.js | 42 +++ x-pack/test/functional/apps/maps/index.js | 1 + .../es_archives/maps/kibana/data.json | 58 +++- 49 files changed, 1214 insertions(+), 284 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/common/data_request_descriptor_types.d.ts create mode 100644 x-pack/legacy/plugins/maps/common/migrations/scaling_type.test.ts create mode 100644 x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts create mode 100644 x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts create mode 100644 x-pack/legacy/plugins/maps/public/layers/blended_vector_layer.ts create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.d.ts rename x-pack/legacy/plugins/maps/public/layers/util/{data_request.js => data_request.ts} (61%) create mode 100644 x-pack/test/functional/apps/maps/blended_vector_layer.js diff --git a/x-pack/legacy/plugins/maps/common/data_request_descriptor_types.d.ts b/x-pack/legacy/plugins/maps/common/data_request_descriptor_types.d.ts new file mode 100644 index 00000000000000..3281fb5892eac7 --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/data_request_descriptor_types.d.ts @@ -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. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +// Global map state passed to every layer. +export type MapFilters = { + buffer: unknown; + extent: unknown; + filters: unknown[]; + query: unknown; + refreshTimerLastTriggeredAt: string; + timeFilters: unknown; + zoom: number; +}; + +export type VectorLayerRequestMeta = MapFilters & { + applyGlobalQuery: boolean; + fieldNames: string[]; + geogridPrecision: number; + sourceQuery: unknown; + sourceMeta: unknown; +}; + +export type ESSearchSourceResponseMeta = { + areResultsTrimmed?: boolean; + sourceType?: string; + + // top hits meta + areEntitiesTrimmed?: boolean; + entityCount?: number; + totalEntities?: number; +}; + +// Partial because objects are justified downstream in constructors +export type DataMeta = Partial & Partial; + +export type DataRequestDescriptor = { + dataId: string; + dataMetaAtStart?: DataMeta; + dataRequestToken?: symbol; + data?: object; + dataMeta?: DataMeta; +}; diff --git a/x-pack/legacy/plugins/maps/common/descriptor_types.d.ts b/x-pack/legacy/plugins/maps/common/descriptor_types.d.ts index ce0743ba2baedf..2f45c525828dbb 100644 --- a/x-pack/legacy/plugins/maps/common/descriptor_types.d.ts +++ b/x-pack/legacy/plugins/maps/common/descriptor_types.d.ts @@ -5,7 +5,8 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER } from './constants'; +import { DataRequestDescriptor } from './data_request_descriptor_types'; +import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER, SCALING_TYPES } from './constants'; export type AbstractSourceDescriptor = { id?: string; @@ -49,7 +50,7 @@ export type ESSearchSourceDescriptor = AbstractESSourceDescriptor & { tooltipProperties?: string[]; sortField?: string; sortOrder?: SORT_ORDER; - useTopHits?: boolean; + scalingType: SCALING_TYPES; topHitsSplitField?: string; topHitsSize?: number; }; @@ -93,14 +94,6 @@ export type JoinDescriptor = { right: ESTermSourceDescriptor; }; -export type DataRequestDescriptor = { - dataId: string; - dataMetaAtStart: object; - dataRequestToken: symbol; - data: object; - dataMeta: object; -}; - export type LayerDescriptor = { __dataRequests?: DataRequestDescriptor[]; __isInErrorState?: boolean; diff --git a/x-pack/legacy/plugins/maps/common/migrations/scaling_type.test.ts b/x-pack/legacy/plugins/maps/common/migrations/scaling_type.test.ts new file mode 100644 index 00000000000000..4fbb1ef4c55ed3 --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/scaling_type.test.ts @@ -0,0 +1,74 @@ +/* + * 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 { migrateUseTopHitsToScalingType } from './scaling_type'; + +describe('migrateUseTopHitsToScalingType', () => { + test('Should handle missing layerListJSON attribute', () => { + const attributes = { + title: 'my map', + }; + expect(migrateUseTopHitsToScalingType({ attributes })).toEqual({ + title: 'my map', + }); + }); + + test('Should migrate useTopHits: true to scalingType TOP_HITS for ES documents sources', () => { + const layerListJSON = JSON.stringify([ + { + sourceDescriptor: { + type: 'ES_SEARCH', + useTopHits: true, + }, + }, + ]); + const attributes = { + title: 'my map', + layerListJSON, + }; + expect(migrateUseTopHitsToScalingType({ attributes })).toEqual({ + title: 'my map', + layerListJSON: '[{"sourceDescriptor":{"type":"ES_SEARCH","scalingType":"TOP_HITS"}}]', + }); + }); + + test('Should migrate useTopHits: false to scalingType LIMIT for ES documents sources', () => { + const layerListJSON = JSON.stringify([ + { + sourceDescriptor: { + type: 'ES_SEARCH', + useTopHits: false, + }, + }, + ]); + const attributes = { + title: 'my map', + layerListJSON, + }; + expect(migrateUseTopHitsToScalingType({ attributes })).toEqual({ + title: 'my map', + layerListJSON: '[{"sourceDescriptor":{"type":"ES_SEARCH","scalingType":"LIMIT"}}]', + }); + }); + + test('Should set scalingType to LIMIT when useTopHits is not set', () => { + const layerListJSON = JSON.stringify([ + { + sourceDescriptor: { + type: 'ES_SEARCH', + }, + }, + ]); + const attributes = { + title: 'my map', + layerListJSON, + }; + expect(migrateUseTopHitsToScalingType({ attributes })).toEqual({ + title: 'my map', + layerListJSON: '[{"sourceDescriptor":{"type":"ES_SEARCH","scalingType":"LIMIT"}}]', + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts b/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts new file mode 100644 index 00000000000000..5823ddd6b42e35 --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts @@ -0,0 +1,43 @@ +/* + * 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 _ from 'lodash'; +import { ES_SEARCH, SCALING_TYPES } from '../constants'; +import { LayerDescriptor, ESSearchSourceDescriptor } from '../descriptor_types'; +import { MapSavedObjectAttributes } from '../../../../../plugins/maps/common/map_saved_object_type'; + +function isEsDocumentSource(layerDescriptor: LayerDescriptor) { + const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); + return sourceType === ES_SEARCH; +} + +export function migrateUseTopHitsToScalingType({ + attributes, +}: { + attributes: MapSavedObjectAttributes; +}): MapSavedObjectAttributes { + if (!attributes || !attributes.layerListJSON) { + return attributes; + } + + const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + layerList.forEach((layerDescriptor: LayerDescriptor) => { + if (isEsDocumentSource(layerDescriptor)) { + const sourceDescriptor = layerDescriptor.sourceDescriptor as ESSearchSourceDescriptor; + sourceDescriptor.scalingType = _.get(layerDescriptor, 'sourceDescriptor.useTopHits', false) + ? SCALING_TYPES.TOP_HITS + : SCALING_TYPES.LIMIT; + // @ts-ignore useTopHits no longer in type definition but that does not mean its not in live data + // hence the entire point of this method + delete sourceDescriptor.useTopHits; + } + }); + + return { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }; +} diff --git a/x-pack/legacy/plugins/maps/migrations.js b/x-pack/legacy/plugins/maps/migrations.js index 9622f6ba63fac5..6a1f5bc937497b 100644 --- a/x-pack/legacy/plugins/maps/migrations.js +++ b/x-pack/legacy/plugins/maps/migrations.js @@ -10,6 +10,7 @@ import { topHitsTimeToSort } from './common/migrations/top_hits_time_to_sort'; import { moveApplyGlobalQueryToSources } from './common/migrations/move_apply_global_query'; import { addFieldMetaOptions } from './common/migrations/add_field_meta_options'; import { migrateSymbolStyleDescriptor } from './common/migrations/migrate_symbol_style_descriptor'; +import { migrateUseTopHitsToScalingType } from './common/migrations/scaling_type'; export const migrations = { map: { @@ -48,11 +49,12 @@ export const migrations = { }; }, '7.7.0': doc => { - const attributes = migrateSymbolStyleDescriptor(doc); + const attributesPhase1 = migrateSymbolStyleDescriptor(doc); + const attributesPhase2 = migrateUseTopHitsToScalingType({ attributes: attributesPhase1 }); return { ...doc, - attributes, + attributes: attributesPhase2, }; }, }, diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts b/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts new file mode 100644 index 00000000000000..418f2880c10773 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { DataMeta, MapFilters } from '../../common/data_request_descriptor_types'; + +export type SyncContext = { + startLoading(dataId: string, requestToken: symbol, meta: DataMeta): void; + stopLoading(dataId: string, requestToken: symbol, data: unknown, meta: DataMeta): void; + onLoadError(dataId: string, requestToken: symbol, errorMessage: string): void; + updateSourceData(newData: unknown): void; + isRequestStillActive(dataId: string, requestToken: symbol): boolean; + registerCancelCallback(requestToken: symbol, callback: () => void): void; + dataFilters: MapFilters; +}; diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js index 7a1e5e5266246d..415630d9f730bc 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js @@ -649,13 +649,14 @@ export function onDataLoadError(layerId, dataId, requestToken, errorMessage) { }; } -export function updateSourceProp(layerId, propName, value) { +export function updateSourceProp(layerId, propName, value, newLayerType) { return async dispatch => { dispatch({ type: UPDATE_SOURCE_PROP, layerId, propName, value, + newLayerType, }); await dispatch(clearMissingStyleProperties(layerId)); dispatch(syncDataForLayer(layerId)); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index 94e855fc6708f0..60bbaa9825db74 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -16,8 +16,6 @@ import { EuiTextColor, EuiTextAlign, EuiButtonEmpty, - EuiFormRow, - EuiSwitch, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -80,14 +78,6 @@ export class FilterEditor extends Component { this._close(); }; - _onFilterByMapBoundsChange = event => { - this.props.updateSourceProp( - this.props.layer.getId(), - 'filterByMapBounds', - event.target.checked - ); - }; - _onApplyGlobalQueryChange = applyGlobalQuery => { this.props.updateSourceProp(this.props.layer.getId(), 'applyGlobalQuery', applyGlobalQuery); }; @@ -182,22 +172,6 @@ export class FilterEditor extends Component { } render() { - let filterByBoundsSwitch; - if (this.props.layer.getSource().isFilterByMapBoundsConfigurable()) { - filterByBoundsSwitch = ( - - - - ); - } - return ( @@ -217,8 +191,6 @@ export class FilterEditor extends Component { - {filterByBoundsSwitch} - { dispatch(fitToLayerExtent(layerId)); }, - updateSourceProp: (id, propName, value) => dispatch(updateSourceProp(id, propName, value)), + updateSourceProp: (id, propName, value, newLayerType) => + dispatch(updateSourceProp(id, propName, value, newLayerType)), }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js index 755d4bb6b323ac..1b269e388bea02 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js @@ -99,8 +99,8 @@ export class LayerPanel extends React.Component { } } - _onSourceChange = ({ propName, value }) => { - this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value); + _onSourceChange = ({ propName, value, newLayerType }) => { + this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType); }; _renderFilterSection() { diff --git a/x-pack/legacy/plugins/maps/public/layers/blended_vector_layer.ts b/x-pack/legacy/plugins/maps/public/layers/blended_vector_layer.ts new file mode 100644 index 00000000000000..b35eeedfa44fa3 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/blended_vector_layer.ts @@ -0,0 +1,261 @@ +/* + * 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 { VectorLayer } from './vector_layer'; +import { IVectorStyle, VectorStyle } from './styles/vector/vector_style'; +// @ts-ignore +import { getDefaultDynamicProperties, VECTOR_STYLES } from './styles/vector/vector_style_defaults'; +import { IDynamicStyleProperty } from './styles/vector/properties/dynamic_style_property'; +import { IStyleProperty } from './styles/vector/properties/style_property'; +import { + COUNT_PROP_LABEL, + COUNT_PROP_NAME, + ES_GEO_GRID, + LAYER_TYPE, + AGG_TYPE, + SOURCE_DATA_ID_ORIGIN, + RENDER_AS, + STYLE_TYPE, +} from '../../common/constants'; +import { ESGeoGridSource } from './sources/es_geo_grid_source/es_geo_grid_source'; +// @ts-ignore +import { canSkipSourceUpdate } from './util/can_skip_fetch'; +import { IVectorLayer, VectorLayerArguments } from './vector_layer'; +import { IESSource } from './sources/es_source'; +import { IESAggSource } from './sources/es_agg_source'; +import { ISource } from './sources/source'; +import { SyncContext } from '../actions/map_actions'; +import { DataRequestAbortError } from './util/data_request'; + +const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; + +function getAggType(dynamicProperty: IDynamicStyleProperty): AGG_TYPE { + return dynamicProperty.isOrdinal() ? AGG_TYPE.AVG : AGG_TYPE.TERMS; +} + +function getClusterSource(documentSource: IESSource, documentStyle: IVectorStyle): IESAggSource { + const clusterSourceDescriptor = ESGeoGridSource.createDescriptor({ + indexPatternId: documentSource.getIndexPatternId(), + geoField: documentSource.getGeoFieldName(), + requestType: RENDER_AS.POINT, + }); + clusterSourceDescriptor.metrics = [ + { + type: AGG_TYPE.COUNT, + label: COUNT_PROP_LABEL, + }, + ...documentStyle.getDynamicPropertiesArray().map(dynamicProperty => { + return { + type: getAggType(dynamicProperty), + field: dynamicProperty.getFieldName(), + }; + }), + ]; + clusterSourceDescriptor.id = documentSource.getId(); + return new ESGeoGridSource(clusterSourceDescriptor, documentSource.getInspectorAdapters()); +} + +function getClusterStyleDescriptor( + documentStyle: IVectorStyle, + clusterSource: IESAggSource +): unknown { + const defaultDynamicProperties = getDefaultDynamicProperties(); + const clusterStyleDescriptor: any = { + ...documentStyle.getDescriptor(), + properties: { + [VECTOR_STYLES.LABEL_TEXT]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options, + field: { + name: COUNT_PROP_NAME, + origin: SOURCE_DATA_ID_ORIGIN, + }, + }, + }, + [VECTOR_STYLES.ICON_SIZE]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options, + field: { + name: COUNT_PROP_NAME, + origin: SOURCE_DATA_ID_ORIGIN, + }, + }, + }, + }, + }; + documentStyle.getAllStyleProperties().forEach((styleProperty: IStyleProperty) => { + const styleName = styleProperty.getStyleName(); + if ( + [VECTOR_STYLES.LABEL_TEXT, VECTOR_STYLES.ICON_SIZE].includes(styleName) && + (!styleProperty.isDynamic() || !styleProperty.isComplete()) + ) { + // Do not migrate static label and icon size properties to provide unique cluster styling out of the box + return; + } + + if (styleProperty.isDynamic()) { + const options = (styleProperty as IDynamicStyleProperty).getOptions(); + const field = + options && options.field && options.field.name + ? { + ...options.field, + name: clusterSource.getAggKey( + getAggType(styleProperty as IDynamicStyleProperty), + options.field.name + ), + } + : undefined; + clusterStyleDescriptor.properties[styleName] = { + type: STYLE_TYPE.DYNAMIC, + options: { + ...options, + field, + }, + }; + } else { + clusterStyleDescriptor.properties[styleName] = { + type: STYLE_TYPE.STATIC, + options: { ...styleProperty.getOptions() }, + }; + } + }); + + return clusterStyleDescriptor; +} + +export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { + static type = LAYER_TYPE.BLENDED_VECTOR; + + static createDescriptor(options: VectorLayerArguments, mapColors: string[]) { + const layerDescriptor = VectorLayer.createDescriptor(options, mapColors); + layerDescriptor.type = BlendedVectorLayer.type; + return layerDescriptor; + } + + private readonly _isClustered: boolean; + private readonly _clusterSource: IESAggSource; + private readonly _clusterStyle: IVectorStyle; + private readonly _documentSource: IESSource; + private readonly _documentStyle: IVectorStyle; + + constructor(options: VectorLayerArguments) { + super(options); + + this._documentSource = this._source as IESSource; // VectorLayer constructor sets _source as document source + this._documentStyle = this._style; // VectorLayer constructor sets _style as document source + + this._clusterSource = getClusterSource(this._documentSource, this._documentStyle); + const clusterStyleDescriptor = getClusterStyleDescriptor( + this._documentStyle, + this._clusterSource + ); + this._clusterStyle = new VectorStyle(clusterStyleDescriptor, this._clusterSource, this); + + let isClustered = false; + const sourceDataRequest = this.getSourceDataRequest(); + if (sourceDataRequest) { + const requestMeta = sourceDataRequest.getMeta(); + if (requestMeta && requestMeta.sourceType && requestMeta.sourceType === ES_GEO_GRID) { + isClustered = true; + } + } + this._isClustered = isClustered; + } + + destroy() { + if (this._documentSource) { + this._documentSource.destroy(); + } + if (this._clusterSource) { + this._clusterSource.destroy(); + } + } + + async getDisplayName(source: ISource) { + const displayName = await super.getDisplayName(source); + return this._isClustered + ? i18n.translate('xpack.maps.blendedVectorLayer.clusteredLayerName', { + defaultMessage: 'Clustered {displayName}', + values: { displayName }, + }) + : displayName; + } + + isJoinable() { + return false; + } + + getJoins() { + return []; + } + + getSource() { + return this._isClustered ? this._clusterSource : this._documentSource; + } + + getSourceForEditing() { + // Layer is based on this._documentSource + // this._clusterSource is a derived source for rendering only. + // Regardless of this._activeSource, this._documentSource should always be displayed in the editor + return this._documentSource; + } + + getCurrentStyle() { + return this._isClustered ? this._clusterStyle : this._documentStyle; + } + + getStyleForEditing() { + return this._documentStyle; + } + + async syncData(syncContext: SyncContext) { + const dataRequestId = ACTIVE_COUNT_DATA_ID; + const requestToken = Symbol(`layer-active-count:${this.getId()}`); + const searchFilters = this._getSearchFilters( + syncContext.dataFilters, + this.getSource(), + this.getCurrentStyle() + ); + const canSkipFetch = await canSkipSourceUpdate({ + source: this.getSource(), + prevDataRequest: this.getDataRequest(dataRequestId), + nextMeta: searchFilters, + }); + if (canSkipFetch) { + return; + } + + let isSyncClustered; + try { + syncContext.startLoading(dataRequestId, requestToken, searchFilters); + const searchSource = await this._documentSource.makeSearchSource(searchFilters, 0); + const resp = await searchSource.fetch(); + const maxResultWindow = await this._documentSource.getMaxResultWindow(); + isSyncClustered = resp.hits.total > maxResultWindow; + syncContext.stopLoading(dataRequestId, requestToken, { isSyncClustered }, searchFilters); + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + syncContext.onLoadError(dataRequestId, requestToken, error.message); + } + return; + } + + let activeSource; + let activeStyle; + if (isSyncClustered) { + activeSource = this._clusterSource; + activeStyle = this._clusterStyle; + } else { + activeSource = this._documentSource; + activeStyle = this._documentStyle; + } + + super._syncData(syncContext, activeSource, activeStyle); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js b/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js index 29223d6a67c6b8..ef78b5afe3a3a0 100644 --- a/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js @@ -32,7 +32,7 @@ export class HeatmapLayer extends VectorLayer { } _getPropKeyOfSelectedMetric() { - const metricfields = this._source.getMetricFields(); + const metricfields = this.getSource().getMetricFields(); return metricfields[0].getName(); } @@ -84,11 +84,11 @@ export class HeatmapLayer extends VectorLayer { } this.syncVisibilityWithMb(mbMap, heatmapLayerId); - this._style.setMBPaintProperties({ + this.getCurrentStyle().setMBPaintProperties({ mbMap, layerId: heatmapLayerId, propertyName: SCALED_PROPERTY_NAME, - resolution: this._source.getGridResolution(), + resolution: this.getSource().getGridResolution(), }); mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha()); mbMap.setLayerZoomRange(heatmapLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); @@ -103,7 +103,7 @@ export class HeatmapLayer extends VectorLayer { } renderLegendDetails() { - const metricFields = this._source.getMetricFields(); - return this._style.renderLegendDetails(metricFields[0]); + const metricFields = this.getSource().getMetricFields(); + return this.getCurrentStyle().renderLegendDetails(metricFields[0]); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.d.ts b/x-pack/legacy/plugins/maps/public/layers/layer.d.ts index eebbaac7d4f977..777566298e6077 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/layer.d.ts @@ -5,9 +5,17 @@ */ import { LayerDescriptor } from '../../common/descriptor_types'; import { ISource } from './sources/source'; +import { DataRequest } from './util/data_request'; +import { SyncContext } from '../actions/map_actions'; export interface ILayer { - getDisplayName(): Promise; + getDataRequest(id: string): DataRequest | undefined; + getDisplayName(source?: ISource): Promise; + getId(): string; + getSourceDataRequest(): DataRequest | undefined; + getSource(): ISource; + getSourceForEditing(): ISource; + syncData(syncContext: SyncContext): Promise; } export interface ILayerArguments { @@ -17,5 +25,11 @@ export interface ILayerArguments { export class AbstractLayer implements ILayer { constructor(layerArguments: ILayerArguments); - getDisplayName(): Promise; + getDataRequest(id: string): DataRequest | undefined; + getDisplayName(source?: ISource): Promise; + getId(): string; + getSourceDataRequest(): DataRequest | undefined; + getSource(): ISource; + getSourceForEditing(): ISource; + syncData(syncContext: SyncContext): Promise; } diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 5c9532a3841f36..d162e342dfd1a8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -63,7 +63,7 @@ export class AbstractLayer { clonedDescriptor.id = uuid(); const displayName = await this.getDisplayName(); clonedDescriptor.label = `Clone of ${displayName}`; - clonedDescriptor.sourceDescriptor = this._source.cloneDescriptor(); + clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor(); if (clonedDescriptor.joins) { clonedDescriptor.joins.forEach(joinDescriptor => { // right.id is uuid used to track requests in inspector @@ -78,28 +78,31 @@ export class AbstractLayer { } isJoinable() { - return this._source.isJoinable(); + return this.getSource().isJoinable(); } supportsElasticsearchFilters() { - return this._source.isESSource(); + return this.getSource().isESSource(); } async supportsFitToBounds() { - return await this._source.supportsFitToBounds(); + return await this.getSource().supportsFitToBounds(); } - async getDisplayName() { + async getDisplayName(source) { if (this._descriptor.label) { return this._descriptor.label; } - return (await this._source.getDisplayName()) || `Layer ${this._descriptor.id}`; + const sourceDisplayName = source + ? await source.getDisplayName() + : await this.getSource().getDisplayName(); + return sourceDisplayName || `Layer ${this._descriptor.id}`; } async getAttributions() { if (!this.hasErrors()) { - return await this._source.getAttributions(); + return await this.getSource().getAttributions(); } return []; } @@ -191,6 +194,10 @@ export class AbstractLayer { return this._source; } + getSourceForEditing() { + return this._source; + } + isVisible() { return this._descriptor.visible; } @@ -226,12 +233,16 @@ export class AbstractLayer { return this._style; } + getStyleForEditing() { + return this._style; + } + async getImmutableSourceProperties() { - return this._source.getImmutableProperties(); + return this.getSource().getImmutableProperties(); } renderSourceSettingsEditor = ({ onChange }) => { - return this._source.renderSourceSettingsEditor({ onChange }); + return this.getSourceForEditing().renderSourceSettingsEditor({ onChange }); }; getPrevRequestToken(dataId) { @@ -319,10 +330,11 @@ export class AbstractLayer { } renderStyleEditor({ onStyleDescriptorChange }) { - if (!this._style) { + const style = this.getStyleForEditing(); + if (!style) { return null; } - return this._style.renderEditor({ layer: this, onStyleDescriptorChange }); + return style.renderEditor({ layer: this, onStyleDescriptorChange }); } getIndexPatternIds() { @@ -333,10 +345,6 @@ export class AbstractLayer { return []; } - async getFields() { - return []; - } - syncVisibilityWithMb(mbMap, mbLayerId) { mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none'); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts index 48e90b6c41d51a..3f596cea1ae393 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts @@ -6,10 +6,23 @@ import { AbstractESAggSource } from '../es_agg_source'; import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; -import { GRID_RESOLUTION } from '../../../../common/constants'; +import { GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants'; export class ESGeoGridSource extends AbstractESAggSource { + static createDescriptor({ + indexPatternId, + geoField, + requestType, + resolution, + }: { + indexPatternId: string; + geoField: string; + requestType: RENDER_AS; + resolution?: GRID_RESOLUTION; + }): ESGeoGridSourceDescriptor; + constructor(sourceDescriptor: ESGeoGridSourceDescriptor, inspectorAdapters: unknown); + getGridResolution(): GRID_RESOLUTION; getGeoGridPrecision(zoom: number): number; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index 3b3e8004ded053..5ad202a02ae6d1 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -75,7 +75,7 @@ export class ESGeoGridSource extends AbstractESAggSource { renderSourceSettingsEditor({ onChange }) { return ( { @@ -325,6 +325,7 @@ export class ESGeoGridSource extends AbstractESAggSource { }, meta: { areResultsTrimmed: false, + sourceType: ES_GEO_GRID, }, }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index 53536b11aaca66..8e1145c531f9e5 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -64,7 +64,7 @@ export class ESPewPewSource extends AbstractESAggSource { renderSourceSettingsEditor({ onChange }) { return ( - + + + +
+ +
+
+ + + + @@ -112,7 +152,7 @@ exports[`should enable sort order select when sort field provided 1`] = `
`; -exports[`should render top hits form when useTopHits is true 1`] = ` +exports[`should render top hits form when scaling type is TOP_HITS 1`] = ` - + + + +
+ +
+
+ + + + + - + + + +
+ +
+
+ + + + diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts index 5d8188f19f4eaa..0a4e48a195ec66 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts @@ -8,5 +8,5 @@ import { AbstractESSource } from '../es_source'; import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; export class ESSearchSource extends AbstractESSource { - constructor(sourceDescriptor: ESSearchSourceDescriptor, inspectorAdapters: unknown); + constructor(sourceDescriptor: Partial, inspectorAdapters: unknown); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index 7f0e8707605120..440b9aa89a9457 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -11,6 +11,8 @@ import uuid from 'uuid/v4'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { AbstractESSource } from '../es_source'; import { SearchSource } from '../../../kibana_services'; +import { VectorStyle } from '../../styles/vector/vector_style'; +import { VectorLayer } from '../../vector_layer'; import { hitsToGeoJson } from '../../../elasticsearch_geo_utils'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; @@ -19,11 +21,13 @@ import { ES_GEO_FIELD_TYPE, DEFAULT_MAX_BUCKETS_LIMIT, SORT_ORDER, + SCALING_TYPES, } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { getSourceFields } from '../../../index_pattern_util'; import { loadIndexSettings } from './load_index_settings'; +import { BlendedVectorLayer } from '../../blended_vector_layer'; import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; import { ESDocField } from '../../fields/es_doc_field'; @@ -99,7 +103,7 @@ export class ESSearchSource extends AbstractESSource { tooltipProperties: _.get(descriptor, 'tooltipProperties', []), sortField: _.get(descriptor, 'sortField', ''), sortOrder: _.get(descriptor, 'sortOrder', SORT_ORDER.DESC), - useTopHits: _.get(descriptor, 'useTopHits', false), + scalingType: _.get(descriptor, 'scalingType', SCALING_TYPES.LIMIT), topHitsSplitField: descriptor.topHitsSplitField, topHitsSize: _.get(descriptor, 'topHitsSize', 1), }, @@ -111,6 +115,32 @@ export class ESSearchSource extends AbstractESSource { ); } + createDefaultLayer(options, mapColors) { + if (this._descriptor.scalingType === SCALING_TYPES.CLUSTERS) { + const layerDescriptor = BlendedVectorLayer.createDescriptor( + { + sourceDescriptor: this._descriptor, + ...options, + }, + mapColors + ); + const style = new VectorStyle(layerDescriptor.style, this); + return new BlendedVectorLayer({ + layerDescriptor: layerDescriptor, + source: this, + style, + }); + } + + const layerDescriptor = this._createDefaultLayerDescriptor(options, mapColors); + const style = new VectorStyle(layerDescriptor.style, this); + return new VectorLayer({ + layerDescriptor: layerDescriptor, + source: this, + style, + }); + } + createField({ fieldName }) { return new ESDocField({ fieldName, @@ -122,12 +152,14 @@ export class ESSearchSource extends AbstractESSource { return ( @@ -157,7 +189,7 @@ export class ESSearchSource extends AbstractESSource { } async getImmutableProperties() { - let indexPatternTitle = this._descriptor.indexPatternId; + let indexPatternTitle = this.getIndexPatternId(); let geoFieldType = ''; try { const indexPattern = await this.getIndexPattern(); @@ -239,7 +271,7 @@ export class ESSearchSource extends AbstractESSource { shard_size: DEFAULT_MAX_BUCKETS_LIMIT, }; - const searchSource = await this._makeSearchSource(searchFilters, 0); + const searchSource = await this.makeSearchSource(searchFilters, 0); searchSource.setField('aggs', { totalEntities: { cardinality: addFieldToDSL(cardinalityAgg, topHitsSplitField), @@ -300,7 +332,7 @@ export class ESSearchSource extends AbstractESSource { ); const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source - const searchSource = await this._makeSearchSource( + const searchSource = await this.makeSearchSource( searchFilters, maxResultWindow, initialSearchContext @@ -332,8 +364,8 @@ export class ESSearchSource extends AbstractESSource { } _isTopHits() { - const { useTopHits, topHitsSplitField } = this._descriptor; - return !!(useTopHits && topHitsSplitField); + const { scalingType, topHitsSplitField } = this._descriptor; + return !!(scalingType === SCALING_TYPES.TOP_HITS && topHitsSplitField); } _hasSort() { @@ -341,6 +373,12 @@ export class ESSearchSource extends AbstractESSource { return !!sortField && !!sortOrder; } + async getMaxResultWindow() { + const indexPattern = await this.getIndexPattern(); + const indexSettings = await loadIndexSettings(indexPattern.title); + return indexSettings.maxResultWindow; + } + async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) { const indexPattern = await this.getIndexPattern(); @@ -383,7 +421,7 @@ export class ESSearchSource extends AbstractESSource { return { data: featureCollection, - meta, + meta: { ...meta, sourceType: ES_SEARCH }, }; } @@ -442,11 +480,9 @@ export class ESSearchSource extends AbstractESSource { } isFilterByMapBounds() { - return _.get(this._descriptor, 'filterByMapBounds', false); - } - - isFilterByMapBoundsConfigurable() { - return true; + return this._descriptor.scalingType === SCALING_TYPES.CLUSTER + ? true + : this._descriptor.filterByMapBounds; } async getLeftJoinFields() { @@ -533,7 +569,7 @@ export class ESSearchSource extends AbstractESSource { return { sortField: this._descriptor.sortField, sortOrder: this._descriptor.sortOrder, - useTopHits: this._descriptor.useTopHits, + scalingType: this._descriptor.scalingType, topHitsSplitField: this._descriptor.topHitsSplitField, topHitsSize: this._descriptor.topHitsSize, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts index 1e10923cea1d0e..59120e221ca499 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts @@ -7,7 +7,7 @@ jest.mock('ui/new_platform'); import { ESSearchSource } from './es_search_source'; import { VectorLayer } from '../../vector_layer'; -import { ES_SEARCH } from '../../../../common/constants'; +import { ES_SEARCH, SCALING_TYPES } from '../../../../common/constants'; import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; const descriptor: ESSearchSourceDescriptor = { @@ -15,6 +15,7 @@ const descriptor: ESSearchSourceDescriptor = { id: '1234', indexPatternId: 'myIndexPattern', geoField: 'myLocation', + scalingType: SCALING_TYPES.LIMIT, }; describe('ES Search Source', () => { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js index 52702c1f4ecc79..b85cca113cf98a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js @@ -14,6 +14,7 @@ import { EuiPanel, EuiSpacer, EuiHorizontalRule, + EuiRadioGroup, } from '@elastic/eui'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { TooltipSelector } from '../../../components/tooltip_selector'; @@ -22,7 +23,13 @@ import { indexPatternService } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; import { getTermsFields, getSourceFields } from '../../../index_pattern_util'; import { ValidatedRange } from '../../../components/validated_range'; -import { DEFAULT_MAX_INNER_RESULT_WINDOW, SORT_ORDER } from '../../../../common/constants'; +import { + DEFAULT_MAX_INNER_RESULT_WINDOW, + DEFAULT_MAX_RESULT_WINDOW, + SORT_ORDER, + SCALING_TYPES, + LAYER_TYPE, +} from '../../../../common/constants'; import { ESDocField } from '../../fields/es_doc_field'; import { FormattedMessage } from '@kbn/i18n/react'; import { loadIndexSettings } from './load_index_settings'; @@ -35,7 +42,7 @@ export class UpdateSourceEditor extends Component { tooltipFields: PropTypes.arrayOf(PropTypes.object).isRequired, sortField: PropTypes.string, sortOrder: PropTypes.string.isRequired, - useTopHits: PropTypes.bool.isRequired, + scalingType: PropTypes.string.isRequired, topHitsSplitField: PropTypes.string, topHitsSize: PropTypes.number.isRequired, source: PropTypes.object, @@ -46,6 +53,8 @@ export class UpdateSourceEditor extends Component { termFields: null, sortFields: null, maxInnerResultWindow: DEFAULT_MAX_INNER_RESULT_WINDOW, + maxResultWindow: DEFAULT_MAX_RESULT_WINDOW, + supportsClustering: false, }; componentDidMount() { @@ -61,9 +70,9 @@ export class UpdateSourceEditor extends Component { async loadIndexSettings() { try { const indexPattern = await indexPatternService.get(this.props.indexPatternId); - const { maxInnerResultWindow } = await loadIndexSettings(indexPattern.title); + const { maxInnerResultWindow, maxResultWindow } = await loadIndexSettings(indexPattern.title); if (this._isMounted) { - this.setState({ maxInnerResultWindow }); + this.setState({ maxInnerResultWindow, maxResultWindow }); } } catch (err) { return; @@ -88,6 +97,16 @@ export class UpdateSourceEditor extends Component { return; } + let geoField; + try { + geoField = await this.props.getGeoField(); + } catch (err) { + if (this._isMounted) { + this.setState({ loadError: err.message }); + } + return; + } + if (!this._isMounted) { return; } @@ -102,6 +121,7 @@ export class UpdateSourceEditor extends Component { }); this.setState({ + supportsClustering: geoField.aggregatable, sourceFields: sourceFields, termFields: getTermsFields(indexPattern.fields), //todo change term fields to use fields sortFields: indexPattern.fields.filter( @@ -113,8 +133,14 @@ export class UpdateSourceEditor extends Component { this.props.onChange({ propName: 'tooltipProperties', value: propertyNames }); }; - onUseTopHitsChange = event => { - this.props.onChange({ propName: 'useTopHits', value: event.target.checked }); + _onScalingTypeChange = optionId => { + const layerType = + optionId === SCALING_TYPES.CLUSTERS ? LAYER_TYPE.BLENDED_VECTOR : LAYER_TYPE.VECTOR; + this.props.onChange({ propName: 'scalingType', value: optionId, newLayerType: layerType }); + }; + + _onFilterByMapBoundsChange = event => { + this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked }); }; onTopHitsSplitFieldChange = topHitsSplitField => { @@ -133,29 +159,7 @@ export class UpdateSourceEditor extends Component { this.props.onChange({ propName: 'topHitsSize', value: size }); }; - renderTopHitsForm() { - const topHitsSwitch = ( - - - - ); - - if (!this.props.useTopHits) { - return topHitsSwitch; - } - + _renderTopHitsForm() { let sizeSlider; if (this.props.topHitsSplitField) { sizeSlider = ( @@ -183,7 +187,6 @@ export class UpdateSourceEditor extends Component { return ( - {topHitsSwitch} +
+ ); + } + + _renderScalingPanel() { + const scalingOptions = [ + { + id: SCALING_TYPES.LIMIT, + label: i18n.translate('xpack.maps.source.esSearch.limitScalingLabel', { + defaultMessage: 'Limit results to {maxResultWindow}.', + values: { maxResultWindow: this.state.maxResultWindow }, + }), + }, + { + id: SCALING_TYPES.TOP_HITS, + label: i18n.translate('xpack.maps.source.esSearch.useTopHitsLabel', { + defaultMessage: 'Show top hits per entity.', + }), + }, + ]; + if (this.state.supportsClustering) { + scalingOptions.push({ + id: SCALING_TYPES.CLUSTERS, + label: i18n.translate('xpack.maps.source.esSearch.clusterScalingLabel', { + defaultMessage: 'Show clusters when results exceed {maxResultWindow}.', + values: { maxResultWindow: this.state.maxResultWindow }, + }), + }); + } - - {this.renderTopHitsForm()} + let filterByBoundsSwitch; + if (this.props.scalingType !== SCALING_TYPES.CLUSTERS) { + filterByBoundsSwitch = ( + + + + ); + } + + let scalingForm = null; + if (this.props.scalingType === SCALING_TYPES.TOP_HITS) { + scalingForm = ( + + + {this._renderTopHitsForm()} + + ); + } + + return ( + + +
+ +
+
+ + + + + + + + {filterByBoundsSwitch} + + {scalingForm}
); } @@ -302,6 +379,9 @@ export class UpdateSourceEditor extends Component { {this._renderSortPanel()} + + {this._renderScalingPanel()} +
); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js index badfba7665dfdd..e8a845c4b16696 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js @@ -16,6 +16,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { UpdateSourceEditor } from './update_source_editor'; +import { SCALING_TYPES } from '../../../../common/constants'; const defaultProps = { indexPatternId: 'indexPattern1', @@ -23,7 +24,7 @@ const defaultProps = { filterByMapBounds: true, tooltipFields: [], sortOrder: 'DESC', - useTopHits: false, + scalingType: SCALING_TYPES.LIMIT, topHitsSplitField: 'trackId', topHitsSize: 1, }; @@ -40,8 +41,10 @@ test('should enable sort order select when sort field provided', async () => { expect(component).toMatchSnapshot(); }); -test('should render top hits form when useTopHits is true', async () => { - const component = shallow(); +test('should render top hits form when scaling type is TOP_HITS', async () => { + const component = shallow( + + ); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.d.ts index 25c4fae89f0247..963a30c7413e83 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.d.ts @@ -6,12 +6,31 @@ import { AbstractVectorSource } from './vector_source'; import { IVectorSource } from './vector_source'; -import { IndexPattern } from '../../../../../../../src/plugins/data/public'; +import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/public'; +import { VectorLayerRequestMeta } from '../../../common/data_request_descriptor_types'; export interface IESSource extends IVectorSource { + getId(): string; getIndexPattern(): Promise; + getIndexPatternId(): string; + getGeoFieldName(): string; + getMaxResultWindow(): Promise; + makeSearchSource( + searchFilters: VectorLayerRequestMeta, + limit: number, + initialSearchContext?: object + ): Promise; } export class AbstractESSource extends AbstractVectorSource implements IESSource { + getId(): string; getIndexPattern(): Promise; + getIndexPatternId(): string; + getGeoFieldName(): string; + getMaxResultWindow(): Promise; + makeSearchSource( + searchFilters: VectorLayerRequestMeta, + limit: number, + initialSearchContext?: object + ): Promise; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index 1552db277e609e..c5bf9a8be75bd1 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -35,6 +35,10 @@ export class AbstractESSource extends AbstractVectorSource { ); } + getId() { + return this._descriptor.id; + } + isFieldAware() { return true; } @@ -48,12 +52,12 @@ export class AbstractESSource extends AbstractVectorSource { } getIndexPatternIds() { - return [this._descriptor.indexPatternId]; + return [this.getIndexPatternId()]; } getQueryableIndexPatternIds() { if (this.getApplyGlobalQuery()) { - return [this._descriptor.indexPatternId]; + return [this.getIndexPatternId()]; } return []; } @@ -106,7 +110,7 @@ export class AbstractESSource extends AbstractVectorSource { } } - async _makeSearchSource(searchFilters, limit, initialSearchContext) { + async makeSearchSource(searchFilters, limit, initialSearchContext) { const indexPattern = await this.getIndexPattern(); const isTimeAware = await this.isTimeAware(); const applyGlobalQuery = _.get(searchFilters, 'applyGlobalQuery', true); @@ -143,7 +147,7 @@ export class AbstractESSource extends AbstractVectorSource { } async getBoundsForFilters({ sourceQuery, query, timeFilters, filters, applyGlobalQuery }) { - const searchSource = await this._makeSearchSource( + const searchSource = await this.makeSearchSource( { sourceQuery, query, timeFilters, filters, applyGlobalQuery }, 0 ); @@ -190,19 +194,27 @@ export class AbstractESSource extends AbstractVectorSource { } } + getIndexPatternId() { + return this._descriptor.indexPatternId; + } + + getGeoFieldName() { + return this._descriptor.geoField; + } + async getIndexPattern() { if (this.indexPattern) { return this.indexPattern; } try { - this.indexPattern = await indexPatternService.get(this._descriptor.indexPatternId); + this.indexPattern = await indexPatternService.get(this.getIndexPatternId()); return this.indexPattern; } catch (error) { throw new Error( i18n.translate('xpack.maps.source.esSource.noIndexPatternErrorMessage', { defaultMessage: `Unable to find Index pattern for id: {indexPatternId}`, - values: { indexPatternId: this._descriptor.indexPatternId }, + values: { indexPatternId: this.getIndexPatternId() }, }) ); } @@ -219,7 +231,7 @@ export class AbstractESSource extends AbstractVectorSource { } } - async _getGeoField() { + _getGeoField = async () => { const indexPattern = await this.getIndexPattern(); const geoField = indexPattern.fields.getByName(this._descriptor.geoField); if (!geoField) { @@ -231,7 +243,7 @@ export class AbstractESSource extends AbstractVectorSource { ); } return geoField; - } + }; async getDisplayName() { try { @@ -239,7 +251,7 @@ export class AbstractESSource extends AbstractVectorSource { return indexPattern.title; } catch (error) { // Unable to load index pattern, just return id as display name - return this._descriptor.indexPatternId; + return this.getIndexPatternId(); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js index c12b4befc0684d..3ce0fb58aba195 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js @@ -51,10 +51,6 @@ export class ESTermSource extends AbstractESAggSource { return _.has(this._descriptor, 'indexPatternId') && _.has(this._descriptor, 'term'); } - getIndexPatternIds() { - return [this._descriptor.indexPatternId]; - } - getTermField() { return this._termField; } @@ -90,7 +86,7 @@ export class ESTermSource extends AbstractESAggSource { } const indexPattern = await this.getIndexPattern(); - const searchSource = await this._makeSearchSource(searchFilters, 0); + const searchSource = await this.makeSearchSource(searchFilters, 0); const termsField = getField(indexPattern, this._termField.getName()); const termsAgg = { size: DEFAULT_MAX_BUCKETS_LIMIT }; searchSource.setField('aggs', { @@ -126,7 +122,7 @@ export class ESTermSource extends AbstractESAggSource { async getDisplayName() { //no need to localize. this is never rendered. - return `es_table ${this._descriptor.indexPatternId}`; + return `es_table ${this.getIndexPatternId()}`; } async filterAndFormatPropertiesToHtml(properties) { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/source.d.ts index b5b34efabda0ae..2ca18e47a4bf93 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/source.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/source.d.ts @@ -10,10 +10,14 @@ import { ILayer } from '../layer'; export interface ISource { createDefaultLayer(): ILayer; getDisplayName(): Promise; + destroy(): void; + getInspectorAdapters(): object; } export class AbstractSource implements ISource { constructor(sourceDescriptor: AbstractSourceDescriptor, inspectorAdapters: object); createDefaultLayer(): ILayer; getDisplayName(): Promise; + destroy(): void; + getInspectorAdapters(): object; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts index 7de3fe1823cb7a..14fc23751ac1a9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts @@ -7,13 +7,9 @@ import { AbstractSource, ISource } from './source'; import { IField } from '../fields/field'; +import { ESSearchSourceResponseMeta } from '../../../common/data_request_descriptor_types'; -export type GeoJsonFetchMeta = { - areResultsTrimmed: boolean; - areEntitiesTrimmed?: boolean; - entityCount?: number; - totalEntities?: number; -}; +export type GeoJsonFetchMeta = ESSearchSourceResponseMeta; export type GeoJsonWithMeta = { data: unknown; // geojson feature collection diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js index 0f74dd605c8f1b..7ff1c735c8613a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js @@ -98,10 +98,6 @@ export class AbstractVectorSource extends AbstractSource { return false; } - isFilterByMapBoundsConfigurable() { - return false; - } - isBoundsAware() { return false; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 8e05cf287efa62..acc26e5fce6996 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -69,7 +69,7 @@ export class VectorStyleEditor extends Component { }; //These are all fields (only used for text labeling) - const fields = await this.props.layer.getFields(); + const fields = await this.props.layer.getStyleEditorFields(); const fieldPromises = fields.map(getFieldMeta); const fieldsArrayAll = await Promise.all(fieldPromises); if (!this._isMounted || _.isEqual(fieldsArrayAll, this.state.fields)) { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js index f74deb17fff7c8..5b5028f68f08cf 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js @@ -71,7 +71,7 @@ class MockLayer { return new MockStyle(); } - findDataRequestById() { + getDataRequest() { return null; } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts index f4c487b28757eb..25063944b88914 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts @@ -7,13 +7,17 @@ import { IStyleProperty } from './style_property'; import { FIELD_ORIGIN } from '../../../../../common/constants'; -import { FieldMetaOptions } from '../../../../../common/style_property_descriptor_types'; +import { + FieldMetaOptions, + DynamicStylePropertyOptions, +} from '../../../../../common/style_property_descriptor_types'; import { IField } from '../../../fields/field'; import { IVectorLayer } from '../../../vector_layer'; import { IVectorSource } from '../../../sources/vector_source'; import { CategoryFieldMeta, RangeFieldMeta } from '../../../../../common/descriptor_types'; export interface IDynamicStyleProperty extends IStyleProperty { + getOptions(): DynamicStylePropertyOptions; getFieldMetaOptions(): FieldMetaOptions; getField(): IField | undefined; getFieldName(): string; @@ -22,6 +26,7 @@ export interface IDynamicStyleProperty extends IStyleProperty { getRangeFieldMeta(): RangeFieldMeta; getCategoryFieldMeta(): CategoryFieldMeta; isFieldMetaEnabled(): boolean; + isOrdinal(): boolean; supportsFieldMeta(): boolean; getFieldMetaRequest(): Promise; supportsMbFeatureState(): boolean; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index 030d3a2a1ef87c..68e06bacfa7b7d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -62,7 +62,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return rangeFieldMetaFromLocalFeatures; } - const styleMetaDataRequest = this._layer.findDataRequestById(dataRequestId); + const styleMetaDataRequest = this._layer.getDataRequest(dataRequestId); if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { return rangeFieldMetaFromLocalFeatures; } @@ -87,7 +87,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return categoryFieldMetaFromLocalFeatures; } - const styleMetaDataRequest = this._layer.findDataRequestById(dataRequestId); + const styleMetaDataRequest = this._layer.getDataRequest(dataRequestId); if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { return categoryFieldMetaFromLocalFeatures; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.d.ts b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.d.ts new file mode 100644 index 00000000000000..ac84a3b6447d2a --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.d.ts @@ -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 { IStyleProperty } from './properties/style_property'; +import { IDynamicStyleProperty } from './properties/dynamic_style_property'; +import { IVectorLayer } from '../../vector_layer'; +import { IVectorSource } from '../../sources/vector_source'; + +export interface IVectorStyle { + getAllStyleProperties(): IStyleProperty[]; + getDescriptor(): object; + getDynamicPropertiesArray(): IDynamicStyleProperty[]; +} + +export class VectorStyle implements IVectorStyle { + constructor(descriptor: unknown, source: IVectorSource, layer: IVectorLayer); + + getAllStyleProperties(): IStyleProperty[]; + getDescriptor(): object; + getDynamicPropertiesArray(): IDynamicStyleProperty[]; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 1c8ff3e205a38e..6ad60e15f10e16 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -123,7 +123,7 @@ export class VectorStyle extends AbstractStyle { ); } - _getAllStyleProperties() { + getAllStyleProperties() { return [ this._symbolizeAsStyleProperty, this._iconStyleProperty, @@ -164,7 +164,7 @@ export class VectorStyle extends AbstractStyle { }); const styleProperties = {}; - this._getAllStyleProperties().forEach(styleProperty => { + this.getAllStyleProperties().forEach(styleProperty => { styleProperties[styleProperty.getStyleName()] = styleProperty; }); @@ -339,7 +339,7 @@ export class VectorStyle extends AbstractStyle { } getDynamicPropertiesArray() { - const styleProperties = this._getAllStyleProperties(); + const styleProperties = this.getAllStyleProperties(); return styleProperties.filter( styleProperty => styleProperty.isDynamic() && styleProperty.isComplete() ); @@ -390,7 +390,7 @@ export class VectorStyle extends AbstractStyle { return null; } - const formattersDataRequest = this._layer.findDataRequestById(dataRequestId); + const formattersDataRequest = this._layer.getDataRequest(dataRequestId); if (!formattersDataRequest || !formattersDataRequest.hasData()) { return null; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js index 8bc397dd98b56a..dd2cf79318d8e6 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js @@ -16,7 +16,7 @@ import chrome from 'ui/chrome'; export const MIN_SIZE = 1; export const MAX_SIZE = 64; -export const DEFAULT_MIN_SIZE = 4; +export const DEFAULT_MIN_SIZE = 7; // Make default large enough to fit default label size export const DEFAULT_MAX_SIZE = 32; export const DEFAULT_SIGMA = 3; export const DEFAULT_LABEL_SIZE = 14; diff --git a/x-pack/legacy/plugins/maps/public/layers/tile_layer.js b/x-pack/legacy/plugins/maps/public/layers/tile_layer.js index b35adcad976c32..aa2619e96f834a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tile_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/tile_layer.js @@ -30,7 +30,7 @@ export class TileLayer extends AbstractLayer { const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, dataFilters); try { - const url = await this._source.getUrlTemplate(); + const url = await this.getSource().getUrlTemplate(); stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, url, {}); } catch (error) { onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message); diff --git a/x-pack/legacy/plugins/maps/public/layers/tile_layer.test.ts b/x-pack/legacy/plugins/maps/public/layers/tile_layer.test.ts index 065fbd79d97895..0ec9385194cc09 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tile_layer.test.ts +++ b/x-pack/legacy/plugins/maps/public/layers/tile_layer.test.ts @@ -32,6 +32,14 @@ class MockTileSource implements ITMSSource { async getUrlTemplate(): Promise { return 'template/{x}/{y}/{z}.png'; } + + destroy(): void { + // no-op + } + + getInspectorAdapters(): object { + return {}; + } } describe('TileLayer', () => { diff --git a/x-pack/legacy/plugins/maps/public/layers/util/data_request.js b/x-pack/legacy/plugins/maps/public/layers/util/data_request.ts similarity index 61% rename from x-pack/legacy/plugins/maps/public/layers/util/data_request.js rename to x-pack/legacy/plugins/maps/public/layers/util/data_request.ts index 3a6c10a9f07a6e..e361574194628b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/data_request.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/data_request.ts @@ -3,42 +3,47 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable max-classes-per-file */ + import _ from 'lodash'; +import { DataRequestDescriptor, DataMeta } from '../../../common/data_request_descriptor_types'; export class DataRequest { - constructor(descriptor) { + private readonly _descriptor: DataRequestDescriptor; + + constructor(descriptor: DataRequestDescriptor) { this._descriptor = { ...descriptor, }; } - getData() { + getData(): object | undefined { return this._descriptor.data; } - isLoading() { + isLoading(): boolean { return !!this._descriptor.dataRequestToken; } - getMeta() { + getMeta(): DataMeta { return this.hasData() ? _.get(this._descriptor, 'dataMeta', {}) : _.get(this._descriptor, 'dataMetaAtStart', {}); } - hasData() { + hasData(): boolean { return !!this._descriptor.data; } - hasDataOrRequestInProgress() { - return this._descriptor.data || this._descriptor.dataRequestToken; + hasDataOrRequestInProgress(): boolean { + return this.hasData() || this.isLoading(); } - getDataId() { + getDataId(): string { return this._descriptor.dataId; } - getRequestToken() { + getRequestToken(): symbol | undefined { return this._descriptor.dataRequestToken; } } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.d.ts b/x-pack/legacy/plugins/maps/public/layers/vector_layer.d.ts index 748b2fd1d782c8..77e8ab768cd00e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.d.ts @@ -8,20 +8,43 @@ import { AbstractLayer } from './layer'; import { IVectorSource } from './sources/vector_source'; import { VectorLayerDescriptor } from '../../common/descriptor_types'; +import { MapFilters, VectorLayerRequestMeta } from '../../common/data_request_descriptor_types'; import { ILayer } from './layer'; import { IJoin } from './joins/join'; +import { IVectorStyle } from './styles/vector/vector_style'; +import { IField } from './fields/field'; +import { SyncContext } from '../actions/map_actions'; type VectorLayerArguments = { source: IVectorSource; + joins: IJoin[]; layerDescriptor: VectorLayerDescriptor; }; export interface IVectorLayer extends ILayer { + getFields(): Promise; + getStyleEditorFields(): Promise; getValidJoins(): IJoin[]; } export class VectorLayer extends AbstractLayer implements IVectorLayer { + static createDescriptor( + options: VectorLayerArguments, + mapColors: string[] + ): VectorLayerDescriptor; + + protected readonly _source: IVectorSource; + protected readonly _style: IVectorStyle; + constructor(options: VectorLayerArguments); + getFields(): Promise; + getStyleEditorFields(): Promise; getValidJoins(): IJoin[]; + _getSearchFilters( + dataFilters: MapFilters, + source: IVectorSource, + style: IVectorStyle + ): VectorLayerRequestMeta; + _syncData(syncContext: SyncContext, source: IVectorSource, style: IVectorStyle): Promise; } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 70bba3d91c7237..6b89554546330b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -58,7 +58,7 @@ export class VectorLayer extends AbstractLayer { constructor({ layerDescriptor, source, joins = [] }) { super({ layerDescriptor, source }); this._joins = joins; - this._style = new VectorStyle(this._descriptor.style, this._source, this); + this._style = new VectorStyle(this._descriptor.style, source, this); } getStyle() { @@ -66,10 +66,10 @@ export class VectorLayer extends AbstractLayer { } destroy() { - if (this._source) { - this._source.destroy(); + if (this.getSource()) { + this.getSource().destroy(); } - this._joins.forEach(joinSource => { + this.getJoins().forEach(joinSource => { joinSource.destroy(); }); } @@ -79,7 +79,7 @@ export class VectorLayer extends AbstractLayer { } getValidJoins() { - return this._joins.filter(join => { + return this.getJoins().filter(join => { return join.hasCompleteConfig(); }); } @@ -119,7 +119,7 @@ export class VectorLayer extends AbstractLayer { } if ( - this._joins.length && + this.getJoins().length && !featureCollection.features.some(feature => feature.properties[FEATURE_VISIBLE_PROPERTY_NAME]) ) { return { @@ -131,11 +131,11 @@ export class VectorLayer extends AbstractLayer { } const sourceDataRequest = this.getSourceDataRequest(); - const { tooltipContent, areResultsTrimmed } = this._source.getSourceTooltipContent( + const { tooltipContent, areResultsTrimmed } = this.getSource().getSourceTooltipContent( sourceDataRequest ); return { - icon: this._style.getIcon(), + icon: this.getCurrentStyle().getIcon(), tooltipContent: tooltipContent, areResultsTrimmed: areResultsTrimmed, }; @@ -146,11 +146,11 @@ export class VectorLayer extends AbstractLayer { } async hasLegendDetails() { - return this._style.hasLegendDetails(); + return this.getCurrentStyle().hasLegendDetails(); } renderLegendDetails() { - return this._style.renderLegendDetails(); + return this.getCurrentStyle().renderLegendDetails(); } _getBoundsBasedOnData() { @@ -175,17 +175,22 @@ export class VectorLayer extends AbstractLayer { } async getBounds(dataFilters) { - const isStaticLayer = !this._source.isBoundsAware() || !this._source.isFilterByMapBounds(); + const isStaticLayer = + !this.getSource().isBoundsAware() || !this.getSource().isFilterByMapBounds(); if (isStaticLayer) { return this._getBoundsBasedOnData(); } - const searchFilters = this._getSearchFilters(dataFilters); - return await this._source.getBoundsForFilters(searchFilters); + const searchFilters = this._getSearchFilters( + dataFilters, + this.getSource(), + this.getCurrentStyle() + ); + return await this.getSource().getBoundsForFilters(searchFilters); } async getLeftJoinFields() { - return await this._source.getLeftJoinFields(); + return await this.getSource().getLeftJoinFields(); } _getJoinFields() { @@ -198,12 +203,17 @@ export class VectorLayer extends AbstractLayer { } async getFields() { - const sourceFields = await this._source.getFields(); + const sourceFields = await this.getSource().getFields(); + return [...sourceFields, ...this._getJoinFields()]; + } + + async getStyleEditorFields() { + const sourceFields = await this.getSourceForEditing().getFields(); return [...sourceFields, ...this._getJoinFields()]; } getIndexPatternIds() { - const indexPatternIds = this._source.getIndexPatternIds(); + const indexPatternIds = this.getSource().getIndexPatternIds(); this.getValidJoins().forEach(join => { indexPatternIds.push(...join.getIndexPatternIds()); }); @@ -211,17 +221,13 @@ export class VectorLayer extends AbstractLayer { } getQueryableIndexPatternIds() { - const indexPatternIds = this._source.getQueryableIndexPatternIds(); + const indexPatternIds = this.getSource().getQueryableIndexPatternIds(); this.getValidJoins().forEach(join => { indexPatternIds.push(...join.getQueryableIndexPatternIds()); }); return indexPatternIds; } - findDataRequestById(sourceDataId) { - return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId); - } - async _syncJoin({ join, startLoading, @@ -239,7 +245,7 @@ export class VectorLayer extends AbstractLayer { sourceQuery: joinSource.getWhereQuery(), applyGlobalQuery: joinSource.getApplyGlobalQuery(), }; - const prevDataRequest = this.findDataRequestById(sourceDataId); + const prevDataRequest = this.getDataRequest(sourceDataId); const canSkipFetch = await canSkipSourceUpdate({ source: joinSource, @@ -281,30 +287,30 @@ export class VectorLayer extends AbstractLayer { } } - async _syncJoins(syncContext) { + async _syncJoins(syncContext, style) { const joinSyncs = this.getValidJoins().map(async join => { - await this._syncJoinStyleMeta(syncContext, join); - await this._syncJoinFormatters(syncContext, join); + await this._syncJoinStyleMeta(syncContext, join, style); + await this._syncJoinFormatters(syncContext, join, style); return this._syncJoin({ join, ...syncContext }); }); return await Promise.all(joinSyncs); } - _getSearchFilters(dataFilters) { + _getSearchFilters(dataFilters, source, style) { const fieldNames = [ - ...this._source.getFieldNames(), - ...this._style.getSourceFieldNames(), + ...source.getFieldNames(), + ...style.getSourceFieldNames(), ...this.getValidJoins().map(join => join.getLeftField().getName()), ]; return { ...dataFilters, fieldNames: _.uniq(fieldNames).sort(), - geogridPrecision: this._source.getGeoGridPrecision(dataFilters.zoom), + geogridPrecision: source.getGeoGridPrecision(dataFilters.zoom), sourceQuery: this.getQuery(), - applyGlobalQuery: this._source.getApplyGlobalQuery(), - sourceMeta: this._source.getSyncMeta(), + applyGlobalQuery: source.getApplyGlobalQuery(), + sourceMeta: source.getSyncMeta(), }; } @@ -347,20 +353,21 @@ export class VectorLayer extends AbstractLayer { } } - async _syncSource({ - startLoading, - stopLoading, - onLoadError, - registerCancelCallback, - dataFilters, - isRequestStillActive, - }) { + async _syncSource(syncContext, source, style) { + const { + startLoading, + stopLoading, + onLoadError, + registerCancelCallback, + dataFilters, + isRequestStillActive, + } = syncContext; const dataRequestId = SOURCE_DATA_ID_ORIGIN; const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); - const searchFilters = this._getSearchFilters(dataFilters); + const searchFilters = this._getSearchFilters(dataFilters, source, style); const prevDataRequest = this.getSourceDataRequest(); const canSkipFetch = await canSkipSourceUpdate({ - source: this._source, + source, prevDataRequest, nextMeta: searchFilters, }); @@ -373,8 +380,8 @@ export class VectorLayer extends AbstractLayer { try { startLoading(dataRequestId, requestToken, searchFilters); - const layerName = await this.getDisplayName(); - const { data: sourceFeatureCollection, meta } = await this._source.getGeoJsonWithMeta( + const layerName = await this.getDisplayName(source); + const { data: sourceFeatureCollection, meta } = await source.getGeoJsonWithMeta( layerName, searchFilters, registerCancelCallback.bind(null, requestToken), @@ -398,16 +405,17 @@ export class VectorLayer extends AbstractLayer { } } - async _syncSourceStyleMeta(syncContext) { - if (this._style.constructor.type !== LAYER_STYLE_TYPE.VECTOR) { + async _syncSourceStyleMeta(syncContext, source, style) { + if (this.getCurrentStyle().constructor.type !== LAYER_STYLE_TYPE.VECTOR) { return; } return this._syncStyleMeta({ - source: this._source, + source, + style, sourceQuery: this.getQuery(), dataRequestId: SOURCE_META_ID_ORIGIN, - dynamicStyleProps: this._style.getDynamicPropertiesArray().filter(dynamicStyleProp => { + dynamicStyleProps: style.getDynamicPropertiesArray().filter(dynamicStyleProp => { return ( dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE && dynamicStyleProp.isFieldMetaEnabled() @@ -417,28 +425,32 @@ export class VectorLayer extends AbstractLayer { }); } - async _syncJoinStyleMeta(syncContext, join) { + async _syncJoinStyleMeta(syncContext, join, style) { const joinSource = join.getRightJoinSource(); return this._syncStyleMeta({ source: joinSource, + style, sourceQuery: joinSource.getWhereQuery(), dataRequestId: join.getSourceMetaDataRequestId(), - dynamicStyleProps: this._style.getDynamicPropertiesArray().filter(dynamicStyleProp => { - const matchingField = joinSource.getMetricFieldForName( - dynamicStyleProp.getField().getName() - ); - return ( - dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && - !!matchingField && - dynamicStyleProp.isFieldMetaEnabled() - ); - }), + dynamicStyleProps: this.getCurrentStyle() + .getDynamicPropertiesArray() + .filter(dynamicStyleProp => { + const matchingField = joinSource.getMetricFieldForName( + dynamicStyleProp.getField().getName() + ); + return ( + dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && + !!matchingField && + dynamicStyleProp.isFieldMetaEnabled() + ); + }), ...syncContext, }); } async _syncStyleMeta({ source, + style, sourceQuery, dataRequestId, dynamicStyleProps, @@ -459,10 +471,10 @@ export class VectorLayer extends AbstractLayer { const nextMeta = { dynamicStyleFields: _.uniq(dynamicStyleFields).sort(), sourceQuery, - isTimeAware: this._style.isTimeAware() && (await source.isTimeAware()), + isTimeAware: this.getCurrentStyle().isTimeAware() && (await source.isTimeAware()), timeFilters: dataFilters.timeFilters, }; - const prevDataRequest = this.findDataRequestById(dataRequestId); + const prevDataRequest = this.getDataRequest(dataRequestId); const canSkipFetch = canSkipStyleMetaUpdate({ prevDataRequest, nextMeta }); if (canSkipFetch) { return; @@ -471,10 +483,10 @@ export class VectorLayer extends AbstractLayer { const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); try { startLoading(dataRequestId, requestToken, nextMeta); - const layerName = await this.getDisplayName(); + const layerName = await this.getDisplayName(source); const styleMeta = await source.loadStylePropsMeta( layerName, - this._style, + style, dynamicStyleProps, registerCancelCallback, nextMeta @@ -487,15 +499,15 @@ export class VectorLayer extends AbstractLayer { } } - async _syncSourceFormatters(syncContext) { - if (this._style.constructor.type !== LAYER_STYLE_TYPE.VECTOR) { + async _syncSourceFormatters(syncContext, source, style) { + if (style.constructor.type !== LAYER_STYLE_TYPE.VECTOR) { return; } return this._syncFormatters({ - source: this._source, + source, dataRequestId: SOURCE_FORMATTERS_ID_ORIGIN, - fields: this._style + fields: style .getDynamicPropertiesArray() .filter(dynamicStyleProp => { return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE; @@ -507,12 +519,12 @@ export class VectorLayer extends AbstractLayer { }); } - async _syncJoinFormatters(syncContext, join) { + async _syncJoinFormatters(syncContext, join, style) { const joinSource = join.getRightJoinSource(); return this._syncFormatters({ source: joinSource, dataRequestId: join.getSourceFormattersDataRequestId(), - fields: this._style + fields: style .getDynamicPropertiesArray() .filter(dynamicStyleProp => { const matchingField = joinSource.getMetricFieldForName( @@ -538,7 +550,7 @@ export class VectorLayer extends AbstractLayer { const nextMeta = { fieldNames: _.uniq(fieldNames).sort(), }; - const prevDataRequest = this.findDataRequestById(dataRequestId); + const prevDataRequest = this.getDataRequest(dataRequestId); const canSkipUpdate = canSkipFormattersUpdate({ prevDataRequest, nextMeta }); if (canSkipUpdate) { return; @@ -565,13 +577,27 @@ export class VectorLayer extends AbstractLayer { } async syncData(syncContext) { + this._syncData(syncContext, this.getSource(), this.getCurrentStyle()); + } + + // TLDR: Do not call getSource or getCurrentStyle in syncData flow. Use 'source' and 'style' arguments instead. + // + // 1) State is contained in the redux store. Layer instance state is readonly. + // 2) Even though data request descriptor updates trigger new instances for rendering, + // syncing data executes on a single object instance. Syncing data can not use updated redux store state. + // + // Blended layer data syncing branches on the source/style depending on whether clustering is used or not. + // Given 1 above, which source/style to use can not be stored in Layer instance state. + // Given 2 above, which source/style to use can not be pulled from data request state. + // Therefore, source and style are provided as arugments and must be used instead of calling getSource or getCurrentStyle. + async _syncData(syncContext, source, style) { if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { return; } - await this._syncSourceStyleMeta(syncContext); - await this._syncSourceFormatters(syncContext); - const sourceResult = await this._syncSource(syncContext); + await this._syncSourceStyleMeta(syncContext, source, style); + await this._syncSourceFormatters(syncContext, source, style); + const sourceResult = await this._syncSource(syncContext, source, style); if ( !sourceResult.featureCollection || !sourceResult.featureCollection.features.length || @@ -580,7 +606,7 @@ export class VectorLayer extends AbstractLayer { return; } - const joinStates = await this._syncJoins(syncContext); + const joinStates = await this._syncJoins(syncContext, style); await this._performInnerJoins(sourceResult, joinStates, syncContext.updateSourceData); } @@ -596,7 +622,7 @@ export class VectorLayer extends AbstractLayer { if (!featureCollection) { if (featureCollectionOnMap) { - this._style.clearFeatureState(featureCollectionOnMap, mbMap, this.getId()); + this.getCurrentStyle().clearFeatureState(featureCollectionOnMap, mbMap, this.getId()); } mbGeoJSONSource.setData(EMPTY_FEATURE_COLLECTION); return; @@ -605,7 +631,7 @@ export class VectorLayer extends AbstractLayer { // "feature-state" data expressions are not supported with layout properties. // To work around this limitation, // scaled layout properties (like icon-size) must fall back to geojson property values :( - const hasGeoJsonProperties = this._style.setFeatureStateAndStyleProps( + const hasGeoJsonProperties = this.getCurrentStyle().setFeatureStateAndStyleProps( featureCollection, mbMap, this.getId() @@ -626,7 +652,7 @@ export class VectorLayer extends AbstractLayer { // Point layers symbolized as icons only contain a single mapbox layer. let markerLayerId; let textLayerId; - if (this._style.arePointsSymbolizedAsCircles()) { + if (this.getCurrentStyle().arePointsSymbolizedAsCircles()) { markerLayerId = pointLayerId; textLayerId = this._getMbTextLayerId(); if (symbolLayer) { @@ -680,13 +706,13 @@ export class VectorLayer extends AbstractLayer { mbMap.setFilter(textLayerId, filterExpr); } - this._style.setMBPaintPropertiesForPoints({ + this.getCurrentStyle().setMBPaintPropertiesForPoints({ alpha: this.getAlpha(), mbMap, pointLayerId, }); - this._style.setMBPropertiesForLabelText({ + this.getCurrentStyle().setMBPropertiesForLabelText({ alpha: this.getAlpha(), mbMap, textLayerId, @@ -711,13 +737,13 @@ export class VectorLayer extends AbstractLayer { mbMap.setFilter(symbolLayerId, filterExpr); } - this._style.setMBSymbolPropertiesForPoints({ + this.getCurrentStyle().setMBSymbolPropertiesForPoints({ alpha: this.getAlpha(), mbMap, symbolLayerId, }); - this._style.setMBPropertiesForLabelText({ + this.getCurrentStyle().setMBPropertiesForLabelText({ alpha: this.getAlpha(), mbMap, textLayerId: symbolLayerId, @@ -745,7 +771,7 @@ export class VectorLayer extends AbstractLayer { paint: {}, }); } - this._style.setMBPaintProperties({ + this.getCurrentStyle().setMBPaintProperties({ alpha: this.getAlpha(), mbMap, fillLayerId, @@ -830,9 +856,13 @@ export class VectorLayer extends AbstractLayer { for (let i = 0; i < tooltipsFromSource.length; i++) { const tooltipProperty = tooltipsFromSource[i]; const matchingJoins = []; - for (let j = 0; j < this._joins.length; j++) { - if (this._joins[j].getLeftField().getName() === tooltipProperty.getPropertyKey()) { - matchingJoins.push(this._joins[j]); + for (let j = 0; j < this.getJoins().length; j++) { + if ( + this.getJoins() + [j].getLeftField() + .getName() === tooltipProperty.getPropertyKey() + ) { + matchingJoins.push(this.getJoins()[j]); } } if (matchingJoins.length) { @@ -842,18 +872,22 @@ export class VectorLayer extends AbstractLayer { } async getPropertiesForTooltip(properties) { - let allTooltips = await this._source.filterAndFormatPropertiesToHtml(properties); + let allTooltips = await this.getSource().filterAndFormatPropertiesToHtml(properties); this._addJoinsToSourceTooltips(allTooltips); - for (let i = 0; i < this._joins.length; i++) { - const propsFromJoin = await this._joins[i].filterAndFormatPropertiesForTooltip(properties); + for (let i = 0; i < this.getJoins().length; i++) { + const propsFromJoin = await this.getJoins()[i].filterAndFormatPropertiesForTooltip( + properties + ); allTooltips = [...allTooltips, ...propsFromJoin]; } return allTooltips; } canShowTooltip() { - return this.isVisible() && (this._source.canFormatFeatureProperties() || this._joins.length); + return ( + this.isVisible() && (this.getSource().canFormatFeatureProperties() || this.getJoins().length) + ); } getFeatureById(id) { diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js index b09ccdc3af8bae..44987fd3e78f02 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js @@ -48,7 +48,7 @@ export class VectorTileLayer extends TileLayer { return; } - const nextMeta = { tileLayerId: this._source.getTileLayerId() }; + const nextMeta = { tileLayerId: this.getSource().getTileLayerId() }; const canSkipSync = this._canSkipSync({ prevDataRequest: this.getSourceDataRequest(), nextMeta, @@ -60,7 +60,7 @@ export class VectorTileLayer extends TileLayer { const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); try { startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, dataFilters); - const styleAndSprites = await this._source.getVectorStyleSheetAndSpriteMeta(isRetina()); + const styleAndSprites = await this.getSource().getVectorStyleSheetAndSpriteMeta(isRetina()); const spriteSheetImageData = await loadSpriteSheetImageData(styleAndSprites.spriteMeta.png); const data = { ...styleAndSprites, @@ -78,7 +78,7 @@ export class VectorTileLayer extends TileLayer { _generateMbSourceIdPrefix() { const DELIMITTER = '___'; - return `${this.getId()}${DELIMITTER}${this._source.getTileLayerId()}${DELIMITTER}`; + return `${this.getId()}${DELIMITTER}${this.getSource().getTileLayerId()}${DELIMITTER}`; } _generateMbSourceId(name) { @@ -141,7 +141,7 @@ export class VectorTileLayer extends TileLayer { } _makeNamespacedImageId(imageId) { - const prefix = this._source.getSpriteNamespacePrefix() + '/'; + const prefix = this.getSource().getSpriteNamespacePrefix() + '/'; return prefix + imageId; } diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js index e5eaf8870aa77f..79d890bc21f148 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js @@ -10,6 +10,7 @@ import { TileLayer } from '../layers/tile_layer'; import { VectorTileLayer } from '../layers/vector_tile_layer'; import { VectorLayer } from '../layers/vector_layer'; import { HeatmapLayer } from '../layers/heatmap_layer'; +import { BlendedVectorLayer } from '../layers/blended_vector_layer'; import { ALL_SOURCES } from '../layers/sources/all_sources'; import { timefilter } from 'ui/timefilter'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -40,6 +41,8 @@ function createLayerInstance(layerDescriptor, inspectorAdapters) { return new VectorTileLayer({ layerDescriptor, source }); case HeatmapLayer.type: return new HeatmapLayer({ layerDescriptor, source }); + case BlendedVectorLayer.type: + return new BlendedVectorLayer({ layerDescriptor, source }); default: throw new Error(`Unrecognized layerType ${layerDescriptor.type}`); } diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js index 5ec40a57ebc7f4..ef2e23e51a0924 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js @@ -5,6 +5,7 @@ */ jest.mock('../layers/vector_layer', () => {}); +jest.mock('../layers/blended_vector_layer', () => {}); jest.mock('../layers/heatmap_layer', () => {}); jest.mock('../layers/vector_tile_layer', () => {}); jest.mock('../layers/sources/all_sources', () => {}); diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index b608151d26ae89..3b1513f4bb95dd 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -48,6 +48,7 @@ export const LAYER_TYPE = { VECTOR: 'VECTOR', VECTOR_TILE: 'VECTOR_TILE', HEATMAP: 'HEATMAP', + BLENDED_VECTOR: 'BLENDED_VECTOR', }; export enum SORT_ORDER { @@ -188,3 +189,9 @@ export enum LABEL_BORDER_SIZES { } export const DEFAULT_ICON = 'airfield'; + +export enum SCALING_TYPES { + LIMIT = 'LIMIT', + CLUSTERS = 'CLUSTERS', + TOP_HITS = 'TOP_HITS', +} diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js index 7e07569b44b830..1e20df89c8fad0 100644 --- a/x-pack/plugins/maps/public/reducers/map.js +++ b/x-pack/plugins/maps/public/reducers/map.js @@ -74,7 +74,7 @@ const updateLayerInList = (state, layerId, attribute, newValue) => { return { ...state, layerList: updatedList }; }; -const updateLayerSourceDescriptorProp = (state, layerId, propName, value) => { +const updateLayerSourceDescriptorProp = (state, layerId, propName, value, newLayerType) => { const { layerList } = state; const layerIdx = getLayerIndex(layerList, layerId); const updatedLayer = { @@ -84,6 +84,9 @@ const updateLayerSourceDescriptorProp = (state, layerId, propName, value) => { [propName]: value, }, }; + if (newLayerType) { + updatedLayer.type = newLayerType; + } const updatedList = [ ...layerList.slice(0, layerIdx), updatedLayer, @@ -258,7 +261,13 @@ export function map(state = INITIAL_STATE, action) { case UPDATE_LAYER_PROP: return updateLayerInList(state, action.id, action.propName, action.newValue); case UPDATE_SOURCE_PROP: - return updateLayerSourceDescriptorProp(state, action.layerId, action.propName, action.value); + return updateLayerSourceDescriptorProp( + state, + action.layerId, + action.propName, + action.value, + action.newLayerType + ); case SET_JOINS: const layerDescriptor = state.layerList.find( descriptor => descriptor.id === action.layer.getId() diff --git a/x-pack/test/functional/apps/maps/blended_vector_layer.js b/x-pack/test/functional/apps/maps/blended_vector_layer.js new file mode 100644 index 00000000000000..a01f796fe34556 --- /dev/null +++ b/x-pack/test/functional/apps/maps/blended_vector_layer.js @@ -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 expect from '@kbn/expect'; + +export default function({ getPageObjects, getService }) { + const PageObjects = getPageObjects(['maps']); + const inspector = getService('inspector'); + + describe('blended vector layer', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('blended document example'); + }); + + it('should request documents when zoomed to smaller regions showing less data', async () => { + const hits = await PageObjects.maps.getHits(); + expect(hits).to.equal('33'); + }); + + it('should request clusters when zoomed to larger regions showing lots of data', async () => { + await PageObjects.maps.setView(20, -90, 2); + await inspector.open(); + await inspector.openInspectorRequestsView(); + const requestStats = await inspector.getTableData(); + const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); + const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); + await inspector.close(); + + expect(hits).to.equal('0'); + expect(totalHits).to.equal('14000'); + }); + + it('should request documents when query narrows data', async () => { + await PageObjects.maps.setAndSubmitQuery('bytes > 19000'); + const hits = await PageObjects.maps.getHits(); + expect(hits).to.equal('75'); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 44a7c4c9a5f866..ae7de986cf8677 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -30,6 +30,7 @@ export default function({ loadTestFile, getService }) { describe('', function() { this.tags('ciGroup7'); loadTestFile(require.resolve('./documents_source')); + loadTestFile(require.resolve('./blended_vector_layer')); loadTestFile(require.resolve('./saved_object_management')); loadTestFile(require.resolve('./sample_data')); loadTestFile(require.resolve('./feature_controls/maps_security')); diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index e50ec593cc9902..cb3598652a39ae 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -288,7 +288,7 @@ "title" : "document example top hits split with scripted field", "description" : "", "mapStateJSON" : "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-24T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]}", - "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"useTopHits\":true,\"topHitsSplitField\":\"hour_of_day\",\"topHitsSize\":1,\"sortField\":\"@timestamp\",\"sortOrder\":\"desc\",\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"airfield\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"scalingType\":\"TOP_HITS\",\"topHitsSplitField\":\"hour_of_day\",\"topHitsSize\":1,\"sortField\":\"@timestamp\",\"sortOrder\":\"desc\",\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"airfield\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}", "bounds" : { "type" : "Polygon", @@ -861,6 +861,62 @@ } } +{ + "type": "doc", + "value": { + "id": "map:279e1f20-6883-11ea-952a-b102add99cf8", + "index": ".kibana", + "source": { + "map" : { + "title" : "blended document example", + "description" : "", + "mapStateJSON" : "{\"zoom\":10.27,\"center\":{\"lon\":-83.70716,\"lat\":32.73679},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-23T00:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]}", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"43a70a86-00fd-43af-9e84-4d9fe2d7513d\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{},\"type\":\"VECTOR_TILE\"},{\"id\":\"307c8495-89f7-431b-83d8-78724d9a8f72\",\"label\":\"logstash-*\",\"sourceDescriptor\":{\"geoField\":\"geo.coordinates\",\"id\":\"20fc58c3-3c0a-4c7b-9cdc-37552cafdc21\",\"tooltipProperties\":[],\"type\":\"ES_SEARCH\",\"scalingType\":\"CLUSTERS\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"type\":\"BLENDED_VECTOR\",\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"airfield\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true}}]", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}", + "bounds" : { + "type" : "Polygon", + "coordinates" : [ + [ + [ + -84.07816, + 32.95327 + ], + [ + -84.07816, + 32.51978 + ], + [ + -83.33616, + 32.51978 + ], + [ + -83.33616, + 32.95327 + ], + [ + -84.07816, + 32.95327 + ] + ] + ] + } + }, + "type" : "map", + "references" : [ + { + "name" : "layer_1_source_index_pattern", + "type" : "index-pattern", + "id" : "c698b940-e149-11e8-a35a-370a8516603a" + } + ], + "migrationVersion" : { + "map" : "7.7.0" + }, + "updated_at" : "2020-03-17T19:11:50.290Z" + } + } +} + { "type": "doc", "value": { From 4e5aa93f45754ce4b4fb4217df9e7e7dc16d0cda Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 18 Mar 2020 14:45:17 -0400 Subject: [PATCH 12/42] [Fleet] Fix privileges for enrollment and access api keys (#60534) --- .../services/api_keys/enrollment_api_key.ts | 14 ++++- .../server/services/api_keys/index.ts | 12 +++- .../apis/fleet/agents/enroll.ts | 59 ++++++++++++++++++- .../apis/fleet/agents/services.ts | 17 +++++- .../apis/fleet/enrollment_api_keys/crud.ts | 53 ++++++++++++++++- 5 files changed, 148 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts index d81b998d5a752c..59604416355248 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts @@ -93,7 +93,19 @@ export async function generateEnrollmentAPIKey( const name = providedKeyName ? `${providedKeyName} (${id})` : id; - const key = await createAPIKey(soClient, name, {}); + const key = await createAPIKey(soClient, name, { + // Useless role to avoid to have the privilege of the user that created the key + 'fleet-apikey-enroll': { + cluster: [], + applications: [ + { + application: '.fleet', + privileges: ['no-privileges'], + resources: ['*'], + }, + ], + }, + }); if (!key) { throw new Error('Unable to create an enrollment api key'); diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts index 9b0182b86fc885..5c05d5612e2007 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts @@ -42,7 +42,17 @@ export async function generateAccessApiKey( configId: string ) { const key = await createAPIKey(soClient, agentId, { - 'fleet-agent': {}, + // Useless role to avoid to have the privilege of the user that created the key + 'fleet-apikey-access': { + cluster: [], + applications: [ + { + application: '.fleet', + privileges: ['no-privileges'], + resources: ['*'], + }, + ], + }, }); if (!key) { diff --git a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts index 666d97452ad3d5..d8e9749744ea49 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts @@ -6,8 +6,9 @@ import expect from '@kbn/expect'; import uuid from 'uuid'; + import { FtrProviderContext } from '../../../ftr_provider_context'; -import { getSupertestWithoutAuth, setupIngest } from './services'; +import { getSupertestWithoutAuth, setupIngest, getEsClientForAPIKey } from './services'; export default function(providerContext: FtrProviderContext) { const { getService } = providerContext; @@ -104,5 +105,61 @@ export default function(providerContext: FtrProviderContext) { expect(apiResponse.success).to.eql(true); expect(apiResponse.item).to.have.keys('id', 'active', 'access_api_key', 'type', 'config_id'); }); + + it('when enrolling an agent it should generate an access api key with limited privileges', async () => { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/fleet/agents/enroll`) + .set('kbn-xsrf', 'xxx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .send({ + type: 'PERMANENT', + metadata: { + local: {}, + user_provided: {}, + }, + }) + .expect(200); + expect(apiResponse.success).to.eql(true); + const { body: privileges } = await getEsClientForAPIKey( + providerContext, + apiResponse.item.access_api_key + ).security.hasPrivileges({ + body: { + cluster: ['all', 'monitor', 'manage_api_key'], + index: [ + { + names: ['log-*', 'metrics-*', 'events-*', '*'], + privileges: ['write', 'create_index'], + }, + ], + }, + }); + expect(privileges.cluster).to.eql({ + all: false, + monitor: false, + manage_api_key: false, + }); + expect(privileges.index).to.eql({ + '*': { + create_index: false, + write: false, + }, + 'events-*': { + create_index: false, + write: false, + }, + 'log-*': { + create_index: false, + write: false, + }, + 'metrics-*': { + create_index: false, + write: false, + }, + }); + }); }); } diff --git a/x-pack/test/api_integration/apis/fleet/agents/services.ts b/x-pack/test/api_integration/apis/fleet/agents/services.ts index 5c111b8ea9a847..9946135568e36c 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/services.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/services.ts @@ -5,7 +5,8 @@ */ import supertestAsPromised from 'supertest-as-promised'; -import url from 'url'; +import { Client } from '@elastic/elasticsearch'; +import { format as formatUrl } from 'url'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -15,7 +16,19 @@ export function getSupertestWithoutAuth({ getService }: FtrProviderContext) { kibanaUrl.auth = null; kibanaUrl.password = null; - return supertestAsPromised(url.format(kibanaUrl)); + return supertestAsPromised(formatUrl(kibanaUrl)); +} + +export function getEsClientForAPIKey({ getService }: FtrProviderContext, esApiKey: string) { + const config = getService('config'); + const url = formatUrl({ ...config.get('servers.elasticsearch'), auth: false }); + return new Client({ + nodes: [url], + auth: { + apiKey: esApiKey, + }, + requestTimeout: config.get('timeouts.esRequestTimeout'), + }); } export function setupIngest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/fleet/enrollment_api_keys/crud.ts b/x-pack/test/api_integration/apis/fleet/enrollment_api_keys/crud.ts index 800e0147528e50..89e05573da1c6a 100644 --- a/x-pack/test/api_integration/apis/fleet/enrollment_api_keys/crud.ts +++ b/x-pack/test/api_integration/apis/fleet/enrollment_api_keys/crud.ts @@ -7,11 +7,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { setupIngest } from '../agents/services'; +import { setupIngest, getEsClientForAPIKey } from '../agents/services'; const ENROLLMENT_KEY_ID = 'ed22ca17-e178-4cfe-8b02-54ea29fbd6d0'; -export default function({ getService }: FtrProviderContext) { +export default function(providerContext: FtrProviderContext) { + const { getService } = providerContext; const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); @@ -78,6 +79,54 @@ export default function({ getService }: FtrProviderContext) { expect(apiResponse.success).to.eql(true); expect(apiResponse.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'config_id'); }); + + it('should create an ES ApiKey with limited privileges', async () => { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + config_id: 'policy1', + }) + .expect(200); + expect(apiResponse.success).to.eql(true); + const { body: privileges } = await getEsClientForAPIKey( + providerContext, + apiResponse.item.api_key + ).security.hasPrivileges({ + body: { + cluster: ['all', 'monitor', 'manage_api_key'], + index: [ + { + names: ['log-*', 'metrics-*', 'events-*', '*'], + privileges: ['write', 'create_index'], + }, + ], + }, + }); + expect(privileges.cluster).to.eql({ + all: false, + monitor: false, + manage_api_key: false, + }); + expect(privileges.index).to.eql({ + '*': { + create_index: false, + write: false, + }, + 'events-*': { + create_index: false, + write: false, + }, + 'log-*': { + create_index: false, + write: false, + }, + 'metrics-*': { + create_index: false, + write: false, + }, + }); + }); }); }); } From 24534e832e8fb8691d0559a6f29825345f086e09 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 18 Mar 2020 20:46:05 +0200 Subject: [PATCH 13/42] ServiceNow action improvements (#60052) * Apply action types to fields * Add information to each field * Do not create or update comments when actionType is set to nothing * Improve helpers tests * Improve tests * Refactor: Use transformers and pipes * Better types * Refactor tests to new changes * Better error messages * Improve field formatting and display * Improve integration tests * Make username mandatory field * Translate transformers * Refactor schema * Translate appendInformationToField helper * Improve intergration tests Co-authored-by: Elastic Machine --- .../servicenow/action_handlers.test.ts | 766 ++++++++++++++++-- .../servicenow/action_handlers.ts | 88 +- .../servicenow/helpers.test.ts | 298 ++++++- .../servicenow/helpers.ts | 99 ++- .../servicenow/index.test.ts | 23 +- .../builtin_action_types/servicenow/index.ts | 27 +- .../servicenow/lib/index.test.ts | 110 ++- .../servicenow/lib/index.ts | 114 ++- .../servicenow/lib/types.ts | 3 +- .../builtin_action_types/servicenow/mock.ts | 36 +- .../builtin_action_types/servicenow/schema.ts | 25 +- .../servicenow/transformers.ts | 43 + .../servicenow/translations.ts | 29 + .../builtin_action_types/servicenow/types.ts | 78 +- .../plugins/actions/servicenow_simulation.ts | 85 +- .../builtin_action_types/servicenow.ts | 144 +++- 16 files changed, 1714 insertions(+), 254 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts index 381b44439033ca..be687e33e22015 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts @@ -4,68 +4,157 @@ * you may not use this file except in compliance with the Elastic License. */ -import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; +import { + handleCreateIncident, + handleUpdateIncident, + handleIncident, + createComments, +} from './action_handlers'; import { ServiceNow } from './lib'; -import { finalMapping } from './mock'; -import { Incident } from './lib/types'; +import { Mapping } from './types'; jest.mock('./lib'); const ServiceNowMock = ServiceNow as jest.Mock; -const incident: Incident = { - short_description: 'A title', - description: 'A description', -}; +const finalMapping: Mapping = new Map(); + +finalMapping.set('title', { + target: 'short_description', + actionType: 'overwrite', +}); -const comments = [ - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'A comment', - incidentCommentId: undefined, +finalMapping.set('description', { + target: 'description', + actionType: 'overwrite', +}); + +finalMapping.set('comments', { + target: 'comments', + actionType: 'append', +}); + +finalMapping.set('short_description', { + target: 'title', + actionType: 'overwrite', +}); + +const params = { + caseId: '123', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + incidentId: null, + incident: { + short_description: 'a title', + description: 'a description', }, -]; + comments: [ + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ], +}; -describe('handleCreateIncident', () => { - beforeAll(() => { - ServiceNowMock.mockImplementation(() => { - return { - serviceNow: { - getUserID: jest.fn().mockResolvedValue('1234'), - createIncident: jest.fn().mockResolvedValue({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - }), - updateIncident: jest.fn().mockResolvedValue({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - }), - batchCreateComments: jest - .fn() - .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), - batchUpdateComments: jest - .fn() - .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), +beforeAll(() => { + ServiceNowMock.mockImplementation(() => { + return { + serviceNow: { + getUserID: jest.fn().mockResolvedValue('1234'), + getIncident: jest.fn().mockResolvedValue({ + short_description: 'servicenow title', + description: 'servicenow desc', + }), + createIncident: jest.fn().mockResolvedValue({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }), + updateIncident: jest.fn().mockResolvedValue({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }), + batchCreateComments: jest + .fn() + .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), + }, + }; + }); +}); + +describe('handleIncident', () => { + test('create an incident', async () => { + const { serviceNow } = new ServiceNowMock(); + + const res = await handleIncident({ + incidentId: null, + serviceNow, + params, + comments: params.comments, + mapping: finalMapping, + }); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + comments: [ + { + commentId: '456', + pushedDate: '2020-03-10T12:24:20.000Z', }, - }; + ], }); }); + test('update an incident', async () => { + const { serviceNow } = new ServiceNowMock(); + const res = await handleIncident({ + incidentId: '123', + serviceNow, + params, + comments: params.comments, + mapping: finalMapping, + }); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + comments: [ + { + commentId: '456', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); +}); + +describe('handleCreateIncident', () => { test('create an incident without comments', async () => { const { serviceNow } = new ServiceNowMock(); const res = await handleCreateIncident({ serviceNow, - params: incident, + params, comments: [], mapping: finalMapping, }); expect(serviceNow.createIncident).toHaveBeenCalled(); - expect(serviceNow.createIncident).toHaveBeenCalledWith(incident); + expect(serviceNow.createIncident).toHaveBeenCalledWith({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }); expect(serviceNow.createIncident).toHaveReturned(); expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); expect(res).toEqual({ @@ -80,16 +169,36 @@ describe('handleCreateIncident', () => { const res = await handleCreateIncident({ serviceNow, - params: incident, - comments, + params, + comments: params.comments, mapping: finalMapping, }); expect(serviceNow.createIncident).toHaveBeenCalled(); - expect(serviceNow.createIncident).toHaveBeenCalledWith(incident); + expect(serviceNow.createIncident).toHaveBeenCalledWith({ + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }); expect(serviceNow.createIncident).toHaveReturned(); expect(serviceNow.batchCreateComments).toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith('123', comments, 'comments'); + expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( + '123', + [ + { + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + commentId: '456', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: null, + updatedBy: null, + version: 'WzU3LDFd', + }, + ], + 'comments' + ); expect(res).toEqual({ incidentId: '123', number: 'INC01', @@ -102,22 +211,27 @@ describe('handleCreateIncident', () => { ], }); }); +}); +describe('handleUpdateIncident', () => { test('update an incident without comments', async () => { const { serviceNow } = new ServiceNowMock(); const res = await handleUpdateIncident({ incidentId: '123', serviceNow, - params: incident, + params, comments: [], mapping: finalMapping, }); expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', incident); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchUpdateComments).not.toHaveBeenCalled(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); expect(res).toEqual({ incidentId: '123', number: 'INC01', @@ -125,23 +239,89 @@ describe('handleCreateIncident', () => { }); }); - test('update an incident and create new comments', async () => { + test('update an incident with comments', async () => { const { serviceNow } = new ServiceNowMock(); + serviceNow.batchCreateComments.mockResolvedValue([ + { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }, + { commentId: '789', pushedDate: '2020-03-10T12:24:20.000Z' }, + ]); const res = await handleUpdateIncident({ incidentId: '123', serviceNow, - params: incident, - comments, + params, + comments: [ + { + comment: 'first comment', + commentId: '456', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: null, + updatedBy: null, + version: 'WzU3LDFd', + }, + { + comment: 'second comment', + commentId: '789', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + version: 'WzU3LDFd', + }, + ], mapping: finalMapping, }); expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', incident); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchUpdateComments).not.toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith('123', comments, 'comments'); - + expect(serviceNow.batchCreateComments).toHaveBeenCalled(); + expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( + '123', + [ + { + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + commentId: '456', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: null, + updatedBy: null, + version: 'WzU3LDFd', + }, + { + comment: 'second comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + commentId: '789', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + version: 'WzU3LDFd', + }, + ], + 'comments' + ); expect(res).toEqual({ incidentId: '123', number: 'INC01', @@ -151,7 +331,487 @@ describe('handleCreateIncident', () => { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z', }, + { + commentId: '789', + pushedDate: '2020-03-10T12:24:20.000Z', + }, ], }); }); }); + +describe('handleUpdateIncident: different action types', () => { + test('overwrite & append', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'append', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('nothing & append', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'append', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + description: + 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('append & append', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'append', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: + 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('nothing & nothing', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {}); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('overwrite & nothing', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('overwrite & overwrite', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('nothing & overwrite', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('append & overwrite', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: + 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('append & nothing', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: + 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); +}); + +describe('createComments', () => { + test('create comments correctly', async () => { + const { serviceNow } = new ServiceNowMock(); + serviceNow.batchCreateComments.mockResolvedValue([ + { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }, + { commentId: '789', pushedDate: '2020-03-10T12:24:20.000Z' }, + ]); + + const comments = [ + { + comment: 'first comment', + commentId: '456', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: null, + updatedBy: null, + version: 'WzU3LDFd', + }, + { + comment: 'second comment', + commentId: '789', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + version: 'WzU3LDFd', + }, + ]; + + const res = await createComments(serviceNow, '123', 'comments', comments); + + expect(serviceNow.batchCreateComments).toHaveBeenCalled(); + expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( + '123', + [ + { + comment: 'first comment', + commentId: '456', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: null, + updatedBy: null, + version: 'WzU3LDFd', + }, + { + comment: 'second comment', + commentId: '789', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + version: 'WzU3LDFd', + }, + ], + 'comments' + ); + expect(res).toEqual([ + { + commentId: '456', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + { + commentId: '789', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ]); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index 47120c5da096dd..6439a68813fd5e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -5,26 +5,27 @@ */ import { zipWith } from 'lodash'; -import { Incident, CommentResponse } from './lib/types'; +import { CommentResponse } from './lib/types'; import { - ActionHandlerArguments, - UpdateParamsType, - UpdateActionHandlerArguments, - IncidentCreationResponse, - CommentType, - CommentsZipped, + HandlerResponse, + Comment, + SimpleComment, + CreateHandlerArguments, + UpdateHandlerArguments, + IncidentHandlerArguments, } from './types'; import { ServiceNow } from './lib'; +import { transformFields, prepareFieldsForTransformation, transformComments } from './helpers'; -const createComments = async ( +export const createComments = async ( serviceNow: ServiceNow, incidentId: string, key: string, - comments: CommentType[] -): Promise => { + comments: Comment[] +): Promise => { const createdComments = await serviceNow.batchCreateComments(incidentId, comments, key); - return zipWith(comments, createdComments, (a: CommentType, b: CommentResponse) => ({ + return zipWith(comments, createdComments, (a: Comment, b: CommentResponse) => ({ commentId: a.commentId, pushedDate: b.pushedDate, })); @@ -35,16 +36,30 @@ export const handleCreateIncident = async ({ params, comments, mapping, -}: ActionHandlerArguments): Promise => { - const paramsAsIncident = params as Incident; +}: CreateHandlerArguments): Promise => { + const fields = prepareFieldsForTransformation({ + params, + mapping, + }); + + const incident = transformFields({ + params, + fields, + }); const { incidentId, number, pushedDate } = await serviceNow.createIncident({ - ...paramsAsIncident, + ...incident, }); - const res: IncidentCreationResponse = { incidentId, number, pushedDate }; + const res: HandlerResponse = { incidentId, number, pushedDate }; - if (comments && Array.isArray(comments) && comments.length > 0) { + if ( + comments && + Array.isArray(comments) && + comments.length > 0 && + mapping.get('comments').actionType !== 'nothing' + ) { + comments = transformComments(comments, params, ['informationAdded']); res.comments = [ ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), ]; @@ -59,16 +74,33 @@ export const handleUpdateIncident = async ({ params, comments, mapping, -}: UpdateActionHandlerArguments): Promise => { - const paramsAsIncident = params as UpdateParamsType; +}: UpdateHandlerArguments): Promise => { + const currentIncident = await serviceNow.getIncident(incidentId); + const fields = prepareFieldsForTransformation({ + params, + mapping, + defaultPipes: ['informationUpdated'], + }); + + const incident = transformFields({ + params, + fields, + currentIncident, + }); const { number, pushedDate } = await serviceNow.updateIncident(incidentId, { - ...paramsAsIncident, + ...incident, }); - const res: IncidentCreationResponse = { incidentId, number, pushedDate }; + const res: HandlerResponse = { incidentId, number, pushedDate }; - if (comments && Array.isArray(comments) && comments.length > 0) { + if ( + comments && + Array.isArray(comments) && + comments.length > 0 && + mapping.get('comments').actionType !== 'nothing' + ) { + comments = transformComments(comments, params, ['informationAdded']); res.comments = [ ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), ]; @@ -76,3 +108,17 @@ export const handleUpdateIncident = async ({ return { ...res }; }; + +export const handleIncident = async ({ + incidentId, + serviceNow, + params, + comments, + mapping, +}: IncidentHandlerArguments): Promise => { + if (!incidentId) { + return await handleCreateIncident({ serviceNow, params, comments, mapping }); + } else { + return await handleUpdateIncident({ incidentId, serviceNow, params, comments, mapping }); + } +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts index 96962b41b3c680..ce8c3542ab69f2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts @@ -4,18 +4,62 @@ * you may not use this file except in compliance with the Elastic License. */ -import { normalizeMapping, buildMap, mapParams } from './helpers'; +import { + normalizeMapping, + buildMap, + mapParams, + appendField, + appendInformationToField, + prepareFieldsForTransformation, + transformFields, + transformComments, +} from './helpers'; import { mapping, finalMapping } from './mock'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { MapsType } from './types'; +import { MapEntry, Params, Comment } from './types'; -const maliciousMapping: MapsType[] = [ +const maliciousMapping: MapEntry[] = [ { source: '__proto__', target: 'short_description', actionType: 'nothing' }, { source: 'description', target: '__proto__', actionType: 'nothing' }, { source: 'comments', target: 'comments', actionType: 'nothing' }, { source: 'unsupportedSource', target: 'comments', actionType: 'nothing' }, ]; +const fullParams: Params = { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + incidentId: null, + incident: { + short_description: 'a title', + description: 'a description', + }, + comments: [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'second comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ], +}; + describe('sanitizeMapping', () => { test('remove malicious fields', () => { const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); @@ -81,3 +125,251 @@ describe('mapParams', () => { expect(fields).not.toEqual(expect.objectContaining(unexpectedFields)); }); }); + +describe('prepareFieldsForTransformation', () => { + test('prepare fields with defaults', () => { + const res = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + }); + expect(res).toEqual([ + { + key: 'short_description', + value: 'a title', + actionType: 'overwrite', + pipes: ['informationCreated'], + }, + { + key: 'description', + value: 'a description', + actionType: 'append', + pipes: ['informationCreated', 'append'], + }, + ]); + }); + + test('prepare fields with default pipes', () => { + const res = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + defaultPipes: ['myTestPipe'], + }); + expect(res).toEqual([ + { + key: 'short_description', + value: 'a title', + actionType: 'overwrite', + pipes: ['myTestPipe'], + }, + { + key: 'description', + value: 'a description', + actionType: 'append', + pipes: ['myTestPipe', 'append'], + }, + ]); + }); +}); + +describe('transformFields', () => { + test('transform fields for creation correctly', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + }); + + const res = transformFields({ + params: fullParams, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + }); + + test('transform fields for update correctly', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params: fullParams, + fields, + currentIncident: { + short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + expect(res).toEqual({ + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + }); + + test('add newline character to descripton', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params: fullParams, + fields, + currentIncident: { + short_description: 'first title', + description: 'first description', + }, + }); + expect(res.description?.includes('\r\n')).toBe(true); + }); + + test('append username if fullname is undefined', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + }); + + const res = transformFields({ + params: { ...fullParams, createdBy: { fullName: null, username: 'elastic' } }, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)', + }); + }); +}); + +describe('appendField', () => { + test('prefix correctly', () => { + expect('my_prefixmy_value ').toEqual(appendField({ value: 'my_value', prefix: 'my_prefix' })); + }); + + test('suffix correctly', () => { + expect('my_value my_suffix').toEqual(appendField({ value: 'my_value', suffix: 'my_suffix' })); + }); + + test('prefix and suffix correctly', () => { + expect('my_prefixmy_value my_suffix').toEqual( + appendField({ value: 'my_value', prefix: 'my_prefix', suffix: 'my_suffix' }) + ); + }); +}); + +describe('appendInformationToField', () => { + test('creation mode', () => { + const res = appendInformationToField({ + value: 'my value', + user: 'Elastic Test User', + date: '2020-03-13T08:34:53.450Z', + mode: 'create', + }); + expect(res).toEqual('my value (created at 2020-03-13T08:34:53.450Z by Elastic Test User)'); + }); + + test('update mode', () => { + const res = appendInformationToField({ + value: 'my value', + user: 'Elastic Test User', + date: '2020-03-13T08:34:53.450Z', + mode: 'update', + }); + expect(res).toEqual('my value (updated at 2020-03-13T08:34:53.450Z by Elastic Test User)'); + }); + + test('add mode', () => { + const res = appendInformationToField({ + value: 'my value', + user: 'Elastic Test User', + date: '2020-03-13T08:34:53.450Z', + mode: 'add', + }); + expect(res).toEqual('my value (added at 2020-03-13T08:34:53.450Z by Elastic Test User)'); + }); +}); + +describe('transformComments', () => { + test('transform creation comments', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, fullParams, ['informationCreated']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]); + }); + + test('transform update comments', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, fullParams, ['informationUpdated']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]); + }); + test('transform added comments', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, fullParams, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts index 99e67c1c43f35e..46d4789e0bd534 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -3,18 +3,34 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { flow } from 'lodash'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { MapsType, FinalMapping } from './types'; +import { + MapEntry, + Mapping, + AppendFieldArgs, + AppendInformationFieldArgs, + Params, + Comment, + TransformFieldsArgs, + PipedField, + PrepareFieldsForTransformArgs, + KeyAny, +} from './types'; +import { Incident } from './lib/types'; -export const normalizeMapping = (fields: string[], mapping: MapsType[]): MapsType[] => { +import * as transformers from './transformers'; +import * as i18n from './translations'; + +export const normalizeMapping = (supportedFields: string[], mapping: MapEntry[]): MapEntry[] => { // Prevent prototype pollution and remove unsupported fields return mapping.filter( - m => m.source !== '__proto__' && m.target !== '__proto__' && fields.includes(m.source) + m => m.source !== '__proto__' && m.target !== '__proto__' && supportedFields.includes(m.source) ); }; -export const buildMap = (mapping: MapsType[]): FinalMapping => { +export const buildMap = (mapping: MapEntry[]): Mapping => { return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { const { source, target, actionType } = field; fieldsMap.set(source, { target, actionType }); @@ -23,11 +39,7 @@ export const buildMap = (mapping: MapsType[]): FinalMapping => { }, new Map()); }; -interface KeyAny { - [key: string]: unknown; -} - -export const mapParams = (params: any, mapping: FinalMapping) => { +export const mapParams = (params: any, mapping: Mapping) => { return Object.keys(params).reduce((prev: KeyAny, curr: string): KeyAny => { const field = mapping.get(curr); if (field) { @@ -36,3 +48,72 @@ export const mapParams = (params: any, mapping: FinalMapping) => { return prev; }, {}); }; + +export const appendField = ({ value, prefix = '', suffix = '' }: AppendFieldArgs): string => { + return `${prefix}${value} ${suffix}`; +}; + +const t = { ...transformers } as { [index: string]: Function }; // TODO: Find a better solution exists. + +export const prepareFieldsForTransformation = ({ + params, + mapping, + defaultPipes = ['informationCreated'], +}: PrepareFieldsForTransformArgs): PipedField[] => { + return Object.keys(params.incident) + .filter(p => mapping.get(p).actionType !== 'nothing') + .map(p => ({ + key: p, + value: params.incident[p], + actionType: mapping.get(p).actionType, + pipes: [...defaultPipes], + })) + .map(p => ({ + ...p, + pipes: p.actionType === 'append' ? [...p.pipes, 'append'] : p.pipes, + })); +}; + +export const transformFields = ({ + params, + fields, + currentIncident, +}: TransformFieldsArgs): Incident => { + return fields.reduce((prev: Incident, cur) => { + const transform = flow(...cur.pipes.map(p => t[p])); + prev[cur.key] = transform({ + value: cur.value, + date: params.createdAt, + user: params.createdBy.fullName ?? params.createdBy.username, + previousValue: currentIncident ? currentIncident[cur.key] : '', + }).value; + return prev; + }, {} as Incident); +}; + +export const appendInformationToField = ({ + value, + user, + date, + mode = 'create', +}: AppendInformationFieldArgs): string => { + return appendField({ + value, + suffix: i18n.FIELD_INFORMATION(mode, date, user), + }); +}; + +export const transformComments = ( + comments: Comment[], + params: Params, + pipes: string[] +): Comment[] => { + return comments.map(c => ({ + ...c, + comment: flow(...pipes.map(p => t[p]))({ + value: c.comment, + date: params.createdAt, + user: params.createdBy.fullName ?? '', + }).value, + })); +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index a1df243b0ee7c6..8ee81c5e764513 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -14,13 +14,12 @@ import { configUtilsMock } from '../../actions_config.mock'; import { ACTION_TYPE_ID } from './constants'; import * as i18n from './translations'; -import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; +import { handleIncident } from './action_handlers'; import { incidentResponse } from './mock'; jest.mock('./action_handlers'); -const handleCreateIncidentMock = handleCreateIncident as jest.Mock; -const handleUpdateIncidentMock = handleUpdateIncident as jest.Mock; +const handleIncidentMock = handleIncident as jest.Mock; const services: Services = { callCluster: async (path: string, opts: any) => {}, @@ -63,12 +62,19 @@ const mockOptions = { incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', title: 'Incident title', description: 'Incident description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, comments: [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', version: 'WzU3LDFd', comment: 'A comment', - incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, }, ], }, @@ -169,8 +175,7 @@ describe('validateParams()', () => { describe('execute()', () => { beforeEach(() => { - handleCreateIncidentMock.mockReset(); - handleUpdateIncidentMock.mockReset(); + handleIncidentMock.mockReset(); }); test('should create an incident', async () => { @@ -185,7 +190,7 @@ describe('execute()', () => { services, }; - handleCreateIncidentMock.mockImplementation(() => incidentResponse); + handleIncidentMock.mockImplementation(() => incidentResponse); const actionResponse = await actionType.executor(executorOptions); expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse }); @@ -205,7 +210,7 @@ describe('execute()', () => { }; const errorMessage = 'Failed to create incident'; - handleCreateIncidentMock.mockImplementation(() => { + handleIncidentMock.mockImplementation(() => { throw new Error(errorMessage); }); @@ -243,7 +248,7 @@ describe('execute()', () => { }; const errorMessage = 'Failed to update incident'; - handleUpdateIncidentMock.mockImplementation(() => { + handleIncidentMock.mockImplementation(() => { throw new Error(errorMessage); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 01e566af17d08b..f844bef6441ee9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -18,12 +18,12 @@ import { ServiceNow } from './lib'; import * as i18n from './translations'; import { ACTION_TYPE_ID } from './constants'; -import { ConfigType, SecretsType, ParamsType, CommentType } from './types'; +import { ConfigType, SecretsType, Comment, ExecutorParams } from './types'; import { ConfigSchemaProps, SecretsSchemaProps, ParamsSchema } from './schema'; import { buildMap, mapParams } from './helpers'; -import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; +import { handleIncident } from './action_handlers'; function validateConfig( configurationUtilities: ActionsConfigurationUtilities, @@ -77,21 +77,22 @@ async function serviceNowExecutor( const actionId = execOptions.actionId; const { apiUrl, - casesConfiguration: { mapping }, + casesConfiguration: { mapping: configurationMapping }, } = execOptions.config as ConfigType; const { username, password } = execOptions.secrets as SecretsType; - const params = execOptions.params as ParamsType; + const params = execOptions.params as ExecutorParams; const { comments, incidentId, ...restParams } = params; - const finalMap = buildMap(mapping); - const restParamsMapped = mapParams(restParams, finalMap); + const mapping = buildMap(configurationMapping); + const incident = mapParams(restParams, mapping); const serviceNow = new ServiceNow({ url: apiUrl, username, password }); const handlerInput = { + incidentId, serviceNow, - params: restParamsMapped, - comments: comments as CommentType[], - mapping: finalMap, + params: { ...params, incident }, + comments: comments as Comment[], + mapping, }; const res: Pick & @@ -100,13 +101,7 @@ async function serviceNowExecutor( actionId, }; - let data = {}; - - if (!incidentId) { - data = await handleCreateIncident(handlerInput); - } else { - data = await handleUpdateIncident({ incidentId, ...handlerInput }); - } + const data = await handleIncident(handlerInput); return { ...res, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts index 22be625611e853..17c8bce6514030 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts @@ -132,7 +132,10 @@ describe('ServiceNow lib', () => { commentId: '456', version: 'WzU3LDFd', comment: 'A comment', - incidentCommentId: undefined, + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, }; const res = await serviceNow.createComment('123', comment, 'comments'); @@ -173,13 +176,19 @@ describe('ServiceNow lib', () => { commentId: '123', version: 'WzU3LDFd', comment: 'A comment', - incidentCommentId: undefined, + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, }, { commentId: '456', version: 'WzU3LDFd', comment: 'A second comment', - incidentCommentId: undefined, + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, }, ]; const res = await serviceNow.batchCreateComments('000', comments, 'comments'); @@ -210,7 +219,9 @@ describe('ServiceNow lib', () => { try { await serviceNow.getUserID(); } catch (error) { - expect(error.message).toEqual('[ServiceNow]: Instance is not alive.'); + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to get user id. Error: [ServiceNow]: Instance is not alive.' + ); } }); @@ -226,7 +237,96 @@ describe('ServiceNow lib', () => { try { await serviceNow.getUserID(); } catch (error) { - expect(error.message).toEqual('[ServiceNow]: Instance is not alive.'); + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to get user id. Error: [ServiceNow]: Instance is not alive.' + ); + } + }); + + test('check error when getting user', async () => { + expect.assertions(1); + + axiosMock.mockImplementationOnce(() => { + throw new Error('Bad request.'); + }); + try { + await serviceNow.getUserID(); + } catch (error) { + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to get user id. Error: Bad request.' + ); + } + }); + + test('check error when getting incident', async () => { + expect.assertions(1); + + axiosMock.mockImplementationOnce(() => { + throw new Error('Bad request.'); + }); + try { + await serviceNow.getIncident('123'); + } catch (error) { + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to get incident with id 123. Error: Bad request.' + ); + } + }); + + test('check error when creating incident', async () => { + expect.assertions(1); + + axiosMock.mockImplementationOnce(() => { + throw new Error('Bad request.'); + }); + try { + await serviceNow.createIncident({ short_description: 'title' }); + } catch (error) { + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to create incident. Error: Bad request.' + ); + } + }); + + test('check error when updating incident', async () => { + expect.assertions(1); + + axiosMock.mockImplementationOnce(() => { + throw new Error('Bad request.'); + }); + try { + await serviceNow.updateIncident('123', { short_description: 'title' }); + } catch (error) { + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to update incident with id 123. Error: Bad request.' + ); + } + }); + + test('check error when creating comment', async () => { + expect.assertions(1); + + axiosMock.mockImplementationOnce(() => { + throw new Error('Bad request.'); + }); + try { + await serviceNow.createComment( + '123', + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'A second comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + 'comment' + ); + } catch (error) { + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to create comment at incident with id 123. Error: Bad request.' + ); } }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts index b3d17affb14c2c..2d1d8975c9efc7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts @@ -8,7 +8,7 @@ import axios, { AxiosInstance, Method, AxiosResponse } from 'axios'; import { INCIDENT_URL, USER_URL, COMMENT_URL } from './constants'; import { Instance, Incident, IncidentResponse, UpdateIncident, CommentResponse } from './types'; -import { CommentType } from '../types'; +import { Comment } from '../types'; const validStatusCodes = [200, 201]; @@ -68,41 +68,77 @@ class ServiceNow { return `${date} GMT`; } + private _getErrorMessage(msg: string) { + return `[Action][ServiceNow]: ${msg}`; + } + async getUserID(): Promise { - const res = await this._request({ url: `${this.userUrl}${this.instance.username}` }); - return res.data.result[0].sys_id; + try { + const res = await this._request({ url: `${this.userUrl}${this.instance.username}` }); + return res.data.result[0].sys_id; + } catch (error) { + throw new Error(this._getErrorMessage(`Unable to get user id. Error: ${error.message}`)); + } } - async createIncident(incident: Incident): Promise { - const res = await this._request({ - url: `${this.incidentUrl}`, - method: 'post', - data: { ...incident }, - }); + async getIncident(incidentId: string) { + try { + const res = await this._request({ + url: `${this.incidentUrl}/${incidentId}`, + }); + + return { ...res.data.result }; + } catch (error) { + throw new Error( + this._getErrorMessage( + `Unable to get incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } + } - return { - number: res.data.result.number, - incidentId: res.data.result.sys_id, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), - }; + async createIncident(incident: Incident): Promise { + try { + const res = await this._request({ + url: `${this.incidentUrl}`, + method: 'post', + data: { ...incident }, + }); + + return { + number: res.data.result.number, + incidentId: res.data.result.sys_id, + pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), + }; + } catch (error) { + throw new Error(this._getErrorMessage(`Unable to create incident. Error: ${error.message}`)); + } } async updateIncident(incidentId: string, incident: UpdateIncident): Promise { - const res = await this._patch({ - url: `${this.incidentUrl}/${incidentId}`, - data: { ...incident }, - }); - - return { - number: res.data.result.number, - incidentId: res.data.result.sys_id, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - }; + try { + const res = await this._patch({ + url: `${this.incidentUrl}/${incidentId}`, + data: { ...incident }, + }); + + return { + number: res.data.result.number, + incidentId: res.data.result.sys_id, + pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + }; + } catch (error) { + throw new Error( + this._getErrorMessage( + `Unable to update incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } } async batchCreateComments( incidentId: string, - comments: CommentType[], + comments: Comment[], field: string ): Promise { const res = await Promise.all(comments.map(c => this.createComment(incidentId, c, field))); @@ -111,18 +147,26 @@ class ServiceNow { async createComment( incidentId: string, - comment: CommentType, + comment: Comment, field: string ): Promise { - const res = await this._patch({ - url: `${this.commentUrl}/${incidentId}`, - data: { [field]: comment.comment }, - }); - - return { - commentId: comment.commentId, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - }; + try { + const res = await this._patch({ + url: `${this.commentUrl}/${incidentId}`, + data: { [field]: comment.comment }, + }); + + return { + commentId: comment.commentId, + pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + }; + } catch (error) { + throw new Error( + this._getErrorMessage( + `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } } } diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts index 4a3c5c42fcb440..3c245bf3f688f4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts @@ -11,9 +11,10 @@ export interface Instance { } export interface Incident { - short_description?: string; + short_description: string; description?: string; caller_id?: string; + [index: string]: string | undefined; } export interface IncidentResponse { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts index 9a150bbede5f88..b9608511159b61 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts @@ -4,40 +4,44 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MapsType, FinalMapping, ParamsType } from './types'; +import { MapEntry, Mapping, ExecutorParams } from './types'; import { Incident } from './lib/types'; -const mapping: MapsType[] = [ - { source: 'title', target: 'short_description', actionType: 'nothing' }, - { source: 'description', target: 'description', actionType: 'nothing' }, - { source: 'comments', target: 'comments', actionType: 'nothing' }, +const mapping: MapEntry[] = [ + { source: 'title', target: 'short_description', actionType: 'overwrite' }, + { source: 'description', target: 'description', actionType: 'append' }, + { source: 'comments', target: 'comments', actionType: 'append' }, ]; -const finalMapping: FinalMapping = new Map(); +const finalMapping: Mapping = new Map(); finalMapping.set('title', { target: 'short_description', - actionType: 'nothing', + actionType: 'overwrite', }); finalMapping.set('description', { target: 'description', - actionType: 'nothing', + actionType: 'append', }); finalMapping.set('comments', { target: 'comments', - actionType: 'nothing', + actionType: 'append', }); finalMapping.set('short_description', { target: 'title', - actionType: 'nothing', + actionType: 'overwrite', }); -const params: ParamsType = { +const params: ExecutorParams = { caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, title: 'Incident title', description: 'Incident description', comments: [ @@ -45,13 +49,19 @@ const params: ParamsType = { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', version: 'WzU3LDFd', comment: 'A comment', - incidentCommentId: '263ede42075300100e48fbbf7c1ed047', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, }, { commentId: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d', version: 'WlK3LDFd', comment: 'Another comment', - incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, }, ], }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 0bb4f50819665e..889b57c8e92e23 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; -export const MapsSchema = schema.object({ +export const MapEntrySchema = schema.object({ source: schema.string(), target: schema.string(), actionType: schema.oneOf([ @@ -17,7 +17,7 @@ export const MapsSchema = schema.object({ }); export const CasesConfigurationSchema = schema.object({ - mapping: schema.arrayOf(MapsSchema), + mapping: schema.arrayOf(MapEntrySchema), }); export const ConfigSchemaProps = { @@ -34,11 +34,25 @@ export const SecretsSchemaProps = { export const SecretsSchema = schema.object(SecretsSchemaProps); +export const UserSchema = schema.object({ + fullName: schema.nullable(schema.string()), + username: schema.string(), +}); + +const EntityInformationSchemaProps = { + createdAt: schema.string(), + createdBy: UserSchema, + updatedAt: schema.nullable(schema.string()), + updatedBy: schema.nullable(UserSchema), +}; + +export const EntityInformationSchema = schema.object(EntityInformationSchemaProps); + export const CommentSchema = schema.object({ commentId: schema.string(), comment: schema.string(), version: schema.maybe(schema.string()), - incidentCommentId: schema.maybe(schema.string()), + ...EntityInformationSchemaProps, }); export const ExecutorAction = schema.oneOf([ @@ -48,8 +62,9 @@ export const ExecutorAction = schema.oneOf([ export const ParamsSchema = schema.object({ caseId: schema.string(), + title: schema.string(), comments: schema.maybe(schema.arrayOf(CommentSchema)), description: schema.maybe(schema.string()), - title: schema.maybe(schema.string()), - incidentId: schema.maybe(schema.string()), + incidentId: schema.nullable(schema.string()), + ...EntityInformationSchemaProps, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts new file mode 100644 index 00000000000000..dc0a03fab8c715 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts @@ -0,0 +1,43 @@ +/* + * 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 { TransformerArgs } from './types'; +import * as i18n from './translations'; + +export const informationCreated = ({ + value, + date, + user, + ...rest +}: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`, + ...rest, +}); + +export const informationUpdated = ({ + value, + date, + user, + ...rest +}: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`, + ...rest, +}); + +export const informationAdded = ({ + value, + date, + user, + ...rest +}: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`, + ...rest, +}); + +export const append = ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ + value: previousValue ? `${previousValue} \r\n${value}` : `${value}`, + ...rest, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index 8601c5ce772db3..3b216a6c3260ac 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -51,3 +51,32 @@ export const UNEXPECTED_STATUS = (status: number) => status, }, }); + +export const FIELD_INFORMATION = ( + mode: string, + date: string | undefined, + user: string | undefined +) => { + switch (mode) { + case 'create': + return i18n.translate('xpack.actions.builtin.servicenow.informationCreated', { + values: { date, user }, + defaultMessage: '(created at {date} by {user})', + }); + case 'update': + return i18n.translate('xpack.actions.builtin.servicenow.informationUpdated', { + values: { date, user }, + defaultMessage: '(updated at {date} by {user})', + }); + case 'add': + return i18n.translate('xpack.actions.builtin.servicenow.informationAdded', { + values: { date, user }, + defaultMessage: '(added at {date} by {user})', + }); + default: + return i18n.translate('xpack.actions.builtin.servicenow.informationDefault', { + values: { date, user }, + defaultMessage: '(created at {date} by {user})', + }); + } +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 7442f14fed0648..418b78add2429e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -11,11 +11,12 @@ import { SecretsSchema, ParamsSchema, CasesConfigurationSchema, - MapsSchema, + MapEntrySchema, CommentSchema, } from './schema'; import { ServiceNow } from './lib'; +import { Incident } from './lib/types'; // config definition export type ConfigType = TypeOf; @@ -23,34 +24,83 @@ export type ConfigType = TypeOf; // secrets definition export type SecretsType = TypeOf; -export type ParamsType = TypeOf; +export type ExecutorParams = TypeOf; export type CasesConfigurationType = TypeOf; -export type MapsType = TypeOf; -export type CommentType = TypeOf; +export type MapEntry = TypeOf; +export type Comment = TypeOf; -export type FinalMapping = Map; +export type Mapping = Map; -export interface ActionHandlerArguments { +export interface Params extends ExecutorParams { + incident: Record; +} +export interface CreateHandlerArguments { serviceNow: ServiceNow; - params: any; - comments: CommentType[]; - mapping: FinalMapping; + params: Params; + comments: Comment[]; + mapping: Mapping; } -export type UpdateParamsType = Partial; -export type UpdateActionHandlerArguments = ActionHandlerArguments & { +export type UpdateHandlerArguments = CreateHandlerArguments & { incidentId: string; }; -export interface IncidentCreationResponse { +export type IncidentHandlerArguments = CreateHandlerArguments & { + incidentId: string | null; +}; + +export interface HandlerResponse { incidentId: string; number: string; - comments?: CommentsZipped[]; + comments?: SimpleComment[]; pushedDate: string; } -export interface CommentsZipped { +export interface SimpleComment { commentId: string; pushedDate: string; } + +export interface AppendFieldArgs { + value: string; + prefix?: string; + suffix?: string; +} + +export interface KeyAny { + [index: string]: string; +} + +export interface AppendInformationFieldArgs { + value: string; + user: string; + date: string; + mode: string; +} + +export interface TransformerArgs { + value: string; + date?: string; + user?: string; + previousValue?: string; +} + +export interface PrepareFieldsForTransformArgs { + params: Params; + mapping: Mapping; + defaultPipes?: string[]; +} + +export interface PipedField { + key: string; + value: string; + actionType: string; + pipes: string[]; +} + +export interface TransformFieldsArgs { + params: Params; + fields: PipedField[]; + currentIncident?: Incident; +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts index 3f1a0952389399..329262044357bc 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts @@ -9,95 +9,72 @@ import Hapi from 'hapi'; interface ServiceNowRequest extends Hapi.Request { payload: { - caseId: string; - title?: string; + short_description: string; description?: string; - comments?: Array<{ commentId: string; version: string; comment: string }>; + comments?: string; }; } export function initPlugin(server: Hapi.Server, path: string) { server.route({ method: 'POST', - path, + path: `${path}/api/now/v2/table/incident`, options: { auth: false, - validate: { - options: { abortEarly: false }, - payload: Joi.object().keys({ - caseId: Joi.string(), - title: Joi.string(), - description: Joi.string(), - comments: Joi.array().items( - Joi.object({ - commentId: Joi.string(), - version: Joi.string(), - comment: Joi.string(), - }) - ), - }), - }, }, - handler: servicenowHandler, + handler: createHandler, }); server.route({ - method: 'POST', - path: `${path}/api/now/v2/table/incident`, + method: 'PATCH', + path: `${path}/api/now/v2/table/incident/{id}`, options: { auth: false, validate: { - options: { abortEarly: false }, - payload: Joi.object().keys({ - caseId: Joi.string(), - title: Joi.string(), - description: Joi.string(), - comments: Joi.array().items( - Joi.object({ - commentId: Joi.string(), - version: Joi.string(), - comment: Joi.string(), - }) - ), + params: Joi.object({ + id: Joi.string(), }), }, }, - handler: servicenowHandler, + handler: updateHandler, }); server.route({ - method: 'PATCH', + method: 'GET', path: `${path}/api/now/v2/table/incident`, options: { auth: false, - validate: { - options: { abortEarly: false }, - payload: Joi.object().keys({ - caseId: Joi.string(), - title: Joi.string(), - description: Joi.string(), - comments: Joi.array().items( - Joi.object({ - commentId: Joi.string(), - version: Joi.string(), - comment: Joi.string(), - }) - ), - }), - }, }, - handler: servicenowHandler, + handler: getHandler, }); } + // ServiceNow simulator: create a servicenow action pointing here, and you can get // different responses based on the message posted. See the README.md for // more info. - -function servicenowHandler(request: ServiceNowRequest, h: any) { +function createHandler(request: ServiceNowRequest, h: any) { return jsonResponse(h, 200, { result: { sys_id: '123', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' }, }); } +function updateHandler(request: ServiceNowRequest, h: any) { + return jsonResponse(h, 200, { + result: { sys_id: '123', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' }, + }); +} + +function getHandler(request: ServiceNowRequest, h: any) { + return jsonResponse(h, 200, { + result: { + sys_id: '123', + number: 'INC01', + sys_created_on: '2020-03-10 12:24:20', + short_description: 'title', + description: 'description', + }, + }); +} + function jsonResponse(h: any, code: number, object?: any) { if (object == null) { return h.response('').code(code); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 63c118966cfaef..b735dae2ca5b19 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -18,18 +18,18 @@ import { const mapping = [ { source: 'title', - target: 'description', - actionType: 'nothing', + target: 'short_description', + actionType: 'overwrite', }, { source: 'description', - target: 'short_description', - actionType: 'nothing', + target: 'description', + actionType: 'append', }, { source: 'comments', target: 'comments', - actionType: 'nothing', + actionType: 'append', }, ]; @@ -49,19 +49,23 @@ export default function servicenowTest({ getService }: FtrProviderContext) { username: 'changeme', }, params: { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - title: 'A title', - description: 'A description', + caseId: '123', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + incidentId: null, comments: [ - { - commentId: '123', - version: 'WzU3LDFd', - comment: 'A comment', - }, { commentId: '456', - version: 'WzU5LVFd', - comment: 'Another comment', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, }, ], }, @@ -283,7 +287,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { caseId: 'success' }, + params: { ...mockServiceNow.params, title: 'success', comments: [] }, }) .expect(200); @@ -311,5 +315,113 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); }); + + it('should handle failing with a simulated success without title', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { caseId: 'success' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [title]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { caseId: 'success', title: 'success' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [createdAt]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{}], + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [comments.0.commentId]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success' }], + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [comments.0.comment]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without comment.createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success', comment: 'success' }], + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [comments.0.createdAt]: expected value of type [string] but got [undefined]', + }); + }); + }); }); } From 9aad8986e1f11b199af1ce0a1570a1c54995ade4 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 18 Mar 2020 13:07:41 -0700 Subject: [PATCH 14/42] Move ui/indices into es_ui_shared plugin. (#60186) * Convert js files to ts. * Add indices namespace. --- src/plugins/data/README.md | 2 +- src/plugins/es_ui_shared/public/index.ts | 2 ++ .../public/indices/constants/index.ts} | 2 +- .../es_ui_shared/public/indices/index.ts} | 11 ++++++-- .../public/indices/validate/index.ts} | 0 .../indices/validate/validate_index.test.ts} | 0 .../indices/validate/validate_index.ts} | 26 ++++++++++++------- .../helpers/field_validators/index_name.ts | 9 +++---- .../components/auto_follow_pattern_form.js | 6 ++--- .../follower_index_form.js | 4 +-- .../follower_index_form.test.js | 3 --- .../auto_follow_pattern_validators.js | 9 ++++--- .../public/app/services/input_validation.js | 4 +-- .../job_create/steps/step_logistics.js | 8 +++--- .../steps_config/validate_rollup_index.js | 4 +-- .../plugins/rollup/public/legacy_imports.ts | 3 --- .../plugins/rollup/public/shared_imports.ts | 7 +++++ 17 files changed, 58 insertions(+), 42 deletions(-) rename src/{legacy/ui/public/indices/constants/index.js => plugins/es_ui_shared/public/indices/constants/index.ts} (94%) rename src/{legacy/ui/public/indices/index.js => plugins/es_ui_shared/public/indices/index.ts} (79%) rename src/{legacy/ui/public/indices/validate/index.js => plugins/es_ui_shared/public/indices/validate/index.ts} (100%) rename src/{legacy/ui/public/indices/validate/validate_index.test.js => plugins/es_ui_shared/public/indices/validate/validate_index.test.ts} (100%) rename src/{legacy/ui/public/indices/validate/validate_index.js => plugins/es_ui_shared/public/indices/validate/validate_index.ts} (67%) create mode 100644 x-pack/legacy/plugins/rollup/public/shared_imports.ts diff --git a/src/plugins/data/README.md b/src/plugins/data/README.md index 53618ec049e7c4..0fa304c9889359 100644 --- a/src/plugins/data/README.md +++ b/src/plugins/data/README.md @@ -6,4 +6,4 @@ - `filter` - `index_patterns` - `query` -- `search` +- `search` \ No newline at end of file diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 2925e5e16458e6..8ed01b9b61c7e2 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -27,3 +27,5 @@ export { sendRequest, useRequest, } from './request/np_ready_request'; + +export { indices } from './indices'; diff --git a/src/legacy/ui/public/indices/constants/index.js b/src/plugins/es_ui_shared/public/indices/constants/index.ts similarity index 94% rename from src/legacy/ui/public/indices/constants/index.js rename to src/plugins/es_ui_shared/public/indices/constants/index.ts index 72ecc2e4c87de2..825975fa161b55 100644 --- a/src/legacy/ui/public/indices/constants/index.js +++ b/src/plugins/es_ui_shared/public/indices/constants/index.ts @@ -17,7 +17,7 @@ * under the License. */ -import { indexPatterns } from '../../../../../plugins/data/public'; +import { indexPatterns } from '../../../../data/public'; export const INDEX_ILLEGAL_CHARACTERS_VISIBLE = [...indexPatterns.ILLEGAL_CHARACTERS_VISIBLE, '*']; diff --git a/src/legacy/ui/public/indices/index.js b/src/plugins/es_ui_shared/public/indices/index.ts similarity index 79% rename from src/legacy/ui/public/indices/index.js rename to src/plugins/es_ui_shared/public/indices/index.ts index c1646bd66e3678..a6d279a5c2b4f0 100644 --- a/src/legacy/ui/public/indices/index.js +++ b/src/plugins/es_ui_shared/public/indices/index.ts @@ -17,10 +17,17 @@ * under the License. */ -export { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from './constants'; +import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from './constants'; -export { +import { indexNameBeginsWithPeriod, findIllegalCharactersInIndexName, indexNameContainsSpaces, } from './validate'; + +export const indices = { + INDEX_ILLEGAL_CHARACTERS_VISIBLE, + indexNameBeginsWithPeriod, + findIllegalCharactersInIndexName, + indexNameContainsSpaces, +}; diff --git a/src/legacy/ui/public/indices/validate/index.js b/src/plugins/es_ui_shared/public/indices/validate/index.ts similarity index 100% rename from src/legacy/ui/public/indices/validate/index.js rename to src/plugins/es_ui_shared/public/indices/validate/index.ts diff --git a/src/legacy/ui/public/indices/validate/validate_index.test.js b/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts similarity index 100% rename from src/legacy/ui/public/indices/validate/validate_index.test.js rename to src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts diff --git a/src/legacy/ui/public/indices/validate/validate_index.js b/src/plugins/es_ui_shared/public/indices/validate/validate_index.ts similarity index 67% rename from src/legacy/ui/public/indices/validate/validate_index.js rename to src/plugins/es_ui_shared/public/indices/validate/validate_index.ts index 5deaa83a807d98..00ac1342400ace 100644 --- a/src/legacy/ui/public/indices/validate/validate_index.js +++ b/src/plugins/es_ui_shared/public/indices/validate/validate_index.ts @@ -19,23 +19,29 @@ import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from '../constants'; -// Names beginning with periods are reserved for system indices. -export function indexNameBeginsWithPeriod(indexName = '') { +// Names beginning with periods are reserved for hidden indices. +export function indexNameBeginsWithPeriod(indexName?: string): boolean { + if (indexName === undefined) { + return false; + } return indexName[0] === '.'; } -export function findIllegalCharactersInIndexName(indexName) { - const illegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.reduce((chars, char) => { - if (indexName.includes(char)) { - chars.push(char); - } +export function findIllegalCharactersInIndexName(indexName: string): string[] { + const illegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.reduce( + (chars: string[], char: string): string[] => { + if (indexName.includes(char)) { + chars.push(char); + } - return chars; - }, []); + return chars; + }, + [] + ); return illegalCharacters; } -export function indexNameContainsSpaces(indexName) { +export function indexNameContainsSpaces(indexName: string): boolean { return indexName.includes(' '); } diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_name.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_name.ts index 524cac27341abd..5e969fa7151724 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_name.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_name.ts @@ -17,14 +17,11 @@ * under the License. */ -// Note: we can't import from "ui/indices" as the TS Type definition don't exist -// import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices'; +import { indices } from '../../../../public'; import { ValidationFunc } from '../../hook_form_lib'; import { startsWith, containsChars } from '../../../validators/string'; import { ERROR_CODE } from './types'; -const INDEX_ILLEGAL_CHARACTERS = ['\\', '/', '?', '"', '<', '>', '|', '*']; - export const indexNameField = (i18n: any) => ( ...args: Parameters ): ReturnType> => { @@ -51,7 +48,9 @@ export const indexNameField = (i18n: any) => ( }; } - const { charsFound, doesContain } = containsChars(INDEX_ILLEGAL_CHARACTERS)(value as string); + const { charsFound, doesContain } = containsChars(indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE)( + value as string + ); if (doesContain) { return { message: i18n.translate('esUi.forms.fieldValidation.indexNameInvalidCharactersError', { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js index ebb731a1b1acaf..d4e418a964c8ff 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js @@ -29,7 +29,8 @@ import { EuiTitle, } from '@elastic/eui'; -import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices'; +import { indices } from '../../../../../../../src/plugins/es_ui_shared/public'; +import { indexPatterns } from '../../../../../../../src/plugins/data/public'; import routing from '../services/routing'; import { extractQueryParams } from '../services/query_params'; @@ -44,10 +45,9 @@ import { } from '../services/auto_follow_pattern_validators'; import { AutoFollowPatternRequestFlyout } from './auto_follow_pattern_request_flyout'; -import { indexPatterns } from '../../../../../../../src/plugins/data/public'; const indexPatternIllegalCharacters = indexPatterns.ILLEGAL_CHARACTERS_VISIBLE.join(' '); -const indexNameIllegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); +const indexNameIllegalCharacters = indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); const getEmptyAutoFollowPattern = (remoteClusterName = '') => ({ name: '', diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js index 329ef4756133db..fc8f9398807e72 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js @@ -9,7 +9,6 @@ import PropTypes from 'prop-types'; import { debounce } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices'; import { fatalError } from 'ui/notify'; import { @@ -30,6 +29,7 @@ import { EuiTitle, } from '@elastic/eui'; +import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; import { indexNameValidator, leaderIndexValidator } from '../../services/input_validation'; import routing from '../../services/routing'; import { loadIndices } from '../../services/api'; @@ -47,7 +47,7 @@ import { RemoteClustersFormField } from '../remote_clusters_form_field'; import { FollowerIndexRequestFlyout } from './follower_index_request_flyout'; -const indexNameIllegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); +const indexNameIllegalCharacters = indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); const fieldToValidatorMap = advancedSettingsFields.reduce( (map, advancedSetting) => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js index aac04270988131..93da20a8ed93c6 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js @@ -7,9 +7,6 @@ import { updateFields, updateFormErrors } from './follower_index_form'; jest.mock('ui/new_platform'); -jest.mock('ui/indices', () => ({ - INDEX_ILLEGAL_CHARACTERS_VISIBLE: [], -})); describe(' state transitions', () => { it('updateFormErrors() should merge errors with existing fieldsErrors', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js index 18610c87c0a51a..5186a02383d33c 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js @@ -8,13 +8,14 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { +import { indices } from '../../../../../../../src/plugins/es_ui_shared/public'; +import { indexPatterns } from '../../../../../../../src/plugins/data/public'; + +const { indexNameBeginsWithPeriod, findIllegalCharactersInIndexName, indexNameContainsSpaces, -} from 'ui/indices'; - -import { indexPatterns } from '../../../../../../../src/plugins/data/public'; +} = indices; export const validateName = (name = '') => { let errorMsg = null; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/input_validation.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/input_validation.js index 22f7d3be2795fb..981b3f59297513 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/input_validation.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/input_validation.js @@ -6,7 +6,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices'; +import { indices } from '../../../../../../../src/plugins/es_ui_shared/public'; const isEmpty = value => { return !value || !value.trim().length; @@ -19,7 +19,7 @@ const beginsWithPeriod = value => { }; const findIllegalCharacters = value => { - return INDEX_ILLEGAL_CHARACTERS_VISIBLE.reduce((chars, char) => { + return indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.reduce((chars, char) => { if (value.includes(char)) { chars.push(char); } diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js index 024001d463240b..c3996fe3231b1b 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js @@ -26,14 +26,14 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { CronEditor } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/cron_editor'; -import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from '../../../../legacy_imports'; +import { indexPatterns } from '../../../../../../../../../src/plugins/data/public'; + +import { indices } from '../../../../shared_imports'; import { getLogisticalDetailsUrl, getCronUrl } from '../../../services'; import { StepError } from './components'; -import { indexPatterns } from '../../../../../../../../../src/plugins/data/public'; - const indexPatternIllegalCharacters = indexPatterns.ILLEGAL_CHARACTERS_VISIBLE.join(' '); -const indexIllegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); +const indexIllegalCharacters = indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); export class StepLogistics extends Component { static propTypes = { diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js index 637caa2199c42b..ac4bacc291ea3e 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js @@ -6,7 +6,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { findIllegalCharactersInIndexName } from '../../../../legacy_imports'; +import { indices } from '../../../../shared_imports'; export function validateRollupIndex(rollupIndex, indexPattern) { if (!rollupIndex || !rollupIndex.trim()) { @@ -27,7 +27,7 @@ export function validateRollupIndex(rollupIndex, indexPattern) { ]; } - const illegalCharacters = findIllegalCharactersInIndexName(rollupIndex); + const illegalCharacters = indices.findIllegalCharactersInIndexName(rollupIndex); if (illegalCharacters.length) { return [ diff --git a/x-pack/legacy/plugins/rollup/public/legacy_imports.ts b/x-pack/legacy/plugins/rollup/public/legacy_imports.ts index 07155a4b0a60ea..85fa3022f59ed8 100644 --- a/x-pack/legacy/plugins/rollup/public/legacy_imports.ts +++ b/x-pack/legacy/plugins/rollup/public/legacy_imports.ts @@ -4,8 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore -export { findIllegalCharactersInIndexName, INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices'; - export { AggTypeFilters } from 'ui/agg_types'; export { AggTypeFieldFilters } from 'ui/agg_types'; diff --git a/x-pack/legacy/plugins/rollup/public/shared_imports.ts b/x-pack/legacy/plugins/rollup/public/shared_imports.ts new file mode 100644 index 00000000000000..6bf74da6db6fe5 --- /dev/null +++ b/x-pack/legacy/plugins/rollup/public/shared_imports.ts @@ -0,0 +1,7 @@ +/* + * 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 { indices } from '../../../../../src/plugins/es_ui_shared/public'; From a35267afd5e70048001caa9cf189016b17a2bb6f Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Wed, 18 Mar 2020 17:18:03 -0400 Subject: [PATCH 15/42] [Maps] Mark instance state as readonly (#60557) --- .../plugins/maps/public/layers/fields/es_agg_field.ts | 10 +++++----- .../public/layers/fields/top_term_percentage_field.ts | 2 +- .../layers/styles/vector/properties/style_property.ts | 4 ++-- .../plugins/maps/public/layers/tile_layer.test.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.ts b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.ts index c9dfdf6ad1fefe..65f952ca01038c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.ts +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.ts @@ -23,11 +23,11 @@ export interface IESAggField extends IField { } export class ESAggField implements IESAggField { - private _source: IESAggSource; - private _origin: FIELD_ORIGIN; - private _label?: string; - private _aggType: AGG_TYPE; - private _esDocField?: IField | undefined; + private readonly _source: IESAggSource; + private readonly _origin: FIELD_ORIGIN; + private readonly _label?: string; + private readonly _aggType: AGG_TYPE; + private readonly _esDocField?: IField | undefined; constructor({ label, diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/top_term_percentage_field.ts b/x-pack/legacy/plugins/maps/public/layers/fields/top_term_percentage_field.ts index bb40a24288a28c..84bade4d944901 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/top_term_percentage_field.ts +++ b/x-pack/legacy/plugins/maps/public/layers/fields/top_term_percentage_field.ts @@ -12,7 +12,7 @@ import { TOP_TERM_PERCENTAGE_SUFFIX } from '../../../common/constants'; import { FIELD_ORIGIN } from '../../../common/constants'; export class TopTermPercentageField implements IESAggField { - private _topTermAggField: IESAggField; + private readonly _topTermAggField: IESAggField; constructor(topTermAggField: IESAggField) { this._topTermAggField = topTermAggField; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.ts b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.ts index bba6cdb48e6729..6c00c01dec4427 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.ts +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.ts @@ -34,8 +34,8 @@ export interface IStyleProperty { } export class AbstractStyleProperty implements IStyleProperty { - private _options: StylePropertyOptions; - private _styleName: string; + private readonly _options: StylePropertyOptions; + private readonly _styleName: string; constructor(options: StylePropertyOptions, styleName: string) { this._options = options; diff --git a/x-pack/legacy/plugins/maps/public/layers/tile_layer.test.ts b/x-pack/legacy/plugins/maps/public/layers/tile_layer.test.ts index 0ec9385194cc09..8ce38a128ebc4c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tile_layer.test.ts +++ b/x-pack/legacy/plugins/maps/public/layers/tile_layer.test.ts @@ -17,7 +17,7 @@ const sourceDescriptor: XYZTMSSourceDescriptor = { }; class MockTileSource implements ITMSSource { - private _descriptor: XYZTMSSourceDescriptor; + private readonly _descriptor: XYZTMSSourceDescriptor; constructor(descriptor: XYZTMSSourceDescriptor) { this._descriptor = descriptor; } From 2d44870e062c36da2b105bce11e0988f11878e5f Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 18 Mar 2020 14:29:40 -0700 Subject: [PATCH 16/42] Solved the issue for a GROUP BY expression validation (#60558) * Solved the issue for a GROUP BY expression validation * fixed labels --- .../builtin_alert_types/threshold/index.ts | 93 +------------- .../threshold/validation.test.ts | 96 ++++++++++++++ .../threshold/validation.ts | 119 ++++++++++++++++++ .../action_connector_form/action_form.tsx | 18 ++- 4 files changed, 226 insertions(+), 100 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts index a94d2319d6e4dd..ecf60e995d1a18 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts @@ -3,11 +3,9 @@ * 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 { AlertTypeModel, ValidationResult } from '../../../../types'; +import { AlertTypeModel } from '../../../../types'; import { IndexThresholdAlertTypeExpression } from './expression'; -import { IndexThresholdAlertParams } from './types'; -import { builtInGroupByTypes, builtInAggregationTypes } from '../../../../common/constants'; +import { validateExpression } from './validation'; export function getAlertType(): AlertTypeModel { return { @@ -15,91 +13,6 @@ export function getAlertType(): AlertTypeModel { name: 'Index Threshold', iconClass: 'alert', alertParamsExpression: IndexThresholdAlertTypeExpression, - validate: (alertParams: IndexThresholdAlertParams): ValidationResult => { - const { - index, - timeField, - aggType, - aggField, - groupBy, - termSize, - termField, - threshold, - timeWindowSize, - } = alertParams; - const validationResult = { errors: {} }; - const errors = { - aggField: new Array(), - termSize: new Array(), - termField: new Array(), - timeWindowSize: new Array(), - threshold0: new Array(), - threshold1: new Array(), - index: new Array(), - timeField: new Array(), - }; - validationResult.errors = errors; - if (!index || index.length === 0) { - errors.index.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredIndexText', { - defaultMessage: 'Index is required.', - }) - ); - } - if (!timeField) { - errors.timeField.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText', { - defaultMessage: 'Time field is required.', - }) - ); - } - if (aggType && builtInAggregationTypes[aggType].fieldRequired && !aggField) { - errors.aggField.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText', { - defaultMessage: 'Aggregation field is required.', - }) - ); - } - if (!termSize) { - errors.termSize.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTermSizedText', { - defaultMessage: 'Term size is required.', - }) - ); - } - if (!termField && groupBy && builtInGroupByTypes[groupBy].sizeRequired) { - errors.termField.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText', { - defaultMessage: 'Term field is required.', - }) - ); - } - if (!timeWindowSize) { - errors.timeWindowSize.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText', - { - defaultMessage: 'Time window size is required.', - } - ) - ); - } - if (threshold && threshold.length > 0 && !threshold[0]) { - errors.threshold0.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold0Text', { - defaultMessage: 'Threshold0, is required.', - }) - ); - } - if (threshold && threshold.length > 1 && !threshold[1]) { - errors.threshold1.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold1Text', { - defaultMessage: 'Threshold1 is required.', - }) - ); - } - return validationResult; - }, - defaultActionMessage: 'Alert [{{ctx.metadata.name}}] has exceeded the threshold', + validate: validateExpression, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.test.ts new file mode 100644 index 00000000000000..1f24a094d0ecef --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.test.ts @@ -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 { IndexThresholdAlertParams } from './types'; +import { validateExpression } from './validation'; + +describe('expression params validation', () => { + test('if index property is invalid should return proper error message', () => { + const initialParams: IndexThresholdAlertParams = { + index: [], + aggType: 'count', + threshold: [], + timeWindowSize: 1, + timeWindowUnit: 's', + }; + expect(validateExpression(initialParams).errors.index.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.index[0]).toBe('Index is required.'); + }); + test('if timeField property is not defined should return proper error message', () => { + const initialParams: IndexThresholdAlertParams = { + index: ['test'], + aggType: 'count', + threshold: [], + timeWindowSize: 1, + timeWindowUnit: 's', + }; + expect(validateExpression(initialParams).errors.timeField.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.timeField[0]).toBe('Time field is required.'); + }); + test('if aggField property is invalid should return proper error message', () => { + const initialParams: IndexThresholdAlertParams = { + index: ['test'], + aggType: 'avg', + threshold: [], + timeWindowSize: 1, + timeWindowUnit: 's', + }; + expect(validateExpression(initialParams).errors.aggField.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.aggField[0]).toBe( + 'Aggregation field is required.' + ); + }); + test('if termSize property is not set should return proper error message', () => { + const initialParams: IndexThresholdAlertParams = { + index: ['test'], + aggType: 'count', + groupBy: 'top', + threshold: [], + timeWindowSize: 1, + timeWindowUnit: 's', + }; + expect(validateExpression(initialParams).errors.termSize.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.termSize[0]).toBe('Term size is required.'); + }); + test('if termField property is not set should return proper error message', () => { + const initialParams: IndexThresholdAlertParams = { + index: ['test'], + aggType: 'count', + groupBy: 'top', + threshold: [], + timeWindowSize: 1, + timeWindowUnit: 's', + }; + expect(validateExpression(initialParams).errors.termField.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.termField[0]).toBe('Term field is required.'); + }); + test('if threshold0 property is not set should return proper error message', () => { + const initialParams: IndexThresholdAlertParams = { + index: ['test'], + aggType: 'count', + groupBy: 'top', + threshold: [], + timeWindowSize: 1, + timeWindowUnit: 's', + thresholdComparator: '<', + }; + expect(validateExpression(initialParams).errors.threshold0.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.threshold0[0]).toBe('Threshold0 is required.'); + }); + test('if threshold1 property is not set should return proper error message', () => { + const initialParams: IndexThresholdAlertParams = { + index: ['test'], + aggType: 'count', + groupBy: 'top', + threshold: [1], + timeWindowSize: 1, + timeWindowUnit: 's', + thresholdComparator: 'between', + }; + expect(validateExpression(initialParams).errors.threshold1.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.threshold1[0]).toBe('Threshold1 is required.'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts new file mode 100644 index 00000000000000..44be2b6139aa46 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts @@ -0,0 +1,119 @@ +/* + * 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 { ValidationResult } from '../../../../types'; +import { IndexThresholdAlertParams } from './types'; +import { + builtInGroupByTypes, + builtInAggregationTypes, + builtInComparators, +} from '../../../../common/constants'; + +export const validateExpression = (alertParams: IndexThresholdAlertParams): ValidationResult => { + const { + index, + timeField, + aggType, + aggField, + groupBy, + termSize, + termField, + threshold, + timeWindowSize, + thresholdComparator, + } = alertParams; + const validationResult = { errors: {} }; + const errors = { + aggField: new Array(), + termSize: new Array(), + termField: new Array(), + timeWindowSize: new Array(), + threshold0: new Array(), + threshold1: new Array(), + index: new Array(), + timeField: new Array(), + }; + validationResult.errors = errors; + if (!index || index.length === 0) { + errors.index.push( + i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredIndexText', { + defaultMessage: 'Index is required.', + }) + ); + } + if (!timeField) { + errors.timeField.push( + i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText', { + defaultMessage: 'Time field is required.', + }) + ); + } + if (aggType && builtInAggregationTypes[aggType].fieldRequired && !aggField) { + errors.aggField.push( + i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText', { + defaultMessage: 'Aggregation field is required.', + }) + ); + } + if ( + groupBy && + builtInGroupByTypes[groupBy] && + builtInGroupByTypes[groupBy].sizeRequired && + !termSize + ) { + errors.termSize.push( + i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTermSizedText', { + defaultMessage: 'Term size is required.', + }) + ); + } + if ( + groupBy && + builtInGroupByTypes[groupBy].validNormalizedTypes && + builtInGroupByTypes[groupBy].validNormalizedTypes.length > 0 && + !termField + ) { + errors.termField.push( + i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText', { + defaultMessage: 'Term field is required.', + }) + ); + } + if (!timeWindowSize) { + errors.timeWindowSize.push( + i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText', { + defaultMessage: 'Time window size is required.', + }) + ); + } + if (!threshold || threshold.length === 0 || (threshold.length === 1 && !threshold[0])) { + errors.threshold0.push( + i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold0Text', { + defaultMessage: 'Threshold0 is required.', + }) + ); + } + if ( + thresholdComparator && + builtInComparators[thresholdComparator].requiredValues > 1 && + (!threshold || + (threshold && threshold.length < builtInComparators[thresholdComparator!].requiredValues)) + ) { + errors.threshold1.push( + i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold1Text', { + defaultMessage: 'Threshold1 is required.', + }) + ); + } + if (threshold && threshold.length === 2 && threshold[0] > threshold[1]) { + errors.threshold1.push( + i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.greaterThenThreshold0Text', { + defaultMessage: 'Threshold1 should be > Threshold0.', + }) + ); + } + return validationResult; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index a43aa22026710a..64be161fc90b31 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -117,16 +117,14 @@ export const ActionForm = ({ const actionsResponse = await loadAllActions({ http }); setConnectors(actionsResponse.data); } catch (e) { - if (toastNotifications) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage', - { - defaultMessage: 'Unable to load connectors', - } - ), - }); - } + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage', + { + defaultMessage: 'Unable to load connectors', + } + ), + }); } } From 60d385ed89ab142d7067431a367b347d0e367495 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 18 Mar 2020 15:59:38 -0700 Subject: [PATCH 17/42] [Ingest] Add support for `yaml` field types (#60440) * Support yaml var type: * Change stream config model to save type and value, instead of just value * Add code editor for configuring yaml vars * Adjust tests * Account for empty yaml value * Better account for invalid yaml parsing --- .../datasource_to_agent_datasource.test.ts | 34 +++++++++-- .../datasource_to_agent_datasource.ts | 27 +++++++-- .../common/services/package_to_config.test.ts | 38 +++++++----- .../common/services/package_to_config.ts | 4 +- .../common/types/models/datasource.ts | 8 ++- .../components/datasource_input_config.tsx | 18 ++++-- .../datasource_input_stream_config.tsx | 18 ++++-- .../components/datasource_input_var_field.tsx | 60 ++++++++++++++----- .../server/types/models/datasource.ts | 8 ++- 9 files changed, 158 insertions(+), 57 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts index 9201cdcb6bbac6..7b4e4adc4e4fc2 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { NewDatasource } from '../types'; +import { NewDatasource, DatasourceInput } from '../types'; import { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource'; describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { @@ -17,7 +17,7 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { inputs: [], }; - const mockInput = { + const mockInput: DatasourceInput = { type: 'test-logs', enabled: true, streams: [ @@ -25,13 +25,29 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { id: 'test-logs-foo', enabled: true, dataset: 'foo', - config: { fooVar: 'foo-value', fooVar2: [1, 2] }, + config: { fooVar: { value: 'foo-value' }, fooVar2: { value: [1, 2] } }, }, { id: 'test-logs-bar', enabled: false, dataset: 'bar', - config: { barVar: 'bar-value', barVar2: [1, 2] }, + config: { + barVar: { value: 'bar-value' }, + barVar2: { value: [1, 2] }, + barVar3: { + type: 'yaml', + value: + '- namespace: mockNamespace\n #disabledProp: ["test"]\n anotherProp: test\n- namespace: mockNamespace2\n #disabledProp: ["test2"]\n anotherProp: test2', + }, + barVar4: { + type: 'yaml', + value: '', + }, + barVar5: { + type: 'yaml', + value: 'testField: test\n invalidSpacing: foo', + }, + }, }, ], }; @@ -91,6 +107,16 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { dataset: 'bar', barVar: 'bar-value', barVar2: [1, 2], + barVar3: [ + { + namespace: 'mockNamespace', + anotherProp: 'test', + }, + { + namespace: 'mockNamespace2', + anotherProp: 'test2', + }, + ], }, ], }, diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts index 57627fa60fe43a..ea048b84afef33 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { safeLoad } from 'js-yaml'; import { Datasource, NewDatasource, FullAgentConfigDatasource } from '../types'; import { DEFAULT_OUTPUT } from '../constants'; @@ -23,12 +24,26 @@ export const storedDatasourceToAgentDatasource = ( if (stream.config) { const fullStream = { ...stream, - ...Object.entries(stream.config).reduce((acc, [configName, configValue]) => { - if (configValue !== undefined) { - acc[configName] = configValue; - } - return acc; - }, {} as { [key: string]: any }), + ...Object.entries(stream.config).reduce( + (acc, [configName, { type: configType, value: configValue }]) => { + if (configValue !== undefined) { + if (configType === 'yaml') { + try { + const yamlValue = safeLoad(configValue); + if (yamlValue) { + acc[configName] = yamlValue; + } + } catch (e) { + // Silently swallow parsing error + } + } else { + acc[configName] = configValue; + } + } + return acc; + }, + {} as { [key: string]: any } + ), }; delete fullStream.config; return fullStream; diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts index d312e7aa35cc01..e54e59dd24df37 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts @@ -108,8 +108,14 @@ describe('Ingest Manager - packageToConfig', () => { { type: 'bar', streams: [ - { dataset: 'bar', vars: [{ default: 'bar-var-value', name: 'var-name' }] }, - { dataset: 'bar2', vars: [{ default: 'bar2-var-value', name: 'var-name' }] }, + { + dataset: 'bar', + vars: [{ default: 'bar-var-value', name: 'var-name', type: 'text' }], + }, + { + dataset: 'bar2', + vars: [{ default: 'bar2-var-value', name: 'var-name', type: 'yaml' }], + }, ], }, ], @@ -125,7 +131,7 @@ describe('Ingest Manager - packageToConfig', () => { id: 'foo-foo', enabled: true, dataset: 'foo', - config: { 'var-name': 'foo-var-value' }, + config: { 'var-name': { value: 'foo-var-value' } }, }, ], }, @@ -137,13 +143,13 @@ describe('Ingest Manager - packageToConfig', () => { id: 'bar-bar', enabled: true, dataset: 'bar', - config: { 'var-name': 'bar-var-value' }, + config: { 'var-name': { type: 'text', value: 'bar-var-value' } }, }, { id: 'bar-bar2', enabled: true, dataset: 'bar2', - config: { 'var-name': 'bar2-var-value' }, + config: { 'var-name': { type: 'yaml', value: 'bar2-var-value' } }, }, ], }, @@ -204,10 +210,10 @@ describe('Ingest Manager - packageToConfig', () => { enabled: true, dataset: 'foo', config: { - 'var-name': 'foo-var-value', - 'foo-input-var-name': 'foo-input-var-value', - 'foo-input2-var-name': 'foo-input2-var-value', - 'foo-input3-var-name': undefined, + 'var-name': { value: 'foo-var-value' }, + 'foo-input-var-name': { value: 'foo-input-var-value' }, + 'foo-input2-var-name': { value: 'foo-input2-var-value' }, + 'foo-input3-var-name': { value: undefined }, }, }, ], @@ -221,9 +227,9 @@ describe('Ingest Manager - packageToConfig', () => { enabled: true, dataset: 'bar', config: { - 'var-name': 'bar-var-value', - 'bar-input-var-name': ['value1', 'value2'], - 'bar-input2-var-name': 123456, + 'var-name': { value: 'bar-var-value' }, + 'bar-input-var-name': { value: ['value1', 'value2'] }, + 'bar-input2-var-name': { value: 123456 }, }, }, { @@ -231,9 +237,9 @@ describe('Ingest Manager - packageToConfig', () => { enabled: true, dataset: 'bar2', config: { - 'var-name': 'bar2-var-value', - 'bar-input-var-name': ['value1', 'value2'], - 'bar-input2-var-name': 123456, + 'var-name': { value: 'bar2-var-value' }, + 'bar-input-var-name': { value: ['value1', 'value2'] }, + 'bar-input2-var-name': { value: 123456 }, }, }, ], @@ -247,7 +253,7 @@ describe('Ingest Manager - packageToConfig', () => { enabled: false, dataset: 'disabled', config: { - 'var-name': [], + 'var-name': { value: [] }, }, }, { diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.ts index 9785edbff11127..6de75a004303e1 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.ts @@ -41,9 +41,9 @@ export const packageToConfigDatasourceInputs = (packageInfo: PackageInfo): Datas streamVar: RegistryVarsEntry ): DatasourceInputStream['config'] => { if (!streamVar.default && streamVar.multi) { - configObject![streamVar.name] = []; + configObject![streamVar.name] = { type: streamVar.type, value: [] }; } else { - configObject![streamVar.name] = streamVar.default; + configObject![streamVar.name] = { type: streamVar.type, value: streamVar.default }; } return configObject; }; diff --git a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts index 3503bbdcd40e31..3ad7a15d0c7398 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts @@ -15,7 +15,13 @@ export interface DatasourceInputStream { enabled: boolean; dataset: string; processors?: string[]; - config?: Record; + config?: Record< + string, + { + type?: string; + value: any; + } + >; } export interface DatasourceInput { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx index 69d21946384415..1128f25818d7c8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx @@ -63,8 +63,8 @@ export const DatasourceInputConfig: React.FunctionComponent<{ {requiredVars.map(varDef => { - const varName = varDef.name; - const value = datasourceInput.streams[0].config![varName]; + const { name: varName, type: varType } = varDef; + const value = datasourceInput.streams[0].config![varName].value; return ( {isShowingAdvanced ? advancedVars.map(varDef => { - const varName = varDef.name; - const value = datasourceInput.streams[0].config![varName]; + const { name: varName, type: varType } = varDef; + const value = datasourceInput.streams[0].config![varName].value; return ( {requiredVars.map(varDef => { - const varName = varDef.name; - const value = datasourceInputStream.config![varName]; + const { name: varName, type: varType } = varDef; + const value = datasourceInputStream.config![varName].value; return ( {isShowingAdvanced ? advancedVars.map(varDef => { - const varName = varDef.name; - const value = datasourceInputStream.config![varName]; + const { name: varName, type: varType } = varDef; + const value = datasourceInputStream.config![varName].value; return ( void; }> = ({ varDef, value, onChange }) => { + const renderField = () => { + if (varDef.multi) { + return ( + ({ label: val }))} + onCreateOption={(newVal: any) => { + onChange([...value, newVal]); + }} + onChange={(newVals: any[]) => { + onChange(newVals.map(val => val.label)); + }} + /> + ); + } + if (varDef.type === 'yaml') { + return ( + onChange(newVal)} + /> + ); + } + return ( + onChange(e.target.value)} + /> + ); + }; + return ( } > - {varDef.multi ? ( - ({ label: val }))} - onCreateOption={(newVal: any) => { - onChange([...value, newVal]); - }} - onChange={(newVals: any[]) => { - onChange(newVals.map(val => val.label)); - }} - /> - ) : ( - onChange(e.target.value)} /> - )} + {renderField()} ); }; diff --git a/x-pack/plugins/ingest_manager/server/types/models/datasource.ts b/x-pack/plugins/ingest_manager/server/types/models/datasource.ts index 94d0a1cc1aabf7..51687016f6aad6 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/datasource.ts @@ -31,7 +31,13 @@ const DatasourceBaseSchema = { enabled: schema.boolean(), dataset: schema.string(), processors: schema.maybe(schema.arrayOf(schema.string())), - config: schema.recordOf(schema.string(), schema.any()), + config: schema.recordOf( + schema.string(), + schema.object({ + type: schema.maybe(schema.string()), + value: schema.any(), + }) + ), }) ), }) From 3600f5b90b1d9caeea846e2af0ea20bea15e41b4 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 18 Mar 2020 16:43:22 -0700 Subject: [PATCH 18/42] Fixed default message for index threshold includes both threshold values (#60545) * Fixed default message for index threshold includes both threshold values even if not used * fixed due to review comments * Fixed validation errors with ability to clear input --- .../threshold/expression.tsx | 2 +- .../threshold/validation.ts | 3 ++- .../common/expression_items/threshold.test.tsx | 4 ++-- .../common/expression_items/threshold.tsx | 18 ++++++++++++++---- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index 5c7f48de81f757..728418bf3c3368 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -50,7 +50,7 @@ const DEFAULT_VALUES = { THRESHOLD_COMPARATOR: COMPARATORS.GREATER_THAN, TIME_WINDOW_SIZE: 5, TIME_WINDOW_UNIT: 'm', - THRESHOLD: [1000, 5000], + THRESHOLD: [1000], GROUP_BY: 'all', }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts index 44be2b6139aa46..3912b2fffae1eb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts @@ -89,7 +89,7 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali }) ); } - if (!threshold || threshold.length === 0 || (threshold.length === 1 && !threshold[0])) { + if (!threshold || threshold.length === 0 || threshold[0] === undefined) { errors.threshold0.push( i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold0Text', { defaultMessage: 'Threshold0 is required.', @@ -100,6 +100,7 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali thresholdComparator && builtInComparators[thresholdComparator].requiredValues > 1 && (!threshold || + threshold[1] === undefined || (threshold && threshold.length < builtInComparators[thresholdComparator!].requiredValues)) ) { errors.threshold1.push( diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.test.tsx index bd3c7383d4b9c4..92880bd1245079 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.test.tsx @@ -15,7 +15,7 @@ describe('threshold expression', () => { const wrapper = shallow( @@ -59,7 +59,7 @@ describe('threshold expression', () => { const wrapper = shallow( diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx index ecbf0aee63e2d2..d0de7ae77a81e0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx @@ -105,6 +105,11 @@ export const ThresholdExpression = ({ value={thresholdComparator} onChange={e => { onChangeSelectedThresholdComparator(e.target.value); + const thresholdValues = threshold.slice( + 0, + comparators[e.target.value].requiredValues + ); + onChangeSelectedThreshold(thresholdValues); }} options={Object.values(comparators).map(({ text, value }) => { return { text, value }; @@ -123,18 +128,23 @@ export const ThresholdExpression = ({ ) : null} - + 0 || !threshold[i]} + error={errors[`threshold${i}`]} + > 0 || !threshold[i]} onChange={e => { const { value } = e.target; const thresholdVal = value !== '' ? parseFloat(value) : undefined; const newThreshold = [...threshold]; - if (thresholdVal) { + if (thresholdVal !== undefined) { newThreshold[i] = thresholdVal; + } else { + delete newThreshold[i]; } onChangeSelectedThreshold(newThreshold); }} From cf1a33020680ce18d6f29ae8b69bd6e41b812f07 Mon Sep 17 00:00:00 2001 From: marshallmain <55718608+marshallmain@users.noreply.github.com> Date: Wed, 18 Mar 2020 19:46:54 -0400 Subject: [PATCH 19/42] fix agent type (#60554) Co-authored-by: Elastic Machine --- x-pack/plugins/endpoint/common/generate_data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 2e1d6074d0c2fb..f5ed6da197273e 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -248,7 +248,7 @@ export class EndpointDocGenerator { public generateEvent(options: EventOptions = {}): EndpointEvent { return { '@timestamp': options.timestamp ? options.timestamp : new Date().getTime(), - agent: { ...this.commonInfo.agent, type: 'endgame' }, + agent: { ...this.commonInfo.agent, type: 'endpoint' }, ecs: { version: '1.4.0', }, From 357ed0e10c9aad3ef77f53d025a2346543f55dff Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 18 Mar 2020 17:13:34 -0700 Subject: [PATCH 20/42] skip flaky suite (#60559) --- .../apps/discover/feature_controls/discover_spaces.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index 4bedc757f0b575..f33b8b4899d161 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -24,7 +24,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - describe('spaces', () => { + // FLAKY: https://github.com/elastic/kibana/issues/60559 + describe.skip('spaces', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); }); From a05a61286f43cbabbacd43ee6b22a0cda66aa7be Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 18 Mar 2020 19:26:42 -0500 Subject: [PATCH 21/42] [SIEM] Create ML Rules (#58053) * Remove unnecessary linter exceptions Not sure what was causing issues here, but it's gone now. * WIP: Simple form to test creation of ML rules This will be integrated into the regular rule creation workflow, but for now this simple form should allow us to exercise the full ML rule workflow. * WIP: Adds POST to backend, and type/payload changes necessary to make that work * Simplify logic with Math.min * WIP: Failed spike of making an http call * WIP: Hacking together an ML client The rest of this is going to be easier if I have actual data. For now this is mostly copy/pasted and simplified ML code. I've hardcoded time ranges to a period I know has data for a particular job. * Threading through our new ML Rule params It's a bummer that we normalize our rule alert params across all rule types currently, but that's the deal. * Retrieve our anomalies during rule execution Next step: generate signals * WIP: Generate ECS-compatible ML Signals This uses as much of the existing signal-creation code as possible. I skipped the search_after stuff for now because it would require us recreating the anomalies query which we really shouldn't own. For now, here's how it works: * Adds a separate branch of the rule executor for machine_learning rules * In that branch, we call our new bulkCreateMlSignal function * This function first transforms the anomaly document into ECS fields * We then pass the transformed documents to singleBulkCreate, which does the rest * After both branches, we update the rule's status appropriately. We need to do some more work on the anomaly transformation, but this works! * Extract setting of rule failure to helper function We were doing this identically in three places. * Remove unused import * Define a field for our Rule Type selection This adds most of the markup and logic to allow an ML rule type to be selected. We still need to add things like license-checking and showing/hiding of fields based on type. * Hide Query Fields when ML is selected These are still getting set on the form. We'll need to filter these fields before we send off the data, and not show them on the readonly display either. ALso, edit is majorly broken. * Add input field for anomaly threshold * Display numberic values in the readonly view of a step TIL that isEmpty returns false for numbers and other non-iterable values. I don't think it's exactly what we want here, but until I figure out the intention this gets our anomalyThreshold showing up without a separate logic branch here. Removes the unnecessary branch that was redundant with the 'else' clause. * Add field for selecting an ML job This is not the same as the mockups and lacks some functionality, but it'll allow us to select a job for now. * Format our new ML Fields when sending them to the server So that we don't get rejected due to snake case vs camelcase. * Put back code that respects a rule's schedule It was previously hardcoded to a time period I knew had anomalies. * ML fields are optional in our creation step In that we don't initialize them like we do the query (default) fields. * Only send along type-specific Rule fields from form This makes any query- or ML-specific fields optional on a Rule, and performs some logic on the frontend to group and include these fieldsets conditionally based on the user's selection. The one place we don't handle this well is on the readonly view of a completed step in the rules creation, but we'll address that. * Rename anomalies query It's no longer tabular data. If we need that, we can use the ML client. * Remove spike page with simple form * Remove unneeded ES option This response isn't going to HTTP, which is where this option would matter. * Fix bulk create logic I made a happy accident and flipped the logic here, which meant we weren't capping the signals we created. * Rename argument Value is a little more ambiguous than data, here: this is our step data. * Create Rule form stores all values, but filters by type for use When sending off to the backend, or displaying on the readonly view, we inspect which rule type we've currently selected, and filter our form values appropriately. * Fix editing of ML fields on Rule Create We need to inherit the field value from our form on initial render, and everything works as expected. * Clear form errors when switching between rule types Validation errors prevent us from moving to the next step, so it was previously possible to get an error for Query fields, switch to an ML rule, and be unable to continue because the form had Query errors. This also adds a helper for checking whether a ruleType is ML, to prevent having to change all these references if the type string changes. * Validate the selection of an ML Job * Fix type errors on frontend According to the types, this is essentially the opposite of formatRule, so we need to reinflate all potential form values from the rule. * Don't set defaults for query-specific rules For ML rules these types should not be included. * Return ML Fields in Rule responses This adds these fields to our rule serialization, and then adds conditional validation around those fields if the rule type is ML. Conversely, we moved the 'language' and 'query' fields to be conditionally validated if the rule is a query/saved_query rule. * Fix editing of ML rules by changing who controls the field values The source of truth for their state is the parent form object; these inputs should not have local state. * Fix type errors related to new ML fields In adding the new ML fields, some other fields (e.g. `query` and `index`) that were previously required but implicitly part of Query Rules are now marked as optional. Consequently, any downstream code that actually required these fields started to complain. In general, the fix was to verify that those fields exist, and throw an error otherwise as to appease the linter. Runtime-wise, the new ML rules/signals follow a separate code path and both branches should be unaffected by these changes; the issue is simply that our conditional types don't work well with Typescript. * Fix failing route tests Error message changed. * Fix integration tests We were not sending required properties when creating a rule(index and language). * Fix non-ML Rule creation I was accidentally dropping this parameter for our POST payload. Whoops. * More informative logging during ML signal generation The messaging diverged from the normal path here because we don't have index patterns to display. However, we have the rest of the rule context, and should report it appropriately. * Prefer keyof for string union types * Tidy up our new form components * Type them as React.FCs * Remove unnecessary use of styled-components * Prefer destructuring to lodash's omit * Fix mock params for helper functions These were updated to take simpler parameters. * Remove any type This could have been a boolean all along, whoops * Fix mock types * Update outdated tests These were added on master, but behavior has been changed on my branch. * Add some tests around our helper function I need to refactor it, so this is as good a time as any to pin down the behavior. * Remove uses of any in favor of actual types Mainly leverages ML typings instead of our placeholder types. This required handling a null case in our formatting of anomalies. * Annotate our anomalies with @timestamp field We were notably lacking this ECS field in our post-conversion anomalies, and typescript was rightly complaining about it. * ml_job_id -> machine_learning_job_id * PR Feedback * Stricter threshold type * More robust date parsing * More informative log/error messages * Remove redundant runtime checks * Cleaning up our new ML types * Fix types on our Rest types * Use less ambiguous machineLearningJobId over mlJobId * Declare our ML params as required keys, and ensure we pass them around everywhere we might need them (creating, importing, updating rules). * Use implicit type to avoid the need for a ts-ignore FormSchema has a very generic index signature such that our filterRuleFieldsForType helper cannot infer that it has our necessary rule fields (when in fact it does). By removing the FormSchema hint we get the actual keys of our schema, and things work as expected. All other uses of schema continue to work because they're expecting FormSchema, which is effectively { [key: string]: any }. * New ML params are not nullable Rather than setting a null and then never using it, let's just make it truly optional in terms of default values. * Query and language are conditional based on rule type For ML Rules, we don't use them. * Remove defaulted parameter in API test We don't need to specify this, and we should continue not to for backwards compatibility. * Use explicit types over implicit ones The concern is that not typing our schemae as FormSchema could break our form if there are upstream changes. For now, we simply use the intersection of FormSchema and our generic parameter to satisfy our use within the function. * Add integration test for creation of ML Rule * Add ML fields to route schemae * threshold and job id are conditional on type * makes query and language mutually exclusive with above * Fix router test for creating an ML rule We were sending invalid parameters. * Remove null check against index for query rules We support not having an index here, as getInputIndex will return the current UI setting if none is specified. * Add regression test for API compatibility We were previously able to create a rule without an input index; we should continue to support that, as verified by this test! * Respect the index pattern determined at runtime when performing search_after If a rule does not specify an input index pattern on creation, we use the current UI default when the rule is evaluated. This ensures that any subsequent searches use that same index. We're not currently persisting that runtime index to the generated signal, but we should. * Fix type errors in our bulk create tests We added a new argument, but didn't update the tests. --- .../detection_engine/rules/types.ts | 31 ++-- .../rules/all/__mocks__/mock.ts | 3 + .../anomaly_threshold_slider/index.tsx | 45 ++++++ .../description_step/helpers.test.tsx | 27 +--- .../components/description_step/helpers.tsx | 4 +- .../components/description_step/index.tsx | 39 +++-- .../components/description_step/types.ts | 3 +- .../rules/components/ml_job_select/index.tsx | 58 ++++++++ .../rules/components/query_bar/index.tsx | 2 +- .../components/select_rule_type/index.tsx | 60 ++++++++ .../select_rule_type/translations.ts | 42 ++++++ .../components/step_define_rule/index.tsx | 133 +++++++++++------- .../components/step_define_rule/schema.tsx | 94 +++++++++++-- .../rules/create/helpers.test.ts | 9 +- .../detection_engine/rules/create/helpers.ts | 78 ++++++---- .../detection_engine/rules/create/index.tsx | 3 - .../detection_engine/rules/edit/index.tsx | 12 +- .../detection_engine/rules/helpers.test.tsx | 13 +- .../pages/detection_engine/rules/helpers.tsx | 20 +-- .../pages/detection_engine/rules/types.ts | 17 ++- .../routes/__mocks__/request_responses.ts | 17 +++ .../rules/create_rules_bulk_route.test.ts | 2 +- .../routes/rules/create_rules_bulk_route.ts | 4 + .../routes/rules/create_rules_route.test.ts | 10 +- .../routes/rules/create_rules_route.ts | 4 + .../routes/rules/import_rules_route.ts | 5 + .../rules/patch_rules_bulk_route.test.ts | 2 +- .../routes/rules/patch_rules_route.test.ts | 2 +- .../rules/update_rules_bulk_route.test.ts | 2 +- .../routes/rules/update_rules_bulk_route.ts | 4 + .../routes/rules/update_rules_route.test.ts | 2 +- .../routes/rules/update_rules_route.ts | 4 + .../routes/rules/utils.test.ts | 30 +++- .../detection_engine/routes/rules/utils.ts | 2 + .../schemas/add_prepackaged_rules_schema.ts | 24 +++- .../routes/schemas/create_rules_schema.ts | 24 +++- .../routes/schemas/import_rules_schema.ts | 24 +++- .../routes/schemas/patch_rules_schema.ts | 4 + .../schemas/response/__mocks__/utils.ts | 12 ++ .../response/check_type_dependents.test.ts | 68 ++++++++- .../schemas/response/check_type_dependents.ts | 26 ++++ .../routes/schemas/response/rules_schema.ts | 12 +- .../routes/schemas/response/schemas.ts | 4 +- .../routes/schemas/schemas.ts | 7 +- .../routes/schemas/update_rules_schema.ts | 24 +++- .../detection_engine/rules/create_rules.ts | 4 + .../rules/install_prepacked_rules.ts | 4 + .../signals/__mocks__/es_results.ts | 2 + .../detection_engine/signals/build_rule.ts | 2 + .../signals/bulk_create_ml_signals.test.ts | 90 ++++++++++++ .../signals/bulk_create_ml_signals.ts | 80 +++++++++++ .../signals/find_ml_signals.ts | 29 ++++ .../detection_engine/signals/get_filter.ts | 5 + .../signals/search_after_bulk_create.test.ts | 10 ++ .../signals/search_after_bulk_create.ts | 8 +- .../signals/signal_params_schema.ts | 2 + .../signals/signal_rule_alert_type.ts | 129 ++++++++++------- .../signals/single_search_after.test.ts | 16 ++- .../signals/single_search_after.ts | 15 +- .../lib/detection_engine/signals/types.ts | 2 +- .../siem/server/lib/detection_engine/types.ts | 12 +- .../siem/server/lib/machine_learning/index.ts | 90 ++++++++++++ .../security_and_spaces/tests/create_rules.ts | 27 ++++ .../security_and_spaces/tests/utils.ts | 31 ++++ 64 files changed, 1297 insertions(+), 273 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/machine_learning/index.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index f962204c6b1b4d..5466ba2203714f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -6,26 +6,35 @@ import * as t from 'io-ts'; +export const RuleTypeSchema = t.keyof({ + query: null, + saved_query: null, + machine_learning: null, +}); +export type RuleType = t.TypeOf; + export const NewRuleSchema = t.intersection([ t.type({ description: t.string, enabled: t.boolean, - filters: t.array(t.unknown), - index: t.array(t.string), interval: t.string, - language: t.string, name: t.string, - query: t.string, risk_score: t.number, severity: t.string, - type: t.union([t.literal('query'), t.literal('saved_query')]), + type: RuleTypeSchema, }), t.partial({ + anomaly_threshold: t.number, created_by: t.string, false_positives: t.array(t.string), + filters: t.array(t.unknown), from: t.string, id: t.string, + index: t.array(t.string), + language: t.string, + machine_learning_job_id: t.string, max_signals: t.number, + query: t.string, references: t.array(t.string), rule_id: t.string, saved_id: t.string, @@ -56,32 +65,34 @@ export const RuleSchema = t.intersection([ description: t.string, enabled: t.boolean, false_positives: t.array(t.string), - filters: t.array(t.unknown), from: t.string, id: t.string, - index: t.array(t.string), interval: t.string, immutable: t.boolean, - language: t.string, name: t.string, max_signals: t.number, - query: t.string, references: t.array(t.string), risk_score: t.number, rule_id: t.string, severity: t.string, tags: t.array(t.string), - type: t.string, + type: RuleTypeSchema, to: t.string, threat: t.array(t.unknown), updated_at: t.string, updated_by: t.string, }), t.partial({ + anomaly_threshold: t.number, + filters: t.array(t.unknown), + index: t.array(t.string), + language: t.string, last_failure_at: t.string, last_failure_message: t.string, meta: MetaRule, + machine_learning_job_id: t.string, output_index: t.string, + query: t.string, saved_id: t.string, status: t.string, status_date: t.string, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts index 5627d338185009..011a2614c1af94 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -181,6 +181,9 @@ export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ isNew, + ruleType: 'query', + anomalyThreshold: 50, + machineLearningJobId: '', index: ['filebeat-'], queryBar: mockQueryBar, }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx new file mode 100644 index 00000000000000..18970ff935b8dd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { EuiFlexGrid, EuiFlexItem, EuiRange, EuiFormRow } from '@elastic/eui'; + +import { FieldHook } from '../../../../../shared_imports'; + +interface AnomalyThresholdSliderProps { + field: FieldHook; +} +type Event = React.ChangeEvent; +type EventArg = Event | React.MouseEvent; + +export const AnomalyThresholdSlider: React.FC = ({ field }) => { + const threshold = field.value as number; + const onThresholdChange = useCallback( + (event: EventArg) => { + const thresholdValue = Number((event as Event).target.value); + field.setValue(thresholdValue); + }, + [field] + ); + + return ( + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx index 56c9d6da156074..7a3f0105d3d158 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx @@ -38,10 +38,7 @@ setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); const mockFilterManager = new FilterManager(setupMock.uiSettings); const mockQueryBar = { - query: { - query: 'test query', - language: 'kuery', - }, + query: 'test query', filters: [ { $state: { @@ -93,10 +90,7 @@ describe('helpers', () => { describe('buildQueryBarDescription', () => { test('returns empty array if no filters, query or savedId exist', () => { const emptyMockQueryBar = { - query: { - query: '', - language: 'kuery', - }, + query: '', filters: [], saved_id: '', }; @@ -113,10 +107,7 @@ describe('helpers', () => { test('returns expected array of ListItems when filters exists, but no indexPatterns passed in', () => { const mockQueryBarWithFilters = { ...mockQueryBar, - query: { - query: '', - language: 'kuery', - }, + query: '', saved_id: '', }; const result: ListItems[] = buildQueryBarDescription({ @@ -135,10 +126,7 @@ describe('helpers', () => { test('returns expected array of ListItems when filters AND indexPatterns exist', () => { const mockQueryBarWithFilters = { ...mockQueryBar, - query: { - query: '', - language: 'kuery', - }, + query: '', saved_id: '', }; const result: ListItems[] = buildQueryBarDescription({ @@ -171,16 +159,13 @@ describe('helpers', () => { savedId: mockQueryBarWithQuery.saved_id, }); expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); - expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query.query} ); + expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query} ); }); test('returns expected array of ListItems when "savedId" exists', () => { const mockQueryBarWithSavedId = { ...mockQueryBar, - query: { - query: '', - language: 'kuery', - }, + query: '', filters: [], }; const result: ListItems[] = buildQueryBarDescription({ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index bc454ecb1134a8..7b22078c89d1bb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -77,12 +77,12 @@ export const buildQueryBarDescription = ({ }, ]; } - if (!isEmpty(query.query)) { + if (!isEmpty(query)) { items = [ ...items, { title: <>{i18n.QUERY_LABEL} , - description: <>{query.query} , + description: <>{query} , }, ]; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index 1d58ef8014899a..43b4a5f781b89c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -5,7 +5,7 @@ */ import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { isEmpty, chunk, get, pick } from 'lodash/fp'; +import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; import React, { memo, useState } from 'react'; import styled from 'styled-components'; @@ -14,7 +14,6 @@ import { Filter, esFilters, FilterManager, - Query, } from '../../../../../../../../../../src/plugins/data/public'; import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; import { useKibana } from '../../../../../lib/kibana'; @@ -133,14 +132,14 @@ export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => { export const getDescriptionItem = ( field: string, label: string, - value: unknown, + data: unknown, filterManager: FilterManager, indexPatterns?: IIndexPattern ): ListItems[] => { if (field === 'queryBar') { - const filters = addFilterStateIfNotThere(get('queryBar.filters', value) ?? []); - const query = get('queryBar.query', value) as Query; - const savedId = get('queryBar.saved_id', value); + const filters = addFilterStateIfNotThere(get('queryBar.filters', data) ?? []); + const query = get('queryBar.query.query', data); + const savedId = get('queryBar.saved_id', data); return buildQueryBarDescription({ field, filters, @@ -150,31 +149,24 @@ export const getDescriptionItem = ( indexPatterns, }); } else if (field === 'threat') { - const threat: IMitreEnterpriseAttack[] = get(field, value).filter( + const threat: IMitreEnterpriseAttack[] = get(field, data).filter( (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' ); return buildThreatDescription({ label, threat }); } else if (field === 'references') { - const urls: string[] = get(field, value); + const urls: string[] = get(field, data); return buildUrlsDescription(label, urls); } else if (field === 'falsePositives') { - const values: string[] = get(field, value); + const values: string[] = get(field, data); return buildUnorderedListArrayDescription(label, field, values); - } else if (Array.isArray(get(field, value))) { - const values: string[] = get(field, value); + } else if (Array.isArray(get(field, data))) { + const values: string[] = get(field, data); return buildStringArrayDescription(label, field, values); } else if (field === 'severity') { - const val: string = get(field, value); + const val: string = get(field, data); return buildSeverityDescription(label, val); - } else if (field === 'riskScore') { - return [ - { - title: label, - description: get(field, value), - }, - ]; } else if (field === 'timeline') { - const timeline = get(field, value) as FieldValueTimeline; + const timeline = get(field, data) as FieldValueTimeline; return [ { title: label, @@ -182,11 +174,12 @@ export const getDescriptionItem = ( }, ]; } else if (field === 'note') { - const val: string = get(field, value); + const val: string = get(field, data); return buildNoteDescription(label, val); } - const description: string = get(field, value); - if (!isEmpty(description)) { + + const description: string = get(field, data); + if (isNumber(description) || !isEmpty(description)) { return [ { title: label, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts index ab73c52ae9070c..bfca6b20684432 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts @@ -9,7 +9,6 @@ import { IIndexPattern, Filter, FilterManager, - Query, } from '../../../../../../../../../../src/plugins/data/public'; import { IMitreEnterpriseAttack } from '../../types'; @@ -22,7 +21,7 @@ export interface BuildQueryBarDescription { field: string; filters: Filter[]; filterManager: FilterManager; - query: Query; + query: string; savedId: string; indexPatterns?: IIndexPattern; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx new file mode 100644 index 00000000000000..627fa21cc2f611 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSuperSelect, EuiText } from '@elastic/eui'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; +import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; + +const JobDisplay = ({ title, description }: { title: string; description: string }) => ( + <> + {title} + +

{description}

+
+ +); + +interface MlJobSelectProps { + field: FieldHook; +} + +export const MlJobSelect: React.FC = ({ field }) => { + const jobId = field.value as string; + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const [isLoading, siemJobs] = useSiemJobs(false); + const handleJobChange = useCallback( + (machineLearningJobId: string) => { + field.setValue(machineLearningJobId); + }, + [field] + ); + + const options = siemJobs.map(job => ({ + value: job.id, + inputDisplay: job.id, + dropdownDisplay: , + })); + + return ( + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx index 5886a76182eec4..d232c86c19e6fd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -35,7 +35,7 @@ import * as i18n from './translations'; export interface FieldValueQueryBar { filters: Filter[]; query: Query; - saved_id: string | null; + saved_id?: string; } interface QueryBarDefineRuleProps { browserFields: BrowserFields; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx new file mode 100644 index 00000000000000..b3b35699914f6b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx @@ -0,0 +1,60 @@ +/* + * 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, { useCallback } from 'react'; +import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiIcon, EuiFormRow } from '@elastic/eui'; + +import { FieldHook } from '../../../../../shared_imports'; +import { RuleType } from '../../../../../containers/detection_engine/rules/types'; +import * as i18n from './translations'; +import { isMlRule } from '../../helpers'; + +interface SelectRuleTypeProps { + field: FieldHook; +} + +export const SelectRuleType: React.FC = ({ field }) => { + const ruleType = field.value as RuleType; + const setType = useCallback( + (type: RuleType) => { + field.setValue(type); + }, + [field] + ); + const setMl = useCallback(() => setType('machine_learning'), [setType]); + const setQuery = useCallback(() => setType('query'), [setType]); + const license = true; // TODO + + return ( + + + + } + selectable={{ + onClick: setQuery, + isSelected: !isMlRule(ruleType), + }} + /> + + + } + selectable={{ + onClick: setMl, + isSelected: isMlRule(ruleType), + }} + /> + + + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts new file mode 100644 index 00000000000000..32b860e8f703ea --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts @@ -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 { i18n } from '@kbn/i18n'; + +export const QUERY_TYPE_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.queryTypeTitle', + { + defaultMessage: 'Custom query', + } +); + +export const QUERY_TYPE_DESCRIPTION = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.queryTypeDescription', + { + defaultMessage: 'Use KQL or Lucene to detect issues across indices.', + } +); + +export const ML_TYPE_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeTitle', + { + defaultMessage: 'Machine Learning', + } +); + +export const ML_TYPE_DESCRIPTION = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDescription', + { + defaultMessage: 'Select ML job to detect anomalous activity.', + } +); + +export const ML_TYPE_DISABLED_DESCRIPTION = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDisabledDescription', + { + defaultMessage: 'Access to ML requires a Platinum subscription.', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 2327ac36a5906e..6b1a9a828d9501 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -9,6 +9,7 @@ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, + EuiFormRow, EuiButton, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; @@ -20,11 +21,14 @@ import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/pu import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants'; import { useUiSetting$ } from '../../../../../lib/kibana'; -import { setFieldValue } from '../../helpers'; +import { setFieldValue, isMlRule } from '../../helpers'; import * as RuleI18n from '../../translations'; import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; +import { SelectRuleType } from '../select_rule_type'; +import { AnomalyThresholdSlider } from '../anomaly_threshold_slider'; +import { MlJobSelect } from '../ml_job_select'; import { StepContentWrapper } from '../step_content_wrapper'; import { Field, @@ -33,9 +37,11 @@ import { getUseField, UseField, useForm, + FormSchema, } from '../../../../../shared_imports'; import { schema } from './schema'; import * as i18n from './translations'; +import { filterRuleFieldsForType, RuleFields } from '../../create/helpers'; const CommonUseField = getUseField({ component: Field }); @@ -43,13 +49,16 @@ interface StepDefineRuleProps extends RuleStepProps { defaultValues?: DefineStepRule | null; } -const stepDefineDefaultValue = { +const stepDefineDefaultValue: DefineStepRule = { + anomalyThreshold: 50, index: [], isNew: true, + machineLearningJobId: '', + ruleType: 'query', queryBar: { query: { query: '', language: 'kuery' }, filters: [], - saved_id: null, + saved_id: undefined, }, }; @@ -96,6 +105,7 @@ const StepDefineRuleComponent: FC = ({ }) => { const [openTimelineSearch, setOpenTimelineSearch] = useState(false); const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(false); + const [localIsMlRule, setIsMlRule] = useState(false); const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); const [mylocalIndicesConfig, setMyLocalIndicesConfig] = useState( defaultValues != null ? defaultValues.index : indicesConfig ?? [] @@ -112,6 +122,7 @@ const StepDefineRuleComponent: FC = ({ options: { stripEmptyFields: false }, schema, }); + const clearErrors = useCallback(() => form.reset({ resetValues: false }), [form]); const onSubmit = useCallback(async () => { if (setStepData) { @@ -154,64 +165,75 @@ const StepDefineRuleComponent: FC = ({ setOpenTimelineSearch(false); }, []); - return isReadOnlyView && myStepData?.queryBar != null ? ( + return isReadOnlyView ? ( ) : ( <>
- - {i18n.RESET_DEFAULT_INDEX} - - ) : null, - }} - componentProps={{ - idAria: 'detectionEngineStepDefineRuleIndices', - 'data-test-subj': 'detectionEngineStepDefineRuleIndices', - euiFieldProps: { - fullWidth: true, - isDisabled: isLoading, - placeholder: '', - }, - }} - /> - - {i18n.IMPORT_TIMELINE_QUERY} - - ), - }} - component={QueryBarDefineRule} - componentProps={{ - browserFields, - loading: indexPatternLoadingQueryBar, - idAria: 'detectionEngineStepDefineRuleQueryBar', - indexPattern: indexPatternQueryBar, - isDisabled: isLoading, - isLoading: indexPatternLoadingQueryBar, - dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', - openTimelineSearch, - onCloseTimelineSearch: handleCloseTimelineSearch, - }} - /> - - {({ index }) => { + + + <> + + {i18n.RESET_DEFAULT_INDEX} + + ) : null, + }} + componentProps={{ + idAria: 'detectionEngineStepDefineRuleIndices', + 'data-test-subj': 'detectionEngineStepDefineRuleIndices', + euiFieldProps: { + fullWidth: true, + isDisabled: isLoading, + placeholder: '', + }, + }} + /> + + {i18n.IMPORT_TIMELINE_QUERY} + + ), + }} + component={QueryBarDefineRule} + componentProps={{ + browserFields, + loading: indexPatternLoadingQueryBar, + idAria: 'detectionEngineStepDefineRuleQueryBar', + indexPattern: indexPatternQueryBar, + isDisabled: isLoading, + isLoading: indexPatternLoadingQueryBar, + dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', + openTimelineSearch, + onCloseTimelineSearch: handleCloseTimelineSearch, + }} + /> + + + + <> + + + + + + {({ index, ruleType }) => { if (index != null) { if (deepEqual(index, indicesConfig) && !localUseIndicesConfig) { setLocalUseIndicesConfig(true); @@ -223,6 +245,15 @@ const StepDefineRuleComponent: FC = ({ setMyLocalIndicesConfig(index); } } + + if (isMlRule(ruleType) && !localIsMlRule) { + setIsMlRule(true); + clearErrors(); + } else if (!isMlRule(ruleType) && localIsMlRule) { + setIsMlRule(false); + clearErrors(); + } + return null; }} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx index e202ff030cd905..bcfcd4f4ee09d6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -19,8 +19,7 @@ import { ValidationFunc, } from '../../../../../shared_imports'; import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; - -const { emptyField } = fieldValidators; +import { isMlRule } from '../../helpers'; export const schema: FormSchema = { index: { @@ -34,14 +33,25 @@ export const schema: FormSchema = { helpText: {INDEX_HELPER_TEXT}, validations: [ { - validator: emptyField( - i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', - { - defaultMessage: 'A minimum of one index pattern is required.', - } - ) - ), + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + const needsValidation = !isMlRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + return fieldValidators.emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', + { + defaultMessage: 'A minimum of one index pattern is required.', + } + ) + )(...args); + }, }, ], }, @@ -57,8 +67,13 @@ export const schema: FormSchema = { validator: ( ...args: Parameters ): ReturnType> | undefined => { - const [{ value, path }] = args; + const [{ value, path, formData }] = args; const { query, filters } = value as FieldValueQueryBar; + const needsValidation = !isMlRule(formData.ruleType); + if (!needsValidation) { + return; + } + return isEmpty(query.query as string) && isEmpty(filters) ? { code: 'ERR_FIELD_MISSING', @@ -72,8 +87,13 @@ export const schema: FormSchema = { validator: ( ...args: Parameters ): ReturnType> | undefined => { - const [{ value, path }] = args; + const [{ value, path, formData }] = args; const { query } = value as FieldValueQueryBar; + const needsValidation = !isMlRule(formData.ruleType); + if (!needsValidation) { + return; + } + if (!isEmpty(query.query as string) && query.language === 'kuery') { try { esKuery.fromKueryExpression(query.query); @@ -85,7 +105,55 @@ export const schema: FormSchema = { }; } } - return undefined; + }, + }, + ], + }, + ruleType: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel', + { + defaultMessage: 'Rule type', + } + ), + validations: [], + }, + anomalyThreshold: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel', + { + defaultMessage: 'Anomaly score threshold', + } + ), + validations: [], + }, + machineLearningJobId: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel', + { + defaultMessage: 'Machine Learning job', + } + ), + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + const needsValidation = isMlRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + return fieldValidators.emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired', + { + defaultMessage: 'A Machine Learning job is required.', + } + ) + )(...args); }, }, ], diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts index dbc5dd9bbe29a8..ea6b02924cb3e6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts @@ -87,6 +87,7 @@ describe('helpers', () => { query: 'test query', saved_id: 'test123', index: ['filebeat-'], + type: 'saved_query', }; expect(result).toEqual(expected); @@ -106,6 +107,8 @@ describe('helpers', () => { filters: mockQueryBar.filters, query: 'test query', index: ['filebeat-'], + saved_id: '', + type: 'query', }; expect(result).toEqual(expected); @@ -574,12 +577,6 @@ describe('helpers', () => { expect(result.type).toEqual('query'); }); - test('returns NewRule with id set to ruleId if ruleId exists', () => { - const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, 'query-with-rule-id'); - - expect(result.id).toEqual('query-with-rule-id'); - }); - test('returns NewRule without id if ruleId does not exist', () => { const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts index 07578e870bf2be..1f3379bf681bbe 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash/fp'; +import { has, isEmpty } from 'lodash/fp'; import moment from 'moment'; -import { NewRule } from '../../../../containers/detection_engine/rules'; +import { NewRule, RuleType } from '../../../../containers/detection_engine/rules'; import { AboutStepRule, @@ -16,8 +16,8 @@ import { DefineStepRuleJson, ScheduleStepRuleJson, AboutStepRuleJson, - FormatRuleType, } from '../types'; +import { isMlRule } from '../helpers'; export const getTimeTypeValue = (time: string): { unit: string; value: number } => { const timeObj = { @@ -39,16 +39,52 @@ export const getTimeTypeValue = (time: string): { unit: string; value: number } return timeObj; }; +export interface RuleFields { + anomalyThreshold: unknown; + machineLearningJobId: unknown; + queryBar: unknown; + index: unknown; + ruleType: unknown; +} +type QueryRuleFields = Omit; +type MlRuleFields = Omit; + +const isMlFields = (fields: QueryRuleFields | MlRuleFields): fields is MlRuleFields => + has('anomalyThreshold', fields); + +export const filterRuleFieldsForType = (fields: T, type: RuleType) => { + if (isMlRule(type)) { + const { index, queryBar, ...mlRuleFields } = fields; + return mlRuleFields; + } else { + const { anomalyThreshold, machineLearningJobId, ...queryRuleFields } = fields; + return queryRuleFields; + } +}; + export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { - const { queryBar, isNew, ...rest } = defineStepData; - const { filters, query, saved_id: savedId } = queryBar; - return { - ...rest, - language: query.language, - filters, - query: query.query as string, - ...(savedId != null && savedId !== '' ? { saved_id: savedId } : {}), - }; + const ruleFields = filterRuleFieldsForType(defineStepData, defineStepData.ruleType); + + if (isMlFields(ruleFields)) { + const { anomalyThreshold, machineLearningJobId, isNew, ruleType, ...rest } = ruleFields; + return { + ...rest, + type: ruleType, + anomaly_threshold: anomalyThreshold, + machine_learning_job_id: machineLearningJobId, + }; + } else { + const { queryBar, isNew, ruleType, ...rest } = ruleFields; + return { + ...rest, + type: ruleType, + filters: queryBar?.filters, + language: queryBar?.query?.language, + query: queryBar?.query?.query as string, + saved_id: queryBar?.saved_id, + ...(ruleType === 'query' && queryBar?.saved_id ? { type: 'saved_query' as RuleType } : {}), + }; + } }; export const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { @@ -110,15 +146,9 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule export const formatRule = ( defineStepData: DefineStepRule, aboutStepData: AboutStepRule, - scheduleData: ScheduleStepRule, - ruleId?: string -): NewRule => { - const type: FormatRuleType = !isEmpty(defineStepData.queryBar.saved_id) ? 'saved_query' : 'query'; - const persistData = { - type, - ...formatDefineStepData(defineStepData), - ...formatAboutStepData(aboutStepData), - ...formatScheduleStepData(scheduleData), - }; - return ruleId != null ? { id: ruleId, ...persistData } : persistData; -}; + scheduleData: ScheduleStepRule +): NewRule => ({ + ...formatDefineStepData(defineStepData), + ...formatAboutStepData(aboutStepData), + ...formatScheduleStepData(scheduleData), +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index c9f44ab0048f94..67aaabfe70fda5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -98,7 +98,6 @@ const CreateRulePageComponent: React.FC = () => { const userHasNoPermissions = canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; - // eslint-disable-next-line react-hooks/rules-of-hooks const setStepData = useCallback( (step: RuleStep, data: unknown, isValid: boolean) => { stepsData.current[step] = { ...stepsData.current[step], data, isValid }; @@ -138,12 +137,10 @@ const CreateRulePageComponent: React.FC = () => { [isStepRuleInReadOnlyView, openAccordionId, stepsData.current, setRule] ); - // eslint-disable-next-line react-hooks/rules-of-hooks const setStepsForm = useCallback((step: RuleStep, form: FormHook) => { stepsForm.current[step] = form; }, []); - // eslint-disable-next-line react-hooks/rules-of-hooks const getAccordionType = useCallback( (accordionId: RuleStep) => { if (accordionId === openAccordionId) { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 5e0e4223e3e272..8618bf95048619 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -195,8 +195,8 @@ const EditRulePageComponent: FC = () => { if (invalidForms.length === 0 && activeForm != null) { setTabHasError([]); - setRule( - formatRule( + setRule({ + ...formatRule( (activeFormId === RuleStep.defineRule ? activeForm.data : myDefineRuleForm.data) as DefineStepRule, @@ -205,10 +205,10 @@ const EditRulePageComponent: FC = () => { : myAboutRuleForm.data) as AboutStepRule, (activeFormId === RuleStep.scheduleRule ? activeForm.data - : myScheduleRuleForm.data) as ScheduleStepRule, - ruleId - ) - ); + : myScheduleRuleForm.data) as ScheduleStepRule + ), + ...(ruleId ? { id: ruleId } : {}), + }); } else { setTabHasError(invalidForms); } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx index 0c29bc31cdebc8..ee43ae5f1d6e2b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx @@ -32,7 +32,10 @@ describe('rule helpers', () => { }); const defineRuleStepData = { isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, index: ['auditbeat-*'], + machineLearningJobId: '', queryBar: { query: { query: 'user.name: root or user.name: admin', @@ -180,6 +183,9 @@ describe('rule helpers', () => { const result: DefineStepRule = getDefineStepsData(mockRule('test-id')); const expected = { isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + machineLearningJobId: '', index: ['auditbeat-*'], queryBar: { query: { @@ -194,7 +200,7 @@ describe('rule helpers', () => { expect(result).toEqual(expected); }); - test('returns with saved_id of null if value does not exist on rule', () => { + test('returns with saved_id of undefined if value does not exist on rule', () => { const mockedRule = { ...mockRule('test-id'), }; @@ -202,6 +208,9 @@ describe('rule helpers', () => { const result: DefineStepRule = getDefineStepsData(mockedRule); const expected = { isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + machineLearningJobId: '', index: ['auditbeat-*'], queryBar: { query: { @@ -209,7 +218,7 @@ describe('rule helpers', () => { language: 'kuery', }, filters: [], - saved_id: null, + saved_id: undefined, }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index 1fc8a86a476f2d..e59ca5e7e14e52 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -10,7 +10,7 @@ import moment from 'moment'; import { useLocation } from 'react-router-dom'; import { Filter } from '../../../../../../../../src/plugins/data/public'; -import { Rule } from '../../../containers/detection_engine/rules'; +import { Rule, RuleType } from '../../../containers/detection_engine/rules'; import { FormData, FormHook, FormSchema } from '../../../shared_imports'; import { AboutStepRule, @@ -43,18 +43,16 @@ export const getStepsData = ({ }; export const getDefineStepsData = (rule: Rule): DefineStepRule => { - const { index, query, language, filters, saved_id: savedId } = rule; - return { isNew: false, - index, + ruleType: rule.type, + anomalyThreshold: rule.anomaly_threshold ?? 50, + machineLearningJobId: rule.machine_learning_job_id ?? '', + index: rule.index ?? [], queryBar: { - query: { - query, - language, - }, - filters: filters as Filter[], - saved_id: savedId ?? null, + query: { query: rule.query ?? '', language: rule.language ?? '' }, + filters: (rule.filters ?? []) as Filter[], + saved_id: rule.saved_id, }, }; }; @@ -195,6 +193,8 @@ export const setFieldValue = ( } }); +export const isMlRule = (ruleType: RuleType) => ruleType === 'machine_learning'; + export const redirectToDetections = ( isSignalIndexExists: boolean | null, isAuthenticated: boolean | null, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index aa50626a1231af..447b5dc6325eef 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -5,6 +5,7 @@ */ import { Filter } from '../../../../../../../../src/plugins/data/common'; +import { RuleType } from '../../../containers/detection_engine/rules/types'; import { FieldValueQueryBar } from './components/query_bar'; import { FormData, FormHook } from '../../../shared_imports'; import { FieldValueTimeline } from './components/pick_timeline'; @@ -67,8 +68,11 @@ export interface AboutStepRuleDetails { } export interface DefineStepRule extends StepRuleData { + anomalyThreshold: number; index: string[]; + machineLearningJobId: string; queryBar: FieldValueQueryBar; + ruleType: RuleType; } export interface ScheduleStepRule extends StepRuleData { @@ -79,11 +83,14 @@ export interface ScheduleStepRule extends StepRuleData { } export interface DefineStepRuleJson { - index: string[]; - filters: Filter[]; + anomaly_threshold?: number; + index?: string[]; + filters?: Filter[]; + machine_learning_job_id?: string; saved_id?: string; - query: string; - language: string; + query?: string; + language?: string; + type: RuleType; } export interface AboutStepRuleJson { @@ -112,8 +119,6 @@ export type MyRule = Omit body: typicalPayload(), }); +export const createMlRuleRequest = () => { + const { query, language, index, ...mlParams } = typicalPayload(); + + return requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_RULES_URL, + body: { + ...mlParams, + type: 'machine_learning', + anomaly_threshold: 50, + machine_learning_job_id: 'some-uuid', + }, + }); +}; + export const getSetSignalStatusByIdsRequest = () => requestMock.create({ method: 'post', @@ -349,6 +364,7 @@ export const getResult = (): RuleAlertType => ({ alertTypeId: 'siem.signals', consumer: 'siem', params: { + anomalyThreshold: undefined, description: 'Detecting root and admin users', ruleId: 'rule-1', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -357,6 +373,7 @@ export const getResult = (): RuleAlertType => ({ immutable: false, query: 'user.name: root or user.name: admin', language: 'kuery', + machineLearningJobId: undefined, outputIndex: '.siem-signals', timelineId: 'some-timeline-id', timelineTitle: 'some-timeline-title', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 6ad9efebce2dd1..2b31d37dddddb5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -137,7 +137,7 @@ describe('create_rules_bulk', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query]]]' + '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index d727bbb953d2a0..b819bc69192745 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -56,12 +56,14 @@ export const createRulesBulkRoute = (router: IRouter) => { .filter(rule => rule.rule_id == null || !dupes.includes(rule.rule_id)) .map(async payloadRule => { const { + anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, from, query, language, + machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, meta, @@ -107,6 +109,7 @@ export const createRulesBulkRoute = (router: IRouter) => { const createdRule = await createRules({ alertsClient, actionsClient, + anomalyThreshold, description, enabled, falsePositives, @@ -114,6 +117,7 @@ export const createRulesBulkRoute = (router: IRouter) => { immutable: false, query, language, + machineLearningJobId, outputIndex: finalIndex, savedId, timelineId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index d019668e2a8d18..976f371c6b1a69 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -14,6 +14,7 @@ import { getNonEmptyIndex, getEmptyIndex, getFindResultWithSingleHit, + createMlRuleRequest, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesRoute } from './create_rules_route'; @@ -48,6 +49,13 @@ describe('create_rules', () => { }); }); + describe('creating an ML Rule', () => { + it('is successful', async () => { + const response = await server.inject(createMlRuleRequest(), context); + expect(response.status).toEqual(200); + }); + }); + describe('unhappy paths', () => { test('it returns a 400 if the index does not exist', async () => { clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); @@ -111,7 +119,7 @@ describe('create_rules', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "type" fails because ["type" must be one of [query, saved_query]]' + 'child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index fcfcee99f369e1..42bade1ba08553 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -31,6 +31,7 @@ export const createRulesRoute = (router: IRouter): void => { }, async (context, request, response) => { const { + anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, @@ -42,6 +43,7 @@ export const createRulesRoute = (router: IRouter): void => { timeline_id: timelineId, timeline_title: timelineTitle, meta, + machine_learning_job_id: machineLearningJobId, filters, rule_id: ruleId, index, @@ -93,6 +95,7 @@ export const createRulesRoute = (router: IRouter): void => { const createdRule = await createRules({ alertsClient, actionsClient, + anomalyThreshold, description, enabled, falsePositives, @@ -105,6 +108,7 @@ export const createRulesRoute = (router: IRouter): void => { timelineId, timelineTitle, meta, + machineLearningJobId, filters, ruleId: ruleId ?? uuid.v4(), index, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index ec4e707f46e50a..d92ef316aef0cd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -111,6 +111,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config return null; } const { + anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, @@ -118,6 +119,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config immutable, query, language, + machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, meta, @@ -139,6 +141,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config timeline_title: timelineTitle, version, } = parsedRule; + try { const signalsIndex = siemClient.signalsIndex; const indexExists = await getIndexExists( @@ -159,6 +162,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config await createRules({ alertsClient, actionsClient, + anomalyThreshold, description, enabled, falsePositives, @@ -166,6 +170,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config immutable, query, language, + machineLearningJobId, outputIndex: signalsIndex, savedId, timelineId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index 19bcd2e7f0596a..967fd46f7e3da5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -89,7 +89,7 @@ describe('patch_rules_bulk', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query]]]' + '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index 1658de77e3390f..0c2ca882a55907 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -112,7 +112,7 @@ describe('patch_rules', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "type" fails because ["type" must be one of [query, saved_query]]' + 'child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 7a9159ecc852b0..46639e1fe33808 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -110,7 +110,7 @@ describe('update_rules_bulk', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query]]]' + '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 777b9f3cc7a9dd..859935d8511264 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -47,12 +47,14 @@ export const updateRulesBulkRoute = (router: IRouter) => { const rules = await Promise.all( request.body.map(async payloadRule => { const { + anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, from, query, language, + machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, @@ -81,6 +83,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { const rule = await updateRules({ alertsClient, actionsClient, + anomalyThreshold, description, enabled, immutable: false, @@ -88,6 +91,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { from, query, language, + machineLearningJobId, outputIndex: finalIndex, savedId, savedObjectsClient, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 6ef508b8177133..a6da8cd56ec17f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -115,7 +115,7 @@ describe('update_rules', () => { const result = await server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "type" fails because ["type" must be one of [query, saved_query]]' + 'child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 1393de8c725cbe..a9982a9896633e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -30,12 +30,14 @@ export const updateRulesRoute = (router: IRouter) => { }, async (context, request, response) => { const { + anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, from, query, language, + machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, @@ -77,6 +79,7 @@ export const updateRulesRoute = (router: IRouter) => { const rule = await updateRules({ alertsClient, actionsClient, + anomalyThreshold, description, enabled, falsePositives, @@ -84,6 +87,7 @@ export const updateRulesRoute = (router: IRouter) => { immutable: false, query, language, + machineLearningJobId, outputIndex: finalIndex, savedId, savedObjectsClient, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index 70fcbb2c163ca2..3243ccb14f89c2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -36,7 +36,7 @@ describe('utils', () => { test('should work with a full data set', () => { const fullRule = getResult(); const rule = transformAlertToRule(fullRule); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', @@ -358,7 +358,7 @@ describe('utils', () => { const fullRule = getResult(); fullRule.enabled = false; const ruleWithEnabledFalse = transformAlertToRule(fullRule); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', @@ -424,7 +424,7 @@ describe('utils', () => { const fullRule = getResult(); fullRule.params.immutable = false; const ruleWithEnabledFalse = transformAlertToRule(fullRule); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', @@ -490,7 +490,7 @@ describe('utils', () => { const fullRule = getResult(); fullRule.tags = ['tag 1', 'tag 2', `${INTERNAL_IDENTIFIER}_some_other_value`]; const rule = transformAlertToRule(fullRule); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', created_by: 'elastic', @@ -551,6 +551,22 @@ describe('utils', () => { }; expect(rule).toEqual(expected); }); + + it('transforms ML Rule fields', () => { + const mlRule = getResult(); + mlRule.params.anomalyThreshold = 55; + mlRule.params.machineLearningJobId = 'some_job_id'; + mlRule.params.type = 'machine_learning'; + + const rule = transformAlertToRule(mlRule); + expect(rule).toEqual( + expect.objectContaining({ + anomaly_threshold: 55, + machine_learning_job_id: 'some_job_id', + type: 'machine_learning', + }) + ); + }); }); describe('getIdError', () => { @@ -640,7 +656,7 @@ describe('utils', () => { total: 0, data: [getResult()], }); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', @@ -722,7 +738,7 @@ describe('utils', () => { describe('transform', () => { test('outputs 200 if the data is of type siem alert', () => { const output = transform(getResult()); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', @@ -895,7 +911,7 @@ describe('utils', () => { describe('transformOrBulkError', () => { test('outputs 200 if the data is of type siem alert', () => { const output = transformOrBulkError('rule-1', getResult()); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index ecf669b0106c39..abd8dd7e87f033 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -106,6 +106,7 @@ export const transformAlertToRule = ( created_by: alert.createdBy, description: alert.params.description, enabled: alert.enabled, + anomaly_threshold: alert.params.anomalyThreshold, false_positives: alert.params.falsePositives, filters: alert.params.filters, from: alert.params.from, @@ -117,6 +118,7 @@ export const transformAlertToRule = ( language: alert.params.language, output_index: alert.params.outputIndex, max_signals: alert.params.maxSignals, + machine_learning_job_id: alert.params.machineLearningJobId, risk_score: alert.params.riskScore, name: alert.name, query: alert.params.query, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts index 974ddcf35eeb49..ec0a8e7871b5ba 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts @@ -34,6 +34,8 @@ import { references, note, version, + anomaly_threshold, + machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -49,6 +51,11 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; * - index is a required field that must exist */ export const addPrepackagedRulesSchema = Joi.object({ + anomaly_threshold: anomaly_threshold.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), description: description.required(), enabled: enabled.default(false), false_positives: false_positives.default([]), @@ -61,8 +68,21 @@ export const addPrepackagedRulesSchema = Joi.object({ .valid(true), index: index.required(), interval: interval.default('5m'), - query: query.allow('').default(''), - language: language.default('kuery'), + query: query.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: query.allow('').default(''), + }), + language: language.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: language.default('kuery'), + }), + machine_learning_job_id: machine_learning_job_id.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), saved_id: saved_id.when('type', { is: 'saved_query', then: Joi.required(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts index c9b380d3c67e1c..e86963fd4594c7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts @@ -8,6 +8,7 @@ import Joi from 'joi'; /* eslint-disable @typescript-eslint/camelcase */ import { + anomaly_threshold, enabled, description, false_positives, @@ -34,12 +35,18 @@ import { references, note, version, + machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; export const createRulesSchema = Joi.object({ + anomaly_threshold: anomaly_threshold.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), description: description.required(), enabled: enabled.default(true), false_positives: false_positives.default([]), @@ -48,8 +55,16 @@ export const createRulesSchema = Joi.object({ rule_id, index, interval: interval.default('5m'), - query: query.allow('').default(''), - language: language.default('kuery'), + query: query.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: query.allow('').default(''), + }), + language: language.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: language.default('kuery'), + }), output_index, saved_id: saved_id.when('type', { is: 'saved_query', @@ -59,6 +74,11 @@ export const createRulesSchema = Joi.object({ timeline_id, timeline_title, meta, + machine_learning_job_id: machine_learning_job_id.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), risk_score: risk_score.required(), max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), name: name.required(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts index bd12872c4dc722..92718b7ae71ba1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts @@ -40,6 +40,8 @@ import { references, note, version, + anomaly_threshold, + machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -55,6 +57,11 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; * - updated_by is optional (but ignored in the import code) */ export const importRulesSchema = Joi.object({ + anomaly_threshold: anomaly_threshold.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), id, description: description.required(), enabled: enabled.default(true), @@ -65,9 +72,22 @@ export const importRulesSchema = Joi.object({ immutable: immutable.default(false).valid(false), index, interval: interval.default('5m'), - query: query.allow('').default(''), - language: language.default('kuery'), + query: query.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: query.allow('').default(''), + }), + language: language.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: language.default('kuery'), + }), output_index, + machine_learning_job_id: machine_learning_job_id.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), saved_id: saved_id.when('type', { is: 'saved_query', then: Joi.required(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts index 4d1b73fb69e5b5..4496a808f68694 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts @@ -35,10 +35,13 @@ import { note, id, version, + anomaly_threshold, + machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ export const patchRulesSchema = Joi.object({ + anomaly_threshold, description, enabled, false_positives, @@ -50,6 +53,7 @@ export const patchRulesSchema = Joi.object({ interval, query: query.allow(''), language, + machine_learning_job_id, output_index, saved_id, timeline_id, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts index 05b85ffab7263a..dd88bd80d57879 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts @@ -67,6 +67,18 @@ export const getBaseResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesS export const getRulesBulkPayload = (): RulesBulkSchema => [getBaseResponsePayload()]; +export const getMlRuleResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesSchema => { + const basePayload = getBaseResponsePayload(anchorDate); + const { filters, index, query, language, ...rest } = basePayload; + + return { + ...rest, + type: 'machine_learning', + anomaly_threshold: 59, + machine_learning_job_id: 'some_machine_learning_job_id', + }; +}; + export const getErrorPayload = ( id: string = '819eded6-e9c8-445b-a647-519aea39e063' ): ErrorSchema => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts index fc1c019ff97b56..1a5ee793a25da6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts @@ -12,8 +12,15 @@ import { getDependents, addSavedId, addTimelineTitle, + addQueryFields, + addMlFields, } from './check_type_dependents'; -import { foldLeftRight, getBaseResponsePayload, getPaths } from './__mocks__/utils'; +import { + foldLeftRight, + getBaseResponsePayload, + getPaths, + getMlRuleResponsePayload, +} from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { exactCheck } from './exact_check'; import { RulesSchema } from './rules_schema'; @@ -375,6 +382,34 @@ describe('check_type_dependents', () => { ]); expect(message.schema).toEqual({}); }); + + test('it validates an ML rule response', () => { + const payload = getMlRuleResponsePayload(); + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getMlRuleResponsePayload(); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it rejects a response with both ML and query properties', () => { + const payload = { + ...getBaseResponsePayload(), + ...getMlRuleResponsePayload(), + }; + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "query,language"']); + expect(message.schema).toEqual({}); + }); }); describe('addSavedId', () => { @@ -402,4 +437,35 @@ describe('check_type_dependents', () => { expect(array.length).toEqual(2); }); }); + + describe('addQueryFields', () => { + test('should return empty array if type is not "query"', () => { + const fields = addQueryFields({ type: 'machine_learning' }); + const expected: t.Mixed[] = []; + expect(fields).toEqual(expected); + }); + + test('should return two fields for a rule of type "query"', () => { + const fields = addQueryFields({ type: 'query' }); + expect(fields.length).toEqual(2); + }); + + test('should return two fields for a rule of type "saved_query"', () => { + const fields = addQueryFields({ type: 'saved_query' }); + expect(fields.length).toEqual(2); + }); + }); + + describe('addMlFields', () => { + test('should return empty array if type is not "machine_learning"', () => { + const fields = addMlFields({ type: 'query' }); + const expected: t.Mixed[] = []; + expect(fields).toEqual(expected); + }); + + test('should return two fields for a rule of type "machine_learning"', () => { + const fields = addMlFields({ type: 'machine_learning' }); + expect(fields.length).toEqual(2); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts index 09142c8568b2d5..b5a01e3e5c6df6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts @@ -35,12 +35,38 @@ export const addTimelineTitle = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mi } }; +export const addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.type === 'query' || typeAndTimelineOnly.type === 'saved_query') { + return [ + t.exact(t.type({ query: dependentRulesSchema.props.query })), + t.exact(t.type({ language: dependentRulesSchema.props.language })), + ]; + } else { + return []; + } +}; + +export const addMlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.type === 'machine_learning') { + return [ + t.exact(t.type({ anomaly_threshold: dependentRulesSchema.props.anomaly_threshold })), + t.exact( + t.type({ machine_learning_job_id: dependentRulesSchema.props.machine_learning_job_id }) + ), + ]; + } else { + return []; + } +}; + export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => { const dependents: t.Mixed[] = [ t.exact(requiredRulesSchema), t.exact(partialRulesSchema), ...addSavedId(typeAndTimelineOnly), ...addTimelineTitle(typeAndTimelineOnly), + ...addQueryFields(typeAndTimelineOnly), + ...addMlFields(typeAndTimelineOnly), ]; if (dependents.length > 1) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts index 945b5651be0662..28b588a86aeb0d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts @@ -11,6 +11,7 @@ import { Either } from 'fp-ts/lib/Either'; import { checkTypeDependents } from './check_type_dependents'; import { + anomaly_threshold, description, enabled, false_positives, @@ -24,6 +25,7 @@ import { name, output_index, max_signals, + machine_learning_job_id, query, references, severity, @@ -65,12 +67,10 @@ export const requiredRulesSchema = t.type({ immutable, interval, rule_id, - language, output_index, max_signals, risk_score, name, - query, references, severity, updated_by, @@ -91,12 +91,20 @@ export type RequiredRulesSchema = t.TypeOf; * check_type_dependents file for whichever REST flow it is going through. */ export const dependentRulesSchema = t.partial({ + // query fields + language, + query, + // when type = saved_query, saved_is is required saved_id, // These two are required together or not at all. timeline_id, timeline_title, + + // ML fields + anomaly_threshold, + machine_learning_job_id, }); /** diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts index 16f6c0fd6b8b44..072e3f5beefe2c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts @@ -45,6 +45,8 @@ export const output_index = t.string; export const saved_id = t.string; export const timeline_id = t.string; export const timeline_title = t.string; +export const anomaly_threshold = PositiveInteger; +export const machine_learning_job_id = t.string; /** * Note that this is a plain unknown object because we allow the UI @@ -64,7 +66,7 @@ export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run // TODO: Create a regular expression type or custom date math part type here export const to = t.string; -export const type = t.keyof({ query: null, saved_query: null }); +export const type = t.keyof({ machine_learning: null, query: null, saved_query: null }); export const queryFilter = t.string; export const references = t.array(t.string); export const per_page = PositiveInteger; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts index 2ba9ec7f832537..ad7050e8dd65c6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts @@ -7,6 +7,10 @@ import Joi from 'joi'; /* eslint-disable @typescript-eslint/camelcase */ +export const anomaly_threshold = Joi.number() + .integer() + .greater(-1) + .less(101); export const description = Joi.string(); export const enabled = Joi.boolean(); export const exclude_export_details = Joi.boolean(); @@ -48,7 +52,8 @@ export const risk_score = Joi.number() export const severity = Joi.string().valid('low', 'medium', 'high', 'critical'); export const status = Joi.string().valid('open', 'closed'); export const to = Joi.string(); -export const type = Joi.string().valid('query', 'saved_query'); +export const type = Joi.string().valid('query', 'saved_query', 'machine_learning'); +export const machine_learning_job_id = Joi.string(); export const queryFilter = Joi.string(); export const references = Joi.array() .items(Joi.string()) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts index a72105142d2875..f7a53385200dfa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts @@ -35,6 +35,8 @@ import { id, note, version, + anomaly_threshold, + machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -48,6 +50,11 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; * - id is on here because you can pass in an id to update using it instead of rule_id. */ export const updateRulesSchema = Joi.object({ + anomaly_threshold: anomaly_threshold.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), description: description.required(), enabled: enabled.default(true), id, @@ -57,8 +64,21 @@ export const updateRulesSchema = Joi.object({ rule_id, index, interval: interval.default('5m'), - query: query.allow('').default(''), - language: language.default('kuery'), + query: query.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: query.allow('').default(''), + }), + language: language.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: language.default('kuery'), + }), + machine_learning_job_id: machine_learning_job_id.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), output_index, saved_id: saved_id.when('type', { is: 'saved_query', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index ea87950a59b789..1b4c06fb5d8287 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -12,6 +12,7 @@ import { addTags } from './add_tags'; export const createRules = ({ alertsClient, actionsClient, // TODO: Use this actionsClient once we have actions such as email, etc... + anomalyThreshold, description, enabled, falsePositives, @@ -22,6 +23,7 @@ export const createRules = ({ timelineId, timelineTitle, meta, + machineLearningJobId, filters, ruleId, immutable, @@ -47,6 +49,7 @@ export const createRules = ({ alertTypeId: SIGNALS_ID, consumer: APP_ID, params: { + anomalyThreshold, description, ruleId, index, @@ -60,6 +63,7 @@ export const createRules = ({ timelineId, timelineTitle, meta, + machineLearningJobId, filters, maxSignals, riskScore, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index 3b5ef57d3dcb6d..dc71ae3678f2ee 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -18,6 +18,7 @@ export const installPrepackagedRules = ( ): Array> => rules.reduce>>((acc, rule) => { const { + anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, @@ -25,6 +26,7 @@ export const installPrepackagedRules = ( immutable, query, language, + machine_learning_job_id: machineLearningJobId, saved_id: savedId, timeline_id: timelineId, timeline_title: timelineTitle, @@ -50,6 +52,7 @@ export const installPrepackagedRules = ( createRules({ alertsClient, actionsClient, + anomalyThreshold, description, enabled, falsePositives, @@ -57,6 +60,7 @@ export const installPrepackagedRules = ( immutable, query, language, + machineLearningJobId, outputIndex, savedId, timelineId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 922651edc40823..010f6b2ee98ff2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -29,6 +29,8 @@ export const sampleRuleAlertParams = ( riskScore: riskScore ? riskScore : 50, maxSignals: maxSignals ? maxSignals : 10000, note: '', + anomalyThreshold: undefined, + machineLearningJobId: undefined, filters: undefined, savedId: undefined, timelineId: undefined, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts index 9baf6a55b7f483..a9ccda2efe99c9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts @@ -65,5 +65,7 @@ export const buildRule = ({ version: ruleParams.version, created_at: createdAt, updated_at: updatedAt, + machine_learning_job_id: ruleParams.machineLearningJobId, + anomaly_threshold: ruleParams.anomalyThreshold, }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.test.ts new file mode 100644 index 00000000000000..d9fb9d4bbabde9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { transformAnomalyFieldsToEcs } from './bulk_create_ml_signals'; + +const buildMockAnomaly = () => ({ + job_id: 'rare_process_by_host_linux_ecs', + result_type: 'record', + probability: 0.03406145177566593, + multi_bucket_impact: -0.0, + record_score: 10.86784984522809, + initial_record_score: 10.86784984522809, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1584482400000, + by_field_name: 'process.name', + by_field_value: 'gzip', + partition_field_name: 'host.name', + partition_field_value: 'rock01', + function: 'rare', + function_description: 'rare', + typical: [0.03406145177566593], + actual: [1.0], + influencers: [ + { + influencer_field_name: 'user.name', + influencer_field_values: ['root'], + }, + { + influencer_field_name: 'process.pid', + influencer_field_values: ['123'], + }, + { + influencer_field_name: 'host.name', + influencer_field_values: ['rock01'], + }, + ], + 'process.name': ['gzip'], + 'process.pid': ['123'], + 'user.name': ['root'], + 'host.name': ['rock01'], +}); + +describe('transformAnomalyFieldsToEcs', () => { + it('adds a @timestamp field based on timestamp', () => { + const anomaly = buildMockAnomaly(); + const result = transformAnomalyFieldsToEcs(anomaly); + const expectedTime = '2020-03-17T22:00:00.000Z'; + + expect(result['@timestamp']).toEqual(expectedTime); + }); + + it('deletes dotted influencer fields', () => { + const anomaly = buildMockAnomaly(); + const result = transformAnomalyFieldsToEcs(anomaly); + + const ecsKeys = Object.keys(result); + expect(ecsKeys).not.toContain('user.name'); + expect(ecsKeys).not.toContain('process.pid'); + expect(ecsKeys).not.toContain('host.name'); + }); + + it('deletes dotted entity field', () => { + const anomaly = buildMockAnomaly(); + const result = transformAnomalyFieldsToEcs(anomaly); + + const ecsKeys = Object.keys(result); + expect(ecsKeys).not.toContain('process.name'); + }); + + it('creates nested influencer fields', () => { + const anomaly = buildMockAnomaly(); + const result = transformAnomalyFieldsToEcs(anomaly); + + expect(result.process.pid).toEqual(['123']); + expect(result.user.name).toEqual(['root']); + expect(result.host.name).toEqual(['rock01']); + }); + + it('creates nested entity field', () => { + const anomaly = buildMockAnomaly(); + const result = transformAnomalyFieldsToEcs(anomaly); + + expect(result.process.name).toEqual(['gzip']); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts new file mode 100644 index 00000000000000..1ab34f26d4b705 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { flow, set, omit } from 'lodash/fp'; +import { SearchResponse } from 'elasticsearch'; + +import { Logger } from '../../../../../../../../src/core/server'; +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { RuleTypeParams } from '../types'; +import { singleBulkCreate } from './single_bulk_create'; +import { AnomalyResults, Anomaly } from '../../machine_learning'; + +interface BulkCreateMlSignalsParams { + someResult: AnomalyResults; + ruleParams: RuleTypeParams; + services: AlertServices; + logger: Logger; + id: string; + signalsIndex: string; + name: string; + createdAt: string; + createdBy: string; + updatedAt: string; + updatedBy: string; + interval: string; + enabled: boolean; + tags: string[]; +} + +interface EcsAnomaly extends Anomaly { + '@timestamp': string; +} + +export const transformAnomalyFieldsToEcs = (anomaly: Anomaly): EcsAnomaly => { + const { + by_field_name: entityName, + by_field_value: entityValue, + influencers, + timestamp, + } = anomaly; + let errantFields = (influencers ?? []).map(influencer => ({ + name: influencer.influencer_field_name, + value: influencer.influencer_field_values, + })); + + if (entityName && entityValue) { + errantFields = [...errantFields, { name: entityName, value: [entityValue] }]; + } + + const omitDottedFields = omit(errantFields.map(field => field.name)); + const setNestedFields = errantFields.map(field => set(field.name, field.value)); + const setTimestamp = set('@timestamp', new Date(timestamp).toISOString()); + + return flow(omitDottedFields, setNestedFields, setTimestamp)(anomaly); +}; + +const transformAnomalyResultsToEcs = (results: AnomalyResults): SearchResponse => { + const transformedHits = results.hits.hits.map(({ _source, ...rest }) => ({ + ...rest, + _source: transformAnomalyFieldsToEcs(_source), + })); + + return { + ...results, + hits: { + ...results.hits, + hits: transformedHits, + }, + }; +}; + +export const bulkCreateMlSignals = async (params: BulkCreateMlSignalsParams) => { + const anomalyResults = params.someResult; + const ecsResults = transformAnomalyResultsToEcs(anomalyResults); + + return singleBulkCreate({ ...params, someResult: ecsResults }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts new file mode 100644 index 00000000000000..b7f752e6ba5e0e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts @@ -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 dateMath from '@elastic/datemath'; + +import { AlertServices } from '../../../../../../../plugins/alerting/server'; + +import { getAnomalies } from '../../machine_learning'; + +export const findMlSignals = async ( + jobId: string, + anomalyThreshold: number, + from: string, + to: string, + callCluster: AlertServices['callCluster'] +) => { + const params = { + jobIds: [jobId], + threshold: anomalyThreshold, + earliestMs: dateMath.parse(from)?.valueOf() ?? 0, + latestMs: dateMath.parse(to)?.valueOf() ?? 0, + }; + const relevantAnomalies = await getAnomalies(params, callCluster); + + return relevantAnomalies; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts index 9c3e15de7ce90e..82a50222dc3514 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts @@ -107,6 +107,11 @@ export const getFilter = async ({ throw new BadRequestError('savedId parameter should be defined'); } } + case 'machine_learning': { + throw new BadRequestError( + 'Unsupported Rule of type "machine_learning" supplied to getFilter' + ); + } } return assertUnreachable(type); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index bf7a97a29aef32..09daae84853819 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -26,8 +26,10 @@ export const mockService = { }; describe('searchAfterAndBulkCreate', () => { + let inputIndexPattern: string[] = []; beforeEach(() => { jest.clearAllMocks(); + inputIndexPattern = ['auditbeat-*']; }); test('if successful with empty search results', async () => { @@ -38,6 +40,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -93,6 +96,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -119,6 +123,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -152,6 +157,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -185,6 +191,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -220,6 +227,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -255,6 +263,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -292,6 +301,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index 1cfd2f812a195c..f54ad67af4a48a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -17,6 +17,7 @@ interface SearchAfterAndBulkCreateParams { services: AlertServices; logger: Logger; id: string; + inputIndexPattern: string[]; signalsIndex: string; name: string; createdAt: string; @@ -37,6 +38,7 @@ export const searchAfterAndBulkCreate = async ({ services, logger, id, + inputIndexPattern, signalsIndex, filter, name, @@ -77,7 +79,7 @@ export const searchAfterAndBulkCreate = async ({ // If the total number of hits for the overall search result is greater than // maxSignals, default to requesting a total of maxSignals, otherwise use the // totalHits in the response from the searchAfter query. - const maxTotalHitsSize = totalHits >= ruleParams.maxSignals ? ruleParams.maxSignals : totalHits; + const maxTotalHitsSize = Math.min(totalHits, ruleParams.maxSignals); // number of docs in the current search result let hitsSize = someResult.hits.hits.length; @@ -98,7 +100,9 @@ export const searchAfterAndBulkCreate = async ({ logger.debug(`sortIds: ${sortIds}`); const searchAfterResult: SignalSearchResponse = await singleSearchAfter({ searchAfterSortId: sortId, - ruleParams, + index: inputIndexPattern, + from: ruleParams.from, + to: ruleParams.to, services, logger, filter, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts index adbb5fa6189579..7b0546f56dd157 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts @@ -14,6 +14,7 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; */ export const signalParamsSchema = () => schema.object({ + anomalyThreshold: schema.maybe(schema.number()), description: schema.string(), note: schema.nullable(schema.string()), falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), @@ -27,6 +28,7 @@ export const signalParamsSchema = () => timelineId: schema.nullable(schema.string()), timelineTitle: schema.nullable(schema.string()), meta: schema.nullable(schema.object({}, { unknowns: 'allow' })), + machineLearningJobId: schema.maybe(schema.string()), query: schema.nullable(schema.string()), filters: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index e3ea121a9ebb11..7a4dcf68e0ca94 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -20,6 +20,8 @@ import { writeGapErrorToSavedObject } from './write_gap_error_to_saved_object'; import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects'; import { getCurrentStatusSavedObject } from './get_current_status_saved_object'; import { writeCurrentStatusSucceeded } from './write_current_status_succeeded'; +import { findMlSignals } from './find_ml_signals'; +import { bulkCreateMlSignals } from './bulk_create_ml_signals'; export const signalRulesAlertType = ({ logger, @@ -38,11 +40,13 @@ export const signalRulesAlertType = ({ }, async executor({ previousStartedAt, alertId, services, params }) { const { + anomalyThreshold, from, ruleId, index, filters, language, + machineLearningJobId, outputIndex, savedId, query, @@ -86,33 +90,70 @@ export const signalRulesAlertType = ({ ruleStatusSavedObjects, name, }); - // set searchAfter page size to be the lesser of default page size or maxSignals. - const searchAfterSize = - DEFAULT_SEARCH_AFTER_PAGE_SIZE <= params.maxSignals - ? DEFAULT_SEARCH_AFTER_PAGE_SIZE - : params.maxSignals; + + const searchAfterSize = Math.min(params.maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); + let creationSucceeded = false; + try { - const inputIndex = await getInputIndex(services, version, index); - const esFilter = await getFilter({ - type, - filters, - language, - query, - savedId, - services, - index: inputIndex, - }); + if (type === 'machine_learning') { + if (machineLearningJobId == null || anomalyThreshold == null) { + throw new Error( + `Attempted to execute machine learning rule, but it is missing job id and/or anomaly threshold for rule id: "${ruleId}", name: "${name}", signals index: "${outputIndex}", job id: "${machineLearningJobId}", anomaly threshold: "${anomalyThreshold}"` + ); + } - const noReIndex = buildEventsSearchQuery({ - index: inputIndex, - from, - to, - filter: esFilter, - size: searchAfterSize, - searchAfterSortId: undefined, - }); + const anomalyResults = await findMlSignals( + machineLearningJobId, + anomalyThreshold, + from, + to, + services.callCluster + ); + + const anomalyCount = anomalyResults.hits.hits.length; + if (anomalyCount) { + logger.info( + `Found ${anomalyCount} signals from ML anomalies for signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}", pushing signals to index "${outputIndex}"` + ); + } + + creationSucceeded = await bulkCreateMlSignals({ + someResult: anomalyResults, + ruleParams: params, + services, + logger, + id: alertId, + signalsIndex: outputIndex, + name, + createdBy, + createdAt, + updatedBy, + updatedAt, + interval, + enabled, + tags, + }); + } else { + const inputIndex = await getInputIndex(services, version, index); + const esFilter = await getFilter({ + type, + filters, + language, + query, + savedId, + services, + index: inputIndex, + }); + + const noReIndex = buildEventsSearchQuery({ + index: inputIndex, + from, + to, + filter: esFilter, + size: searchAfterSize, + searchAfterSortId: undefined, + }); - try { logger.debug( `Starting signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"` ); @@ -130,12 +171,13 @@ export const signalRulesAlertType = ({ ); } - const bulkIndexResult = await searchAfterAndBulkCreate({ + creationSucceeded = await searchAfterAndBulkCreate({ someResult: noReIndexResult, ruleParams: params, services, logger, id: alertId, + inputIndexPattern: inputIndex, signalsIndex: outputIndex, filter: esFilter, name, @@ -148,46 +190,35 @@ export const signalRulesAlertType = ({ pageSize: searchAfterSize, tags, }); + } - if (bulkIndexResult) { - logger.debug( - `Finished signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"` - ); - await writeCurrentStatusSucceeded({ - services, - currentStatusSavedObject, - }); - } else { - await writeSignalRuleExceptionToSavedObject({ - name, - alertId, - currentStatusSavedObject, - logger, - message: `Bulk Indexing signals failed. Check logs for further details \nRule name: "${name}"\nid: "${alertId}"\nrule_id: "${ruleId}"\n`, - services, - ruleStatusSavedObjects, - ruleId: ruleId ?? '(unknown rule id)', - }); - } - } catch (err) { + if (creationSucceeded) { + logger.debug( + `Finished signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}", output_index: "${outputIndex}"` + ); + await writeCurrentStatusSucceeded({ + services, + currentStatusSavedObject, + }); + } else { await writeSignalRuleExceptionToSavedObject({ name, alertId, currentStatusSavedObject, logger, - message: err?.message ?? '(no error message given)', + message: `Bulk Indexing signals failed. Check logs for further details Rule name: "${name}" id: "${alertId}" rule_id: "${ruleId}" output_index: "${outputIndex}"`, services, ruleStatusSavedObjects, ruleId: ruleId ?? '(unknown rule id)', }); } - } catch (exception) { + } catch (error) { await writeSignalRuleExceptionToSavedObject({ name, alertId, currentStatusSavedObject, logger, - message: exception?.message ?? '(no error message given)', + message: error?.message ?? '(no error message given)', services, ruleStatusSavedObjects, ruleId: ruleId ?? '(unknown rule id)', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts index a5d1f66d3089e8..1685c6518def33 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts @@ -6,7 +6,6 @@ import { savedObjectsClientMock } from 'src/core/server/mocks'; import { - sampleRuleAlertParams, sampleDocSearchResultsNoSortId, mockLogger, sampleDocSearchResultsWithSortId, @@ -26,12 +25,13 @@ describe('singleSearchAfter', () => { test('if singleSearchAfter works without a given sort id', async () => { let searchAfterSortId; - const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValue(sampleDocSearchResultsNoSortId); await expect( singleSearchAfter({ searchAfterSortId, - ruleParams: sampleParams, + index: [], + from: 'now-360s', + to: 'now', services: mockService, logger: mockLogger, pageSize: 1, @@ -41,11 +41,12 @@ describe('singleSearchAfter', () => { }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId); const searchAfterResult = await singleSearchAfter({ searchAfterSortId, - ruleParams: sampleParams, + index: [], + from: 'now-360s', + to: 'now', services: mockService, logger: mockLogger, pageSize: 1, @@ -55,14 +56,15 @@ describe('singleSearchAfter', () => { }); test('if singleSearchAfter throws error', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockImplementation(async () => { throw Error('Fake Error'); }); await expect( singleSearchAfter({ searchAfterSortId, - ruleParams: sampleParams, + index: [], + from: 'now-360s', + to: 'now', services: mockService, logger: mockLogger, pageSize: 1, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts index a0e7047ad1cd6e..bb12b5a802f8fe 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts @@ -5,14 +5,15 @@ */ import { AlertServices } from '../../../../../../../plugins/alerting/server'; -import { RuleTypeParams } from '../types'; import { Logger } from '../../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { buildEventsSearchQuery } from './build_events_query'; interface SingleSearchAfterParams { searchAfterSortId: string | undefined; - ruleParams: RuleTypeParams; + index: string[]; + from: string; + to: string; services: AlertServices; logger: Logger; pageSize: number; @@ -22,7 +23,9 @@ interface SingleSearchAfterParams { // utilize search_after for paging results into bulk. export const singleSearchAfter = async ({ searchAfterSortId, - ruleParams, + index, + from, + to, services, filter, logger, @@ -33,9 +36,9 @@ export const singleSearchAfter = async ({ } try { const searchAfterQuery = buildEventsSearchQuery({ - index: ruleParams.index, - from: ruleParams.from, - to: ruleParams.to, + index, + from, + to, filter, size: pageSize, searchAfterSortId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index eaed3f2ead3a50..1ee3d4f0eb8e4f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -104,7 +104,7 @@ export interface GetResponse { } export type SignalSearchResponse = SearchResponse; -export type SignalSourceHit = SignalSearchResponse['hits']['hits'][0]; +export type SignalSourceHit = SignalSearchResponse['hits']['hits'][number]; export type RuleExecutorOptions = Omit & { params: RuleAlertParams & { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index fa43ac1debb92f..f77924aafadf86 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -22,7 +22,10 @@ export interface ThreatParams { technique: IMitreAttack[]; } +export type RuleType = 'query' | 'saved_query' | 'machine_learning'; + export interface RuleAlertParams { + anomalyThreshold: number | undefined; description: string; note: string | undefined | null; enabled: boolean; @@ -30,11 +33,12 @@ export interface RuleAlertParams { filters: PartialFilter[] | undefined | null; from: string; immutable: boolean; - index: string[]; + index: string[] | undefined | null; interval: string; ruleId: string | undefined | null; language: string | undefined | null; maxSignals: number; + machineLearningJobId: string | undefined; riskScore: number; outputIndex: string; name: string; @@ -48,7 +52,7 @@ export interface RuleAlertParams { timelineId: string | undefined | null; timelineTitle: string | undefined | null; threat: ThreatParams[] | undefined | null; - type: 'query' | 'saved_query'; + type: RuleType; version: number; throttle?: string; } @@ -57,10 +61,12 @@ export type RuleTypeParams = Omit & { + anomaly_threshold: RuleAlertParams['anomalyThreshold']; rule_id: RuleAlertParams['ruleId']; false_positives: RuleAlertParams['falsePositives']; saved_id?: RuleAlertParams['savedId']; timeline_id: RuleAlertParams['timelineId']; timeline_title: RuleAlertParams['timelineTitle']; max_signals: RuleAlertParams['maxSignals']; + machine_learning_job_id: RuleAlertParams['machineLearningJobId']; risk_score: RuleAlertParams['riskScore']; output_index: RuleAlertParams['outputIndex']; created_at: string; diff --git a/x-pack/legacy/plugins/siem/server/lib/machine_learning/index.ts b/x-pack/legacy/plugins/siem/server/lib/machine_learning/index.ts new file mode 100644 index 00000000000000..aa83df15f68d48 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/machine_learning/index.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; + +import { AlertServices } from '../../../../../../plugins/alerting/server'; +import { AnomalyRecordDoc as Anomaly } from '../../../../../../plugins/ml/common/types/anomalies'; + +export { Anomaly }; +export type AnomalyResults = SearchResponse; + +export interface AnomaliesSearchParams { + jobIds: string[]; + threshold: number; + earliestMs: number; + latestMs: number; + maxRecords?: number; +} + +export const getAnomalies = async ( + params: AnomaliesSearchParams, + callCluster: AlertServices['callCluster'] +): Promise => { + const boolCriteria = buildCriteria(params); + + return callCluster('search', { + index: '.ml-anomalies-*', + size: params.maxRecords || 100, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + analyze_wildcard: false, + }, + }, + { + bool: { + must: boolCriteria, + }, + }, + ], + }, + }, + sort: [{ record_score: { order: 'desc' } }], + }, + }); +}; + +const buildCriteria = (params: AnomaliesSearchParams): object[] => { + const { earliestMs, jobIds, latestMs, threshold } = params; + const jobIdsFilterable = jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*'); + + const boolCriteria: object[] = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + range: { + record_score: { + gte: threshold, + }, + }, + }, + ]; + + if (jobIdsFilterable) { + const jobIdFilter = jobIds.map(jobId => `job_id:${jobId}`).join(' OR '); + + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilter, + }, + }); + } + + return boolCriteria; +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index d6a238e5b09401..91088acb7a51ce 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -18,6 +18,8 @@ import { getSimpleRuleWithoutRuleId, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, + getSimpleMlRule, + getSimpleMlRuleOutput, } from './utils'; // eslint-disable-next-line import/no-default-export @@ -63,6 +65,20 @@ export default ({ getService }: FtrProviderContext) => { expect(bodyToCompare).to.eql(getSimpleRuleOutput()); }); + it('should create a single rule without an input index', async () => { + const { index, ...payload } = getSimpleRule(); + const { index: _index, ...expected } = getSimpleRuleOutput(); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(payload) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(expected); + }); + it('should create a single rule without a rule_id', async () => { const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) @@ -74,6 +90,17 @@ export default ({ getService }: FtrProviderContext) => { expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); }); + it('should create a single Machine Learning rule', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleMlRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleMlRuleOutput()); + }); + it('should cause a 409 conflict if we attempt to create the same rule_id twice', async () => { await supertest .post(DETECTION_ENGINE_RULES_URL) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts index 1570124cdb92b6..8847a2fdb21af2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts @@ -49,10 +49,26 @@ export const getSimpleRule = (ruleId = 'rule-1'): Partial = risk_score: 1, rule_id: ruleId, severity: 'high', + index: ['auditbeat-*'], type: 'query', query: 'user.name: root or user.name: admin', }); +/** + * This is a representative ML rule payload as expected by the server + * @param ruleId + */ +export const getSimpleMlRule = (ruleId = 'rule-1'): Partial => ({ + name: 'Simple ML Rule', + description: 'Simple Machine Learning Rule', + anomaly_threshold: 44, + risk_score: 1, + rule_id: ruleId, + severity: 'high', + machine_learning_job_id: 'some_job_id', + type: 'machine_learning', +}); + export const getSignalStatus = () => ({ aggs: { statuses: { terms: { field: 'signal.status', size: 10 } } }, }); @@ -118,6 +134,7 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial => { + const rule = getSimpleRuleOutput(ruleId); + const { query, language, index, ...rest } = rule; + + return { + ...rest, + name: 'Simple ML Rule', + description: 'Simple Machine Learning Rule', + anomaly_threshold: 44, + machine_learning_job_id: 'some_job_id', + type: 'machine_learning', + }; +}; + /** * Remove all alerts from the .kibana index * @param es The ElasticSearch handle From 8c5071939b8bf3338128539b23f7b9db43c8798c Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Wed, 18 Mar 2020 20:28:34 -0400 Subject: [PATCH 22/42] [Ingest] Agent Config Details - Data sources list ui (#60429) * refactor `use_details_uri` hook and introduce `useAgentConfigLink` * Refactor structure for datasources view * Sync up table columns * Added row actions to Datasources list * Datasources table filters * Support deleting datasource action * Added PackageIcon to datasources list --- .../ingest_manager/common/services/routes.ts | 4 + .../components/package_icon.tsx | 80 +++++ .../hooks/use_request/datasource.ts | 12 + .../danger_eui_context_menu_item.tsx | 12 + .../components/datasource_delete_provider.tsx | 237 +++++++++++++ .../components/table_row_actions.tsx | 38 +++ .../datasources/datasources_table.tsx | 310 ++++++++++++++++++ .../components/datasources/index.tsx | 20 ++ .../components/datasources/no_datasources.tsx | 44 +++ .../components/datasources_table.tsx | 131 -------- .../details_page/components/index.ts | 2 +- .../details_page/hooks/use_details_uri.ts | 60 ++-- .../agent_config/details_page/index.tsx | 96 ++---- .../sections/agent_config/list_page/index.tsx | 133 +++----- 14 files changed, 872 insertions(+), 307 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/package_icon.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/danger_eui_context_menu_item.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/table_row_actions.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/index.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/no_datasources.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources_table.tsx diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index 7ad3944096a5f0..01b3b1983486cb 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -56,6 +56,10 @@ export const datasourceRouteService = { getUpdatePath: (datasourceId: string) => { return DATASOURCE_API_ROUTES.UPDATE_PATTERN.replace('{datasourceId}', datasourceId); }, + + getDeletePath: () => { + return DATASOURCE_API_ROUTES.DELETE_PATTERN; + }, }; export const agentConfigRouteService = { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/package_icon.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/package_icon.tsx new file mode 100644 index 00000000000000..1ac222802e7d49 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/package_icon.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useEffect, useMemo, useState } from 'react'; +import { ICON_TYPES, EuiIcon, EuiIconProps } from '@elastic/eui'; +import { PackageInfo, PackageListItem } from '../../../../common/types/models'; +import { useLinks } from '../sections/epm/hooks'; +import { epmRouteService } from '../../../../common/services'; +import { sendRequest } from '../hooks/use_request'; +import { GetInfoResponse } from '../types'; +type Package = PackageInfo | PackageListItem; + +const CACHED_ICONS = new Map(); + +export const PackageIcon: React.FunctionComponent<{ + packageName: string; + version?: string; + icons?: Package['icons']; +} & Omit> = ({ packageName, version, icons, ...euiIconProps }) => { + const iconType = usePackageIcon(packageName, version, icons); + return ; +}; + +const usePackageIcon = (packageName: string, version?: string, icons?: Package['icons']) => { + const { toImage } = useLinks(); + const [iconType, setIconType] = useState(''); + const pkgKey = `${packageName}-${version ?? ''}`; + + // Generates an icon path or Eui Icon name based on an icon list from the package + // or by using the package name against logo icons from Eui + const fromInput = useMemo(() => { + return (iconList?: Package['icons']) => { + const svgIcons = iconList?.filter(iconDef => iconDef.type === 'image/svg+xml'); + const localIconSrc = Array.isArray(svgIcons) && svgIcons[0]?.src; + if (localIconSrc) { + CACHED_ICONS.set(pkgKey, toImage(localIconSrc)); + setIconType(CACHED_ICONS.get(pkgKey) as string); + return; + } + + const euiLogoIcon = ICON_TYPES.find(key => key.toLowerCase() === `logo${packageName}`); + if (euiLogoIcon) { + CACHED_ICONS.set(pkgKey, euiLogoIcon); + setIconType(euiLogoIcon); + return; + } + + CACHED_ICONS.set(pkgKey, 'package'); + setIconType('package'); + }; + }, [packageName, pkgKey, toImage]); + + useEffect(() => { + if (CACHED_ICONS.has(pkgKey)) { + setIconType(CACHED_ICONS.get(pkgKey) as string); + return; + } + + // Use API to see if package has icons defined + if (!icons && version !== undefined) { + fromPackageInfo(pkgKey) + .catch(() => undefined) // ignore API errors + .then(fromInput); + } else { + fromInput(icons); + } + }, [icons, toImage, packageName, version, fromInput, pkgKey]); + + return iconType; +}; + +const fromPackageInfo = async (pkgKey: string) => { + const { data } = await sendRequest({ + path: epmRouteService.getInfoPath(pkgKey), + method: 'get', + }); + return data?.response?.icons; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts index 60fbb9f0d2afa5..d0072f03559932 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts @@ -6,6 +6,10 @@ import { sendRequest } from './use_request'; import { datasourceRouteService } from '../../services'; import { CreateDatasourceRequest, CreateDatasourceResponse } from '../../types'; +import { + DeleteDatasourcesRequest, + DeleteDatasourcesResponse, +} from '../../../../../common/types/rest_spec'; export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => { return sendRequest({ @@ -14,3 +18,11 @@ export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => { body: JSON.stringify(body), }); }; + +export const sendDeleteDatasource = (body: DeleteDatasourcesRequest['body']) => { + return sendRequest({ + path: datasourceRouteService.getDeletePath(), + method: 'post', + body: JSON.stringify(body), + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/danger_eui_context_menu_item.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/danger_eui_context_menu_item.tsx new file mode 100644 index 00000000000000..bc4d28ba0e313c --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/danger_eui_context_menu_item.tsx @@ -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 styled from 'styled-components'; +import { EuiContextMenuItem } from '@elastic/eui'; + +export const DangerEuiContextMenuItem = styled(EuiContextMenuItem)` + color: ${props => props.theme.eui.textColors.danger}; +`; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx new file mode 100644 index 00000000000000..089b0631c20904 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx @@ -0,0 +1,237 @@ +/* + * 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, useMemo, useRef, useState } from 'react'; +import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useCore, sendRequest, sendDeleteDatasource, useConfig } from '../../../hooks'; +import { AGENT_API_ROUTES } from '../../../../../../common/constants'; +import { AgentConfig } from '../../../../../../common/types/models'; + +interface Props { + agentConfig: AgentConfig; + children: (deleteDatasourcePrompt: DeleteAgentConfigDatasourcePrompt) => React.ReactElement; +} + +export type DeleteAgentConfigDatasourcePrompt = ( + datasourcesToDelete: string[], + onSuccess?: OnSuccessCallback +) => void; + +type OnSuccessCallback = (datasourcesDeleted: string[]) => void; + +export const DatasourceDeleteProvider: React.FunctionComponent = ({ + agentConfig, + children, +}) => { + const { notifications } = useCore(); + const { + fleet: { enabled: isFleetEnabled }, + } = useConfig(); + const [datasources, setDatasources] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isLoadingAgentsCount, setIsLoadingAgentsCount] = useState(false); + const [agentsCount, setAgentsCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const onSuccessCallback = useRef(null); + + const fetchAgentsCount = useMemo( + () => async () => { + if (isLoadingAgentsCount || !isFleetEnabled) { + return; + } + setIsLoadingAgentsCount(true); + const { data } = await sendRequest<{ total: number }>({ + path: AGENT_API_ROUTES.LIST_PATTERN, + method: 'get', + query: { + page: 1, + perPage: 1, + kuery: `agents.config_id : ${agentConfig.id}`, + }, + }); + setAgentsCount(data?.total || 0); + setIsLoadingAgentsCount(false); + }, + [agentConfig.id, isFleetEnabled, isLoadingAgentsCount] + ); + + const deleteDatasourcesPrompt = useMemo( + (): DeleteAgentConfigDatasourcePrompt => (datasourcesToDelete, onSuccess = () => undefined) => { + if (!Array.isArray(datasourcesToDelete) || datasourcesToDelete.length === 0) { + throw new Error('No datasources specified for deletion'); + } + setIsModalOpen(true); + setDatasources(datasourcesToDelete); + fetchAgentsCount(); + onSuccessCallback.current = onSuccess; + }, + [fetchAgentsCount] + ); + + const closeModal = useMemo( + () => () => { + setDatasources([]); + setIsLoading(false); + setIsLoadingAgentsCount(false); + setIsModalOpen(false); + }, + [] + ); + + const deleteDatasources = useMemo( + () => async () => { + setIsLoading(true); + + try { + const { data } = await sendDeleteDatasource({ datasourceIds: datasources }); + const successfulResults = data?.filter(result => result.success) || []; + const failedResults = data?.filter(result => !result.success) || []; + + if (successfulResults.length) { + const hasMultipleSuccesses = successfulResults.length > 1; + const successMessage = hasMultipleSuccesses + ? i18n.translate( + 'xpack.ingestManager.deleteDatasource.successMultipleNotificationTitle', + { + defaultMessage: 'Deleted {count} data sources', + values: { count: successfulResults.length }, + } + ) + : i18n.translate( + 'xpack.ingestManager.deleteDatasource.successSingleNotificationTitle', + { + defaultMessage: "Deleted data source '{id}'", + values: { id: successfulResults[0].id }, + } + ); + notifications.toasts.addSuccess(successMessage); + } + + if (failedResults.length) { + const hasMultipleFailures = failedResults.length > 1; + const failureMessage = hasMultipleFailures + ? i18n.translate( + 'xpack.ingestManager.deleteDatasource.failureMultipleNotificationTitle', + { + defaultMessage: 'Error deleting {count} data sources', + values: { count: failedResults.length }, + } + ) + : i18n.translate( + 'xpack.ingestManager.deleteDatasource.failureSingleNotificationTitle', + { + defaultMessage: "Error deleting data source '{id}'", + values: { id: failedResults[0].id }, + } + ); + notifications.toasts.addDanger(failureMessage); + } + + if (onSuccessCallback.current) { + onSuccessCallback.current(successfulResults.map(result => result.id)); + } + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('xpack.ingestManager.deleteDatasource.fatalErrorNotificationTitle', { + defaultMessage: 'Error deleting data source', + }) + ); + } + closeModal(); + }, + [closeModal, datasources, notifications.toasts] + ); + + const renderModal = () => { + if (!isModalOpen) { + return null; + } + + return ( + + + } + onCancel={closeModal} + onConfirm={deleteDatasources} + cancelButtonText={ + + } + confirmButtonText={ + isLoading || isLoadingAgentsCount ? ( + + ) : ( + + ) + } + buttonColor="danger" + confirmButtonDisabled={isLoading || isLoadingAgentsCount} + > + {isLoadingAgentsCount ? ( + + ) : agentsCount ? ( + <> + + } + > + {agentConfig.name}, + }} + /> + + + + ) : null} + {!isLoadingAgentsCount && ( + + )} + + + ); + }; + + return ( + + {children(deleteDatasourcesPrompt)} + {renderModal()} + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/table_row_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/table_row_actions.tsx new file mode 100644 index 00000000000000..2f9a11ef767048 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/table_row_actions.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useState } from 'react'; +import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiContextMenuPanelProps } from '@elastic/eui/src/components/context_menu/context_menu_panel'; + +export const TableRowActions = React.memo<{ items: EuiContextMenuPanelProps['items'] }>( + ({ items }) => { + const [isOpen, setIsOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); + const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); + + return ( + + } + isOpen={isOpen} + closePopover={handleCloseMenu} + > + + + ); + } +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx new file mode 100644 index 00000000000000..49285707457e13 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx @@ -0,0 +1,310 @@ +/* + * 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, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiInMemoryTable, + EuiInMemoryTableProps, + EuiBadge, + EuiTextColor, + EuiContextMenuItem, + EuiButton, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { AgentConfig, Datasource } from '../../../../../types'; +import { TableRowActions } from '../../../components/table_row_actions'; +import { DangerEuiContextMenuItem } from '../../../components/danger_eui_context_menu_item'; +import { useCapabilities } from '../../../../../hooks'; +import { useAgentConfigLink } from '../../hooks/use_details_uri'; +import { DatasourceDeleteProvider } from '../../../components/datasource_delete_provider'; +import { useConfigRefresh } from '../../hooks/use_config'; +import { PackageIcon } from '../../../../../components/package_icon'; + +interface InMemoryDatasource extends Datasource { + streams: { total: number; enabled: number }; + inputTypes: string[]; + packageName?: string; + packageTitle?: string; + packageVersion?: string; +} + +interface Props { + datasources: Datasource[]; + config: AgentConfig; + // Pass through props to InMemoryTable + loading?: EuiInMemoryTableProps['loading']; + message?: EuiInMemoryTableProps['message']; +} + +interface FilterOption { + name: string; + value: string; +} + +const stringSortAscending = (a: string, b: string): number => a.localeCompare(b); +const toFilterOption = (value: string): FilterOption => ({ name: value, value }); + +export const DatasourcesTable: React.FunctionComponent = ({ + datasources: originalDatasources, + config, + ...rest +}) => { + const hasWriteCapabilities = useCapabilities().write; + const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id }); + const refreshConfig = useConfigRefresh(); + + // With the datasources provided on input, generate the list of datasources + // used in the InMemoryTable (flattens some values for search) as well as + // the list of options that will be used in the filters dropdowns + const [datasources, namespaces, inputTypes] = useMemo((): [ + InMemoryDatasource[], + FilterOption[], + FilterOption[] + ] => { + const namespacesValues: string[] = []; + const inputTypesValues: string[] = []; + const mappedDatasources = originalDatasources.map(datasource => { + if (datasource.namespace && !namespacesValues.includes(datasource.namespace)) { + namespacesValues.push(datasource.namespace); + } + + const dsInputTypes: string[] = []; + const streams = datasource.inputs.reduce( + (streamSummary, input) => { + if (!inputTypesValues.includes(input.type)) { + inputTypesValues.push(input.type); + } + if (!dsInputTypes.includes(input.type)) { + dsInputTypes.push(input.type); + } + + streamSummary.total += input.streams.length; + streamSummary.enabled += input.enabled + ? input.streams.filter(stream => stream.enabled).length + : 0; + + return streamSummary; + }, + { total: 0, enabled: 0 } + ); + + dsInputTypes.sort(stringSortAscending); + + return { + ...datasource, + streams, + inputTypes: dsInputTypes, + packageName: datasource.package?.name ?? '', + packageTitle: datasource.package?.title ?? '', + packageVersion: datasource.package?.version ?? '', + }; + }); + + namespacesValues.sort(stringSortAscending); + inputTypesValues.sort(stringSortAscending); + + return [ + mappedDatasources, + namespacesValues.map(toFilterOption), + inputTypesValues.map(toFilterOption), + ]; + }, [originalDatasources]); + + const columns = useMemo( + (): EuiInMemoryTableProps['columns'] => [ + { + field: 'name', + name: i18n.translate('xpack.ingestManager.configDetails.datasourcesTable.nameColumnTitle', { + defaultMessage: 'Data source', + }), + }, + { + field: 'description', + name: i18n.translate( + 'xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle', + { + defaultMessage: 'Description', + } + ), + truncateText: true, + }, + { + field: 'packageTitle', + name: i18n.translate( + 'xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle', + { + defaultMessage: 'Package', + } + ), + render(packageTitle: string, datasource: InMemoryDatasource) { + return ( + + {datasource.package && ( + + + + )} + {packageTitle} + + ); + }, + }, + { + field: 'namespace', + name: i18n.translate( + 'xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle', + { + defaultMessage: 'Namespace', + } + ), + render: (namespace: InMemoryDatasource['namespace']) => { + return namespace ? {namespace} : ''; + }, + }, + { + field: 'streams', + name: i18n.translate( + 'xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle', + { + defaultMessage: 'Streams', + } + ), + render: (streams: InMemoryDatasource['streams']) => { + return ( + <> + {streams.enabled} +  / {streams.total} + + ); + }, + }, + { + name: i18n.translate( + 'xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle', + { + defaultMessage: 'Actions', + } + ), + actions: [ + { + render: (datasource: InMemoryDatasource) => ( + {}} + key="datasourceView" + > + + , + // FIXME: implement Edit datasource action + {}} + key="datasourceEdit" + > + + , + // FIXME: implement Copy datasource action + {}} key="datasourceCopy"> + + , + + {deleteDatasourcePrompt => { + return ( + { + deleteDatasourcePrompt([datasource.id], refreshConfig); + }} + > + + + ); + }} + , + ]} + /> + ), + }, + ], + }, + ], + [config, hasWriteCapabilities, refreshConfig] + ); + + return ( + + itemId="id" + items={datasources} + columns={columns} + sorting={{ + sort: { + field: 'name', + direction: 'asc', + }, + }} + {...rest} + search={{ + toolsRight: [ + + + , + ], + box: { + incremental: true, + schema: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'namespace', + name: 'Namespace', + options: namespaces, + multiSelect: 'or', + }, + { + type: 'field_value_selection', + field: 'inputTypes', + name: 'Input types', + options: inputTypes, + multiSelect: 'or', + }, + ], + }} + isSelectable={false} + /> + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/index.tsx new file mode 100644 index 00000000000000..346ccde45f3f06 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/index.tsx @@ -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 React, { memo } from 'react'; +import { AgentConfig, Datasource } from '../../../../../../../../common/types/models'; +import { NoDatasources } from './no_datasources'; +import { DatasourcesTable } from './datasources_table'; + +export const ConfigDatasourcesView = memo<{ config: AgentConfig }>(({ config }) => { + if (config.datasources.length === 0) { + return ; + } + + return ( + + ); +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/no_datasources.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/no_datasources.tsx new file mode 100644 index 00000000000000..2d8f73e67cf968 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/no_datasources.tsx @@ -0,0 +1,44 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import React, { memo } from 'react'; +import { useCapabilities } from '../../../../../hooks'; +import { useAgentConfigLink } from '../../hooks/use_details_uri'; + +export const NoDatasources = memo<{ configId: string }>(({ configId }) => { + const hasWriteCapabilities = useCapabilities().write; + const addDatasourceLink = useAgentConfigLink('add-datasource', { configId }); + + return ( + + + + } + body={ + + } + actions={ + + + + } + /> + ); +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources_table.tsx deleted file mode 100644 index 3c982747e1d226..00000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources_table.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiInMemoryTable, EuiInMemoryTableProps, EuiBadge } from '@elastic/eui'; -import { Datasource } from '../../../../types'; - -type DatasourceWithConfig = Datasource & { configs?: string[] }; - -interface InMemoryDatasource { - id: string; - name: string; - streams: number; - packageName?: string; - packageTitle?: string; - packageVersion?: string; - configs: number; -} - -interface Props { - datasources?: DatasourceWithConfig[]; - withConfigsCount?: boolean; - loading?: EuiInMemoryTableProps['loading']; - message?: EuiInMemoryTableProps['message']; - search?: EuiInMemoryTableProps['search']; - selection?: EuiInMemoryTableProps['selection']; - isSelectable?: EuiInMemoryTableProps['isSelectable']; -} - -export const DatasourcesTable: React.FunctionComponent = ( - { datasources: originalDatasources, withConfigsCount, ...rest } = { - datasources: [], - withConfigsCount: false, - } -) => { - // Flatten some values so that they can be searched via in-memory table search - const datasources = - originalDatasources?.map(({ id, name, inputs, package: datasourcePackage, configs }) => ({ - id, - name, - streams: inputs.reduce( - (streamsCount, input) => - streamsCount + - (input.enabled ? input.streams.filter(stream => stream.enabled).length : 0), - 0 - ), - packageName: datasourcePackage?.name, - packageTitle: datasourcePackage?.title, - packageVersion: datasourcePackage?.version, - configs: configs?.length || 0, - })) || []; - - const columns: EuiInMemoryTableProps['columns'] = [ - { - field: 'name', - name: i18n.translate('xpack.ingestManager.configDetails.datasourcesTable.nameColumnTitle', { - defaultMessage: 'Name', - }), - }, - { - field: 'packageTitle', - name: i18n.translate( - 'xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle', - { - defaultMessage: 'Package', - } - ), - }, - { - field: 'packageVersion', - name: i18n.translate( - 'xpack.ingestManager.configDetails.datasourcesTable.packageVersionColumnTitle', - { - defaultMessage: 'Version', - } - ), - }, - { - field: 'streams', - name: i18n.translate( - 'xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle', - { - defaultMessage: 'Streams', - } - ), - }, - ]; - - if (withConfigsCount) { - columns.splice(columns.length - 1, 0, { - field: 'configs', - name: i18n.translate( - 'xpack.ingestManager.configDetails.datasourcesTable.configsColumnTitle', - { - defaultMessage: 'Configs', - } - ), - render: (configs: number) => { - return configs === 0 ? ( - - - - ) : ( - configs - ); - }, - }); - } - - return ( - - itemId="id" - items={datasources || ([] as InMemoryDatasource[])} - columns={columns} - sorting={{ - sort: { - field: 'name', - direction: 'asc', - }, - }} - {...rest} - /> - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts index 51834268ffa5b1..918b361a60d790 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts @@ -3,6 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export { DatasourcesTable } from './datasources_table'; +export { DatasourcesTable } from './datasources/datasources_table'; export { DonutChart } from './donut_chart'; export { EditConfigFlyout } from './edit_config'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts index df43d8e908e41f..9332ce3e0f909c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts @@ -4,29 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useMemo } from 'react'; import { generatePath } from 'react-router-dom'; import { useLink } from '../../../../hooks'; import { AGENT_CONFIG_PATH } from '../../../../constants'; import { DETAILS_ROUTER_PATH, DETAILS_ROUTER_SUB_PATH } from '../constants'; -export const useDetailsUri = (configId: string) => { - const BASE_URI = useLink(''); - return useMemo(() => { - const AGENT_CONFIG_DETAILS = `${BASE_URI}${generatePath(DETAILS_ROUTER_PATH, { configId })}`; +type AgentConfigUriArgs = + | ['list'] + | ['details', { configId: string }] + | ['details-yaml', { configId: string }] + | ['details-settings', { configId: string }] + | ['datasource', { configId: string; datasourceId: string }] + | ['add-datasource', { configId: string }]; + +/** + * Returns a Uri that starts at the Agent Config Route path (`/configs/`). + * These are good for use when needing to use React Router's redirect or + * `history.push(routePath)`. + * @param args + */ +export const useAgentConfigUri = (...args: AgentConfigUriArgs) => { + switch (args[0]) { + case 'list': + return AGENT_CONFIG_PATH; + case 'details': + return generatePath(DETAILS_ROUTER_PATH, args[1]); + case 'details-yaml': + return `${generatePath(DETAILS_ROUTER_SUB_PATH, { ...args[1], tabId: 'yaml' })}`; + case 'details-settings': + return `${generatePath(DETAILS_ROUTER_SUB_PATH, { ...args[1], tabId: 'settings' })}`; + case 'add-datasource': + return `${generatePath(DETAILS_ROUTER_SUB_PATH, { ...args[1], tabId: 'add-datasource' })}`; + case 'datasource': + const [, options] = args; + return `${generatePath(DETAILS_ROUTER_PATH, options)}?datasourceId=${options.datasourceId}`; + } + return '/'; +}; - return { - ADD_DATASOURCE: `${AGENT_CONFIG_DETAILS}/add-datasource`, - AGENT_CONFIG_LIST: `${BASE_URI}${AGENT_CONFIG_PATH}`, - AGENT_CONFIG_DETAILS, - AGENT_CONFIG_DETAILS_YAML: `${BASE_URI}${generatePath(DETAILS_ROUTER_SUB_PATH, { - configId, - tabId: 'yaml', - })}`, - AGENT_CONFIG_DETAILS_SETTINGS: `${BASE_URI}${generatePath(DETAILS_ROUTER_SUB_PATH, { - configId, - tabId: 'settings', - })}`, - }; - }, [BASE_URI, configId]); +/** + * Returns a full Link that includes Kibana basepath (ex. `/app/ingestManager#/configs`). + * These are good for use in `href` properties + * @param args + */ +export const useAgentConfigLink = (...args: AgentConfigUriArgs) => { + const BASE_URI = useLink(''); + const AGENT_CONFIG_ROUTE = useAgentConfigUri(...args); + return `${BASE_URI}${AGENT_CONFIG_ROUTE}`; }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx index 6f72977cb333f1..efb96f64592549 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx @@ -14,9 +14,7 @@ import { EuiText, EuiSpacer, EuiTitle, - EuiButton, EuiButtonEmpty, - EuiEmptyPrompt, EuiI18nNumber, EuiDescriptionList, EuiDescriptionListTitle, @@ -24,15 +22,15 @@ import { } from '@elastic/eui'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import styled from 'styled-components'; -import { useCapabilities, useGetOneAgentConfig } from '../../../hooks'; -import { Datasource } from '../../../types'; +import { useGetOneAgentConfig } from '../../../hooks'; import { Loading } from '../../../components'; import { WithHeaderLayout } from '../../../layouts'; import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; -import { DatasourcesTable, EditConfigFlyout } from './components'; +import { EditConfigFlyout } from './components'; import { LinkedAgentCount } from '../components'; -import { useDetailsUri } from './hooks/use_details_uri'; +import { useAgentConfigLink } from './hooks/use_details_uri'; import { DETAILS_ROUTER_PATH, DETAILS_ROUTER_SUB_PATH } from './constants'; +import { ConfigDatasourcesView } from './components/datasources'; const Divider = styled.div` width: 0; @@ -57,7 +55,6 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { const { params: { configId, tabId = '' }, } = useRouteMatch<{ configId: string; tabId?: string }>(); - const hasWriteCapabilites = useCapabilities().write; const agentConfigRequest = useGetOneAgentConfig(configId); const agentConfig = agentConfigRequest.data ? agentConfigRequest.data.item : null; const { isLoading, error, sendRequest: refreshAgentConfig } = agentConfigRequest; @@ -65,7 +62,12 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { const agentStatusRequest = useGetAgentStatus(configId); const { refreshAgentStatus } = agentStatusRequest; const agentStatus = agentStatusRequest.data?.results; - const URI = useDetailsUri(configId); + + // Links + const configListLink = useAgentConfigLink('list'); + const configDetailsLink = useAgentConfigLink('details', { configId }); + const configDetailsYamlLink = useAgentConfigLink('details-yaml', { configId }); + const configDetailsSettingsLink = useAgentConfigLink('details-settings', { configId }); // Flyout states const [isEditConfigFlyoutOpen, setIsEditConfigFlyoutOpen] = useState(false); @@ -83,12 +85,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => {
- + { ), - [URI.AGENT_CONFIG_LIST, agentConfig, configId] + [configListLink, agentConfig, configId] ); const headerRightContent = useMemo( @@ -134,7 +131,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { label: i18n.translate('xpack.ingestManager.configDetails.summary.revision', { defaultMessage: 'Revision', }), - content: '999', // FIXME: implement version - see: https://github.com/elastic/kibana/issues/56750 + content: agentConfig?.revision ?? 0, }, { isDivider: true }, { @@ -201,15 +198,15 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.configDetails.subTabs.datasouces', { defaultMessage: 'Data sources', }), - href: URI.AGENT_CONFIG_DETAILS, - isSelected: tabId === '', + href: configDetailsLink, + isSelected: tabId === '' || tabId === 'datasources', }, { id: 'yaml', name: i18n.translate('xpack.ingestManager.configDetails.subTabs.yamlFile', { defaultMessage: 'YAML File', }), - href: URI.AGENT_CONFIG_DETAILS_YAML, + href: configDetailsYamlLink, isSelected: tabId === 'yaml', }, { @@ -217,16 +214,11 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.configDetails.subTabs.settings', { defaultMessage: 'Settings', }), - href: URI.AGENT_CONFIG_DETAILS_SETTINGS, + href: configDetailsSettingsLink, isSelected: tabId === 'settings', }, ]; - }, [ - URI.AGENT_CONFIG_DETAILS, - URI.AGENT_CONFIG_DETAILS_SETTINGS, - URI.AGENT_CONFIG_DETAILS_YAML, - tabId, - ]); + }, [configDetailsLink, configDetailsSettingsLink, configDetailsYamlLink, tabId]); if (redirectToAgentConfigList) { return ; @@ -304,57 +296,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { { - return ( - - - - } - actions={ - - - - } - /> - ) : null - } - search={{ - toolsRight: [ - - - , - ], - box: { - incremental: true, - schema: true, - }, - }} - isSelectable={false} - /> - ); + return ; }} /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 31c86d0a4cbf0b..0498e814440c71 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -3,7 +3,7 @@ * 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, { CSSProperties, useCallback, useMemo, useState } from 'react'; +import React, { CSSProperties, memo, useCallback, useMemo, useState } from 'react'; import { EuiSpacer, EuiText, @@ -16,14 +16,10 @@ import { EuiTableActionsColumnType, EuiTableFieldDataColumnType, EuiTextColor, - EuiPopover, - EuiContextMenuPanel, EuiContextMenuItem, - EuiButtonIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; -import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; import { AgentConfig } from '../../../types'; import { @@ -44,6 +40,9 @@ import { AgentConfigDeleteProvider } from '../components'; import { CreateAgentConfigFlyout } from './components'; import { SearchBar } from '../../../components/search_bar'; import { LinkedAgentCount } from '../components'; +import { useAgentConfigLink } from '../details_page/hooks/use_details_uri'; +import { TableRowActions } from '../components/table_row_actions'; +import { DangerEuiContextMenuItem } from '../components/danger_eui_context_menu_item'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ overflow: 'hidden', @@ -82,83 +81,59 @@ const AgentConfigListPageLayout: React.FunctionComponent = ({ children }) => ( ); -const DangerEuiContextMenuItem = styled(EuiContextMenuItem)` - color: ${props => props.theme.eui.textColors.danger}; -`; - -const RowActions = React.memo<{ config: AgentConfig; onDelete: () => void }>( +const ConfigRowActions = memo<{ config: AgentConfig; onDelete: () => void }>( ({ config, onDelete }) => { - const hasWriteCapabilites = useCapabilities().write; - const DETAILS_URI = useLink(`${AGENT_CONFIG_DETAILS_PATH}${config.id}`); - const ADD_DATASOURCE_URI = `${DETAILS_URI}/add-datasource`; - - const [isOpen, setIsOpen] = useState(false); - const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); - const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); + const hasWriteCapabilities = useCapabilities().write; + const detailsLink = useAgentConfigLink('details', { configId: config.id }); + const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id }); return ( - - } - isOpen={isOpen} - closePopover={handleCloseMenu} - > - - - , + + + , - - - , + + + , - - - , + + + , - - {deleteAgentConfigsPrompt => { - return ( - deleteAgentConfigsPrompt([config.id], onDelete)} - > - - - ); - }} - , - ]} - /> - + + {deleteAgentConfigsPrompt => { + return ( + deleteAgentConfigsPrompt([config.id], onDelete)} + > + + + ); + }} + , + ]} + /> ); } ); @@ -287,7 +262,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { actions: [ { render: (config: AgentConfig) => ( - sendRequest()} /> + sendRequest()} /> ), }, ], @@ -330,10 +305,10 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { /> } - actions={hasWriteCapabilites ?? createAgentConfigButton} + actions={createAgentConfigButton} /> ), - [hasWriteCapabilites, createAgentConfigButton] + [createAgentConfigButton] ); return ( From 650943df79ab7f0a511cdb156b2890c84fd75dcc Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 18 Mar 2020 17:41:50 -0700 Subject: [PATCH 23/42] skip flaky suite (#60471) --- x-pack/test/api_integration/apis/fleet/agents/acks.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/fleet/agents/acks.ts b/x-pack/test/api_integration/apis/fleet/agents/acks.ts index a2eba2c23c39d6..db925813b90c41 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/acks.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/acks.ts @@ -18,7 +18,8 @@ export default function(providerContext: FtrProviderContext) { const supertest = getSupertestWithoutAuth(providerContext); let apiKey: { id: string; api_key: string }; - describe('fleet_agents_acks', () => { + // FLAKY: https://github.com/elastic/kibana/issues/60471 + describe.skip('fleet_agents_acks', () => { before(async () => { await esArchiver.loadIfNeeded('fleet/agents'); From cc8f7c43dd0a6230552da1d43355309b105bf1b6 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 18 Mar 2020 17:45:04 -0700 Subject: [PATCH 24/42] upgrade execa to get stdout/stderr in error messages (#60537) * upgrade execa to get stdout/stderr in error messages * rebuild kbn/pm Co-authored-by: spalger Co-authored-by: Elastic Machine --- package.json | 2 +- packages/kbn-dev-utils/package.json | 2 +- packages/kbn-es/package.json | 2 +- packages/kbn-plugin-generator/package.json | 2 +- packages/kbn-plugin-helpers/package.json | 2 +- packages/kbn-pm/dist/index.js | 2876 ++++++++++---------- packages/kbn-pm/package.json | 2 +- packages/kbn-storybook/package.json | 1 - x-pack/package.json | 2 +- yarn.lock | 35 +- 10 files changed, 1453 insertions(+), 1473 deletions(-) diff --git a/package.json b/package.json index aa9c8f6c401608..e7143826d5720a 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,7 @@ "elastic-apm-node": "^3.2.0", "elasticsearch": "^16.5.0", "elasticsearch-browser": "^16.5.0", - "execa": "^3.2.0", + "execa": "^4.0.0", "expiry-js": "0.1.7", "fast-deep-equal": "^3.1.1", "file-loader": "4.2.0", diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index bea153d0a672bc..ee9f349f490517 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -12,7 +12,7 @@ "dependencies": { "chalk": "^2.4.2", "dedent": "^0.7.0", - "execa": "^3.2.0", + "execa": "^4.0.0", "exit-hook": "^2.2.0", "getopts": "^2.2.5", "load-json-file": "^6.2.0", diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index f9d7bffed1e226..8b964d83999040 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -11,7 +11,7 @@ "chalk": "^2.4.2", "dedent": "^0.7.0", "del": "^5.1.0", - "execa": "^3.2.0", + "execa": "^4.0.0", "getopts": "^2.2.4", "glob": "^7.1.2", "node-fetch": "^2.6.0", diff --git a/packages/kbn-plugin-generator/package.json b/packages/kbn-plugin-generator/package.json index ac98a0e675fb18..b3b1eff41e4b5f 100644 --- a/packages/kbn-plugin-generator/package.json +++ b/packages/kbn-plugin-generator/package.json @@ -6,7 +6,7 @@ "dependencies": { "chalk": "^2.4.2", "dedent": "^0.7.0", - "execa": "^3.2.0", + "execa": "^4.0.0", "getopts": "^2.2.4", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index 3b358c03b8053a..c348aa43789d17 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -17,7 +17,7 @@ "argv-split": "^2.0.1", "commander": "^3.0.0", "del": "^5.1.0", - "execa": "^3.2.0", + "execa": "^4.0.0", "globby": "^8.0.1", "gulp-babel": "^8.0.0", "gulp-rename": "1.4.0", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 9fab74ea47a879..285a780ae053ed 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,21 +94,21 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(705); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(704); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(501); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(500); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjects", function() { return _utils_projects__WEBPACK_IMPORTED_MODULE_2__["getProjects"]; }); -/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(516); +/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(515); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Project", function() { return _utils_project__WEBPACK_IMPORTED_MODULE_3__["Project"]; }); -/* harmony import */ var _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(578); +/* harmony import */ var _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(577); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "copyWorkspacePackages", function() { return _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__["copyWorkspacePackages"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(579); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(578); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -152,7 +152,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(17); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(689); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(688); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(34); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -2506,9 +2506,9 @@ module.exports = require("path"); __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(18); -/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(586); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(686); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(687); +/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(585); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(685); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(686); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -2549,10 +2549,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_link_project_executables__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(19); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(34); -/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(500); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(501); -/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(580); -/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(585); +/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(499); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(500); +/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(579); +/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(584); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -4490,10 +4490,10 @@ const tslib_1 = __webpack_require__(36); var proc_runner_1 = __webpack_require__(37); exports.withProcRunner = proc_runner_1.withProcRunner; exports.ProcRunner = proc_runner_1.ProcRunner; -tslib_1.__exportStar(__webpack_require__(415), exports); -var serializers_1 = __webpack_require__(420); +tslib_1.__exportStar(__webpack_require__(414), exports); +var serializers_1 = __webpack_require__(419); exports.createAbsolutePathSerializer = serializers_1.createAbsolutePathSerializer; -var certs_1 = __webpack_require__(445); +var certs_1 = __webpack_require__(444); exports.CA_CERT_PATH = certs_1.CA_CERT_PATH; exports.ES_KEY_PATH = certs_1.ES_KEY_PATH; exports.ES_CERT_PATH = certs_1.ES_CERT_PATH; @@ -4505,17 +4505,17 @@ exports.KBN_KEY_PATH = certs_1.KBN_KEY_PATH; exports.KBN_CERT_PATH = certs_1.KBN_CERT_PATH; exports.KBN_P12_PATH = certs_1.KBN_P12_PATH; exports.KBN_P12_PASSWORD = certs_1.KBN_P12_PASSWORD; -var run_1 = __webpack_require__(446); +var run_1 = __webpack_require__(445); exports.run = run_1.run; exports.createFailError = run_1.createFailError; exports.createFlagError = run_1.createFlagError; exports.combineErrors = run_1.combineErrors; exports.isFailError = run_1.isFailError; -var repo_root_1 = __webpack_require__(422); +var repo_root_1 = __webpack_require__(421); exports.REPO_ROOT = repo_root_1.REPO_ROOT; -var kbn_client_1 = __webpack_require__(451); +var kbn_client_1 = __webpack_require__(450); exports.KbnClient = kbn_client_1.KbnClient; -tslib_1.__exportStar(__webpack_require__(493), exports); +tslib_1.__exportStar(__webpack_require__(492), exports); /***/ }), @@ -32149,13 +32149,13 @@ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); const execa_1 = tslib_1.__importDefault(__webpack_require__(351)); const fs_1 = __webpack_require__(23); -const Rx = tslib_1.__importStar(__webpack_require__(392)); +const Rx = tslib_1.__importStar(__webpack_require__(391)); const operators_1 = __webpack_require__(169); const chalk_1 = tslib_1.__importDefault(__webpack_require__(2)); -const tree_kill_1 = tslib_1.__importDefault(__webpack_require__(412)); +const tree_kill_1 = tslib_1.__importDefault(__webpack_require__(411)); const util_1 = __webpack_require__(29); const treeKillAsync = util_1.promisify((...args) => tree_kill_1.default(...args)); -const observe_lines_1 = __webpack_require__(413); +const observe_lines_1 = __webpack_require__(412); const errors_1 = __webpack_require__(349); const SECOND = 1000; const STOP_TIMEOUT = 30 * SECOND; @@ -32271,9 +32271,9 @@ const onetime = __webpack_require__(368); const makeError = __webpack_require__(370); const normalizeStdio = __webpack_require__(375); const {spawnedKill, spawnedCancel, setupTimeout, setExitHandler} = __webpack_require__(376); -const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = __webpack_require__(381); -const {mergePromise, getSpawnedPromise} = __webpack_require__(390); -const {joinCommand, parseCommand} = __webpack_require__(391); +const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = __webpack_require__(380); +const {mergePromise, getSpawnedPromise} = __webpack_require__(389); +const {joinCommand, parseCommand} = __webpack_require__(390); const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; @@ -32305,8 +32305,8 @@ const handleArgs = (file, args, options = {}) => { reject: true, cleanup: true, all: false, - ...options, - windowsHide: true + windowsHide: true, + ...options }; options.env = getEnv(options); @@ -33430,15 +33430,18 @@ const makeError = ({ const errorCode = error && error.code; const prefix = getErrorPrefix({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}); - const message = `Command ${prefix}: ${command}`; + const execaMessage = `Command ${prefix}: ${command}`; + const shortMessage = error instanceof Error ? `${execaMessage}\n${error.message}` : execaMessage; + const message = [shortMessage, stderr, stdout].filter(Boolean).join('\n'); if (error instanceof Error) { error.originalMessage = error.message; - error.message = `${message}\n${error.message}`; + error.message = message; } else { error = new Error(message); } + error.shortMessage = shortMessage; error.command = command; error.exitCode = exitCode; error.signal = signal; @@ -33954,7 +33957,6 @@ module.exports.node = opts => { const os = __webpack_require__(11); const onExit = __webpack_require__(377); -const pFinally = __webpack_require__(380); const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; @@ -33971,9 +33973,17 @@ const setKillTimeout = (kill, signal, options, killResult) => { } const timeout = getForceKillAfterTimeout(options); - setTimeout(() => { + const t = setTimeout(() => { kill('SIGKILL'); - }, timeout).unref(); + }, timeout); + + // Guarded because there's no `.unref()` when `execa` is used in the renderer + // process in Electron. This cannot be tested since we don't run tests in + // Electron. + // istanbul ignore else + if (t.unref) { + t.unref(); + } }; const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => { @@ -34028,7 +34038,7 @@ const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise }, timeout); }); - const safeSpawnedPromise = pFinally(spawnedPromise, () => { + const safeSpawnedPromise = spawnedPromise.finally(() => { clearTimeout(timeoutId); }); @@ -34036,7 +34046,7 @@ const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise }; // `cleanup` option handling -const setExitHandler = (spawned, {cleanup, detached}, timedPromise) => { +const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => { if (!cleanup || detached) { return timedPromise; } @@ -34045,8 +34055,9 @@ const setExitHandler = (spawned, {cleanup, detached}, timedPromise) => { spawned.kill(); }); - // TODO: Use native "finally" syntax when targeting Node.js 10 - return pFinally(timedPromise, removeExitHandler); + return timedPromise.finally(() => { + removeExitHandler(); + }); }; module.exports = { @@ -34291,33 +34302,9 @@ module.exports = require("events"); "use strict"; - -module.exports = async ( - promise, - onFinally = (() => {}) -) => { - let value; - try { - value = await promise; - } catch (error) { - await onFinally(); - throw error; - } - - await onFinally(); - return value; -}; - - -/***/ }), -/* 381 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -const isStream = __webpack_require__(382); -const getStream = __webpack_require__(383); -const mergeStream = __webpack_require__(389); +const isStream = __webpack_require__(381); +const getStream = __webpack_require__(382); +const mergeStream = __webpack_require__(388); // `input` option const handleInput = (spawned, input) => { @@ -34414,7 +34401,7 @@ module.exports = { /***/ }), -/* 382 */ +/* 381 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34450,13 +34437,13 @@ module.exports = isStream; /***/ }), -/* 383 */ +/* 382 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pump = __webpack_require__(384); -const bufferStream = __webpack_require__(388); +const pump = __webpack_require__(383); +const bufferStream = __webpack_require__(387); class MaxBufferError extends Error { constructor() { @@ -34515,11 +34502,11 @@ module.exports.MaxBufferError = MaxBufferError; /***/ }), -/* 384 */ +/* 383 */ /***/ (function(module, exports, __webpack_require__) { -var once = __webpack_require__(385) -var eos = __webpack_require__(387) +var once = __webpack_require__(384) +var eos = __webpack_require__(386) var fs = __webpack_require__(23) // we only need fs to get the ReadStream and WriteStream prototypes var noop = function () {} @@ -34603,10 +34590,10 @@ module.exports = pump /***/ }), -/* 385 */ +/* 384 */ /***/ (function(module, exports, __webpack_require__) { -var wrappy = __webpack_require__(386) +var wrappy = __webpack_require__(385) module.exports = wrappy(once) module.exports.strict = wrappy(onceStrict) @@ -34651,7 +34638,7 @@ function onceStrict (fn) { /***/ }), -/* 386 */ +/* 385 */ /***/ (function(module, exports) { // Returns a wrapper function that returns a wrapped callback @@ -34690,10 +34677,10 @@ function wrappy (fn, cb) { /***/ }), -/* 387 */ +/* 386 */ /***/ (function(module, exports, __webpack_require__) { -var once = __webpack_require__(385); +var once = __webpack_require__(384); var noop = function() {}; @@ -34783,7 +34770,7 @@ module.exports = eos; /***/ }), -/* 388 */ +/* 387 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34842,7 +34829,7 @@ module.exports = options => { /***/ }), -/* 389 */ +/* 388 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34890,7 +34877,7 @@ module.exports = function (/*streams...*/) { /***/ }), -/* 390 */ +/* 389 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34913,12 +34900,7 @@ const mergePromiseProperty = (spawned, promise, property) => { const mergePromise = (spawned, promise) => { mergePromiseProperty(spawned, promise, 'then'); mergePromiseProperty(spawned, promise, 'catch'); - - // TODO: Remove the `if`-guard when targeting Node.js 10 - if (Promise.prototype.finally) { - mergePromiseProperty(spawned, promise, 'finally'); - } - + mergePromiseProperty(spawned, promise, 'finally'); return spawned; }; @@ -34949,7 +34931,7 @@ module.exports = { /***/ }), -/* 391 */ +/* 390 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34994,7 +34976,7 @@ module.exports = { /***/ }), -/* 392 */ +/* 391 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -35032,10 +35014,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _internal_scheduler_queue__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(298); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "queueScheduler", function() { return _internal_scheduler_queue__WEBPACK_IMPORTED_MODULE_10__["queue"]; }); -/* harmony import */ var _internal_scheduler_animationFrame__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(393); +/* harmony import */ var _internal_scheduler_animationFrame__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(392); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "animationFrameScheduler", function() { return _internal_scheduler_animationFrame__WEBPACK_IMPORTED_MODULE_11__["animationFrame"]; }); -/* harmony import */ var _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(396); +/* harmony import */ var _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(395); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "VirtualTimeScheduler", function() { return _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__["VirtualTimeScheduler"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "VirtualAction", function() { return _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__["VirtualAction"]; }); @@ -35063,7 +35045,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _internal_util_identity__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(232); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "identity", function() { return _internal_util_identity__WEBPACK_IMPORTED_MODULE_19__["identity"]; }); -/* harmony import */ var _internal_util_isObservable__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(397); +/* harmony import */ var _internal_util_isObservable__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(396); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isObservable", function() { return _internal_util_isObservable__WEBPACK_IMPORTED_MODULE_20__["isObservable"]; }); /* harmony import */ var _internal_util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(250); @@ -35081,10 +35063,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _internal_util_TimeoutError__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(335); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "TimeoutError", function() { return _internal_util_TimeoutError__WEBPACK_IMPORTED_MODULE_25__["TimeoutError"]; }); -/* harmony import */ var _internal_observable_bindCallback__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(398); +/* harmony import */ var _internal_observable_bindCallback__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(397); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bindCallback", function() { return _internal_observable_bindCallback__WEBPACK_IMPORTED_MODULE_26__["bindCallback"]; }); -/* harmony import */ var _internal_observable_bindNodeCallback__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(399); +/* harmony import */ var _internal_observable_bindNodeCallback__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(398); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bindNodeCallback", function() { return _internal_observable_bindNodeCallback__WEBPACK_IMPORTED_MODULE_27__["bindNodeCallback"]; }); /* harmony import */ var _internal_observable_combineLatest__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(214); @@ -35099,49 +35081,49 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _internal_observable_empty__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(242); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "empty", function() { return _internal_observable_empty__WEBPACK_IMPORTED_MODULE_31__["empty"]; }); -/* harmony import */ var _internal_observable_forkJoin__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(400); +/* harmony import */ var _internal_observable_forkJoin__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(399); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "forkJoin", function() { return _internal_observable_forkJoin__WEBPACK_IMPORTED_MODULE_32__["forkJoin"]; }); /* harmony import */ var _internal_observable_from__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(218); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "from", function() { return _internal_observable_from__WEBPACK_IMPORTED_MODULE_33__["from"]; }); -/* harmony import */ var _internal_observable_fromEvent__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(401); +/* harmony import */ var _internal_observable_fromEvent__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(400); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "fromEvent", function() { return _internal_observable_fromEvent__WEBPACK_IMPORTED_MODULE_34__["fromEvent"]; }); -/* harmony import */ var _internal_observable_fromEventPattern__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(402); +/* harmony import */ var _internal_observable_fromEventPattern__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(401); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "fromEventPattern", function() { return _internal_observable_fromEventPattern__WEBPACK_IMPORTED_MODULE_35__["fromEventPattern"]; }); -/* harmony import */ var _internal_observable_generate__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(403); +/* harmony import */ var _internal_observable_generate__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(402); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "generate", function() { return _internal_observable_generate__WEBPACK_IMPORTED_MODULE_36__["generate"]; }); -/* harmony import */ var _internal_observable_iif__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(404); +/* harmony import */ var _internal_observable_iif__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(403); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "iif", function() { return _internal_observable_iif__WEBPACK_IMPORTED_MODULE_37__["iif"]; }); -/* harmony import */ var _internal_observable_interval__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(405); +/* harmony import */ var _internal_observable_interval__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(404); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "interval", function() { return _internal_observable_interval__WEBPACK_IMPORTED_MODULE_38__["interval"]; }); /* harmony import */ var _internal_observable_merge__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(278); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_observable_merge__WEBPACK_IMPORTED_MODULE_39__["merge"]; }); -/* harmony import */ var _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(406); +/* harmony import */ var _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(405); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "never", function() { return _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__["never"]; }); /* harmony import */ var _internal_observable_of__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(227); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "of", function() { return _internal_observable_of__WEBPACK_IMPORTED_MODULE_41__["of"]; }); -/* harmony import */ var _internal_observable_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(407); +/* harmony import */ var _internal_observable_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(406); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_observable_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_42__["onErrorResumeNext"]; }); -/* harmony import */ var _internal_observable_pairs__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(408); +/* harmony import */ var _internal_observable_pairs__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(407); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairs", function() { return _internal_observable_pairs__WEBPACK_IMPORTED_MODULE_43__["pairs"]; }); -/* harmony import */ var _internal_observable_partition__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(409); +/* harmony import */ var _internal_observable_partition__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(408); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_observable_partition__WEBPACK_IMPORTED_MODULE_44__["partition"]; }); /* harmony import */ var _internal_observable_race__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(302); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_observable_race__WEBPACK_IMPORTED_MODULE_45__["race"]; }); -/* harmony import */ var _internal_observable_range__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(410); +/* harmony import */ var _internal_observable_range__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(409); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "range", function() { return _internal_observable_range__WEBPACK_IMPORTED_MODULE_46__["range"]; }); /* harmony import */ var _internal_observable_throwError__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(243); @@ -35150,7 +35132,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _internal_observable_timer__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(204); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timer", function() { return _internal_observable_timer__WEBPACK_IMPORTED_MODULE_48__["timer"]; }); -/* harmony import */ var _internal_observable_using__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(411); +/* harmony import */ var _internal_observable_using__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(410); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "using", function() { return _internal_observable_using__WEBPACK_IMPORTED_MODULE_49__["using"]; }); /* harmony import */ var _internal_observable_zip__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(346); @@ -35226,14 +35208,14 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 393 */ +/* 392 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "animationFrame", function() { return animationFrame; }); -/* harmony import */ var _AnimationFrameAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(394); -/* harmony import */ var _AnimationFrameScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(395); +/* harmony import */ var _AnimationFrameAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(393); +/* harmony import */ var _AnimationFrameScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(394); /** PURE_IMPORTS_START _AnimationFrameAction,_AnimationFrameScheduler PURE_IMPORTS_END */ @@ -35242,7 +35224,7 @@ var animationFrame = /*@__PURE__*/ new _AnimationFrameScheduler__WEBPACK_IMPORTE /***/ }), -/* 394 */ +/* 393 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -35291,7 +35273,7 @@ var AnimationFrameAction = /*@__PURE__*/ (function (_super) { /***/ }), -/* 395 */ +/* 394 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -35335,7 +35317,7 @@ var AnimationFrameScheduler = /*@__PURE__*/ (function (_super) { /***/ }), -/* 396 */ +/* 395 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -35458,7 +35440,7 @@ var VirtualAction = /*@__PURE__*/ (function (_super) { /***/ }), -/* 397 */ +/* 396 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -35474,7 +35456,7 @@ function isObservable(obj) { /***/ }), -/* 398 */ +/* 397 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -35594,7 +35576,7 @@ function dispatchError(state) { /***/ }), -/* 399 */ +/* 398 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -35722,7 +35704,7 @@ function dispatchError(arg) { /***/ }), -/* 400 */ +/* 399 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -35805,7 +35787,7 @@ function forkJoinInternal(sources, keys) { /***/ }), -/* 401 */ +/* 400 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -35881,7 +35863,7 @@ function isEventTarget(sourceObj) { /***/ }), -/* 402 */ +/* 401 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -35926,7 +35908,7 @@ function fromEventPattern(addHandler, removeHandler, resultSelector) { /***/ }), -/* 403 */ +/* 402 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -36063,7 +36045,7 @@ function dispatch(state) { /***/ }), -/* 404 */ +/* 403 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -36087,7 +36069,7 @@ function iif(condition, trueResult, falseResult) { /***/ }), -/* 405 */ +/* 404 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -36127,7 +36109,7 @@ function dispatch(state) { /***/ }), -/* 406 */ +/* 405 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -36147,7 +36129,7 @@ function never() { /***/ }), -/* 407 */ +/* 406 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -36187,7 +36169,7 @@ function onErrorResumeNext() { /***/ }), -/* 408 */ +/* 407 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -36238,7 +36220,7 @@ function dispatch(state) { /***/ }), -/* 409 */ +/* 408 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -36263,7 +36245,7 @@ function partition(source, predicate, thisArg) { /***/ }), -/* 410 */ +/* 409 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -36322,7 +36304,7 @@ function dispatch(state) { /***/ }), -/* 411 */ +/* 410 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -36367,7 +36349,7 @@ function using(resourceFactory, observableFactory) { /***/ }), -/* 412 */ +/* 411 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36492,7 +36474,7 @@ function buildProcessTree (parentPid, tree, pidsToProcess, spawnChildProcessesLi /***/ }), -/* 413 */ +/* 412 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36517,10 +36499,10 @@ function buildProcessTree (parentPid, tree, pidsToProcess, spawnChildProcessesLi */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); -const Rx = tslib_1.__importStar(__webpack_require__(392)); +const Rx = tslib_1.__importStar(__webpack_require__(391)); const operators_1 = __webpack_require__(169); const SEP = /\r?\n/; -const observe_readable_1 = __webpack_require__(414); +const observe_readable_1 = __webpack_require__(413); /** * Creates an Observable from a Readable Stream that: * - splits data from `readable` into lines @@ -36561,7 +36543,7 @@ exports.observeLines = observeLines; /***/ }), -/* 414 */ +/* 413 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36586,7 +36568,7 @@ exports.observeLines = observeLines; */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); -const Rx = tslib_1.__importStar(__webpack_require__(392)); +const Rx = tslib_1.__importStar(__webpack_require__(391)); const operators_1 = __webpack_require__(169); /** * Produces an Observable from a ReadableSteam that: @@ -36600,7 +36582,7 @@ exports.observeReadable = observeReadable; /***/ }), -/* 415 */ +/* 414 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36624,19 +36606,19 @@ exports.observeReadable = observeReadable; * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -var tooling_log_1 = __webpack_require__(416); +var tooling_log_1 = __webpack_require__(415); exports.ToolingLog = tooling_log_1.ToolingLog; -var tooling_log_text_writer_1 = __webpack_require__(417); +var tooling_log_text_writer_1 = __webpack_require__(416); exports.ToolingLogTextWriter = tooling_log_text_writer_1.ToolingLogTextWriter; -var log_levels_1 = __webpack_require__(418); +var log_levels_1 = __webpack_require__(417); exports.pickLevelFromFlags = log_levels_1.pickLevelFromFlags; exports.parseLogLevel = log_levels_1.parseLogLevel; -var tooling_log_collecting_writer_1 = __webpack_require__(419); +var tooling_log_collecting_writer_1 = __webpack_require__(418); exports.ToolingLogCollectingWriter = tooling_log_collecting_writer_1.ToolingLogCollectingWriter; /***/ }), -/* 416 */ +/* 415 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36661,8 +36643,8 @@ exports.ToolingLogCollectingWriter = tooling_log_collecting_writer_1.ToolingLogC */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); -const Rx = tslib_1.__importStar(__webpack_require__(392)); -const tooling_log_text_writer_1 = __webpack_require__(417); +const Rx = tslib_1.__importStar(__webpack_require__(391)); +const tooling_log_text_writer_1 = __webpack_require__(416); class ToolingLog { constructor(writerConfig) { this.identWidth = 0; @@ -36724,7 +36706,7 @@ exports.ToolingLog = ToolingLog; /***/ }), -/* 417 */ +/* 416 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36751,7 +36733,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); const util_1 = __webpack_require__(29); const chalk_1 = tslib_1.__importDefault(__webpack_require__(2)); -const log_levels_1 = __webpack_require__(418); +const log_levels_1 = __webpack_require__(417); const { magentaBright, yellow, red, blue, green, dim } = chalk_1.default; const PREFIX_INDENT = ' '.repeat(6); const MSG_PREFIXES = { @@ -36818,7 +36800,7 @@ exports.ToolingLogTextWriter = ToolingLogTextWriter; /***/ }), -/* 418 */ +/* 417 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36874,7 +36856,7 @@ exports.parseLogLevel = parseLogLevel; /***/ }), -/* 419 */ +/* 418 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36898,7 +36880,7 @@ exports.parseLogLevel = parseLogLevel; * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -const tooling_log_text_writer_1 = __webpack_require__(417); +const tooling_log_text_writer_1 = __webpack_require__(416); class ToolingLogCollectingWriter extends tooling_log_text_writer_1.ToolingLogTextWriter { constructor() { super({ @@ -36917,7 +36899,7 @@ exports.ToolingLogCollectingWriter = ToolingLogCollectingWriter; /***/ }), -/* 420 */ +/* 419 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36941,12 +36923,12 @@ exports.ToolingLogCollectingWriter = ToolingLogCollectingWriter; * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -var absolute_path_serializer_1 = __webpack_require__(421); +var absolute_path_serializer_1 = __webpack_require__(420); exports.createAbsolutePathSerializer = absolute_path_serializer_1.createAbsolutePathSerializer; /***/ }), -/* 421 */ +/* 420 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36970,7 +36952,7 @@ exports.createAbsolutePathSerializer = absolute_path_serializer_1.createAbsolute * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -const repo_root_1 = __webpack_require__(422); +const repo_root_1 = __webpack_require__(421); function createAbsolutePathSerializer(rootPath = repo_root_1.REPO_ROOT) { return { print: (value) => value.replace(rootPath, '').replace(/\\/g, '/'), @@ -36981,7 +36963,7 @@ exports.createAbsolutePathSerializer = createAbsolutePathSerializer; /***/ }), -/* 422 */ +/* 421 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -37008,7 +36990,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); const path_1 = tslib_1.__importDefault(__webpack_require__(16)); const fs_1 = tslib_1.__importDefault(__webpack_require__(23)); -const load_json_file_1 = tslib_1.__importDefault(__webpack_require__(423)); +const load_json_file_1 = tslib_1.__importDefault(__webpack_require__(422)); const isKibanaDir = (dir) => { try { const path = path_1.default.resolve(dir, 'package.json'); @@ -37044,16 +37026,16 @@ exports.REPO_ROOT = cursor; /***/ }), -/* 423 */ +/* 422 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {promisify} = __webpack_require__(29); -const fs = __webpack_require__(424); -const stripBom = __webpack_require__(428); -const parseJson = __webpack_require__(429); +const fs = __webpack_require__(423); +const stripBom = __webpack_require__(427); +const parseJson = __webpack_require__(428); const parse = (data, filePath, options = {}) => { data = stripBom(data); @@ -37070,13 +37052,13 @@ module.exports.sync = (filePath, options) => parse(fs.readFileSync(filePath, 'ut /***/ }), -/* 424 */ +/* 423 */ /***/ (function(module, exports, __webpack_require__) { var fs = __webpack_require__(23) -var polyfills = __webpack_require__(425) -var legacy = __webpack_require__(426) -var clone = __webpack_require__(427) +var polyfills = __webpack_require__(424) +var legacy = __webpack_require__(425) +var clone = __webpack_require__(426) var queue = [] @@ -37355,7 +37337,7 @@ function retry () { /***/ }), -/* 425 */ +/* 424 */ /***/ (function(module, exports, __webpack_require__) { var constants = __webpack_require__(25) @@ -37690,7 +37672,7 @@ function patch (fs) { /***/ }), -/* 426 */ +/* 425 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27).Stream @@ -37814,7 +37796,7 @@ function legacy (fs) { /***/ }), -/* 427 */ +/* 426 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -37840,7 +37822,7 @@ function clone (obj) { /***/ }), -/* 428 */ +/* 427 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -37862,15 +37844,15 @@ module.exports = string => { /***/ }), -/* 429 */ +/* 428 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const errorEx = __webpack_require__(430); -const fallback = __webpack_require__(432); -const {default: LinesAndColumns} = __webpack_require__(433); -const {codeFrameColumns} = __webpack_require__(434); +const errorEx = __webpack_require__(429); +const fallback = __webpack_require__(431); +const {default: LinesAndColumns} = __webpack_require__(432); +const {codeFrameColumns} = __webpack_require__(433); const JSONError = errorEx('JSONError', { fileName: errorEx.append('in %s'), @@ -37919,14 +37901,14 @@ module.exports = (string, reviver, filename) => { /***/ }), -/* 430 */ +/* 429 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var isArrayish = __webpack_require__(431); +var isArrayish = __webpack_require__(430); var errorEx = function errorEx(name, properties) { if (!name || name.constructor !== String) { @@ -38059,7 +38041,7 @@ module.exports = errorEx; /***/ }), -/* 431 */ +/* 430 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38076,7 +38058,7 @@ module.exports = function isArrayish(obj) { /***/ }), -/* 432 */ +/* 431 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38115,7 +38097,7 @@ function parseJson (txt, reviver, context) { /***/ }), -/* 433 */ +/* 432 */ /***/ (function(__webpack_module__, __webpack_exports__, __webpack_require__) { "use strict"; @@ -38179,7 +38161,7 @@ var LinesAndColumns = (function () { /***/ }), -/* 434 */ +/* 433 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38192,7 +38174,7 @@ exports.codeFrameColumns = codeFrameColumns; exports.default = _default; function _highlight() { - const data = _interopRequireWildcard(__webpack_require__(435)); + const data = _interopRequireWildcard(__webpack_require__(434)); _highlight = function () { return data; @@ -38358,7 +38340,7 @@ function _default(rawLines, lineNumber, colNumber, opts = {}) { } /***/ }), -/* 435 */ +/* 434 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38372,7 +38354,7 @@ exports.getChalk = getChalk; exports.default = highlight; function _jsTokens() { - const data = _interopRequireWildcard(__webpack_require__(436)); + const data = _interopRequireWildcard(__webpack_require__(435)); _jsTokens = function () { return data; @@ -38382,7 +38364,7 @@ function _jsTokens() { } function _esutils() { - const data = _interopRequireDefault(__webpack_require__(437)); + const data = _interopRequireDefault(__webpack_require__(436)); _esutils = function () { return data; @@ -38392,7 +38374,7 @@ function _esutils() { } function _chalk() { - const data = _interopRequireDefault(__webpack_require__(441)); + const data = _interopRequireDefault(__webpack_require__(440)); _chalk = function () { return data; @@ -38493,7 +38475,7 @@ function highlight(code, options = {}) { } /***/ }), -/* 436 */ +/* 435 */ /***/ (function(module, exports) { // Copyright 2014, 2015, 2016, 2017, 2018 Simon Lydell @@ -38522,7 +38504,7 @@ exports.matchToToken = function(match) { /***/ }), -/* 437 */ +/* 436 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -38553,15 +38535,15 @@ exports.matchToToken = function(match) { (function () { 'use strict'; - exports.ast = __webpack_require__(438); - exports.code = __webpack_require__(439); - exports.keyword = __webpack_require__(440); + exports.ast = __webpack_require__(437); + exports.code = __webpack_require__(438); + exports.keyword = __webpack_require__(439); }()); /* vim: set sw=4 ts=4 et tw=80 : */ /***/ }), -/* 438 */ +/* 437 */ /***/ (function(module, exports) { /* @@ -38711,7 +38693,7 @@ exports.matchToToken = function(match) { /***/ }), -/* 439 */ +/* 438 */ /***/ (function(module, exports) { /* @@ -38852,7 +38834,7 @@ exports.matchToToken = function(match) { /***/ }), -/* 440 */ +/* 439 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -38882,7 +38864,7 @@ exports.matchToToken = function(match) { (function () { 'use strict'; - var code = __webpack_require__(439); + var code = __webpack_require__(438); function isStrictModeReservedWordES6(id) { switch (id) { @@ -39023,16 +39005,16 @@ exports.matchToToken = function(match) { /***/ }), -/* 441 */ +/* 440 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(3); -const ansiStyles = __webpack_require__(442); -const stdoutColor = __webpack_require__(443).stdout; +const ansiStyles = __webpack_require__(441); +const stdoutColor = __webpack_require__(442).stdout; -const template = __webpack_require__(444); +const template = __webpack_require__(443); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -39258,7 +39240,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 442 */ +/* 441 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -39431,7 +39413,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 443 */ +/* 442 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -39573,7 +39555,7 @@ module.exports = { /***/ }), -/* 444 */ +/* 443 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -39708,7 +39690,7 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 445 */ +/* 444 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -39747,7 +39729,7 @@ exports.KBN_P12_PASSWORD = 'storepass'; /***/ }), -/* 446 */ +/* 445 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -39771,9 +39753,9 @@ exports.KBN_P12_PASSWORD = 'storepass'; * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -var run_1 = __webpack_require__(447); +var run_1 = __webpack_require__(446); exports.run = run_1.run; -var fail_1 = __webpack_require__(448); +var fail_1 = __webpack_require__(447); exports.createFailError = fail_1.createFailError; exports.createFlagError = fail_1.createFlagError; exports.combineErrors = fail_1.combineErrors; @@ -39781,7 +39763,7 @@ exports.isFailError = fail_1.isFailError; /***/ }), -/* 447 */ +/* 446 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -39809,9 +39791,9 @@ const tslib_1 = __webpack_require__(36); const util_1 = __webpack_require__(29); // @ts-ignore @types are outdated and module is super simple const exit_hook_1 = tslib_1.__importDefault(__webpack_require__(348)); -const tooling_log_1 = __webpack_require__(415); -const fail_1 = __webpack_require__(448); -const flags_1 = __webpack_require__(449); +const tooling_log_1 = __webpack_require__(414); +const fail_1 = __webpack_require__(447); +const flags_1 = __webpack_require__(448); const proc_runner_1 = __webpack_require__(37); async function run(fn, options = {}) { var _a; @@ -39886,7 +39868,7 @@ exports.run = run; /***/ }), -/* 448 */ +/* 447 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -39954,7 +39936,7 @@ exports.combineErrors = combineErrors; /***/ }), -/* 449 */ +/* 448 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -39981,7 +39963,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); const path_1 = __webpack_require__(16); const dedent_1 = tslib_1.__importDefault(__webpack_require__(14)); -const getopts_1 = tslib_1.__importDefault(__webpack_require__(450)); +const getopts_1 = tslib_1.__importDefault(__webpack_require__(449)); function getFlags(argv, options) { const unexpectedNames = new Set(); const flagOpts = options.flags || {}; @@ -40084,7 +40066,7 @@ exports.getHelp = getHelp; /***/ }), -/* 450 */ +/* 449 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40296,7 +40278,7 @@ module.exports = getopts /***/ }), -/* 451 */ +/* 450 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40320,14 +40302,14 @@ module.exports = getopts * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -var kbn_client_1 = __webpack_require__(452); +var kbn_client_1 = __webpack_require__(451); exports.KbnClient = kbn_client_1.KbnClient; -var kbn_client_requester_1 = __webpack_require__(453); +var kbn_client_requester_1 = __webpack_require__(452); exports.uriencode = kbn_client_requester_1.uriencode; /***/ }), -/* 452 */ +/* 451 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40351,12 +40333,12 @@ exports.uriencode = kbn_client_requester_1.uriencode; * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -const kbn_client_requester_1 = __webpack_require__(453); -const kbn_client_status_1 = __webpack_require__(495); -const kbn_client_plugins_1 = __webpack_require__(496); -const kbn_client_version_1 = __webpack_require__(497); -const kbn_client_saved_objects_1 = __webpack_require__(498); -const kbn_client_ui_settings_1 = __webpack_require__(499); +const kbn_client_requester_1 = __webpack_require__(452); +const kbn_client_status_1 = __webpack_require__(494); +const kbn_client_plugins_1 = __webpack_require__(495); +const kbn_client_version_1 = __webpack_require__(496); +const kbn_client_saved_objects_1 = __webpack_require__(497); +const kbn_client_ui_settings_1 = __webpack_require__(498); class KbnClient { /** * Basic Kibana server client that implements common behaviors for talking @@ -40394,7 +40376,7 @@ exports.KbnClient = KbnClient; /***/ }), -/* 453 */ +/* 452 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40419,9 +40401,9 @@ exports.KbnClient = KbnClient; */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); -const url_1 = tslib_1.__importDefault(__webpack_require__(454)); -const axios_1 = tslib_1.__importDefault(__webpack_require__(455)); -const axios_2 = __webpack_require__(493); +const url_1 = tslib_1.__importDefault(__webpack_require__(453)); +const axios_1 = tslib_1.__importDefault(__webpack_require__(454)); +const axios_2 = __webpack_require__(492); const isConcliftOnGetError = (error) => { return (axios_2.isAxiosResponseError(error) && error.config.method === 'GET' && error.response.status === 409); }; @@ -40505,28 +40487,28 @@ exports.KbnClientRequester = KbnClientRequester; /***/ }), -/* 454 */ +/* 453 */ /***/ (function(module, exports) { module.exports = require("url"); /***/ }), -/* 455 */ +/* 454 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = __webpack_require__(456); +module.exports = __webpack_require__(455); /***/ }), -/* 456 */ +/* 455 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(457); -var bind = __webpack_require__(458); -var Axios = __webpack_require__(460); -var defaults = __webpack_require__(461); +var utils = __webpack_require__(456); +var bind = __webpack_require__(457); +var Axios = __webpack_require__(459); +var defaults = __webpack_require__(460); /** * Create an instance of Axios @@ -40559,15 +40541,15 @@ axios.create = function create(instanceConfig) { }; // Expose Cancel & CancelToken -axios.Cancel = __webpack_require__(490); -axios.CancelToken = __webpack_require__(491); -axios.isCancel = __webpack_require__(487); +axios.Cancel = __webpack_require__(489); +axios.CancelToken = __webpack_require__(490); +axios.isCancel = __webpack_require__(486); // Expose all/spread axios.all = function all(promises) { return Promise.all(promises); }; -axios.spread = __webpack_require__(492); +axios.spread = __webpack_require__(491); module.exports = axios; @@ -40576,14 +40558,14 @@ module.exports.default = axios; /***/ }), -/* 457 */ +/* 456 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var bind = __webpack_require__(458); -var isBuffer = __webpack_require__(459); +var bind = __webpack_require__(457); +var isBuffer = __webpack_require__(458); /*global toString:true*/ @@ -40886,7 +40868,7 @@ module.exports = { /***/ }), -/* 458 */ +/* 457 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40904,7 +40886,7 @@ module.exports = function bind(fn, thisArg) { /***/ }), -/* 459 */ +/* 458 */ /***/ (function(module, exports) { /*! @@ -40921,16 +40903,16 @@ module.exports = function isBuffer (obj) { /***/ }), -/* 460 */ +/* 459 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var defaults = __webpack_require__(461); -var utils = __webpack_require__(457); -var InterceptorManager = __webpack_require__(484); -var dispatchRequest = __webpack_require__(485); +var defaults = __webpack_require__(460); +var utils = __webpack_require__(456); +var InterceptorManager = __webpack_require__(483); +var dispatchRequest = __webpack_require__(484); /** * Create a new instance of Axios @@ -41007,14 +40989,14 @@ module.exports = Axios; /***/ }), -/* 461 */ +/* 460 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(457); -var normalizeHeaderName = __webpack_require__(462); +var utils = __webpack_require__(456); +var normalizeHeaderName = __webpack_require__(461); var DEFAULT_CONTENT_TYPE = { 'Content-Type': 'application/x-www-form-urlencoded' @@ -41030,10 +41012,10 @@ function getDefaultAdapter() { var adapter; if (typeof XMLHttpRequest !== 'undefined') { // For browsers use XHR adapter - adapter = __webpack_require__(463); + adapter = __webpack_require__(462); } else if (typeof process !== 'undefined') { // For node use HTTP adapter - adapter = __webpack_require__(471); + adapter = __webpack_require__(470); } return adapter; } @@ -41110,13 +41092,13 @@ module.exports = defaults; /***/ }), -/* 462 */ +/* 461 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(457); +var utils = __webpack_require__(456); module.exports = function normalizeHeaderName(headers, normalizedName) { utils.forEach(headers, function processHeader(value, name) { @@ -41129,18 +41111,18 @@ module.exports = function normalizeHeaderName(headers, normalizedName) { /***/ }), -/* 463 */ +/* 462 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(457); -var settle = __webpack_require__(464); -var buildURL = __webpack_require__(467); -var parseHeaders = __webpack_require__(468); -var isURLSameOrigin = __webpack_require__(469); -var createError = __webpack_require__(465); +var utils = __webpack_require__(456); +var settle = __webpack_require__(463); +var buildURL = __webpack_require__(466); +var parseHeaders = __webpack_require__(467); +var isURLSameOrigin = __webpack_require__(468); +var createError = __webpack_require__(464); module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { @@ -41220,7 +41202,7 @@ module.exports = function xhrAdapter(config) { // This is only done if running in a standard browser environment. // Specifically not if we're in a web worker, or react-native. if (utils.isStandardBrowserEnv()) { - var cookies = __webpack_require__(470); + var cookies = __webpack_require__(469); // Add xsrf header var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ? @@ -41298,13 +41280,13 @@ module.exports = function xhrAdapter(config) { /***/ }), -/* 464 */ +/* 463 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var createError = __webpack_require__(465); +var createError = __webpack_require__(464); /** * Resolve or reject a Promise based on response status. @@ -41331,13 +41313,13 @@ module.exports = function settle(resolve, reject, response) { /***/ }), -/* 465 */ +/* 464 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var enhanceError = __webpack_require__(466); +var enhanceError = __webpack_require__(465); /** * Create an Error with the specified message, config, error code, request and response. @@ -41356,7 +41338,7 @@ module.exports = function createError(message, config, code, request, response) /***/ }), -/* 466 */ +/* 465 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41384,13 +41366,13 @@ module.exports = function enhanceError(error, config, code, request, response) { /***/ }), -/* 467 */ +/* 466 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(457); +var utils = __webpack_require__(456); function encode(val) { return encodeURIComponent(val). @@ -41457,13 +41439,13 @@ module.exports = function buildURL(url, params, paramsSerializer) { /***/ }), -/* 468 */ +/* 467 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(457); +var utils = __webpack_require__(456); // Headers whose duplicates are ignored by node // c.f. https://nodejs.org/api/http.html#http_message_headers @@ -41517,13 +41499,13 @@ module.exports = function parseHeaders(headers) { /***/ }), -/* 469 */ +/* 468 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(457); +var utils = __webpack_require__(456); module.exports = ( utils.isStandardBrowserEnv() ? @@ -41592,13 +41574,13 @@ module.exports = ( /***/ }), -/* 470 */ +/* 469 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(457); +var utils = __webpack_require__(456); module.exports = ( utils.isStandardBrowserEnv() ? @@ -41652,24 +41634,24 @@ module.exports = ( /***/ }), -/* 471 */ +/* 470 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(457); -var settle = __webpack_require__(464); -var buildURL = __webpack_require__(467); -var http = __webpack_require__(472); -var https = __webpack_require__(473); -var httpFollow = __webpack_require__(474).http; -var httpsFollow = __webpack_require__(474).https; -var url = __webpack_require__(454); -var zlib = __webpack_require__(482); -var pkg = __webpack_require__(483); -var createError = __webpack_require__(465); -var enhanceError = __webpack_require__(466); +var utils = __webpack_require__(456); +var settle = __webpack_require__(463); +var buildURL = __webpack_require__(466); +var http = __webpack_require__(471); +var https = __webpack_require__(472); +var httpFollow = __webpack_require__(473).http; +var httpsFollow = __webpack_require__(473).https; +var url = __webpack_require__(453); +var zlib = __webpack_require__(481); +var pkg = __webpack_require__(482); +var createError = __webpack_require__(464); +var enhanceError = __webpack_require__(465); /*eslint consistent-return:0*/ module.exports = function httpAdapter(config) { @@ -41897,27 +41879,27 @@ module.exports = function httpAdapter(config) { /***/ }), -/* 472 */ +/* 471 */ /***/ (function(module, exports) { module.exports = require("http"); /***/ }), -/* 473 */ +/* 472 */ /***/ (function(module, exports) { module.exports = require("https"); /***/ }), -/* 474 */ +/* 473 */ /***/ (function(module, exports, __webpack_require__) { -var url = __webpack_require__(454); -var http = __webpack_require__(472); -var https = __webpack_require__(473); +var url = __webpack_require__(453); +var http = __webpack_require__(471); +var https = __webpack_require__(472); var assert = __webpack_require__(30); var Writable = __webpack_require__(27).Writable; -var debug = __webpack_require__(475)("follow-redirects"); +var debug = __webpack_require__(474)("follow-redirects"); // RFC7231§4.2.1: Of the request methods defined by this specification, // the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe. @@ -42237,7 +42219,7 @@ module.exports.wrap = wrap; /***/ }), -/* 475 */ +/* 474 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -42246,14 +42228,14 @@ module.exports.wrap = wrap; */ if (typeof process === 'undefined' || process.type === 'renderer') { - module.exports = __webpack_require__(476); + module.exports = __webpack_require__(475); } else { - module.exports = __webpack_require__(479); + module.exports = __webpack_require__(478); } /***/ }), -/* 476 */ +/* 475 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -42262,7 +42244,7 @@ if (typeof process === 'undefined' || process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(477); +exports = module.exports = __webpack_require__(476); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -42454,7 +42436,7 @@ function localstorage() { /***/ }), -/* 477 */ +/* 476 */ /***/ (function(module, exports, __webpack_require__) { @@ -42470,7 +42452,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(478); +exports.humanize = __webpack_require__(477); /** * Active `debug` instances. @@ -42685,7 +42667,7 @@ function coerce(val) { /***/ }), -/* 478 */ +/* 477 */ /***/ (function(module, exports) { /** @@ -42843,14 +42825,14 @@ function plural(ms, n, name) { /***/ }), -/* 479 */ +/* 478 */ /***/ (function(module, exports, __webpack_require__) { /** * Module dependencies. */ -var tty = __webpack_require__(480); +var tty = __webpack_require__(479); var util = __webpack_require__(29); /** @@ -42859,7 +42841,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(477); +exports = module.exports = __webpack_require__(476); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -42874,7 +42856,7 @@ exports.useColors = useColors; exports.colors = [ 6, 2, 3, 4, 5, 1 ]; try { - var supportsColor = __webpack_require__(481); + var supportsColor = __webpack_require__(480); if (supportsColor && supportsColor.level >= 2) { exports.colors = [ 20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62, 63, 68, @@ -43035,13 +43017,13 @@ exports.enable(load()); /***/ }), -/* 480 */ +/* 479 */ /***/ (function(module, exports) { module.exports = require("tty"); /***/ }), -/* 481 */ +/* 480 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43186,25 +43168,25 @@ module.exports = { /***/ }), -/* 482 */ +/* 481 */ /***/ (function(module, exports) { module.exports = require("zlib"); /***/ }), -/* 483 */ +/* 482 */ /***/ (function(module) { module.exports = JSON.parse("{\"name\":\"axios\",\"version\":\"0.18.1\",\"description\":\"Promise based HTTP client for the browser and node.js\",\"main\":\"index.js\",\"scripts\":{\"test\":\"grunt test && bundlesize\",\"start\":\"node ./sandbox/server.js\",\"build\":\"NODE_ENV=production grunt build\",\"preversion\":\"npm test\",\"version\":\"npm run build && grunt version && git add -A dist && git add CHANGELOG.md bower.json package.json\",\"postversion\":\"git push && git push --tags\",\"examples\":\"node ./examples/server.js\",\"coveralls\":\"cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js\"},\"repository\":{\"type\":\"git\",\"url\":\"https://github.com/axios/axios.git\"},\"keywords\":[\"xhr\",\"http\",\"ajax\",\"promise\",\"node\"],\"author\":\"Matt Zabriskie\",\"license\":\"MIT\",\"bugs\":{\"url\":\"https://github.com/axios/axios/issues\"},\"homepage\":\"https://github.com/axios/axios\",\"devDependencies\":{\"bundlesize\":\"^0.5.7\",\"coveralls\":\"^2.11.9\",\"es6-promise\":\"^4.0.5\",\"grunt\":\"^1.0.1\",\"grunt-banner\":\"^0.6.0\",\"grunt-cli\":\"^1.2.0\",\"grunt-contrib-clean\":\"^1.0.0\",\"grunt-contrib-nodeunit\":\"^1.0.0\",\"grunt-contrib-watch\":\"^1.0.0\",\"grunt-eslint\":\"^19.0.0\",\"grunt-karma\":\"^2.0.0\",\"grunt-ts\":\"^6.0.0-beta.3\",\"grunt-webpack\":\"^1.0.18\",\"istanbul-instrumenter-loader\":\"^1.0.0\",\"jasmine-core\":\"^2.4.1\",\"karma\":\"^1.3.0\",\"karma-chrome-launcher\":\"^2.0.0\",\"karma-coverage\":\"^1.0.0\",\"karma-firefox-launcher\":\"^1.0.0\",\"karma-jasmine\":\"^1.0.2\",\"karma-jasmine-ajax\":\"^0.1.13\",\"karma-opera-launcher\":\"^1.0.0\",\"karma-safari-launcher\":\"^1.0.0\",\"karma-sauce-launcher\":\"^1.1.0\",\"karma-sinon\":\"^1.0.5\",\"karma-sourcemap-loader\":\"^0.3.7\",\"karma-webpack\":\"^1.7.0\",\"load-grunt-tasks\":\"^3.5.2\",\"minimist\":\"^1.2.0\",\"sinon\":\"^1.17.4\",\"webpack\":\"^1.13.1\",\"webpack-dev-server\":\"^1.14.1\",\"url-search-params\":\"^0.6.1\",\"typescript\":\"^2.0.3\"},\"browser\":{\"./lib/adapters/http.js\":\"./lib/adapters/xhr.js\"},\"typings\":\"./index.d.ts\",\"dependencies\":{\"follow-redirects\":\"1.5.10\",\"is-buffer\":\"^2.0.2\"},\"bundlesize\":[{\"path\":\"./dist/axios.min.js\",\"threshold\":\"5kB\"}]}"); /***/ }), -/* 484 */ +/* 483 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(457); +var utils = __webpack_require__(456); function InterceptorManager() { this.handlers = []; @@ -43257,18 +43239,18 @@ module.exports = InterceptorManager; /***/ }), -/* 485 */ +/* 484 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(457); -var transformData = __webpack_require__(486); -var isCancel = __webpack_require__(487); -var defaults = __webpack_require__(461); -var isAbsoluteURL = __webpack_require__(488); -var combineURLs = __webpack_require__(489); +var utils = __webpack_require__(456); +var transformData = __webpack_require__(485); +var isCancel = __webpack_require__(486); +var defaults = __webpack_require__(460); +var isAbsoluteURL = __webpack_require__(487); +var combineURLs = __webpack_require__(488); /** * Throws a `Cancel` if cancellation has been requested. @@ -43350,13 +43332,13 @@ module.exports = function dispatchRequest(config) { /***/ }), -/* 486 */ +/* 485 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(457); +var utils = __webpack_require__(456); /** * Transform the data for a request or a response @@ -43377,7 +43359,7 @@ module.exports = function transformData(data, headers, fns) { /***/ }), -/* 487 */ +/* 486 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43389,7 +43371,7 @@ module.exports = function isCancel(value) { /***/ }), -/* 488 */ +/* 487 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43410,7 +43392,7 @@ module.exports = function isAbsoluteURL(url) { /***/ }), -/* 489 */ +/* 488 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43431,7 +43413,7 @@ module.exports = function combineURLs(baseURL, relativeURL) { /***/ }), -/* 490 */ +/* 489 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43457,13 +43439,13 @@ module.exports = Cancel; /***/ }), -/* 491 */ +/* 490 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Cancel = __webpack_require__(490); +var Cancel = __webpack_require__(489); /** * A `CancelToken` is an object that can be used to request cancellation of an operation. @@ -43521,7 +43503,7 @@ module.exports = CancelToken; /***/ }), -/* 492 */ +/* 491 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43555,7 +43537,7 @@ module.exports = function spread(callback) { /***/ }), -/* 493 */ +/* 492 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43580,11 +43562,11 @@ module.exports = function spread(callback) { */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); -tslib_1.__exportStar(__webpack_require__(494), exports); +tslib_1.__exportStar(__webpack_require__(493), exports); /***/ }), -/* 494 */ +/* 493 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43617,7 +43599,7 @@ exports.isAxiosResponseError = (error) => { /***/ }), -/* 495 */ +/* 494 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43666,7 +43648,7 @@ exports.KbnClientStatus = KbnClientStatus; /***/ }), -/* 496 */ +/* 495 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43716,7 +43698,7 @@ exports.KbnClientPlugins = KbnClientPlugins; /***/ }), -/* 497 */ +/* 496 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43757,7 +43739,7 @@ exports.KbnClientVersion = KbnClientVersion; /***/ }), -/* 498 */ +/* 497 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43781,7 +43763,7 @@ exports.KbnClientVersion = KbnClientVersion; * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -const kbn_client_requester_1 = __webpack_require__(453); +const kbn_client_requester_1 = __webpack_require__(452); class KbnClientSavedObjects { constructor(log, requester) { this.log = log; @@ -43866,7 +43848,7 @@ exports.KbnClientSavedObjects = KbnClientSavedObjects; /***/ }), -/* 499 */ +/* 498 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43890,7 +43872,7 @@ exports.KbnClientSavedObjects = KbnClientSavedObjects; * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -const kbn_client_requester_1 = __webpack_require__(453); +const kbn_client_requester_1 = __webpack_require__(452); class KbnClientUiSettings { constructor(log, requester, defaults) { this.log = log; @@ -43966,7 +43948,7 @@ exports.KbnClientUiSettings = KbnClientUiSettings; /***/ }), -/* 500 */ +/* 499 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -44032,7 +44014,7 @@ async function parallelize(items, fn, concurrency = 4) { } /***/ }), -/* 501 */ +/* 500 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -44041,15 +44023,15 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProjectGraph", function() { return buildProjectGraph; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "topologicallyBatchProjects", function() { return topologicallyBatchProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "includeTransitiveProjects", function() { return includeTransitiveProjects; }); -/* harmony import */ var glob__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(502); +/* harmony import */ var glob__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(501); /* harmony import */ var glob__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(glob__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(515); -/* harmony import */ var _project__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(516); -/* harmony import */ var _workspaces__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(578); +/* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(514); +/* harmony import */ var _project__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(515); +/* harmony import */ var _workspaces__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(577); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -44248,7 +44230,7 @@ function includeTransitiveProjects(subsetOfProjects, allProjects, { } /***/ }), -/* 502 */ +/* 501 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -44294,26 +44276,26 @@ function includeTransitiveProjects(subsetOfProjects, allProjects, { module.exports = glob var fs = __webpack_require__(23) -var rp = __webpack_require__(503) -var minimatch = __webpack_require__(505) +var rp = __webpack_require__(502) +var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(509) +var inherits = __webpack_require__(508) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) -var isAbsolute = __webpack_require__(511) -var globSync = __webpack_require__(512) -var common = __webpack_require__(513) +var isAbsolute = __webpack_require__(510) +var globSync = __webpack_require__(511) +var common = __webpack_require__(512) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts var ownProp = common.ownProp -var inflight = __webpack_require__(514) +var inflight = __webpack_require__(513) var util = __webpack_require__(29) var childrenIgnored = common.childrenIgnored var isIgnored = common.isIgnored -var once = __webpack_require__(385) +var once = __webpack_require__(384) function glob (pattern, options, cb) { if (typeof options === 'function') cb = options, options = {} @@ -45044,7 +45026,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 503 */ +/* 502 */ /***/ (function(module, exports, __webpack_require__) { module.exports = realpath @@ -45060,7 +45042,7 @@ var origRealpathSync = fs.realpathSync var version = process.version var ok = /^v[0-5]\./.test(version) -var old = __webpack_require__(504) +var old = __webpack_require__(503) function newError (er) { return er && er.syscall === 'realpath' && ( @@ -45116,7 +45098,7 @@ function unmonkeypatch () { /***/ }), -/* 504 */ +/* 503 */ /***/ (function(module, exports, __webpack_require__) { // Copyright Joyent, Inc. and other Node contributors. @@ -45425,7 +45407,7 @@ exports.realpath = function realpath(p, cache, cb) { /***/ }), -/* 505 */ +/* 504 */ /***/ (function(module, exports, __webpack_require__) { module.exports = minimatch @@ -45437,7 +45419,7 @@ try { } catch (er) {} var GLOBSTAR = minimatch.GLOBSTAR = Minimatch.GLOBSTAR = {} -var expand = __webpack_require__(506) +var expand = __webpack_require__(505) var plTypes = { '!': { open: '(?:(?!(?:', close: '))[^/]*?)'}, @@ -46354,11 +46336,11 @@ function regExpEscape (s) { /***/ }), -/* 506 */ +/* 505 */ /***/ (function(module, exports, __webpack_require__) { -var concatMap = __webpack_require__(507); -var balanced = __webpack_require__(508); +var concatMap = __webpack_require__(506); +var balanced = __webpack_require__(507); module.exports = expandTop; @@ -46561,7 +46543,7 @@ function expand(str, isTop) { /***/ }), -/* 507 */ +/* 506 */ /***/ (function(module, exports) { module.exports = function (xs, fn) { @@ -46580,7 +46562,7 @@ var isArray = Array.isArray || function (xs) { /***/ }), -/* 508 */ +/* 507 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46646,7 +46628,7 @@ function range(a, b, str) { /***/ }), -/* 509 */ +/* 508 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -46656,12 +46638,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(510); + module.exports = __webpack_require__(509); } /***/ }), -/* 510 */ +/* 509 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -46694,7 +46676,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 511 */ +/* 510 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46721,22 +46703,22 @@ module.exports.win32 = win32; /***/ }), -/* 512 */ +/* 511 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync globSync.GlobSync = GlobSync var fs = __webpack_require__(23) -var rp = __webpack_require__(503) -var minimatch = __webpack_require__(505) +var rp = __webpack_require__(502) +var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(502).Glob +var Glob = __webpack_require__(501).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) -var isAbsolute = __webpack_require__(511) -var common = __webpack_require__(513) +var isAbsolute = __webpack_require__(510) +var common = __webpack_require__(512) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -47213,7 +47195,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 513 */ +/* 512 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -47231,8 +47213,8 @@ function ownProp (obj, field) { } var path = __webpack_require__(16) -var minimatch = __webpack_require__(505) -var isAbsolute = __webpack_require__(511) +var minimatch = __webpack_require__(504) +var isAbsolute = __webpack_require__(510) var Minimatch = minimatch.Minimatch function alphasorti (a, b) { @@ -47459,12 +47441,12 @@ function childrenIgnored (self, path) { /***/ }), -/* 514 */ +/* 513 */ /***/ (function(module, exports, __webpack_require__) { -var wrappy = __webpack_require__(386) +var wrappy = __webpack_require__(385) var reqs = Object.create(null) -var once = __webpack_require__(385) +var once = __webpack_require__(384) module.exports = wrappy(inflight) @@ -47519,7 +47501,7 @@ function slice (args) { /***/ }), -/* 515 */ +/* 514 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -47552,7 +47534,7 @@ class CliError extends Error { } /***/ }), -/* 516 */ +/* 515 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -47566,10 +47548,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_3__); -/* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(515); +/* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(514); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(34); -/* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(517); -/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(563); +/* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(516); +/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(562); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -47800,7 +47782,7 @@ function normalizePath(path) { } /***/ }), -/* 517 */ +/* 516 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -47808,9 +47790,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "readPackageJson", function() { return readPackageJson; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "writePackageJson", function() { return writePackageJson; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isLinkDependency", function() { return isLinkDependency; }); -/* harmony import */ var read_pkg__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(518); +/* harmony import */ var read_pkg__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(517); /* harmony import */ var read_pkg__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(read_pkg__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(544); +/* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(543); /* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(write_pkg__WEBPACK_IMPORTED_MODULE_1__); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -47844,7 +47826,7 @@ function writePackageJson(path, json) { const isLinkDependency = depVersion => depVersion.startsWith('link:'); /***/ }), -/* 518 */ +/* 517 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47852,7 +47834,7 @@ const isLinkDependency = depVersion => depVersion.startsWith('link:'); const {promisify} = __webpack_require__(29); const fs = __webpack_require__(23); const path = __webpack_require__(16); -const parseJson = __webpack_require__(519); +const parseJson = __webpack_require__(518); const readFileAsync = promisify(fs.readFile); @@ -47867,7 +47849,7 @@ module.exports = async options => { const json = parseJson(await readFileAsync(filePath, 'utf8')); if (options.normalize) { - __webpack_require__(520)(json); + __webpack_require__(519)(json); } return json; @@ -47884,7 +47866,7 @@ module.exports.sync = options => { const json = parseJson(fs.readFileSync(filePath, 'utf8')); if (options.normalize) { - __webpack_require__(520)(json); + __webpack_require__(519)(json); } return json; @@ -47892,15 +47874,15 @@ module.exports.sync = options => { /***/ }), -/* 519 */ +/* 518 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const errorEx = __webpack_require__(430); -const fallback = __webpack_require__(432); -const {default: LinesAndColumns} = __webpack_require__(433); -const {codeFrameColumns} = __webpack_require__(434); +const errorEx = __webpack_require__(429); +const fallback = __webpack_require__(431); +const {default: LinesAndColumns} = __webpack_require__(432); +const {codeFrameColumns} = __webpack_require__(433); const JSONError = errorEx('JSONError', { fileName: errorEx.append('in %s'), @@ -47949,15 +47931,15 @@ module.exports = (string, reviver, filename) => { /***/ }), -/* 520 */ +/* 519 */ /***/ (function(module, exports, __webpack_require__) { module.exports = normalize -var fixer = __webpack_require__(521) +var fixer = __webpack_require__(520) normalize.fixer = fixer -var makeWarning = __webpack_require__(542) +var makeWarning = __webpack_require__(541) var fieldsToFix = ['name','version','description','repository','modules','scripts' ,'files','bin','man','bugs','keywords','readme','homepage','license'] @@ -47994,17 +47976,17 @@ function ucFirst (string) { /***/ }), -/* 521 */ +/* 520 */ /***/ (function(module, exports, __webpack_require__) { -var semver = __webpack_require__(522) -var validateLicense = __webpack_require__(523); -var hostedGitInfo = __webpack_require__(528) -var isBuiltinModule = __webpack_require__(531).isCore +var semver = __webpack_require__(521) +var validateLicense = __webpack_require__(522); +var hostedGitInfo = __webpack_require__(527) +var isBuiltinModule = __webpack_require__(530).isCore var depTypes = ["dependencies","devDependencies","optionalDependencies"] -var extractDescription = __webpack_require__(540) -var url = __webpack_require__(454) -var typos = __webpack_require__(541) +var extractDescription = __webpack_require__(539) +var url = __webpack_require__(453) +var typos = __webpack_require__(540) var fixer = module.exports = { // default warning function @@ -48418,7 +48400,7 @@ function bugsTypos(bugs, warn) { /***/ }), -/* 522 */ +/* 521 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -49907,11 +49889,11 @@ function coerce (version) { /***/ }), -/* 523 */ +/* 522 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(524); -var correct = __webpack_require__(526); +var parse = __webpack_require__(523); +var correct = __webpack_require__(525); var genericWarning = ( 'license should be ' + @@ -49997,10 +49979,10 @@ module.exports = function(argument) { /***/ }), -/* 524 */ +/* 523 */ /***/ (function(module, exports, __webpack_require__) { -var parser = __webpack_require__(525).parser +var parser = __webpack_require__(524).parser module.exports = function (argument) { return parser.parse(argument) @@ -50008,7 +49990,7 @@ module.exports = function (argument) { /***/ }), -/* 525 */ +/* 524 */ /***/ (function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(module) {/* parser generated by jison 0.4.17 */ @@ -51372,10 +51354,10 @@ if ( true && __webpack_require__.c[__webpack_require__.s] === module) { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 526 */ +/* 525 */ /***/ (function(module, exports, __webpack_require__) { -var licenseIDs = __webpack_require__(527); +var licenseIDs = __webpack_require__(526); function valid(string) { return licenseIDs.indexOf(string) > -1; @@ -51615,20 +51597,20 @@ module.exports = function(identifier) { /***/ }), -/* 527 */ +/* 526 */ /***/ (function(module) { module.exports = JSON.parse("[\"Glide\",\"Abstyles\",\"AFL-1.1\",\"AFL-1.2\",\"AFL-2.0\",\"AFL-2.1\",\"AFL-3.0\",\"AMPAS\",\"APL-1.0\",\"Adobe-Glyph\",\"APAFML\",\"Adobe-2006\",\"AGPL-1.0\",\"Afmparse\",\"Aladdin\",\"ADSL\",\"AMDPLPA\",\"ANTLR-PD\",\"Apache-1.0\",\"Apache-1.1\",\"Apache-2.0\",\"AML\",\"APSL-1.0\",\"APSL-1.1\",\"APSL-1.2\",\"APSL-2.0\",\"Artistic-1.0\",\"Artistic-1.0-Perl\",\"Artistic-1.0-cl8\",\"Artistic-2.0\",\"AAL\",\"Bahyph\",\"Barr\",\"Beerware\",\"BitTorrent-1.0\",\"BitTorrent-1.1\",\"BSL-1.0\",\"Borceux\",\"BSD-2-Clause\",\"BSD-2-Clause-FreeBSD\",\"BSD-2-Clause-NetBSD\",\"BSD-3-Clause\",\"BSD-3-Clause-Clear\",\"BSD-4-Clause\",\"BSD-Protection\",\"BSD-Source-Code\",\"BSD-3-Clause-Attribution\",\"0BSD\",\"BSD-4-Clause-UC\",\"bzip2-1.0.5\",\"bzip2-1.0.6\",\"Caldera\",\"CECILL-1.0\",\"CECILL-1.1\",\"CECILL-2.0\",\"CECILL-2.1\",\"CECILL-B\",\"CECILL-C\",\"ClArtistic\",\"MIT-CMU\",\"CNRI-Jython\",\"CNRI-Python\",\"CNRI-Python-GPL-Compatible\",\"CPOL-1.02\",\"CDDL-1.0\",\"CDDL-1.1\",\"CPAL-1.0\",\"CPL-1.0\",\"CATOSL-1.1\",\"Condor-1.1\",\"CC-BY-1.0\",\"CC-BY-2.0\",\"CC-BY-2.5\",\"CC-BY-3.0\",\"CC-BY-4.0\",\"CC-BY-ND-1.0\",\"CC-BY-ND-2.0\",\"CC-BY-ND-2.5\",\"CC-BY-ND-3.0\",\"CC-BY-ND-4.0\",\"CC-BY-NC-1.0\",\"CC-BY-NC-2.0\",\"CC-BY-NC-2.5\",\"CC-BY-NC-3.0\",\"CC-BY-NC-4.0\",\"CC-BY-NC-ND-1.0\",\"CC-BY-NC-ND-2.0\",\"CC-BY-NC-ND-2.5\",\"CC-BY-NC-ND-3.0\",\"CC-BY-NC-ND-4.0\",\"CC-BY-NC-SA-1.0\",\"CC-BY-NC-SA-2.0\",\"CC-BY-NC-SA-2.5\",\"CC-BY-NC-SA-3.0\",\"CC-BY-NC-SA-4.0\",\"CC-BY-SA-1.0\",\"CC-BY-SA-2.0\",\"CC-BY-SA-2.5\",\"CC-BY-SA-3.0\",\"CC-BY-SA-4.0\",\"CC0-1.0\",\"Crossword\",\"CrystalStacker\",\"CUA-OPL-1.0\",\"Cube\",\"curl\",\"D-FSL-1.0\",\"diffmark\",\"WTFPL\",\"DOC\",\"Dotseqn\",\"DSDP\",\"dvipdfm\",\"EPL-1.0\",\"ECL-1.0\",\"ECL-2.0\",\"eGenix\",\"EFL-1.0\",\"EFL-2.0\",\"MIT-advertising\",\"MIT-enna\",\"Entessa\",\"ErlPL-1.1\",\"EUDatagrid\",\"EUPL-1.0\",\"EUPL-1.1\",\"Eurosym\",\"Fair\",\"MIT-feh\",\"Frameworx-1.0\",\"FreeImage\",\"FTL\",\"FSFAP\",\"FSFUL\",\"FSFULLR\",\"Giftware\",\"GL2PS\",\"Glulxe\",\"AGPL-3.0\",\"GFDL-1.1\",\"GFDL-1.2\",\"GFDL-1.3\",\"GPL-1.0\",\"GPL-2.0\",\"GPL-3.0\",\"LGPL-2.1\",\"LGPL-3.0\",\"LGPL-2.0\",\"gnuplot\",\"gSOAP-1.3b\",\"HaskellReport\",\"HPND\",\"IBM-pibs\",\"IPL-1.0\",\"ICU\",\"ImageMagick\",\"iMatix\",\"Imlib2\",\"IJG\",\"Info-ZIP\",\"Intel-ACPI\",\"Intel\",\"Interbase-1.0\",\"IPA\",\"ISC\",\"JasPer-2.0\",\"JSON\",\"LPPL-1.0\",\"LPPL-1.1\",\"LPPL-1.2\",\"LPPL-1.3a\",\"LPPL-1.3c\",\"Latex2e\",\"BSD-3-Clause-LBNL\",\"Leptonica\",\"LGPLLR\",\"Libpng\",\"libtiff\",\"LAL-1.2\",\"LAL-1.3\",\"LiLiQ-P-1.1\",\"LiLiQ-Rplus-1.1\",\"LiLiQ-R-1.1\",\"LPL-1.02\",\"LPL-1.0\",\"MakeIndex\",\"MTLL\",\"MS-PL\",\"MS-RL\",\"MirOS\",\"MITNFA\",\"MIT\",\"Motosoto\",\"MPL-1.0\",\"MPL-1.1\",\"MPL-2.0\",\"MPL-2.0-no-copyleft-exception\",\"mpich2\",\"Multics\",\"Mup\",\"NASA-1.3\",\"Naumen\",\"NBPL-1.0\",\"NetCDF\",\"NGPL\",\"NOSL\",\"NPL-1.0\",\"NPL-1.1\",\"Newsletr\",\"NLPL\",\"Nokia\",\"NPOSL-3.0\",\"NLOD-1.0\",\"Noweb\",\"NRL\",\"NTP\",\"Nunit\",\"OCLC-2.0\",\"ODbL-1.0\",\"PDDL-1.0\",\"OCCT-PL\",\"OGTSL\",\"OLDAP-2.2.2\",\"OLDAP-1.1\",\"OLDAP-1.2\",\"OLDAP-1.3\",\"OLDAP-1.4\",\"OLDAP-2.0\",\"OLDAP-2.0.1\",\"OLDAP-2.1\",\"OLDAP-2.2\",\"OLDAP-2.2.1\",\"OLDAP-2.3\",\"OLDAP-2.4\",\"OLDAP-2.5\",\"OLDAP-2.6\",\"OLDAP-2.7\",\"OLDAP-2.8\",\"OML\",\"OPL-1.0\",\"OSL-1.0\",\"OSL-1.1\",\"OSL-2.0\",\"OSL-2.1\",\"OSL-3.0\",\"OpenSSL\",\"OSET-PL-2.1\",\"PHP-3.0\",\"PHP-3.01\",\"Plexus\",\"PostgreSQL\",\"psfrag\",\"psutils\",\"Python-2.0\",\"QPL-1.0\",\"Qhull\",\"Rdisc\",\"RPSL-1.0\",\"RPL-1.1\",\"RPL-1.5\",\"RHeCos-1.1\",\"RSCPL\",\"RSA-MD\",\"Ruby\",\"SAX-PD\",\"Saxpath\",\"SCEA\",\"SWL\",\"SMPPL\",\"Sendmail\",\"SGI-B-1.0\",\"SGI-B-1.1\",\"SGI-B-2.0\",\"OFL-1.0\",\"OFL-1.1\",\"SimPL-2.0\",\"Sleepycat\",\"SNIA\",\"Spencer-86\",\"Spencer-94\",\"Spencer-99\",\"SMLNJ\",\"SugarCRM-1.1.3\",\"SISSL\",\"SISSL-1.2\",\"SPL-1.0\",\"Watcom-1.0\",\"TCL\",\"Unlicense\",\"TMate\",\"TORQUE-1.1\",\"TOSL\",\"Unicode-TOU\",\"UPL-1.0\",\"NCSA\",\"Vim\",\"VOSTROM\",\"VSL-1.0\",\"W3C-19980720\",\"W3C\",\"Wsuipa\",\"Xnet\",\"X11\",\"Xerox\",\"XFree86-1.1\",\"xinetd\",\"xpp\",\"XSkat\",\"YPL-1.0\",\"YPL-1.1\",\"Zed\",\"Zend-2.0\",\"Zimbra-1.3\",\"Zimbra-1.4\",\"Zlib\",\"zlib-acknowledgement\",\"ZPL-1.1\",\"ZPL-2.0\",\"ZPL-2.1\",\"BSD-3-Clause-No-Nuclear-License\",\"BSD-3-Clause-No-Nuclear-Warranty\",\"BSD-3-Clause-No-Nuclear-License-2014\",\"eCos-2.0\",\"GPL-2.0-with-autoconf-exception\",\"GPL-2.0-with-bison-exception\",\"GPL-2.0-with-classpath-exception\",\"GPL-2.0-with-font-exception\",\"GPL-2.0-with-GCC-exception\",\"GPL-3.0-with-autoconf-exception\",\"GPL-3.0-with-GCC-exception\",\"StandardML-NJ\",\"WXwindows\"]"); /***/ }), -/* 528 */ +/* 527 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var url = __webpack_require__(454) -var gitHosts = __webpack_require__(529) -var GitHost = module.exports = __webpack_require__(530) +var url = __webpack_require__(453) +var gitHosts = __webpack_require__(528) +var GitHost = module.exports = __webpack_require__(529) var protocolToRepresentationMap = { 'git+ssh': 'sshurl', @@ -51749,7 +51731,7 @@ function parseGitUrl (giturl) { /***/ }), -/* 529 */ +/* 528 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51824,12 +51806,12 @@ Object.keys(gitHosts).forEach(function (name) { /***/ }), -/* 530 */ +/* 529 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var gitHosts = __webpack_require__(529) +var gitHosts = __webpack_require__(528) var extend = Object.assign || __webpack_require__(29)._extend var GitHost = module.exports = function (type, user, auth, project, committish, defaultRepresentation, opts) { @@ -51945,21 +51927,21 @@ GitHost.prototype.toString = function (opts) { /***/ }), -/* 531 */ +/* 530 */ /***/ (function(module, exports, __webpack_require__) { -var core = __webpack_require__(532); -var async = __webpack_require__(534); +var core = __webpack_require__(531); +var async = __webpack_require__(533); async.core = core; async.isCore = function isCore(x) { return core[x]; }; -async.sync = __webpack_require__(539); +async.sync = __webpack_require__(538); exports = async; module.exports = async; /***/ }), -/* 532 */ +/* 531 */ /***/ (function(module, exports, __webpack_require__) { var current = (process.versions && process.versions.node && process.versions.node.split('.')) || []; @@ -52006,7 +51988,7 @@ function versionIncluded(specifierValue) { return matchesRange(specifierValue); } -var data = __webpack_require__(533); +var data = __webpack_require__(532); var core = {}; for (var mod in data) { // eslint-disable-line no-restricted-syntax @@ -52018,21 +52000,21 @@ module.exports = core; /***/ }), -/* 533 */ +/* 532 */ /***/ (function(module) { module.exports = JSON.parse("{\"assert\":true,\"async_hooks\":\">= 8\",\"buffer_ieee754\":\"< 0.9.7\",\"buffer\":true,\"child_process\":true,\"cluster\":true,\"console\":true,\"constants\":true,\"crypto\":true,\"_debugger\":\"< 8\",\"dgram\":true,\"dns\":true,\"domain\":true,\"events\":true,\"freelist\":\"< 6\",\"fs\":true,\"fs/promises\":\">= 10 && < 10.1\",\"_http_agent\":\">= 0.11.1\",\"_http_client\":\">= 0.11.1\",\"_http_common\":\">= 0.11.1\",\"_http_incoming\":\">= 0.11.1\",\"_http_outgoing\":\">= 0.11.1\",\"_http_server\":\">= 0.11.1\",\"http\":true,\"http2\":\">= 8.8\",\"https\":true,\"inspector\":\">= 8.0.0\",\"_linklist\":\"< 8\",\"module\":true,\"net\":true,\"node-inspect/lib/_inspect\":\">= 7.6.0\",\"node-inspect/lib/internal/inspect_client\":\">= 7.6.0\",\"node-inspect/lib/internal/inspect_repl\":\">= 7.6.0\",\"os\":true,\"path\":true,\"perf_hooks\":\">= 8.5\",\"process\":\">= 1\",\"punycode\":true,\"querystring\":true,\"readline\":true,\"repl\":true,\"smalloc\":\">= 0.11.5 && < 3\",\"_stream_duplex\":\">= 0.9.4\",\"_stream_transform\":\">= 0.9.4\",\"_stream_wrap\":\">= 1.4.1\",\"_stream_passthrough\":\">= 0.9.4\",\"_stream_readable\":\">= 0.9.4\",\"_stream_writable\":\">= 0.9.4\",\"stream\":true,\"string_decoder\":true,\"sys\":true,\"timers\":true,\"_tls_common\":\">= 0.11.13\",\"_tls_legacy\":\">= 0.11.3 && < 10\",\"_tls_wrap\":\">= 0.11.3\",\"tls\":true,\"trace_events\":\">= 10\",\"tty\":true,\"url\":true,\"util\":true,\"v8/tools/arguments\":\">= 10\",\"v8/tools/codemap\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/consarray\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/csvparser\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/logreader\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/profile_view\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/splaytree\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8\":\">= 1\",\"vm\":true,\"worker_threads\":\">= 11.7\",\"zlib\":true}"); /***/ }), -/* 534 */ +/* 533 */ /***/ (function(module, exports, __webpack_require__) { -var core = __webpack_require__(532); +var core = __webpack_require__(531); var fs = __webpack_require__(23); var path = __webpack_require__(16); -var caller = __webpack_require__(535); -var nodeModulesPaths = __webpack_require__(536); -var normalizeOptions = __webpack_require__(538); +var caller = __webpack_require__(534); +var nodeModulesPaths = __webpack_require__(535); +var normalizeOptions = __webpack_require__(537); var defaultIsFile = function isFile(file, cb) { fs.stat(file, function (err, stat) { @@ -52259,7 +52241,7 @@ module.exports = function resolve(x, options, callback) { /***/ }), -/* 535 */ +/* 534 */ /***/ (function(module, exports) { module.exports = function () { @@ -52273,11 +52255,11 @@ module.exports = function () { /***/ }), -/* 536 */ +/* 535 */ /***/ (function(module, exports, __webpack_require__) { var path = __webpack_require__(16); -var parse = path.parse || __webpack_require__(537); +var parse = path.parse || __webpack_require__(536); var getNodeModulesDirs = function getNodeModulesDirs(absoluteStart, modules) { var prefix = '/'; @@ -52321,7 +52303,7 @@ module.exports = function nodeModulesPaths(start, opts, request) { /***/ }), -/* 537 */ +/* 536 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52421,7 +52403,7 @@ module.exports.win32 = win32.parse; /***/ }), -/* 538 */ +/* 537 */ /***/ (function(module, exports) { module.exports = function (x, opts) { @@ -52437,15 +52419,15 @@ module.exports = function (x, opts) { /***/ }), -/* 539 */ +/* 538 */ /***/ (function(module, exports, __webpack_require__) { -var core = __webpack_require__(532); +var core = __webpack_require__(531); var fs = __webpack_require__(23); var path = __webpack_require__(16); -var caller = __webpack_require__(535); -var nodeModulesPaths = __webpack_require__(536); -var normalizeOptions = __webpack_require__(538); +var caller = __webpack_require__(534); +var nodeModulesPaths = __webpack_require__(535); +var normalizeOptions = __webpack_require__(537); var defaultIsFile = function isFile(file) { try { @@ -52597,7 +52579,7 @@ module.exports = function (x, options) { /***/ }), -/* 540 */ +/* 539 */ /***/ (function(module, exports) { module.exports = extractDescription @@ -52617,17 +52599,17 @@ function extractDescription (d) { /***/ }), -/* 541 */ +/* 540 */ /***/ (function(module) { module.exports = JSON.parse("{\"topLevel\":{\"dependancies\":\"dependencies\",\"dependecies\":\"dependencies\",\"depdenencies\":\"dependencies\",\"devEependencies\":\"devDependencies\",\"depends\":\"dependencies\",\"dev-dependencies\":\"devDependencies\",\"devDependences\":\"devDependencies\",\"devDepenencies\":\"devDependencies\",\"devdependencies\":\"devDependencies\",\"repostitory\":\"repository\",\"repo\":\"repository\",\"prefereGlobal\":\"preferGlobal\",\"hompage\":\"homepage\",\"hampage\":\"homepage\",\"autohr\":\"author\",\"autor\":\"author\",\"contributers\":\"contributors\",\"publicationConfig\":\"publishConfig\",\"script\":\"scripts\"},\"bugs\":{\"web\":\"url\",\"name\":\"url\"},\"script\":{\"server\":\"start\",\"tests\":\"test\"}}"); /***/ }), -/* 542 */ +/* 541 */ /***/ (function(module, exports, __webpack_require__) { var util = __webpack_require__(29) -var messages = __webpack_require__(543) +var messages = __webpack_require__(542) module.exports = function() { var args = Array.prototype.slice.call(arguments, 0) @@ -52652,20 +52634,20 @@ function makeTypoWarning (providedName, probableName, field) { /***/ }), -/* 543 */ +/* 542 */ /***/ (function(module) { module.exports = JSON.parse("{\"repositories\":\"'repositories' (plural) Not supported. Please pick one as the 'repository' field\",\"missingRepository\":\"No repository field.\",\"brokenGitUrl\":\"Probably broken git url: %s\",\"nonObjectScripts\":\"scripts must be an object\",\"nonStringScript\":\"script values must be string commands\",\"nonArrayFiles\":\"Invalid 'files' member\",\"invalidFilename\":\"Invalid filename in 'files' list: %s\",\"nonArrayBundleDependencies\":\"Invalid 'bundleDependencies' list. Must be array of package names\",\"nonStringBundleDependency\":\"Invalid bundleDependencies member: %s\",\"nonDependencyBundleDependency\":\"Non-dependency in bundleDependencies: %s\",\"nonObjectDependencies\":\"%s field must be an object\",\"nonStringDependency\":\"Invalid dependency: %s %s\",\"deprecatedArrayDependencies\":\"specifying %s as array is deprecated\",\"deprecatedModules\":\"modules field is deprecated\",\"nonArrayKeywords\":\"keywords should be an array of strings\",\"nonStringKeyword\":\"keywords should be an array of strings\",\"conflictingName\":\"%s is also the name of a node core module.\",\"nonStringDescription\":\"'description' field should be a string\",\"missingDescription\":\"No description\",\"missingReadme\":\"No README data\",\"missingLicense\":\"No license field.\",\"nonEmailUrlBugsString\":\"Bug string field must be url, email, or {email,url}\",\"nonUrlBugsUrlField\":\"bugs.url field must be a string url. Deleted.\",\"nonEmailBugsEmailField\":\"bugs.email field must be a string email. Deleted.\",\"emptyNormalizedBugs\":\"Normalized value of bugs field is an empty object. Deleted.\",\"nonUrlHomepage\":\"homepage field must be a string url. Deleted.\",\"invalidLicense\":\"license should be a valid SPDX license expression\",\"typo\":\"%s should probably be %s.\"}"); /***/ }), -/* 544 */ +/* 543 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const writeJsonFile = __webpack_require__(545); -const sortKeys = __webpack_require__(557); +const writeJsonFile = __webpack_require__(544); +const sortKeys = __webpack_require__(556); const dependencyKeys = new Set([ 'dependencies', @@ -52730,18 +52712,18 @@ module.exports.sync = (filePath, data, options) => { /***/ }), -/* 545 */ +/* 544 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const fs = __webpack_require__(546); -const writeFileAtomic = __webpack_require__(550); -const sortKeys = __webpack_require__(557); -const makeDir = __webpack_require__(559); -const pify = __webpack_require__(561); -const detectIndent = __webpack_require__(562); +const fs = __webpack_require__(545); +const writeFileAtomic = __webpack_require__(549); +const sortKeys = __webpack_require__(556); +const makeDir = __webpack_require__(558); +const pify = __webpack_require__(560); +const detectIndent = __webpack_require__(561); const init = (fn, filePath, data, options) => { if (!filePath) { @@ -52813,13 +52795,13 @@ module.exports.sync = (filePath, data, options) => { /***/ }), -/* 546 */ +/* 545 */ /***/ (function(module, exports, __webpack_require__) { var fs = __webpack_require__(23) -var polyfills = __webpack_require__(547) -var legacy = __webpack_require__(548) -var clone = __webpack_require__(549) +var polyfills = __webpack_require__(546) +var legacy = __webpack_require__(547) +var clone = __webpack_require__(548) var queue = [] @@ -53098,7 +53080,7 @@ function retry () { /***/ }), -/* 547 */ +/* 546 */ /***/ (function(module, exports, __webpack_require__) { var constants = __webpack_require__(25) @@ -53433,7 +53415,7 @@ function patch (fs) { /***/ }), -/* 548 */ +/* 547 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27).Stream @@ -53557,7 +53539,7 @@ function legacy (fs) { /***/ }), -/* 549 */ +/* 548 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53583,7 +53565,7 @@ function clone (obj) { /***/ }), -/* 550 */ +/* 549 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53593,8 +53575,8 @@ module.exports.sync = writeFileSync module.exports._getTmpname = getTmpname // for testing module.exports._cleanupOnExit = cleanupOnExit -var fs = __webpack_require__(551) -var MurmurHash3 = __webpack_require__(555) +var fs = __webpack_require__(550) +var MurmurHash3 = __webpack_require__(554) var onExit = __webpack_require__(377) var path = __webpack_require__(16) var activeFiles = {} @@ -53603,7 +53585,7 @@ var activeFiles = {} /* istanbul ignore next */ var threadId = (function getId () { try { - var workerThreads = __webpack_require__(556) + var workerThreads = __webpack_require__(555) /// if we are in main thread, this is set to `0` return workerThreads.threadId @@ -53828,12 +53810,12 @@ function writeFileSync (filename, data, options) { /***/ }), -/* 551 */ +/* 550 */ /***/ (function(module, exports, __webpack_require__) { var fs = __webpack_require__(23) -var polyfills = __webpack_require__(552) -var legacy = __webpack_require__(554) +var polyfills = __webpack_require__(551) +var legacy = __webpack_require__(553) var queue = [] var util = __webpack_require__(29) @@ -53857,7 +53839,7 @@ if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { }) } -module.exports = patch(__webpack_require__(553)) +module.exports = patch(__webpack_require__(552)) if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH) { module.exports = patch(fs) } @@ -54096,10 +54078,10 @@ function retry () { /***/ }), -/* 552 */ +/* 551 */ /***/ (function(module, exports, __webpack_require__) { -var fs = __webpack_require__(553) +var fs = __webpack_require__(552) var constants = __webpack_require__(25) var origCwd = process.cwd @@ -54432,7 +54414,7 @@ function chownErOk (er) { /***/ }), -/* 553 */ +/* 552 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -54460,7 +54442,7 @@ function clone (obj) { /***/ }), -/* 554 */ +/* 553 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27).Stream @@ -54584,7 +54566,7 @@ function legacy (fs) { /***/ }), -/* 555 */ +/* 554 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -54726,18 +54708,18 @@ function legacy (fs) { /***/ }), -/* 556 */ +/* 555 */ /***/ (function(module, exports) { module.exports = require(undefined); /***/ }), -/* 557 */ +/* 556 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const isPlainObj = __webpack_require__(558); +const isPlainObj = __webpack_require__(557); module.exports = (obj, opts) => { if (!isPlainObj(obj)) { @@ -54794,7 +54776,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 558 */ +/* 557 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -54808,15 +54790,15 @@ module.exports = function (x) { /***/ }), -/* 559 */ +/* 558 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); const path = __webpack_require__(16); -const pify = __webpack_require__(560); -const semver = __webpack_require__(522); +const pify = __webpack_require__(559); +const semver = __webpack_require__(521); const defaults = { mode: 0o777 & (~process.umask()), @@ -54954,7 +54936,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 560 */ +/* 559 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55029,7 +55011,7 @@ module.exports = (input, options) => { /***/ }), -/* 561 */ +/* 560 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55104,7 +55086,7 @@ module.exports = (input, options) => { /***/ }), -/* 562 */ +/* 561 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55233,7 +55215,7 @@ module.exports = str => { /***/ }), -/* 563 */ +/* 562 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55242,7 +55224,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackage", function() { return runScriptInPackage; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackageStreaming", function() { return runScriptInPackageStreaming; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "yarnWorkspacesInfo", function() { return yarnWorkspacesInfo; }); -/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(564); +/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(563); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -55312,7 +55294,7 @@ async function yarnWorkspacesInfo(directory) { } /***/ }), -/* 564 */ +/* 563 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55323,9 +55305,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(351); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(565); +/* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(564); /* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(log_symbols__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(570); +/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(569); /* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } @@ -55391,12 +55373,12 @@ function spawnStreaming(command, args, opts, { } /***/ }), -/* 565 */ +/* 564 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(566); +const chalk = __webpack_require__(565); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -55418,16 +55400,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 566 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(3); -const ansiStyles = __webpack_require__(567); -const stdoutColor = __webpack_require__(568).stdout; +const ansiStyles = __webpack_require__(566); +const stdoutColor = __webpack_require__(567).stdout; -const template = __webpack_require__(569); +const template = __webpack_require__(568); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -55653,7 +55635,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 567 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55826,7 +55808,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 568 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55968,7 +55950,7 @@ module.exports = { /***/ }), -/* 569 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56103,7 +56085,7 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 570 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { // Copyright IBM Corp. 2014,2018. All Rights Reserved. @@ -56111,12 +56093,12 @@ module.exports = (chalk, tmp) => { // This file is licensed under the Apache License 2.0. // License text available at https://opensource.org/licenses/Apache-2.0 -module.exports = __webpack_require__(571); -module.exports.cli = __webpack_require__(575); +module.exports = __webpack_require__(570); +module.exports.cli = __webpack_require__(574); /***/ }), -/* 571 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56131,9 +56113,9 @@ var stream = __webpack_require__(27); var util = __webpack_require__(29); var fs = __webpack_require__(23); -var through = __webpack_require__(572); -var duplexer = __webpack_require__(573); -var StringDecoder = __webpack_require__(574).StringDecoder; +var through = __webpack_require__(571); +var duplexer = __webpack_require__(572); +var StringDecoder = __webpack_require__(573).StringDecoder; module.exports = Logger; @@ -56322,7 +56304,7 @@ function lineMerger(host) { /***/ }), -/* 572 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27) @@ -56436,7 +56418,7 @@ function through (write, end, opts) { /***/ }), -/* 573 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27) @@ -56529,13 +56511,13 @@ function duplex(writer, reader) { /***/ }), -/* 574 */ +/* 573 */ /***/ (function(module, exports) { module.exports = require("string_decoder"); /***/ }), -/* 575 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56546,11 +56528,11 @@ module.exports = require("string_decoder"); -var minimist = __webpack_require__(576); +var minimist = __webpack_require__(575); var path = __webpack_require__(16); -var Logger = __webpack_require__(571); -var pkg = __webpack_require__(577); +var Logger = __webpack_require__(570); +var pkg = __webpack_require__(576); module.exports = cli; @@ -56604,7 +56586,7 @@ function usage($0, p) { /***/ }), -/* 576 */ +/* 575 */ /***/ (function(module, exports) { module.exports = function (args, opts) { @@ -56846,29 +56828,29 @@ function isNumber (x) { /***/ }), -/* 577 */ +/* 576 */ /***/ (function(module) { module.exports = JSON.parse("{\"name\":\"strong-log-transformer\",\"version\":\"2.1.0\",\"description\":\"Stream transformer that prefixes lines with timestamps and other things.\",\"author\":\"Ryan Graham \",\"license\":\"Apache-2.0\",\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/strongloop/strong-log-transformer\"},\"keywords\":[\"logging\",\"streams\"],\"bugs\":{\"url\":\"https://github.com/strongloop/strong-log-transformer/issues\"},\"homepage\":\"https://github.com/strongloop/strong-log-transformer\",\"directories\":{\"test\":\"test\"},\"bin\":{\"sl-log-transformer\":\"bin/sl-log-transformer.js\"},\"main\":\"index.js\",\"scripts\":{\"test\":\"tap --100 test/test-*\"},\"dependencies\":{\"duplexer\":\"^0.1.1\",\"minimist\":\"^1.2.0\",\"through\":\"^2.3.4\"},\"devDependencies\":{\"tap\":\"^12.0.1\"},\"engines\":{\"node\":\">=4\"}}"); /***/ }), -/* 578 */ +/* 577 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "workspacePackagePaths", function() { return workspacePackagePaths; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "copyWorkspacePackages", function() { return copyWorkspacePackages; }); -/* harmony import */ var glob__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(502); +/* harmony import */ var glob__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(501); /* harmony import */ var glob__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(glob__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(579); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(578); /* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(20); -/* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(517); -/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(501); +/* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(516); +/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(500); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -56960,7 +56942,7 @@ function packagesFromGlobPattern({ } /***/ }), -/* 579 */ +/* 578 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57029,7 +57011,7 @@ function getProjectPaths({ } /***/ }), -/* 580 */ +/* 579 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57037,13 +57019,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getAllChecksums", function() { return getAllChecksums; }); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(23); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(581); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(580); /* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(351); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_3__); -/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(582); +/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(581); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -57269,19 +57251,19 @@ async function getAllChecksums(kbn, log) { } /***/ }), -/* 581 */ +/* 580 */ /***/ (function(module, exports) { module.exports = require("crypto"); /***/ }), -/* 582 */ +/* 581 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "readYarnLock", function() { return readYarnLock; }); -/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(583); +/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(582); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(20); /* @@ -57325,7 +57307,7 @@ async function readYarnLock(kbn) { } /***/ }), -/* 583 */ +/* 582 */ /***/ (function(module, exports, __webpack_require__) { module.exports = @@ -58884,7 +58866,7 @@ module.exports = invariant; /* 9 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(581); +module.exports = __webpack_require__(580); /***/ }), /* 10 */, @@ -61208,7 +61190,7 @@ function onceStrict (fn) { /* 63 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(584); +module.exports = __webpack_require__(583); /***/ }), /* 64 */, @@ -62146,7 +62128,7 @@ module.exports.win32 = win32; /* 79 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(480); +module.exports = __webpack_require__(479); /***/ }), /* 80 */, @@ -67603,13 +67585,13 @@ module.exports = process && support(supportLevel); /******/ ]); /***/ }), -/* 584 */ +/* 583 */ /***/ (function(module, exports) { module.exports = require("buffer"); /***/ }), -/* 585 */ +/* 584 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -67706,7 +67688,7 @@ class BootstrapCacheFile { } /***/ }), -/* 586 */ +/* 585 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -67714,9 +67696,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CleanCommand", function() { return CleanCommand; }); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(587); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(586); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(675); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(674); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); @@ -67816,21 +67798,21 @@ const CleanCommand = { }; /***/ }), -/* 587 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const path = __webpack_require__(16); -const globby = __webpack_require__(588); -const isGlob = __webpack_require__(605); -const slash = __webpack_require__(666); +const globby = __webpack_require__(587); +const isGlob = __webpack_require__(604); +const slash = __webpack_require__(665); const gracefulFs = __webpack_require__(22); -const isPathCwd = __webpack_require__(668); -const isPathInside = __webpack_require__(669); -const rimraf = __webpack_require__(670); -const pMap = __webpack_require__(671); +const isPathCwd = __webpack_require__(667); +const isPathInside = __webpack_require__(668); +const rimraf = __webpack_require__(669); +const pMap = __webpack_require__(670); const rimrafP = promisify(rimraf); @@ -67944,19 +67926,19 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options /***/ }), -/* 588 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const arrayUnion = __webpack_require__(589); -const merge2 = __webpack_require__(590); -const glob = __webpack_require__(591); -const fastGlob = __webpack_require__(596); -const dirGlob = __webpack_require__(662); -const gitignore = __webpack_require__(664); -const {FilterStream, UniqueStream} = __webpack_require__(667); +const arrayUnion = __webpack_require__(588); +const merge2 = __webpack_require__(589); +const glob = __webpack_require__(590); +const fastGlob = __webpack_require__(595); +const dirGlob = __webpack_require__(661); +const gitignore = __webpack_require__(663); +const {FilterStream, UniqueStream} = __webpack_require__(666); const DEFAULT_FILTER = () => false; @@ -68129,7 +68111,7 @@ module.exports.gitignore = gitignore; /***/ }), -/* 589 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68141,7 +68123,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 590 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68255,7 +68237,7 @@ function pauseStreams (streams, options) { /***/ }), -/* 591 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -68301,26 +68283,26 @@ function pauseStreams (streams, options) { module.exports = glob var fs = __webpack_require__(23) -var rp = __webpack_require__(503) -var minimatch = __webpack_require__(505) +var rp = __webpack_require__(502) +var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(592) +var inherits = __webpack_require__(591) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) -var isAbsolute = __webpack_require__(511) -var globSync = __webpack_require__(594) -var common = __webpack_require__(595) +var isAbsolute = __webpack_require__(510) +var globSync = __webpack_require__(593) +var common = __webpack_require__(594) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts var ownProp = common.ownProp -var inflight = __webpack_require__(514) +var inflight = __webpack_require__(513) var util = __webpack_require__(29) var childrenIgnored = common.childrenIgnored var isIgnored = common.isIgnored -var once = __webpack_require__(385) +var once = __webpack_require__(384) function glob (pattern, options, cb) { if (typeof options === 'function') cb = options, options = {} @@ -69051,7 +69033,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 592 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -69061,12 +69043,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(593); + module.exports = __webpack_require__(592); } /***/ }), -/* 593 */ +/* 592 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -69099,22 +69081,22 @@ if (typeof Object.create === 'function') { /***/ }), -/* 594 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync globSync.GlobSync = GlobSync var fs = __webpack_require__(23) -var rp = __webpack_require__(503) -var minimatch = __webpack_require__(505) +var rp = __webpack_require__(502) +var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(591).Glob +var Glob = __webpack_require__(590).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) -var isAbsolute = __webpack_require__(511) -var common = __webpack_require__(595) +var isAbsolute = __webpack_require__(510) +var common = __webpack_require__(594) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -69591,7 +69573,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 595 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -69609,8 +69591,8 @@ function ownProp (obj, field) { } var path = __webpack_require__(16) -var minimatch = __webpack_require__(505) -var isAbsolute = __webpack_require__(511) +var minimatch = __webpack_require__(504) +var isAbsolute = __webpack_require__(510) var Minimatch = minimatch.Minimatch function alphasorti (a, b) { @@ -69837,17 +69819,17 @@ function childrenIgnored (self, path) { /***/ }), -/* 596 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(597); -const async_1 = __webpack_require__(625); -const stream_1 = __webpack_require__(658); -const sync_1 = __webpack_require__(659); -const settings_1 = __webpack_require__(661); -const utils = __webpack_require__(598); +const taskManager = __webpack_require__(596); +const async_1 = __webpack_require__(624); +const stream_1 = __webpack_require__(657); +const sync_1 = __webpack_require__(658); +const settings_1 = __webpack_require__(660); +const utils = __webpack_require__(597); function FastGlob(source, options) { try { assertPatternsInput(source); @@ -69905,13 +69887,13 @@ module.exports = FastGlob; /***/ }), -/* 597 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(598); +const utils = __webpack_require__(597); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -69979,28 +69961,28 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 598 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const array = __webpack_require__(599); +const array = __webpack_require__(598); exports.array = array; -const errno = __webpack_require__(600); +const errno = __webpack_require__(599); exports.errno = errno; -const fs = __webpack_require__(601); +const fs = __webpack_require__(600); exports.fs = fs; -const path = __webpack_require__(602); +const path = __webpack_require__(601); exports.path = path; -const pattern = __webpack_require__(603); +const pattern = __webpack_require__(602); exports.pattern = pattern; -const stream = __webpack_require__(624); +const stream = __webpack_require__(623); exports.stream = stream; /***/ }), -/* 599 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70013,7 +69995,7 @@ exports.flatten = flatten; /***/ }), -/* 600 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70026,7 +70008,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 601 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70051,7 +70033,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 602 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70072,16 +70054,16 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 603 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const globParent = __webpack_require__(604); -const isGlob = __webpack_require__(605); -const micromatch = __webpack_require__(607); +const globParent = __webpack_require__(603); +const isGlob = __webpack_require__(604); +const micromatch = __webpack_require__(606); const GLOBSTAR = '**'; function isStaticPattern(pattern) { return !isDynamicPattern(pattern); @@ -70170,13 +70152,13 @@ exports.matchAny = matchAny; /***/ }), -/* 604 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isGlob = __webpack_require__(605); +var isGlob = __webpack_require__(604); var pathPosixDirname = __webpack_require__(16).posix.dirname; var isWin32 = __webpack_require__(11).platform() === 'win32'; @@ -70211,7 +70193,7 @@ module.exports = function globParent(str) { /***/ }), -/* 605 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -70221,7 +70203,7 @@ module.exports = function globParent(str) { * Released under the MIT License. */ -var isExtglob = __webpack_require__(606); +var isExtglob = __webpack_require__(605); var chars = { '{': '}', '(': ')', '[': ']'}; var strictRegex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; var relaxedRegex = /\\(.)|(^!|[*?{}()[\]]|\(\?)/; @@ -70265,7 +70247,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 606 */ +/* 605 */ /***/ (function(module, exports) { /*! @@ -70291,16 +70273,16 @@ module.exports = function isExtglob(str) { /***/ }), -/* 607 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const util = __webpack_require__(29); -const braces = __webpack_require__(608); -const picomatch = __webpack_require__(618); -const utils = __webpack_require__(621); +const braces = __webpack_require__(607); +const picomatch = __webpack_require__(617); +const utils = __webpack_require__(620); const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); /** @@ -70765,16 +70747,16 @@ module.exports = micromatch; /***/ }), -/* 608 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(609); -const compile = __webpack_require__(611); -const expand = __webpack_require__(615); -const parse = __webpack_require__(616); +const stringify = __webpack_require__(608); +const compile = __webpack_require__(610); +const expand = __webpack_require__(614); +const parse = __webpack_require__(615); /** * Expand the given pattern or create a regex-compatible string. @@ -70942,13 +70924,13 @@ module.exports = braces; /***/ }), -/* 609 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(610); +const utils = __webpack_require__(609); module.exports = (ast, options = {}) => { let stringify = (node, parent = {}) => { @@ -70981,7 +70963,7 @@ module.exports = (ast, options = {}) => { /***/ }), -/* 610 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71100,14 +71082,14 @@ exports.flatten = (...args) => { /***/ }), -/* 611 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(612); -const utils = __webpack_require__(610); +const fill = __webpack_require__(611); +const utils = __webpack_require__(609); const compile = (ast, options = {}) => { let walk = (node, parent = {}) => { @@ -71164,7 +71146,7 @@ module.exports = compile; /***/ }), -/* 612 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71178,7 +71160,7 @@ module.exports = compile; const util = __webpack_require__(29); -const toRegexRange = __webpack_require__(613); +const toRegexRange = __webpack_require__(612); const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); @@ -71420,7 +71402,7 @@ module.exports = fill; /***/ }), -/* 613 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71433,7 +71415,7 @@ module.exports = fill; -const isNumber = __webpack_require__(614); +const isNumber = __webpack_require__(613); const toRegexRange = (min, max, options) => { if (isNumber(min) === false) { @@ -71715,7 +71697,7 @@ module.exports = toRegexRange; /***/ }), -/* 614 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71740,15 +71722,15 @@ module.exports = function(num) { /***/ }), -/* 615 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(612); -const stringify = __webpack_require__(609); -const utils = __webpack_require__(610); +const fill = __webpack_require__(611); +const stringify = __webpack_require__(608); +const utils = __webpack_require__(609); const append = (queue = '', stash = '', enclose = false) => { let result = []; @@ -71860,13 +71842,13 @@ module.exports = expand; /***/ }), -/* 616 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(609); +const stringify = __webpack_require__(608); /** * Constants @@ -71888,7 +71870,7 @@ const { CHAR_SINGLE_QUOTE, /* ' */ CHAR_NO_BREAK_SPACE, CHAR_ZERO_WIDTH_NOBREAK_SPACE -} = __webpack_require__(617); +} = __webpack_require__(616); /** * parse @@ -72200,7 +72182,7 @@ module.exports = parse; /***/ }), -/* 617 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72264,26 +72246,26 @@ module.exports = { /***/ }), -/* 618 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(619); +module.exports = __webpack_require__(618); /***/ }), -/* 619 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const scan = __webpack_require__(620); -const parse = __webpack_require__(623); -const utils = __webpack_require__(621); +const scan = __webpack_require__(619); +const parse = __webpack_require__(622); +const utils = __webpack_require__(620); /** * Creates a matcher function from one or more glob patterns. The @@ -72586,7 +72568,7 @@ picomatch.toRegex = (source, options) => { * @return {Object} */ -picomatch.constants = __webpack_require__(622); +picomatch.constants = __webpack_require__(621); /** * Expose "picomatch" @@ -72596,13 +72578,13 @@ module.exports = picomatch; /***/ }), -/* 620 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(621); +const utils = __webpack_require__(620); const { CHAR_ASTERISK, /* * */ @@ -72620,7 +72602,7 @@ const { CHAR_RIGHT_CURLY_BRACE, /* } */ CHAR_RIGHT_PARENTHESES, /* ) */ CHAR_RIGHT_SQUARE_BRACKET /* ] */ -} = __webpack_require__(622); +} = __webpack_require__(621); const isPathSeparator = code => { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; @@ -72822,7 +72804,7 @@ module.exports = (input, options) => { /***/ }), -/* 621 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72834,7 +72816,7 @@ const { REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL, REGEX_REMOVE_BACKSLASH -} = __webpack_require__(622); +} = __webpack_require__(621); exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); @@ -72872,7 +72854,7 @@ exports.escapeLast = (input, char, lastIdx) => { /***/ }), -/* 622 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73058,14 +73040,14 @@ module.exports = { /***/ }), -/* 623 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(621); -const constants = __webpack_require__(622); +const utils = __webpack_require__(620); +const constants = __webpack_require__(621); /** * Constants @@ -74076,13 +74058,13 @@ module.exports = parse; /***/ }), -/* 624 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const merge2 = __webpack_require__(590); +const merge2 = __webpack_require__(589); function merge(streams) { const mergedStream = merge2(streams); streams.forEach((stream) => { @@ -74094,14 +74076,14 @@ exports.merge = merge; /***/ }), -/* 625 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(626); -const provider_1 = __webpack_require__(653); +const stream_1 = __webpack_require__(625); +const provider_1 = __webpack_require__(652); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -74129,16 +74111,16 @@ exports.default = ProviderAsync; /***/ }), -/* 626 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(27); -const fsStat = __webpack_require__(627); -const fsWalk = __webpack_require__(632); -const reader_1 = __webpack_require__(652); +const fsStat = __webpack_require__(626); +const fsWalk = __webpack_require__(631); +const reader_1 = __webpack_require__(651); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -74191,15 +74173,15 @@ exports.default = ReaderStream; /***/ }), -/* 627 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(628); -const sync = __webpack_require__(629); -const settings_1 = __webpack_require__(630); +const async = __webpack_require__(627); +const sync = __webpack_require__(628); +const settings_1 = __webpack_require__(629); exports.Settings = settings_1.default; function stat(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -74222,7 +74204,7 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 628 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74260,7 +74242,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 629 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74289,13 +74271,13 @@ exports.read = read; /***/ }), -/* 630 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(631); +const fs = __webpack_require__(630); class Settings { constructor(_options = {}) { this._options = _options; @@ -74312,7 +74294,7 @@ exports.default = Settings; /***/ }), -/* 631 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74335,16 +74317,16 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 632 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(633); -const stream_1 = __webpack_require__(648); -const sync_1 = __webpack_require__(649); -const settings_1 = __webpack_require__(651); +const async_1 = __webpack_require__(632); +const stream_1 = __webpack_require__(647); +const sync_1 = __webpack_require__(648); +const settings_1 = __webpack_require__(650); exports.Settings = settings_1.default; function walk(dir, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -74374,13 +74356,13 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 633 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(634); +const async_1 = __webpack_require__(633); class AsyncProvider { constructor(_root, _settings) { this._root = _root; @@ -74411,17 +74393,17 @@ function callSuccessCallback(callback, entries) { /***/ }), -/* 634 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = __webpack_require__(379); -const fsScandir = __webpack_require__(635); -const fastq = __webpack_require__(644); -const common = __webpack_require__(646); -const reader_1 = __webpack_require__(647); +const fsScandir = __webpack_require__(634); +const fastq = __webpack_require__(643); +const common = __webpack_require__(645); +const reader_1 = __webpack_require__(646); class AsyncReader extends reader_1.default { constructor(_root, _settings) { super(_root, _settings); @@ -74511,15 +74493,15 @@ exports.default = AsyncReader; /***/ }), -/* 635 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(636); -const sync = __webpack_require__(641); -const settings_1 = __webpack_require__(642); +const async = __webpack_require__(635); +const sync = __webpack_require__(640); +const settings_1 = __webpack_require__(641); exports.Settings = settings_1.default; function scandir(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -74542,16 +74524,16 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 636 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(627); -const rpl = __webpack_require__(637); -const constants_1 = __webpack_require__(638); -const utils = __webpack_require__(639); +const fsStat = __webpack_require__(626); +const rpl = __webpack_require__(636); +const constants_1 = __webpack_require__(637); +const utils = __webpack_require__(638); function read(dir, settings, callback) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(dir, settings, callback); @@ -74640,7 +74622,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 637 */ +/* 636 */ /***/ (function(module, exports) { module.exports = runParallel @@ -74694,7 +74676,7 @@ function runParallel (tasks, cb) { /***/ }), -/* 638 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74710,18 +74692,18 @@ exports.IS_SUPPORT_READDIR_WITH_FILE_TYPES = MAJOR_VERSION > 10 || (MAJOR_VERSIO /***/ }), -/* 639 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(640); +const fs = __webpack_require__(639); exports.fs = fs; /***/ }), -/* 640 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74746,15 +74728,15 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 641 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(627); -const constants_1 = __webpack_require__(638); -const utils = __webpack_require__(639); +const fsStat = __webpack_require__(626); +const constants_1 = __webpack_require__(637); +const utils = __webpack_require__(638); function read(dir, settings) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(dir, settings); @@ -74805,15 +74787,15 @@ exports.readdir = readdir; /***/ }), -/* 642 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const fsStat = __webpack_require__(627); -const fs = __webpack_require__(643); +const fsStat = __webpack_require__(626); +const fs = __webpack_require__(642); class Settings { constructor(_options = {}) { this._options = _options; @@ -74836,7 +74818,7 @@ exports.default = Settings; /***/ }), -/* 643 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74861,13 +74843,13 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 644 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var reusify = __webpack_require__(645) +var reusify = __webpack_require__(644) function fastqueue (context, worker, concurrency) { if (typeof context === 'function') { @@ -75041,7 +75023,7 @@ module.exports = fastqueue /***/ }), -/* 645 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75081,7 +75063,7 @@ module.exports = reusify /***/ }), -/* 646 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75112,13 +75094,13 @@ exports.joinPathSegments = joinPathSegments; /***/ }), -/* 647 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const common = __webpack_require__(646); +const common = __webpack_require__(645); class Reader { constructor(_root, _settings) { this._root = _root; @@ -75130,14 +75112,14 @@ exports.default = Reader; /***/ }), -/* 648 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(27); -const async_1 = __webpack_require__(634); +const async_1 = __webpack_require__(633); class StreamProvider { constructor(_root, _settings) { this._root = _root; @@ -75167,13 +75149,13 @@ exports.default = StreamProvider; /***/ }), -/* 649 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(650); +const sync_1 = __webpack_require__(649); class SyncProvider { constructor(_root, _settings) { this._root = _root; @@ -75188,15 +75170,15 @@ exports.default = SyncProvider; /***/ }), -/* 650 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsScandir = __webpack_require__(635); -const common = __webpack_require__(646); -const reader_1 = __webpack_require__(647); +const fsScandir = __webpack_require__(634); +const common = __webpack_require__(645); +const reader_1 = __webpack_require__(646); class SyncReader extends reader_1.default { constructor() { super(...arguments); @@ -75254,14 +75236,14 @@ exports.default = SyncReader; /***/ }), -/* 651 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const fsScandir = __webpack_require__(635); +const fsScandir = __webpack_require__(634); class Settings { constructor(_options = {}) { this._options = _options; @@ -75287,15 +75269,15 @@ exports.default = Settings; /***/ }), -/* 652 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const fsStat = __webpack_require__(627); -const utils = __webpack_require__(598); +const fsStat = __webpack_require__(626); +const utils = __webpack_require__(597); class Reader { constructor(_settings) { this._settings = _settings; @@ -75327,17 +75309,17 @@ exports.default = Reader; /***/ }), -/* 653 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const deep_1 = __webpack_require__(654); -const entry_1 = __webpack_require__(655); -const error_1 = __webpack_require__(656); -const entry_2 = __webpack_require__(657); +const deep_1 = __webpack_require__(653); +const entry_1 = __webpack_require__(654); +const error_1 = __webpack_require__(655); +const entry_2 = __webpack_require__(656); class Provider { constructor(_settings) { this._settings = _settings; @@ -75382,13 +75364,13 @@ exports.default = Provider; /***/ }), -/* 654 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(598); +const utils = __webpack_require__(597); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -75448,13 +75430,13 @@ exports.default = DeepFilter; /***/ }), -/* 655 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(598); +const utils = __webpack_require__(597); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -75509,13 +75491,13 @@ exports.default = EntryFilter; /***/ }), -/* 656 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(598); +const utils = __webpack_require__(597); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -75531,13 +75513,13 @@ exports.default = ErrorFilter; /***/ }), -/* 657 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(598); +const utils = __webpack_require__(597); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -75564,15 +75546,15 @@ exports.default = EntryTransformer; /***/ }), -/* 658 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(27); -const stream_2 = __webpack_require__(626); -const provider_1 = __webpack_require__(653); +const stream_2 = __webpack_require__(625); +const provider_1 = __webpack_require__(652); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -75600,14 +75582,14 @@ exports.default = ProviderStream; /***/ }), -/* 659 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(660); -const provider_1 = __webpack_require__(653); +const sync_1 = __webpack_require__(659); +const provider_1 = __webpack_require__(652); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -75630,15 +75612,15 @@ exports.default = ProviderSync; /***/ }), -/* 660 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(627); -const fsWalk = __webpack_require__(632); -const reader_1 = __webpack_require__(652); +const fsStat = __webpack_require__(626); +const fsWalk = __webpack_require__(631); +const reader_1 = __webpack_require__(651); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -75680,7 +75662,7 @@ exports.default = ReaderSync; /***/ }), -/* 661 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75740,13 +75722,13 @@ exports.default = Settings; /***/ }), -/* 662 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(663); +const pathType = __webpack_require__(662); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -75822,7 +75804,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 663 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75872,7 +75854,7 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 664 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75880,9 +75862,9 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); const {promisify} = __webpack_require__(29); const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(596); -const gitIgnore = __webpack_require__(665); -const slash = __webpack_require__(666); +const fastGlob = __webpack_require__(595); +const gitIgnore = __webpack_require__(664); +const slash = __webpack_require__(665); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -75996,7 +75978,7 @@ module.exports.sync = options => { /***/ }), -/* 665 */ +/* 664 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -76587,7 +76569,7 @@ if ( /***/ }), -/* 666 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76605,7 +76587,7 @@ module.exports = path => { /***/ }), -/* 667 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76658,7 +76640,7 @@ module.exports = { /***/ }), -/* 668 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76680,7 +76662,7 @@ module.exports = path_ => { /***/ }), -/* 669 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76708,7 +76690,7 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 670 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { const assert = __webpack_require__(30) @@ -76716,7 +76698,7 @@ const path = __webpack_require__(16) const fs = __webpack_require__(23) let glob = undefined try { - glob = __webpack_require__(591) + glob = __webpack_require__(590) } catch (_err) { // treat glob as optional. } @@ -77082,12 +77064,12 @@ rimraf.sync = rimrafSync /***/ }), -/* 671 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(672); +const AggregateError = __webpack_require__(671); module.exports = async ( iterable, @@ -77170,13 +77152,13 @@ module.exports = async ( /***/ }), -/* 672 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(673); -const cleanStack = __webpack_require__(674); +const indentString = __webpack_require__(672); +const cleanStack = __webpack_require__(673); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -77224,7 +77206,7 @@ module.exports = AggregateError; /***/ }), -/* 673 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77266,7 +77248,7 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 674 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77313,15 +77295,15 @@ module.exports = (stack, options) => { /***/ }), -/* 675 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(676); -const cliCursor = __webpack_require__(680); -const cliSpinners = __webpack_require__(684); -const logSymbols = __webpack_require__(565); +const chalk = __webpack_require__(675); +const cliCursor = __webpack_require__(679); +const cliSpinners = __webpack_require__(683); +const logSymbols = __webpack_require__(564); class Ora { constructor(options) { @@ -77468,16 +77450,16 @@ module.exports.promise = (action, options) => { /***/ }), -/* 676 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(3); -const ansiStyles = __webpack_require__(677); -const stdoutColor = __webpack_require__(678).stdout; +const ansiStyles = __webpack_require__(676); +const stdoutColor = __webpack_require__(677).stdout; -const template = __webpack_require__(679); +const template = __webpack_require__(678); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -77703,7 +77685,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 677 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77876,7 +77858,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 678 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78018,7 +78000,7 @@ module.exports = { /***/ }), -/* 679 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78153,12 +78135,12 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 680 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(681); +const restoreCursor = __webpack_require__(680); let hidden = false; @@ -78199,12 +78181,12 @@ exports.toggle = (force, stream) => { /***/ }), -/* 681 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const onetime = __webpack_require__(682); +const onetime = __webpack_require__(681); const signalExit = __webpack_require__(377); module.exports = onetime(() => { @@ -78215,12 +78197,12 @@ module.exports = onetime(() => { /***/ }), -/* 682 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const mimicFn = __webpack_require__(683); +const mimicFn = __webpack_require__(682); module.exports = (fn, opts) => { // TODO: Remove this in v3 @@ -78261,7 +78243,7 @@ module.exports = (fn, opts) => { /***/ }), -/* 683 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78277,22 +78259,22 @@ module.exports = (to, from) => { /***/ }), -/* 684 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(685); +module.exports = __webpack_require__(684); /***/ }), -/* 685 */ +/* 684 */ /***/ (function(module) { module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]}}"); /***/ }), -/* 686 */ +/* 685 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78301,8 +78283,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(34); -/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(500); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(501); +/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(499); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(500); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -78352,7 +78334,7 @@ const RunCommand = { }; /***/ }), -/* 687 */ +/* 686 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78361,9 +78343,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(34); -/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(500); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(501); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(688); +/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(499); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(500); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(687); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -78447,13 +78429,13 @@ const WatchCommand = { }; /***/ }), -/* 688 */ +/* 687 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "waitUntilWatchIsReady", function() { return waitUntilWatchIsReady; }); -/* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(392); +/* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(391); /* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(169); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -78521,7 +78503,7 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 689 */ +/* 688 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78529,15 +78511,15 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runCommand", function() { return runCommand; }); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(690); +/* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(689); /* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(indent_string__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(691); +/* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(690); /* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(wrap_ansi__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(515); +/* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(514); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(34); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(501); -/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(698); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(699); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(500); +/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(697); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(698); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -78625,7 +78607,7 @@ function toArray(value) { } /***/ }), -/* 690 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78659,13 +78641,13 @@ module.exports = (str, count, opts) => { /***/ }), -/* 691 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringWidth = __webpack_require__(692); -const stripAnsi = __webpack_require__(696); +const stringWidth = __webpack_require__(691); +const stripAnsi = __webpack_require__(695); const ESCAPES = new Set([ '\u001B', @@ -78859,13 +78841,13 @@ module.exports = (str, cols, opts) => { /***/ }), -/* 692 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stripAnsi = __webpack_require__(693); -const isFullwidthCodePoint = __webpack_require__(695); +const stripAnsi = __webpack_require__(692); +const isFullwidthCodePoint = __webpack_require__(694); module.exports = str => { if (typeof str !== 'string' || str.length === 0) { @@ -78902,18 +78884,18 @@ module.exports = str => { /***/ }), -/* 693 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(694); +const ansiRegex = __webpack_require__(693); module.exports = input => typeof input === 'string' ? input.replace(ansiRegex(), '') : input; /***/ }), -/* 694 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78930,7 +78912,7 @@ module.exports = () => { /***/ }), -/* 695 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78983,18 +78965,18 @@ module.exports = x => { /***/ }), -/* 696 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(697); +const ansiRegex = __webpack_require__(696); module.exports = input => typeof input === 'string' ? input.replace(ansiRegex(), '') : input; /***/ }), -/* 697 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79011,7 +78993,7 @@ module.exports = () => { /***/ }), -/* 698 */ +/* 697 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -79164,7 +79146,7 @@ function addProjectToTree(tree, pathParts, project) { } /***/ }), -/* 699 */ +/* 698 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -79172,12 +79154,12 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Kibana", function() { return Kibana; }); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(700); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(699); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(704); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(703); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(501); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(579); +/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(500); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(578); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -79318,15 +79300,15 @@ class Kibana { } /***/ }), -/* 700 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const minimatch = __webpack_require__(505); -const arrayUnion = __webpack_require__(701); -const arrayDiffer = __webpack_require__(702); -const arrify = __webpack_require__(703); +const minimatch = __webpack_require__(504); +const arrayUnion = __webpack_require__(700); +const arrayDiffer = __webpack_require__(701); +const arrify = __webpack_require__(702); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -79350,7 +79332,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 701 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79362,7 +79344,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 702 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79377,7 +79359,7 @@ module.exports = arrayDiffer; /***/ }), -/* 703 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79407,7 +79389,7 @@ module.exports = arrify; /***/ }), -/* 704 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79435,15 +79417,15 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 705 */ +/* 704 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(705); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(929); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(928); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -79468,23 +79450,23 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 706 */ +/* 705 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(707); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(587); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(586); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(579); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(578); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(20); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(34); -/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(517); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(501); +/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(516); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(500); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -79616,7 +79598,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 707 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79624,13 +79606,13 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(379); const path = __webpack_require__(16); const os = __webpack_require__(11); -const pAll = __webpack_require__(708); -const arrify = __webpack_require__(710); -const globby = __webpack_require__(711); -const isGlob = __webpack_require__(605); -const cpFile = __webpack_require__(914); -const junk = __webpack_require__(926); -const CpyError = __webpack_require__(927); +const pAll = __webpack_require__(707); +const arrify = __webpack_require__(709); +const globby = __webpack_require__(710); +const isGlob = __webpack_require__(604); +const cpFile = __webpack_require__(913); +const junk = __webpack_require__(925); +const CpyError = __webpack_require__(926); const defaultOptions = { ignoreJunk: true @@ -79749,12 +79731,12 @@ module.exports = (source, destination, { /***/ }), -/* 708 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(709); +const pMap = __webpack_require__(708); module.exports = (iterable, options) => pMap(iterable, element => element(), options); // TODO: Remove this for the next major release @@ -79762,7 +79744,7 @@ module.exports.default = module.exports; /***/ }), -/* 709 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79841,7 +79823,7 @@ module.exports.default = pMap; /***/ }), -/* 710 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79871,17 +79853,17 @@ module.exports = arrify; /***/ }), -/* 711 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const arrayUnion = __webpack_require__(712); -const glob = __webpack_require__(714); -const fastGlob = __webpack_require__(719); -const dirGlob = __webpack_require__(907); -const gitignore = __webpack_require__(910); +const arrayUnion = __webpack_require__(711); +const glob = __webpack_require__(713); +const fastGlob = __webpack_require__(718); +const dirGlob = __webpack_require__(906); +const gitignore = __webpack_require__(909); const DEFAULT_FILTER = () => false; @@ -80026,12 +80008,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 712 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(713); +var arrayUniq = __webpack_require__(712); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -80039,7 +80021,7 @@ module.exports = function () { /***/ }), -/* 713 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80108,7 +80090,7 @@ if ('Set' in global) { /***/ }), -/* 714 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -80154,26 +80136,26 @@ if ('Set' in global) { module.exports = glob var fs = __webpack_require__(23) -var rp = __webpack_require__(503) -var minimatch = __webpack_require__(505) +var rp = __webpack_require__(502) +var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(715) +var inherits = __webpack_require__(714) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) -var isAbsolute = __webpack_require__(511) -var globSync = __webpack_require__(717) -var common = __webpack_require__(718) +var isAbsolute = __webpack_require__(510) +var globSync = __webpack_require__(716) +var common = __webpack_require__(717) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts var ownProp = common.ownProp -var inflight = __webpack_require__(514) +var inflight = __webpack_require__(513) var util = __webpack_require__(29) var childrenIgnored = common.childrenIgnored var isIgnored = common.isIgnored -var once = __webpack_require__(385) +var once = __webpack_require__(384) function glob (pattern, options, cb) { if (typeof options === 'function') cb = options, options = {} @@ -80904,7 +80886,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 715 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -80914,12 +80896,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(716); + module.exports = __webpack_require__(715); } /***/ }), -/* 716 */ +/* 715 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -80952,22 +80934,22 @@ if (typeof Object.create === 'function') { /***/ }), -/* 717 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync globSync.GlobSync = GlobSync var fs = __webpack_require__(23) -var rp = __webpack_require__(503) -var minimatch = __webpack_require__(505) +var rp = __webpack_require__(502) +var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(714).Glob +var Glob = __webpack_require__(713).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) -var isAbsolute = __webpack_require__(511) -var common = __webpack_require__(718) +var isAbsolute = __webpack_require__(510) +var common = __webpack_require__(717) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -81444,7 +81426,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 718 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -81462,8 +81444,8 @@ function ownProp (obj, field) { } var path = __webpack_require__(16) -var minimatch = __webpack_require__(505) -var isAbsolute = __webpack_require__(511) +var minimatch = __webpack_require__(504) +var isAbsolute = __webpack_require__(510) var Minimatch = minimatch.Minimatch function alphasorti (a, b) { @@ -81690,10 +81672,10 @@ function childrenIgnored (self, path) { /***/ }), -/* 719 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(720); +const pkg = __webpack_require__(719); module.exports = pkg.async; module.exports.default = pkg.async; @@ -81706,19 +81688,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 720 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(721); -var taskManager = __webpack_require__(722); -var reader_async_1 = __webpack_require__(878); -var reader_stream_1 = __webpack_require__(902); -var reader_sync_1 = __webpack_require__(903); -var arrayUtils = __webpack_require__(905); -var streamUtils = __webpack_require__(906); +var optionsManager = __webpack_require__(720); +var taskManager = __webpack_require__(721); +var reader_async_1 = __webpack_require__(877); +var reader_stream_1 = __webpack_require__(901); +var reader_sync_1 = __webpack_require__(902); +var arrayUtils = __webpack_require__(904); +var streamUtils = __webpack_require__(905); /** * Synchronous API. */ @@ -81784,7 +81766,7 @@ function isString(source) { /***/ }), -/* 721 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81822,13 +81804,13 @@ exports.prepare = prepare; /***/ }), -/* 722 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(723); +var patternUtils = __webpack_require__(722); /** * Generate tasks based on parent directory of each pattern. */ @@ -81919,16 +81901,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 723 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var globParent = __webpack_require__(724); -var isGlob = __webpack_require__(727); -var micromatch = __webpack_require__(728); +var globParent = __webpack_require__(723); +var isGlob = __webpack_require__(726); +var micromatch = __webpack_require__(727); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -82074,15 +82056,15 @@ exports.matchAny = matchAny; /***/ }), -/* 724 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(16); -var isglob = __webpack_require__(725); -var pathDirname = __webpack_require__(726); +var isglob = __webpack_require__(724); +var pathDirname = __webpack_require__(725); var isWin32 = __webpack_require__(11).platform() === 'win32'; module.exports = function globParent(str) { @@ -82105,7 +82087,7 @@ module.exports = function globParent(str) { /***/ }), -/* 725 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -82115,7 +82097,7 @@ module.exports = function globParent(str) { * Licensed under the MIT License. */ -var isExtglob = __webpack_require__(606); +var isExtglob = __webpack_require__(605); module.exports = function isGlob(str) { if (typeof str !== 'string' || str === '') { @@ -82136,7 +82118,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 726 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82286,7 +82268,7 @@ module.exports.win32 = win32; /***/ }), -/* 727 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -82296,7 +82278,7 @@ module.exports.win32 = win32; * Released under the MIT License. */ -var isExtglob = __webpack_require__(606); +var isExtglob = __webpack_require__(605); var chars = { '{': '}', '(': ')', '[': ']'}; module.exports = function isGlob(str, options) { @@ -82338,7 +82320,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 728 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82349,18 +82331,18 @@ module.exports = function isGlob(str, options) { */ var util = __webpack_require__(29); -var braces = __webpack_require__(729); -var toRegex = __webpack_require__(831); -var extend = __webpack_require__(839); +var braces = __webpack_require__(728); +var toRegex = __webpack_require__(830); +var extend = __webpack_require__(838); /** * Local dependencies */ -var compilers = __webpack_require__(842); -var parsers = __webpack_require__(874); -var cache = __webpack_require__(875); -var utils = __webpack_require__(876); +var compilers = __webpack_require__(841); +var parsers = __webpack_require__(873); +var cache = __webpack_require__(874); +var utils = __webpack_require__(875); var MAX_LENGTH = 1024 * 64; /** @@ -83222,7 +83204,7 @@ module.exports = micromatch; /***/ }), -/* 729 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83232,18 +83214,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(730); -var unique = __webpack_require__(742); -var extend = __webpack_require__(739); +var toRegex = __webpack_require__(729); +var unique = __webpack_require__(741); +var extend = __webpack_require__(738); /** * Local dependencies */ -var compilers = __webpack_require__(743); -var parsers = __webpack_require__(758); -var Braces = __webpack_require__(768); -var utils = __webpack_require__(744); +var compilers = __webpack_require__(742); +var parsers = __webpack_require__(757); +var Braces = __webpack_require__(767); +var utils = __webpack_require__(743); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -83547,15 +83529,15 @@ module.exports = braces; /***/ }), -/* 730 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(731); -var extend = __webpack_require__(739); -var not = __webpack_require__(741); +var define = __webpack_require__(730); +var extend = __webpack_require__(738); +var not = __webpack_require__(740); var MAX_LENGTH = 1024 * 64; /** @@ -83702,7 +83684,7 @@ module.exports.makeRe = makeRe; /***/ }), -/* 731 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83715,7 +83697,7 @@ module.exports.makeRe = makeRe; -var isDescriptor = __webpack_require__(732); +var isDescriptor = __webpack_require__(731); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -83740,7 +83722,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 732 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83753,9 +83735,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(733); -var isAccessor = __webpack_require__(734); -var isData = __webpack_require__(737); +var typeOf = __webpack_require__(732); +var isAccessor = __webpack_require__(733); +var isData = __webpack_require__(736); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -83769,7 +83751,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 733 */ +/* 732 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -83922,7 +83904,7 @@ function isBuffer(val) { /***/ }), -/* 734 */ +/* 733 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83935,7 +83917,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(735); +var typeOf = __webpack_require__(734); // accessor descriptor properties var accessor = { @@ -83998,10 +83980,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 735 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(736); +var isBuffer = __webpack_require__(735); var toString = Object.prototype.toString; /** @@ -84120,7 +84102,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 736 */ +/* 735 */ /***/ (function(module, exports) { /*! @@ -84147,7 +84129,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 737 */ +/* 736 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84160,7 +84142,7 @@ function isSlowBuffer (obj) { -var typeOf = __webpack_require__(738); +var typeOf = __webpack_require__(737); // data descriptor properties var data = { @@ -84209,10 +84191,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 738 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(736); +var isBuffer = __webpack_require__(735); var toString = Object.prototype.toString; /** @@ -84331,13 +84313,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 739 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(740); +var isObject = __webpack_require__(739); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -84371,7 +84353,7 @@ function hasOwn(obj, key) { /***/ }), -/* 740 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84391,13 +84373,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 741 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(739); +var extend = __webpack_require__(738); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -84464,7 +84446,7 @@ module.exports = toRegex; /***/ }), -/* 742 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84514,13 +84496,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 743 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(744); +var utils = __webpack_require__(743); module.exports = function(braces, options) { braces.compiler @@ -84803,25 +84785,25 @@ function hasQueue(node) { /***/ }), -/* 744 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(745); +var splitString = __webpack_require__(744); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(739); -utils.flatten = __webpack_require__(751); -utils.isObject = __webpack_require__(749); -utils.fillRange = __webpack_require__(752); -utils.repeat = __webpack_require__(757); -utils.unique = __webpack_require__(742); +utils.extend = __webpack_require__(738); +utils.flatten = __webpack_require__(750); +utils.isObject = __webpack_require__(748); +utils.fillRange = __webpack_require__(751); +utils.repeat = __webpack_require__(756); +utils.unique = __webpack_require__(741); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -85153,7 +85135,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 745 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85166,7 +85148,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(746); +var extend = __webpack_require__(745); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -85331,14 +85313,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 746 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(747); -var assignSymbols = __webpack_require__(750); +var isExtendable = __webpack_require__(746); +var assignSymbols = __webpack_require__(749); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -85398,7 +85380,7 @@ function isEnum(obj, key) { /***/ }), -/* 747 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85411,7 +85393,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(748); +var isPlainObject = __webpack_require__(747); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -85419,7 +85401,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 748 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85432,7 +85414,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(749); +var isObject = __webpack_require__(748); function isObjectObject(o) { return isObject(o) === true @@ -85463,7 +85445,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 749 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85482,7 +85464,7 @@ module.exports = function isObject(val) { /***/ }), -/* 750 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85529,7 +85511,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 751 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85558,7 +85540,7 @@ function flat(arr, res) { /***/ }), -/* 752 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85572,10 +85554,10 @@ function flat(arr, res) { var util = __webpack_require__(29); -var isNumber = __webpack_require__(753); -var extend = __webpack_require__(739); -var repeat = __webpack_require__(755); -var toRegex = __webpack_require__(756); +var isNumber = __webpack_require__(752); +var extend = __webpack_require__(738); +var repeat = __webpack_require__(754); +var toRegex = __webpack_require__(755); /** * Return a range of numbers or letters. @@ -85773,7 +85755,7 @@ module.exports = fillRange; /***/ }), -/* 753 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85786,7 +85768,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(754); +var typeOf = __webpack_require__(753); module.exports = function isNumber(num) { var type = typeOf(num); @@ -85802,10 +85784,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 754 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(736); +var isBuffer = __webpack_require__(735); var toString = Object.prototype.toString; /** @@ -85924,7 +85906,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 755 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86001,7 +85983,7 @@ function repeat(str, num) { /***/ }), -/* 756 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86014,8 +85996,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(755); -var isNumber = __webpack_require__(753); +var repeat = __webpack_require__(754); +var isNumber = __webpack_require__(752); var cache = {}; function toRegexRange(min, max, options) { @@ -86302,7 +86284,7 @@ module.exports = toRegexRange; /***/ }), -/* 757 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86327,14 +86309,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 758 */ +/* 757 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(759); -var utils = __webpack_require__(744); +var Node = __webpack_require__(758); +var utils = __webpack_require__(743); /** * Braces parsers @@ -86694,15 +86676,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 759 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(749); -var define = __webpack_require__(760); -var utils = __webpack_require__(767); +var isObject = __webpack_require__(748); +var define = __webpack_require__(759); +var utils = __webpack_require__(766); var ownNames; /** @@ -87193,7 +87175,7 @@ exports = module.exports = Node; /***/ }), -/* 760 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87206,7 +87188,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(761); +var isDescriptor = __webpack_require__(760); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -87231,7 +87213,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 761 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87244,9 +87226,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(762); -var isAccessor = __webpack_require__(763); -var isData = __webpack_require__(765); +var typeOf = __webpack_require__(761); +var isAccessor = __webpack_require__(762); +var isData = __webpack_require__(764); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -87260,7 +87242,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 762 */ +/* 761 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87395,7 +87377,7 @@ function isBuffer(val) { /***/ }), -/* 763 */ +/* 762 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87408,7 +87390,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(764); +var typeOf = __webpack_require__(763); // accessor descriptor properties var accessor = { @@ -87471,7 +87453,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 764 */ +/* 763 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87606,7 +87588,7 @@ function isBuffer(val) { /***/ }), -/* 765 */ +/* 764 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87619,7 +87601,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(766); +var typeOf = __webpack_require__(765); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -87662,7 +87644,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 766 */ +/* 765 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87797,13 +87779,13 @@ function isBuffer(val) { /***/ }), -/* 767 */ +/* 766 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(754); +var typeOf = __webpack_require__(753); var utils = module.exports; /** @@ -88823,17 +88805,17 @@ function assert(val, message) { /***/ }), -/* 768 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(739); -var Snapdragon = __webpack_require__(769); -var compilers = __webpack_require__(743); -var parsers = __webpack_require__(758); -var utils = __webpack_require__(744); +var extend = __webpack_require__(738); +var Snapdragon = __webpack_require__(768); +var compilers = __webpack_require__(742); +var parsers = __webpack_require__(757); +var utils = __webpack_require__(743); /** * Customize Snapdragon parser and renderer @@ -88934,17 +88916,17 @@ module.exports = Braces; /***/ }), -/* 769 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(770); -var define = __webpack_require__(731); -var Compiler = __webpack_require__(799); -var Parser = __webpack_require__(828); -var utils = __webpack_require__(808); +var Base = __webpack_require__(769); +var define = __webpack_require__(730); +var Compiler = __webpack_require__(798); +var Parser = __webpack_require__(827); +var utils = __webpack_require__(807); var regexCache = {}; var cache = {}; @@ -89115,20 +89097,20 @@ module.exports.Parser = Parser; /***/ }), -/* 770 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var define = __webpack_require__(771); -var CacheBase = __webpack_require__(772); -var Emitter = __webpack_require__(773); -var isObject = __webpack_require__(749); -var merge = __webpack_require__(790); -var pascal = __webpack_require__(793); -var cu = __webpack_require__(794); +var define = __webpack_require__(770); +var CacheBase = __webpack_require__(771); +var Emitter = __webpack_require__(772); +var isObject = __webpack_require__(748); +var merge = __webpack_require__(789); +var pascal = __webpack_require__(792); +var cu = __webpack_require__(793); /** * Optionally define a custom `cache` namespace to use. @@ -89557,7 +89539,7 @@ module.exports.namespace = namespace; /***/ }), -/* 771 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89570,7 +89552,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(761); +var isDescriptor = __webpack_require__(760); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -89595,21 +89577,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 772 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(749); -var Emitter = __webpack_require__(773); -var visit = __webpack_require__(774); -var toPath = __webpack_require__(777); -var union = __webpack_require__(778); -var del = __webpack_require__(782); -var get = __webpack_require__(780); -var has = __webpack_require__(787); -var set = __webpack_require__(781); +var isObject = __webpack_require__(748); +var Emitter = __webpack_require__(772); +var visit = __webpack_require__(773); +var toPath = __webpack_require__(776); +var union = __webpack_require__(777); +var del = __webpack_require__(781); +var get = __webpack_require__(779); +var has = __webpack_require__(786); +var set = __webpack_require__(780); /** * Create a `Cache` constructor that when instantiated will @@ -89863,7 +89845,7 @@ module.exports.namespace = namespace; /***/ }), -/* 773 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { @@ -90032,7 +90014,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 774 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90045,8 +90027,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(775); -var mapVisit = __webpack_require__(776); +var visit = __webpack_require__(774); +var mapVisit = __webpack_require__(775); module.exports = function(collection, method, val) { var result; @@ -90069,7 +90051,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 775 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90082,7 +90064,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(749); +var isObject = __webpack_require__(748); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -90109,14 +90091,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 776 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var visit = __webpack_require__(775); +var visit = __webpack_require__(774); /** * Map `visit` over an array of objects. @@ -90153,7 +90135,7 @@ function isObject(val) { /***/ }), -/* 777 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90166,7 +90148,7 @@ function isObject(val) { -var typeOf = __webpack_require__(754); +var typeOf = __webpack_require__(753); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -90193,16 +90175,16 @@ function filter(arr) { /***/ }), -/* 778 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(740); -var union = __webpack_require__(779); -var get = __webpack_require__(780); -var set = __webpack_require__(781); +var isObject = __webpack_require__(739); +var union = __webpack_require__(778); +var get = __webpack_require__(779); +var set = __webpack_require__(780); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -90230,7 +90212,7 @@ function arrayify(val) { /***/ }), -/* 779 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90266,7 +90248,7 @@ module.exports = function union(init) { /***/ }), -/* 780 */ +/* 779 */ /***/ (function(module, exports) { /*! @@ -90322,7 +90304,7 @@ function toString(val) { /***/ }), -/* 781 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90335,10 +90317,10 @@ function toString(val) { -var split = __webpack_require__(745); -var extend = __webpack_require__(739); -var isPlainObject = __webpack_require__(748); -var isObject = __webpack_require__(740); +var split = __webpack_require__(744); +var extend = __webpack_require__(738); +var isPlainObject = __webpack_require__(747); +var isObject = __webpack_require__(739); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -90384,7 +90366,7 @@ function isValidKey(key) { /***/ }), -/* 782 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90397,8 +90379,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(749); -var has = __webpack_require__(783); +var isObject = __webpack_require__(748); +var has = __webpack_require__(782); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -90423,7 +90405,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 783 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90436,9 +90418,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(784); -var hasValues = __webpack_require__(786); -var get = __webpack_require__(780); +var isObject = __webpack_require__(783); +var hasValues = __webpack_require__(785); +var get = __webpack_require__(779); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -90449,7 +90431,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 784 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90462,7 +90444,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(785); +var isArray = __webpack_require__(784); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -90470,7 +90452,7 @@ module.exports = function isObject(val) { /***/ }), -/* 785 */ +/* 784 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -90481,7 +90463,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 786 */ +/* 785 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90524,7 +90506,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 787 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90537,9 +90519,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(749); -var hasValues = __webpack_require__(788); -var get = __webpack_require__(780); +var isObject = __webpack_require__(748); +var hasValues = __webpack_require__(787); +var get = __webpack_require__(779); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -90547,7 +90529,7 @@ module.exports = function(val, prop) { /***/ }), -/* 788 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90560,8 +90542,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(789); -var isNumber = __webpack_require__(753); +var typeOf = __webpack_require__(788); +var isNumber = __webpack_require__(752); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -90614,10 +90596,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 789 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(736); +var isBuffer = __webpack_require__(735); var toString = Object.prototype.toString; /** @@ -90739,14 +90721,14 @@ module.exports = function kindOf(val) { /***/ }), -/* 790 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(791); -var forIn = __webpack_require__(792); +var isExtendable = __webpack_require__(790); +var forIn = __webpack_require__(791); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -90810,7 +90792,7 @@ module.exports = mixinDeep; /***/ }), -/* 791 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90823,7 +90805,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(748); +var isPlainObject = __webpack_require__(747); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -90831,7 +90813,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 792 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90854,7 +90836,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 793 */ +/* 792 */ /***/ (function(module, exports) { /*! @@ -90881,14 +90863,14 @@ module.exports = pascalcase; /***/ }), -/* 794 */ +/* 793 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var utils = __webpack_require__(795); +var utils = __webpack_require__(794); /** * Expose class utils @@ -91253,7 +91235,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 795 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91267,10 +91249,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(779); -utils.define = __webpack_require__(731); -utils.isObj = __webpack_require__(749); -utils.staticExtend = __webpack_require__(796); +utils.union = __webpack_require__(778); +utils.define = __webpack_require__(730); +utils.isObj = __webpack_require__(748); +utils.staticExtend = __webpack_require__(795); /** @@ -91281,7 +91263,7 @@ module.exports = utils; /***/ }), -/* 796 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91294,8 +91276,8 @@ module.exports = utils; -var copy = __webpack_require__(797); -var define = __webpack_require__(731); +var copy = __webpack_require__(796); +var define = __webpack_require__(730); var util = __webpack_require__(29); /** @@ -91378,15 +91360,15 @@ module.exports = extend; /***/ }), -/* 797 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(754); -var copyDescriptor = __webpack_require__(798); -var define = __webpack_require__(731); +var typeOf = __webpack_require__(753); +var copyDescriptor = __webpack_require__(797); +var define = __webpack_require__(730); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -91559,7 +91541,7 @@ module.exports.has = has; /***/ }), -/* 798 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91647,16 +91629,16 @@ function isObject(val) { /***/ }), -/* 799 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(800); -var define = __webpack_require__(731); -var debug = __webpack_require__(802)('snapdragon:compiler'); -var utils = __webpack_require__(808); +var use = __webpack_require__(799); +var define = __webpack_require__(730); +var debug = __webpack_require__(801)('snapdragon:compiler'); +var utils = __webpack_require__(807); /** * Create a new `Compiler` with the given `options`. @@ -91810,7 +91792,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(827); + var sourcemaps = __webpack_require__(826); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -91831,7 +91813,7 @@ module.exports = Compiler; /***/ }), -/* 800 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91844,7 +91826,7 @@ module.exports = Compiler; -var utils = __webpack_require__(801); +var utils = __webpack_require__(800); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -91959,7 +91941,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 801 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91973,8 +91955,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(731); -utils.isObject = __webpack_require__(749); +utils.define = __webpack_require__(730); +utils.isObject = __webpack_require__(748); utils.isString = function(val) { @@ -91989,7 +91971,7 @@ module.exports = utils; /***/ }), -/* 802 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91998,14 +91980,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(803); + module.exports = __webpack_require__(802); } else { - module.exports = __webpack_require__(806); + module.exports = __webpack_require__(805); } /***/ }), -/* 803 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -92014,7 +91996,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(804); +exports = module.exports = __webpack_require__(803); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -92196,7 +92178,7 @@ function localstorage() { /***/ }), -/* 804 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { @@ -92212,7 +92194,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(805); +exports.humanize = __webpack_require__(804); /** * The currently active debug mode names, and names to skip. @@ -92404,7 +92386,7 @@ function coerce(val) { /***/ }), -/* 805 */ +/* 804 */ /***/ (function(module, exports) { /** @@ -92562,14 +92544,14 @@ function plural(ms, n, name) { /***/ }), -/* 806 */ +/* 805 */ /***/ (function(module, exports, __webpack_require__) { /** * Module dependencies. */ -var tty = __webpack_require__(480); +var tty = __webpack_require__(479); var util = __webpack_require__(29); /** @@ -92578,7 +92560,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(804); +exports = module.exports = __webpack_require__(803); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -92757,7 +92739,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(807); + var net = __webpack_require__(806); stream = new net.Socket({ fd: fd, readable: false, @@ -92816,13 +92798,13 @@ exports.enable(load()); /***/ }), -/* 807 */ +/* 806 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 808 */ +/* 807 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92832,9 +92814,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(739); -exports.SourceMap = __webpack_require__(809); -exports.sourceMapResolve = __webpack_require__(820); +exports.extend = __webpack_require__(738); +exports.SourceMap = __webpack_require__(808); +exports.sourceMapResolve = __webpack_require__(819); /** * Convert backslash in the given string to forward slashes @@ -92877,7 +92859,7 @@ exports.last = function(arr, n) { /***/ }), -/* 809 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -92885,13 +92867,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(810).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(816).SourceMapConsumer; -exports.SourceNode = __webpack_require__(819).SourceNode; +exports.SourceMapGenerator = __webpack_require__(809).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(815).SourceMapConsumer; +exports.SourceNode = __webpack_require__(818).SourceNode; /***/ }), -/* 810 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -92901,10 +92883,10 @@ exports.SourceNode = __webpack_require__(819).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(811); -var util = __webpack_require__(813); -var ArraySet = __webpack_require__(814).ArraySet; -var MappingList = __webpack_require__(815).MappingList; +var base64VLQ = __webpack_require__(810); +var util = __webpack_require__(812); +var ArraySet = __webpack_require__(813).ArraySet; +var MappingList = __webpack_require__(814).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -93313,7 +93295,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 811 */ +/* 810 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93353,7 +93335,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(812); +var base64 = __webpack_require__(811); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -93459,7 +93441,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 812 */ +/* 811 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93532,7 +93514,7 @@ exports.decode = function (charCode) { /***/ }), -/* 813 */ +/* 812 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93955,7 +93937,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 814 */ +/* 813 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93965,7 +93947,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(813); +var util = __webpack_require__(812); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -94082,7 +94064,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 815 */ +/* 814 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94092,7 +94074,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(813); +var util = __webpack_require__(812); /** * Determine whether mappingB is after mappingA with respect to generated @@ -94167,7 +94149,7 @@ exports.MappingList = MappingList; /***/ }), -/* 816 */ +/* 815 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94177,11 +94159,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(813); -var binarySearch = __webpack_require__(817); -var ArraySet = __webpack_require__(814).ArraySet; -var base64VLQ = __webpack_require__(811); -var quickSort = __webpack_require__(818).quickSort; +var util = __webpack_require__(812); +var binarySearch = __webpack_require__(816); +var ArraySet = __webpack_require__(813).ArraySet; +var base64VLQ = __webpack_require__(810); +var quickSort = __webpack_require__(817).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -95255,7 +95237,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 817 */ +/* 816 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95372,7 +95354,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 818 */ +/* 817 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95492,7 +95474,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 819 */ +/* 818 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95502,8 +95484,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(810).SourceMapGenerator; -var util = __webpack_require__(813); +var SourceMapGenerator = __webpack_require__(809).SourceMapGenerator; +var util = __webpack_require__(812); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -95911,17 +95893,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 820 */ +/* 819 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(821) -var resolveUrl = __webpack_require__(822) -var decodeUriComponent = __webpack_require__(823) -var urix = __webpack_require__(825) -var atob = __webpack_require__(826) +var sourceMappingURL = __webpack_require__(820) +var resolveUrl = __webpack_require__(821) +var decodeUriComponent = __webpack_require__(822) +var urix = __webpack_require__(824) +var atob = __webpack_require__(825) @@ -96219,7 +96201,7 @@ module.exports = { /***/ }), -/* 821 */ +/* 820 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -96282,13 +96264,13 @@ void (function(root, factory) { /***/ }), -/* 822 */ +/* 821 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var url = __webpack_require__(454) +var url = __webpack_require__(453) function resolveUrl(/* ...urls */) { return Array.prototype.reduce.call(arguments, function(resolved, nextUrl) { @@ -96300,13 +96282,13 @@ module.exports = resolveUrl /***/ }), -/* 823 */ +/* 822 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(824) +var decodeUriComponent = __webpack_require__(823) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -96317,7 +96299,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 824 */ +/* 823 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96418,7 +96400,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 825 */ +/* 824 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96441,7 +96423,7 @@ module.exports = urix /***/ }), -/* 826 */ +/* 825 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96455,7 +96437,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 827 */ +/* 826 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96463,8 +96445,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(23); var path = __webpack_require__(16); -var define = __webpack_require__(731); -var utils = __webpack_require__(808); +var define = __webpack_require__(730); +var utils = __webpack_require__(807); /** * Expose `mixin()`. @@ -96607,19 +96589,19 @@ exports.comment = function(node) { /***/ }), -/* 828 */ +/* 827 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(800); +var use = __webpack_require__(799); var util = __webpack_require__(29); -var Cache = __webpack_require__(829); -var define = __webpack_require__(731); -var debug = __webpack_require__(802)('snapdragon:parser'); -var Position = __webpack_require__(830); -var utils = __webpack_require__(808); +var Cache = __webpack_require__(828); +var define = __webpack_require__(730); +var debug = __webpack_require__(801)('snapdragon:parser'); +var Position = __webpack_require__(829); +var utils = __webpack_require__(807); /** * Create a new `Parser` with the given `input` and `options`. @@ -97147,7 +97129,7 @@ module.exports = Parser; /***/ }), -/* 829 */ +/* 828 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97254,13 +97236,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 830 */ +/* 829 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(731); +var define = __webpack_require__(730); /** * Store position for a node @@ -97275,16 +97257,16 @@ module.exports = function Position(start, parser) { /***/ }), -/* 831 */ +/* 830 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(832); -var define = __webpack_require__(838); -var extend = __webpack_require__(839); -var not = __webpack_require__(841); +var safe = __webpack_require__(831); +var define = __webpack_require__(837); +var extend = __webpack_require__(838); +var not = __webpack_require__(840); var MAX_LENGTH = 1024 * 64; /** @@ -97437,10 +97419,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 832 */ +/* 831 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(833); +var parse = __webpack_require__(832); var types = parse.types; module.exports = function (re, opts) { @@ -97486,13 +97468,13 @@ function isRegExp (x) { /***/ }), -/* 833 */ +/* 832 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(834); -var types = __webpack_require__(835); -var sets = __webpack_require__(836); -var positions = __webpack_require__(837); +var util = __webpack_require__(833); +var types = __webpack_require__(834); +var sets = __webpack_require__(835); +var positions = __webpack_require__(836); module.exports = function(regexpStr) { @@ -97774,11 +97756,11 @@ module.exports.types = types; /***/ }), -/* 834 */ +/* 833 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(835); -var sets = __webpack_require__(836); +var types = __webpack_require__(834); +var sets = __webpack_require__(835); // All of these are private and only used by randexp. @@ -97891,7 +97873,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 835 */ +/* 834 */ /***/ (function(module, exports) { module.exports = { @@ -97907,10 +97889,10 @@ module.exports = { /***/ }), -/* 836 */ +/* 835 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(835); +var types = __webpack_require__(834); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -97995,10 +97977,10 @@ exports.anyChar = function() { /***/ }), -/* 837 */ +/* 836 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(835); +var types = __webpack_require__(834); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -98018,7 +98000,7 @@ exports.end = function() { /***/ }), -/* 838 */ +/* 837 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98031,8 +98013,8 @@ exports.end = function() { -var isobject = __webpack_require__(749); -var isDescriptor = __webpack_require__(761); +var isobject = __webpack_require__(748); +var isDescriptor = __webpack_require__(760); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -98063,14 +98045,14 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 839 */ +/* 838 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(840); -var assignSymbols = __webpack_require__(750); +var isExtendable = __webpack_require__(839); +var assignSymbols = __webpack_require__(749); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -98130,7 +98112,7 @@ function isEnum(obj, key) { /***/ }), -/* 840 */ +/* 839 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98143,7 +98125,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(748); +var isPlainObject = __webpack_require__(747); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -98151,14 +98133,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 841 */ +/* 840 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(839); -var safe = __webpack_require__(832); +var extend = __webpack_require__(838); +var safe = __webpack_require__(831); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -98230,14 +98212,14 @@ module.exports = toRegex; /***/ }), -/* 842 */ +/* 841 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(843); -var extglob = __webpack_require__(858); +var nanomatch = __webpack_require__(842); +var extglob = __webpack_require__(857); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -98314,7 +98296,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 843 */ +/* 842 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98325,17 +98307,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(29); -var toRegex = __webpack_require__(730); -var extend = __webpack_require__(844); +var toRegex = __webpack_require__(729); +var extend = __webpack_require__(843); /** * Local dependencies */ -var compilers = __webpack_require__(846); -var parsers = __webpack_require__(847); -var cache = __webpack_require__(850); -var utils = __webpack_require__(852); +var compilers = __webpack_require__(845); +var parsers = __webpack_require__(846); +var cache = __webpack_require__(849); +var utils = __webpack_require__(851); var MAX_LENGTH = 1024 * 64; /** @@ -99159,14 +99141,14 @@ module.exports = nanomatch; /***/ }), -/* 844 */ +/* 843 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(845); -var assignSymbols = __webpack_require__(750); +var isExtendable = __webpack_require__(844); +var assignSymbols = __webpack_require__(749); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -99226,7 +99208,7 @@ function isEnum(obj, key) { /***/ }), -/* 845 */ +/* 844 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99239,7 +99221,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(748); +var isPlainObject = __webpack_require__(747); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -99247,7 +99229,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 846 */ +/* 845 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99593,15 +99575,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 847 */ +/* 846 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(741); -var toRegex = __webpack_require__(730); -var isOdd = __webpack_require__(848); +var regexNot = __webpack_require__(740); +var toRegex = __webpack_require__(729); +var isOdd = __webpack_require__(847); /** * Characters to use in negation regex (we want to "not" match @@ -99987,7 +99969,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 848 */ +/* 847 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100000,7 +99982,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(849); +var isNumber = __webpack_require__(848); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -100014,7 +99996,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 849 */ +/* 848 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100042,14 +100024,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 850 */ +/* 849 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(851))(); +module.exports = new (__webpack_require__(850))(); /***/ }), -/* 851 */ +/* 850 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100062,7 +100044,7 @@ module.exports = new (__webpack_require__(851))(); -var MapCache = __webpack_require__(829); +var MapCache = __webpack_require__(828); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -100184,7 +100166,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 852 */ +/* 851 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100197,14 +100179,14 @@ var path = __webpack_require__(16); * Module dependencies */ -var isWindows = __webpack_require__(853)(); -var Snapdragon = __webpack_require__(769); -utils.define = __webpack_require__(854); -utils.diff = __webpack_require__(855); -utils.extend = __webpack_require__(844); -utils.pick = __webpack_require__(856); -utils.typeOf = __webpack_require__(857); -utils.unique = __webpack_require__(742); +var isWindows = __webpack_require__(852)(); +var Snapdragon = __webpack_require__(768); +utils.define = __webpack_require__(853); +utils.diff = __webpack_require__(854); +utils.extend = __webpack_require__(843); +utils.pick = __webpack_require__(855); +utils.typeOf = __webpack_require__(856); +utils.unique = __webpack_require__(741); /** * Returns true if the given value is effectively an empty string @@ -100570,7 +100552,7 @@ utils.unixify = function(options) { /***/ }), -/* 853 */ +/* 852 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -100598,7 +100580,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 854 */ +/* 853 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100611,8 +100593,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(749); -var isDescriptor = __webpack_require__(761); +var isobject = __webpack_require__(748); +var isDescriptor = __webpack_require__(760); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -100643,7 +100625,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 855 */ +/* 854 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100697,7 +100679,7 @@ function diffArray(one, two) { /***/ }), -/* 856 */ +/* 855 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100710,7 +100692,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(749); +var isObject = __webpack_require__(748); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -100739,7 +100721,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 857 */ +/* 856 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -100874,7 +100856,7 @@ function isBuffer(val) { /***/ }), -/* 858 */ +/* 857 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100884,18 +100866,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(739); -var unique = __webpack_require__(742); -var toRegex = __webpack_require__(730); +var extend = __webpack_require__(738); +var unique = __webpack_require__(741); +var toRegex = __webpack_require__(729); /** * Local dependencies */ -var compilers = __webpack_require__(859); -var parsers = __webpack_require__(870); -var Extglob = __webpack_require__(873); -var utils = __webpack_require__(872); +var compilers = __webpack_require__(858); +var parsers = __webpack_require__(869); +var Extglob = __webpack_require__(872); +var utils = __webpack_require__(871); var MAX_LENGTH = 1024 * 64; /** @@ -101212,13 +101194,13 @@ module.exports = extglob; /***/ }), -/* 859 */ +/* 858 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(860); +var brackets = __webpack_require__(859); /** * Extglob compilers @@ -101388,7 +101370,7 @@ module.exports = function(extglob) { /***/ }), -/* 860 */ +/* 859 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101398,17 +101380,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(861); -var parsers = __webpack_require__(863); +var compilers = __webpack_require__(860); +var parsers = __webpack_require__(862); /** * Module dependencies */ -var debug = __webpack_require__(865)('expand-brackets'); -var extend = __webpack_require__(739); -var Snapdragon = __webpack_require__(769); -var toRegex = __webpack_require__(730); +var debug = __webpack_require__(864)('expand-brackets'); +var extend = __webpack_require__(738); +var Snapdragon = __webpack_require__(768); +var toRegex = __webpack_require__(729); /** * Parses the given POSIX character class `pattern` and returns a @@ -101606,13 +101588,13 @@ module.exports = brackets; /***/ }), -/* 861 */ +/* 860 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(862); +var posix = __webpack_require__(861); module.exports = function(brackets) { brackets.compiler @@ -101700,7 +101682,7 @@ module.exports = function(brackets) { /***/ }), -/* 862 */ +/* 861 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101729,14 +101711,14 @@ module.exports = { /***/ }), -/* 863 */ +/* 862 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(864); -var define = __webpack_require__(731); +var utils = __webpack_require__(863); +var define = __webpack_require__(730); /** * Text regex @@ -101955,14 +101937,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 864 */ +/* 863 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(730); -var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(729); +var regexNot = __webpack_require__(740); var cached; /** @@ -101996,7 +101978,7 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 865 */ +/* 864 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -102005,14 +101987,14 @@ exports.createRegex = function(pattern, include) { */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(866); + module.exports = __webpack_require__(865); } else { - module.exports = __webpack_require__(869); + module.exports = __webpack_require__(868); } /***/ }), -/* 866 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -102021,7 +102003,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(867); +exports = module.exports = __webpack_require__(866); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -102203,7 +102185,7 @@ function localstorage() { /***/ }), -/* 867 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { @@ -102219,7 +102201,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(868); +exports.humanize = __webpack_require__(867); /** * The currently active debug mode names, and names to skip. @@ -102411,7 +102393,7 @@ function coerce(val) { /***/ }), -/* 868 */ +/* 867 */ /***/ (function(module, exports) { /** @@ -102569,14 +102551,14 @@ function plural(ms, n, name) { /***/ }), -/* 869 */ +/* 868 */ /***/ (function(module, exports, __webpack_require__) { /** * Module dependencies. */ -var tty = __webpack_require__(480); +var tty = __webpack_require__(479); var util = __webpack_require__(29); /** @@ -102585,7 +102567,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(867); +exports = module.exports = __webpack_require__(866); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -102764,7 +102746,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(807); + var net = __webpack_require__(806); stream = new net.Socket({ fd: fd, readable: false, @@ -102823,15 +102805,15 @@ exports.enable(load()); /***/ }), -/* 870 */ +/* 869 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(860); -var define = __webpack_require__(871); -var utils = __webpack_require__(872); +var brackets = __webpack_require__(859); +var define = __webpack_require__(870); +var utils = __webpack_require__(871); /** * Characters to use in text regex (we want to "not" match @@ -102986,7 +102968,7 @@ module.exports = parsers; /***/ }), -/* 871 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102999,7 +102981,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(761); +var isDescriptor = __webpack_require__(760); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -103024,14 +103006,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 872 */ +/* 871 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(741); -var Cache = __webpack_require__(851); +var regex = __webpack_require__(740); +var Cache = __webpack_require__(850); /** * Utils @@ -103100,7 +103082,7 @@ utils.createRegex = function(str) { /***/ }), -/* 873 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103110,16 +103092,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(769); -var define = __webpack_require__(871); -var extend = __webpack_require__(739); +var Snapdragon = __webpack_require__(768); +var define = __webpack_require__(870); +var extend = __webpack_require__(738); /** * Local dependencies */ -var compilers = __webpack_require__(859); -var parsers = __webpack_require__(870); +var compilers = __webpack_require__(858); +var parsers = __webpack_require__(869); /** * Customize Snapdragon parser and renderer @@ -103185,16 +103167,16 @@ module.exports = Extglob; /***/ }), -/* 874 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(858); -var nanomatch = __webpack_require__(843); -var regexNot = __webpack_require__(741); -var toRegex = __webpack_require__(831); +var extglob = __webpack_require__(857); +var nanomatch = __webpack_require__(842); +var regexNot = __webpack_require__(740); +var toRegex = __webpack_require__(830); var not; /** @@ -103275,14 +103257,14 @@ function textRegex(pattern) { /***/ }), -/* 875 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(851))(); +module.exports = new (__webpack_require__(850))(); /***/ }), -/* 876 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103295,13 +103277,13 @@ var path = __webpack_require__(16); * Module dependencies */ -var Snapdragon = __webpack_require__(769); -utils.define = __webpack_require__(838); -utils.diff = __webpack_require__(855); -utils.extend = __webpack_require__(839); -utils.pick = __webpack_require__(856); -utils.typeOf = __webpack_require__(877); -utils.unique = __webpack_require__(742); +var Snapdragon = __webpack_require__(768); +utils.define = __webpack_require__(837); +utils.diff = __webpack_require__(854); +utils.extend = __webpack_require__(838); +utils.pick = __webpack_require__(855); +utils.typeOf = __webpack_require__(876); +utils.unique = __webpack_require__(741); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -103598,7 +103580,7 @@ utils.unixify = function(options) { /***/ }), -/* 877 */ +/* 876 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -103733,7 +103715,7 @@ function isBuffer(val) { /***/ }), -/* 878 */ +/* 877 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103752,9 +103734,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(879); -var reader_1 = __webpack_require__(892); -var fs_stream_1 = __webpack_require__(896); +var readdir = __webpack_require__(878); +var reader_1 = __webpack_require__(891); +var fs_stream_1 = __webpack_require__(895); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -103815,15 +103797,15 @@ exports.default = ReaderAsync; /***/ }), -/* 879 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(880); -const readdirAsync = __webpack_require__(888); -const readdirStream = __webpack_require__(891); +const readdirSync = __webpack_require__(879); +const readdirAsync = __webpack_require__(887); +const readdirStream = __webpack_require__(890); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -103907,7 +103889,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 880 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103915,11 +103897,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(881); +const DirectoryReader = __webpack_require__(880); let syncFacade = { - fs: __webpack_require__(886), - forEach: __webpack_require__(887), + fs: __webpack_require__(885), + forEach: __webpack_require__(886), sync: true }; @@ -103948,7 +103930,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 881 */ +/* 880 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103957,9 +103939,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; const EventEmitter = __webpack_require__(379).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(882); -const stat = __webpack_require__(884); -const call = __webpack_require__(885); +const normalizeOptions = __webpack_require__(881); +const stat = __webpack_require__(883); +const call = __webpack_require__(884); /** * Asynchronously reads the contents of a directory and streams the results @@ -104335,14 +104317,14 @@ module.exports = DirectoryReader; /***/ }), -/* 882 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(883); +const globToRegExp = __webpack_require__(882); module.exports = normalizeOptions; @@ -104519,7 +104501,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 883 */ +/* 882 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -104656,13 +104638,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 884 */ +/* 883 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(885); +const call = __webpack_require__(884); module.exports = stat; @@ -104737,7 +104719,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 885 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104798,14 +104780,14 @@ function callOnce (fn) { /***/ }), -/* 886 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(885); +const call = __webpack_require__(884); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -104869,7 +104851,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 887 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104898,7 +104880,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 888 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104906,12 +104888,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(889); -const DirectoryReader = __webpack_require__(881); +const maybe = __webpack_require__(888); +const DirectoryReader = __webpack_require__(880); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(890), + forEach: __webpack_require__(889), async: true }; @@ -104953,7 +104935,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 889 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104980,7 +104962,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 890 */ +/* 889 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105016,7 +104998,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 891 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105024,11 +105006,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(881); +const DirectoryReader = __webpack_require__(880); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(890), + forEach: __webpack_require__(889), async: true }; @@ -105048,16 +105030,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 892 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(893); -var entry_1 = __webpack_require__(895); -var pathUtil = __webpack_require__(894); +var deep_1 = __webpack_require__(892); +var entry_1 = __webpack_require__(894); +var pathUtil = __webpack_require__(893); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -105123,14 +105105,14 @@ exports.default = Reader; /***/ }), -/* 893 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(894); -var patternUtils = __webpack_require__(723); +var pathUtils = __webpack_require__(893); +var patternUtils = __webpack_require__(722); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -105213,7 +105195,7 @@ exports.default = DeepFilter; /***/ }), -/* 894 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105244,14 +105226,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 895 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(894); -var patternUtils = __webpack_require__(723); +var pathUtils = __webpack_require__(893); +var patternUtils = __webpack_require__(722); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -105336,7 +105318,7 @@ exports.default = EntryFilter; /***/ }), -/* 896 */ +/* 895 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105356,8 +105338,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(897); -var fs_1 = __webpack_require__(901); +var fsStat = __webpack_require__(896); +var fs_1 = __webpack_require__(900); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -105407,14 +105389,14 @@ exports.default = FileSystemStream; /***/ }), -/* 897 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(898); -const statProvider = __webpack_require__(900); +const optionsManager = __webpack_require__(897); +const statProvider = __webpack_require__(899); /** * Asynchronous API. */ @@ -105445,13 +105427,13 @@ exports.statSync = statSync; /***/ }), -/* 898 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(899); +const fsAdapter = __webpack_require__(898); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -105464,7 +105446,7 @@ exports.prepare = prepare; /***/ }), -/* 899 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105487,7 +105469,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 900 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105539,7 +105521,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 901 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105570,7 +105552,7 @@ exports.default = FileSystem; /***/ }), -/* 902 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105590,9 +105572,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(879); -var reader_1 = __webpack_require__(892); -var fs_stream_1 = __webpack_require__(896); +var readdir = __webpack_require__(878); +var reader_1 = __webpack_require__(891); +var fs_stream_1 = __webpack_require__(895); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -105660,7 +105642,7 @@ exports.default = ReaderStream; /***/ }), -/* 903 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105679,9 +105661,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(879); -var reader_1 = __webpack_require__(892); -var fs_sync_1 = __webpack_require__(904); +var readdir = __webpack_require__(878); +var reader_1 = __webpack_require__(891); +var fs_sync_1 = __webpack_require__(903); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -105741,7 +105723,7 @@ exports.default = ReaderSync; /***/ }), -/* 904 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105760,8 +105742,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(897); -var fs_1 = __webpack_require__(901); +var fsStat = __webpack_require__(896); +var fs_1 = __webpack_require__(900); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -105807,7 +105789,7 @@ exports.default = FileSystemSync; /***/ }), -/* 905 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105823,13 +105805,13 @@ exports.flatten = flatten; /***/ }), -/* 906 */ +/* 905 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var merge2 = __webpack_require__(590); +var merge2 = __webpack_require__(589); /** * Merge multiple streams and propagate their errors into one stream in parallel. */ @@ -105844,13 +105826,13 @@ exports.merge = merge; /***/ }), -/* 907 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(908); +const pathType = __webpack_require__(907); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -105916,13 +105898,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 908 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(909); +const pify = __webpack_require__(908); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -105965,7 +105947,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 909 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106056,17 +106038,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 910 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(719); -const gitIgnore = __webpack_require__(911); -const pify = __webpack_require__(912); -const slash = __webpack_require__(913); +const fastGlob = __webpack_require__(718); +const gitIgnore = __webpack_require__(910); +const pify = __webpack_require__(911); +const slash = __webpack_require__(912); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -106164,7 +106146,7 @@ module.exports.sync = options => { /***/ }), -/* 911 */ +/* 910 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -106633,7 +106615,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 912 */ +/* 911 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106708,7 +106690,7 @@ module.exports = (input, options) => { /***/ }), -/* 913 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106726,17 +106708,17 @@ module.exports = input => { /***/ }), -/* 914 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const pEvent = __webpack_require__(915); -const CpFileError = __webpack_require__(918); -const fs = __webpack_require__(922); -const ProgressEmitter = __webpack_require__(925); +const pEvent = __webpack_require__(914); +const CpFileError = __webpack_require__(917); +const fs = __webpack_require__(921); +const ProgressEmitter = __webpack_require__(924); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -106850,12 +106832,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 915 */ +/* 914 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(916); +const pTimeout = __webpack_require__(915); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -107146,12 +107128,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 916 */ +/* 915 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(917); +const pFinally = __webpack_require__(916); class TimeoutError extends Error { constructor(message) { @@ -107197,7 +107179,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 917 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107219,12 +107201,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 918 */ +/* 917 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(919); +const NestedError = __webpack_require__(918); class CpFileError extends NestedError { constructor(message, nested) { @@ -107238,10 +107220,10 @@ module.exports = CpFileError; /***/ }), -/* 919 */ +/* 918 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(920); +var inherits = __webpack_require__(919); var NestedError = function (message, nested) { this.nested = nested; @@ -107292,7 +107274,7 @@ module.exports = NestedError; /***/ }), -/* 920 */ +/* 919 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -107300,12 +107282,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(921); + module.exports = __webpack_require__(920); } /***/ }), -/* 921 */ +/* 920 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -107334,16 +107316,16 @@ if (typeof Object.create === 'function') { /***/ }), -/* 922 */ +/* 921 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(923); -const pEvent = __webpack_require__(915); -const CpFileError = __webpack_require__(918); +const makeDir = __webpack_require__(922); +const pEvent = __webpack_require__(914); +const CpFileError = __webpack_require__(917); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -107440,7 +107422,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 923 */ +/* 922 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107448,7 +107430,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const {promisify} = __webpack_require__(29); -const semver = __webpack_require__(924); +const semver = __webpack_require__(923); const defaults = { mode: 0o777 & (~process.umask()), @@ -107597,7 +107579,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 924 */ +/* 923 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -109199,7 +109181,7 @@ function coerce (version, options) { /***/ }), -/* 925 */ +/* 924 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109240,7 +109222,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 926 */ +/* 925 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109286,12 +109268,12 @@ exports.default = module.exports; /***/ }), -/* 927 */ +/* 926 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(928); +const NestedError = __webpack_require__(927); class CpyError extends NestedError { constructor(message, nested) { @@ -109305,7 +109287,7 @@ module.exports = CpyError; /***/ }), -/* 928 */ +/* 927 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(29).inherits; @@ -109361,14 +109343,14 @@ module.exports = NestedError; /***/ }), -/* 929 */ +/* 928 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return prepareExternalProjectDependencies; }); -/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(517); -/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(516); +/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(516); +/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(515); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index a05e1634226e55..a236db9eee18af 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -42,7 +42,7 @@ "cpy": "^8.0.0", "dedent": "^0.7.0", "del": "^5.1.0", - "execa": "^3.2.0", + "execa": "^4.0.0", "getopts": "^2.2.4", "glob": "^7.1.2", "globby": "^8.0.1", diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index 73deadba0a6198..0b38554f7806c0 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -15,7 +15,6 @@ "@storybook/react": "^5.2.8", "@storybook/theming": "^5.2.8", "copy-webpack-plugin": "5.0.3", - "execa": "1.0.0", "fast-glob": "2.2.7", "glob-watcher": "5.0.3", "jest-specific-snapshot": "2.0.0", diff --git a/x-pack/package.json b/x-pack/package.json index 192ecd25b582c9..209e05f28b586b 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -125,7 +125,7 @@ "enzyme-adapter-react-16": "^1.15.2", "enzyme-adapter-utils": "^1.13.0", "enzyme-to-json": "^3.4.4", - "execa": "^3.2.0", + "execa": "^4.0.0", "fancy-log": "^1.3.2", "fetch-mock": "^7.3.9", "graphql-code-generator": "^0.18.2", diff --git a/yarn.lock b/yarn.lock index b4945cc3f41008..29b9b972b6a434 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13162,19 +13162,6 @@ exec-sh@^0.3.2: resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== -execa@1.0.0, execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/execa/-/execa-3.3.0.tgz#7e348eef129a1937f21ecbbd53390942653522c1" @@ -13251,10 +13238,23 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-3.2.0.tgz#18326b79c7ab7fbd6610fd900c1b9e95fa48f90a" - integrity sha512-kJJfVbI/lZE1PZYDI5VPxp8zXPO9rtxOkhpZ0jMKha56AI9y2gGVC6bkukStQf0ka5Rh15BA5m7cCCH4jmHqkw== +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.0.tgz#7f37d6ec17f09e6b8fc53288611695b6d12b9daf" + integrity sha512-JbDUxwV3BoT5ZVXQrSVbAiaXhXUkIwvbhPIwZ0N13kX+5yCzOhUNdocxB/UQRuYOHRYYwAxKYwJYc0T4D12pDA== dependencies: cross-spawn "^7.0.0" get-stream "^5.0.0" @@ -13263,7 +13263,6 @@ execa@^3.2.0: merge-stream "^2.0.0" npm-run-path "^4.0.0" onetime "^5.1.0" - p-finally "^2.0.0" signal-exit "^3.0.2" strip-final-newline "^2.0.0" From b9d2affc73ac46690b1dc0d68b625cf8a618147c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2020 17:48:42 -0700 Subject: [PATCH 25/42] Update dependency nock to v12 (#60422) * Update dependency nock to v12 * update yarn.lock file Co-authored-by: Renovate Bot Co-authored-by: spalger Co-authored-by: Elastic Machine --- package.json | 2 +- x-pack/package.json | 2 +- yarn.lock | 63 +++++++++------------------------------------ 3 files changed, 14 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index e7143826d5720a..d4524f07aaaada 100644 --- a/package.json +++ b/package.json @@ -460,7 +460,7 @@ "multistream": "^2.1.1", "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", - "nock": "10.0.6", + "nock": "12.0.3", "node-sass": "^4.13.1", "normalize-path": "^3.0.0", "nyc": "^14.1.1", diff --git a/x-pack/package.json b/x-pack/package.json index 209e05f28b586b..bc00dc21d99089 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -280,7 +280,7 @@ "moment-duration-format": "^2.3.2", "moment-timezone": "^0.5.27", "ngreact": "^0.5.1", - "nock": "10.0.6", + "nock": "12.0.3", "node-fetch": "^2.6.0", "nodemailer": "^4.7.0", "object-hash": "^1.3.1", diff --git a/yarn.lock b/yarn.lock index 29b9b972b6a434..b18bc67413cda0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6968,7 +6968,7 @@ assert@1.4.1, assert@^1.1.1: dependencies: util "0.10.3" -assertion-error@^1.0.1, assertion-error@^1.1.0: +assertion-error@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== @@ -8872,18 +8872,6 @@ chai@3.5.0: deep-eql "^0.1.3" type-detect "^1.0.0" -chai@^4.1.2: - version "4.2.0" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" - integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== - dependencies: - assertion-error "^1.1.0" - check-error "^1.0.2" - deep-eql "^3.0.1" - get-func-name "^2.0.0" - pathval "^1.1.0" - type-detect "^4.0.5" - chalk@2.4.2, chalk@^2.3.2, chalk@^2.4.2, chalk@~2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -9031,11 +9019,6 @@ check-disk-space@^2.1.0: resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-2.1.0.tgz#2e77fe62f30d9676dc37a524ea2008f40c780295" integrity sha512-f0nx9oJF/AVF8nhSYlF1EBvMNnO+CXyLwKhPvN1943iOMI9TWhQigLZm80jAf0wzQhwKkzA8XXjyvuVUeGGcVQ== -check-error@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" - integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= - check-more-types@2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" @@ -11246,13 +11229,6 @@ deep-eql@^0.1.3: dependencies: type-detect "0.1.1" -deep-eql@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" - integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== - dependencies: - type-detect "^4.0.0" - deep-equal@^1.0.0, deep-equal@^1.0.1, deep-equal@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -14699,11 +14675,6 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" - integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= - get-own-enumerable-property-symbols@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz#5c4ad87f2834c4b9b4e84549dc1e0650fb38c24b" @@ -21371,20 +21342,15 @@ no-case@^2.2.0, no-case@^2.3.2: dependencies: lower-case "^1.1.1" -nock@10.0.6: - version "10.0.6" - resolved "https://registry.yarnpkg.com/nock/-/nock-10.0.6.tgz#e6d90ee7a68b8cfc2ab7f6127e7d99aa7d13d111" - integrity sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w== +nock@12.0.3: + version "12.0.3" + resolved "https://registry.yarnpkg.com/nock/-/nock-12.0.3.tgz#83f25076dbc4c9aa82b5cdf54c9604c7a778d1c9" + integrity sha512-QNb/j8kbFnKCiyqi9C5DD0jH/FubFGj5rt9NQFONXwQm3IPB0CULECg/eS3AU1KgZb/6SwUa4/DTRKhVxkGABw== dependencies: - chai "^4.1.2" debug "^4.1.0" - deep-equal "^1.0.0" json-stringify-safe "^5.0.1" - lodash "^4.17.5" - mkdirp "^0.5.0" - propagate "^1.0.0" - qs "^6.5.1" - semver "^5.5.0" + lodash "^4.17.13" + propagate "^2.0.0" node-dir@^0.1.10: version "0.1.17" @@ -22989,11 +22955,6 @@ path2d-polyfill@^0.4.2: resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-0.4.2.tgz#594d3103838ef6b9dd4a7fd498fe9a88f1f28531" integrity sha512-JSeAnUfkFjl+Ml/EZL898ivMSbGHrOH63Mirx5EQ1ycJiryHDmj1Q7Are+uEPvenVGCUN9YbolfGfyUewJfJEg== -pathval@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" - integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= - pbf@^3.0.5: version "3.1.0" resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.1.0.tgz#f70004badcb281761eabb1e76c92f179f08189e9" @@ -23640,10 +23601,10 @@ prop-types@15.7.2, prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, pro object-assign "^4.1.1" react-is "^16.8.1" -propagate@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/propagate/-/propagate-1.0.0.tgz#00c2daeedda20e87e3782b344adba1cddd6ad709" - integrity sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk= +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== proper-lockfile@^3.2.0: version "3.2.0" @@ -29902,7 +29863,7 @@ type-detect@0.1.1: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" integrity sha1-C6XsKohWQORw6k6FBZcZANrFiCI= -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: +type-detect@4.0.8, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== From 18f973ea61de53e4aeef994719103f0589f2e091 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 18 Mar 2020 17:49:40 -0700 Subject: [PATCH 26/42] [junit] only include stdout in report for failures (#60530) * [junit] only include stdout in report for failures * fix assertion Co-authored-by: spalger --- .../kbn-test/src/mocha/__tests__/junit_report_generation.js | 2 +- packages/kbn-test/src/mocha/junit_report_generation.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js index 7472e271bd1e9c..6edd0a551ebd08 100644 --- a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js @@ -129,7 +129,7 @@ describe('dev/mocha/junit report generation', () => { name: 'SUITE SUB_SUITE never runs', 'metadata-json': '{}', }, - 'system-out': testFail['system-out'], + 'system-out': ['-- logs are only reported for failed tests --'], skipped: [''], }); }); diff --git a/packages/kbn-test/src/mocha/junit_report_generation.js b/packages/kbn-test/src/mocha/junit_report_generation.js index 95e84117106a4d..b56741b48d3670 100644 --- a/packages/kbn-test/src/mocha/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/junit_report_generation.js @@ -126,13 +126,15 @@ export function setupJUnitReportGeneration(runner, options = {}) { [...results, ...skippedResults].forEach(result => { const el = addTestcaseEl(result.node); - el.ele('system-out').dat(escapeCdata(getSnapshotOfRunnableLogs(result.node) || '')); if (result.failed) { + el.ele('system-out').dat(escapeCdata(getSnapshotOfRunnableLogs(result.node) || '')); el.ele('failure').dat(escapeCdata(inspect(result.error))); return; } + el.ele('system-out').dat('-- logs are only reported for failed tests --'); + if (result.skipped) { el.ele('skipped'); } From cf08850489df8c240e8d85c6fe99e9cc26affac9 Mon Sep 17 00:00:00 2001 From: Maggie Ghamry <46542915+maggieghamry@users.noreply.github.com> Date: Wed, 18 Mar 2020 21:36:21 -0400 Subject: [PATCH 27/42] Enhancement/update esdocs datasource (#59512) * Initial Commit Update to ESDocs datasource per team feedback * Updates Updates per Ryan's mockups * Updates II Updates per Poff's review * Updates III Update to some of the verbiage and card sizes - working on re-ordering and adding a link to the lucen query syntax * design tweaks * Adding lucene hyperlink update to add hyperlink help for Lucene query syntax * Consollidating datasources to sort Consolidating the ESDocs datasource with the rest, so that we can order them * updates for i18n updates for i18n * Updates Updates from Gail for verbiage and integrating Ryan's change for style * Update ui.ts Updates for i18n * Updates for datasource order moving the esdocs datasource to live with the rest of the UI datasources, and sorting them accordingly. * Update datasource_component.js removing console log, whoops * Update ui.ts Update to fix i18n essql issue * Update ui.ts Updates to fix i18n references for the esdocs datasource move * Update to Timelion URL I noticed that the Timelion datasource showed "Lucene query syntax" which wasn't relevant, so I updated it to "Timelion", along with a tutorial, as the link for current Timelion docs does not provide any syntax tutorial. * Update ui.ts update for i18n * Update ui.ts update for i18n * Update ui.ts Update to removed unused value - the i18n check gave me latent errors, sorry for the repost * i18n updates Updating nomenclature to get past i18n errors * Updates Code review updates to remove extraneous code * Update timelion.js update to remove extraneous comment per code review * More i18n updates translation updates to accommodate the esdocs datasource move * Update datasource_component.js Update to toggle datasource icon in selected element mode * Update ui.ts hopefully last i18n fix Co-authored-by: Ryan Keairns Co-authored-by: Elastic Machine --- .../uis/datasources/demodata.js | 2 +- .../uis}/datasources/esdocs.js | 115 ++++++++++-------- .../uis/datasources/essql.js | 3 +- .../uis/datasources/index.js | 5 +- .../uis/datasources/timelion.js | 18 ++- .../legacy/plugins/canvas/i18n/constants.ts | 2 + .../plugins/canvas/i18n/expression_types.ts | 84 ------------- .../canvas/i18n/functions/dict/demodata.ts | 2 +- x-pack/legacy/plugins/canvas/i18n/ui.ts | 101 +++++++++++++-- .../components/datasource/datasource.scss | 13 +- .../datasource/datasource_component.js | 2 +- .../datasource/datasource_selector.js | 1 + .../expression_types/datasources/index.js | 9 -- .../canvas/public/lib/find_expression_type.js | 5 +- .../public/lib/load_expression_types.js | 4 +- .../legacy/plugins/canvas/public/plugin.tsx | 3 - .../translations/translations/ja-JP.json | 34 +++--- .../translations/translations/zh-CN.json | 34 +++--- 18 files changed, 223 insertions(+), 214 deletions(-) rename x-pack/legacy/plugins/canvas/{public/expression_types => canvas_plugin_src/uis}/datasources/esdocs.js (58%) delete mode 100644 x-pack/legacy/plugins/canvas/public/expression_types/datasources/index.js diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js index 193d99e1c95339..faadfd4bb26d7a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js @@ -21,6 +21,6 @@ export const demodata = () => ({ name: 'demodata', displayName: strings.getDisplayName(), help: strings.getHelp(), - image: 'logoElasticStack', + image: 'training', template: templateFromReactComponent(DemodataDatasource), }); diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/datasources/esdocs.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/esdocs.js similarity index 58% rename from x-pack/legacy/plugins/canvas/public/expression_types/datasources/esdocs.js rename to x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/esdocs.js index eacb7e891b4824..282ec17e94c9b5 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/datasources/esdocs.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/esdocs.js @@ -6,15 +6,24 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiFormRow, EuiSelect, EuiTextArea, EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { getSimpleArg, setSimpleArg } from '../../lib/arg_helpers'; -import { ESFieldsSelect } from '../../components/es_fields_select'; -import { ESFieldSelect } from '../../components/es_field_select'; -import { ESIndexSelect } from '../../components/es_index_select'; -import { templateFromReactComponent } from '../../lib/template_from_react_component'; -import { ExpressionDataSourceStrings } from '../../../i18n'; - -const { ESDocs: strings } = ExpressionDataSourceStrings; +import { + EuiFormRow, + EuiAccordion, + EuiSelect, + EuiTextArea, + EuiCallOut, + EuiSpacer, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { getSimpleArg, setSimpleArg } from '../../../public/lib/arg_helpers'; +import { ESFieldsSelect } from '../../../public/components/es_fields_select'; +import { ESFieldSelect } from '../../../public/components/es_field_select'; +import { ESIndexSelect } from '../../../public/components/es_index_select'; +import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { DataSourceStrings, LUCENE_QUERY_URL } from '../../../i18n'; + +const { ESDocs: strings } = DataSourceStrings; const EsdocsDatasource = ({ args, updateArgs, defaultIndex }) => { const setArg = (name, value) => { @@ -74,12 +83,6 @@ const EsdocsDatasource = ({ args, updateArgs, defaultIndex }) => { return (
- -

{strings.getWarning()}

-
- - - { setArg('index', index)} /> - - setArg(getArgName(), e.target.value)} - compressed - /> - - { /> - + - setArg('sort', [field, sortOrder].join(', '))} - /> - + + + setArg('sort', [field, sortOrder].join(', '))} + /> + + + + setArg('sort', [sortField, e.target.value].join(', '))} + options={sortOptions} + compressed + /> + + + + + {strings.getQueryLabel()} + + + } + display="rowCompressed" + > + setArg(getArgName(), e.target.value)} + compressed + /> + + - - setArg('sort', [sortField, e.target.value].join(', '))} - options={sortOptions} - compressed - /> - + + + +

{strings.getWarning()}

+
); }; @@ -150,6 +165,6 @@ export const esdocs = () => ({ name: 'esdocs', displayName: strings.getDisplayName(), help: strings.getHelp(), - image: 'logoElasticsearch', + image: 'documents', template: templateFromReactComponent(EsdocsDatasource), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js index 707f2305e1368f..44e335dd7b41fc 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js @@ -95,7 +95,6 @@ export const essql = () => ({ name: 'essql', displayName: strings.getDisplayName(), help: strings.getHelp(), - // Replace this with a SQL logo when we have one in EUI - image: 'logoElasticsearch', + image: 'database', template: templateFromReactComponent(EssqlDatasource), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js index 13aa2a06306a08..5bddf1d3f4b6b9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { demodata } from './demodata'; import { essql } from './essql'; +import { esdocs } from './esdocs'; +import { demodata } from './demodata'; import { timelion } from './timelion'; -export const datasourceSpecs = [demodata, essql, timelion]; +export const datasourceSpecs = [essql, esdocs, demodata, timelion]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js index b30e43c1c3c574..b36f1a747f1203 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js @@ -13,12 +13,13 @@ import { EuiSpacer, EuiCode, EuiTextArea, + EuiText, + EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { getSimpleArg, setSimpleArg } from '../../../public/lib/arg_helpers'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; -import { DataSourceStrings, TIMELION, CANVAS } from '../../../i18n'; -import { TooltipIcon } from '../../../public/components/tooltip_icon'; +import { DataSourceStrings, TIMELION_QUERY_URL, TIMELION, CANVAS } from '../../../i18n'; const { Timelion: strings } = DataSourceStrings; @@ -86,8 +87,14 @@ const TimelionDatasource = ({ args, updateArgs, defaultIndex }) => { } + labelAppend={ + + + {strings.queryLabel()} + + + } + display="rowCompressed" > { rows={15} /> + { // TODO: Time timelion interval picker should be a drop down } @@ -124,6 +132,6 @@ export const timelion = () => ({ name: 'timelion', displayName: TIMELION, help: strings.getHelp(), - image: 'timelionApp', + image: 'visTimelion', template: templateFromReactComponent(TimelionDatasource), }); diff --git a/x-pack/legacy/plugins/canvas/i18n/constants.ts b/x-pack/legacy/plugins/canvas/i18n/constants.ts index 4cb05b0426fa11..099effc697fc56 100644 --- a/x-pack/legacy/plugins/canvas/i18n/constants.ts +++ b/x-pack/legacy/plugins/canvas/i18n/constants.ts @@ -25,6 +25,7 @@ export const JS = 'JavaScript'; export const JSON = 'JSON'; export const KIBANA = 'Kibana'; export const LUCENE = 'Lucene'; +export const LUCENE_QUERY_URL = 'https://www.elastic.co/guide/en/kibana/current/lucene-query.html'; export const MARKDOWN = 'Markdown'; export const MOMENTJS = 'MomentJS'; export const MOMENTJS_TIMEZONE_URL = 'https://momentjs.com/timezone/'; @@ -37,6 +38,7 @@ export const SQL_URL = 'https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-spec.html'; export const SVG = 'SVG'; export const TIMELION = 'Timelion'; +export const TIMELION_QUERY_URL = 'https://www.elastic.co/blog/timelion-tutorial-from-zero-to-hero'; export const TINYMATH = '`TinyMath`'; export const TINYMATH_URL = 'https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html'; diff --git a/x-pack/legacy/plugins/canvas/i18n/expression_types.ts b/x-pack/legacy/plugins/canvas/i18n/expression_types.ts index bdd190f26c97aa..5d3a3cd742bb4f 100644 --- a/x-pack/legacy/plugins/canvas/i18n/expression_types.ts +++ b/x-pack/legacy/plugins/canvas/i18n/expression_types.ts @@ -5,7 +5,6 @@ */ import { i18n } from '@kbn/i18n'; -import { LUCENE, ELASTICSEARCH } from './constants'; export const ArgTypesStrings = { Color: { @@ -143,86 +142,3 @@ export const ArgTypesStrings = { }), }, }; - -export const ExpressionDataSourceStrings = { - ESDocs: { - getDisplayName: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocsTitle', { - defaultMessage: 'Elasticsearch raw documents', - }), - getHelp: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocsLabel', { - defaultMessage: 'Pull back raw documents from elasticsearch', - }), - getWarningTitle: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.warningTitle', { - defaultMessage: 'Query with caution', - }), - getWarning: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.warningDescription', { - defaultMessage: ` - This datasource pulls directly from {elasticsearch} - without the use of aggregations. It is best used with low volume datasets and in - situations where you need to view raw documents or plot exact, non-aggregated values on a - chart.`, - values: { - elasticsearch: ELASTICSEARCH, - }, - }), - getIndexTitle: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.indexTitle', { - defaultMessage: 'Index', - }), - getIndexLabel: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.indexLabel', { - defaultMessage: 'Enter an index name or select an index pattern', - }), - getQueryTitle: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.queryTitle', { - defaultMessage: 'Query', - }), - getQueryLabel: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.queryLabel', { - defaultMessage: '{lucene} query string syntax', - values: { - lucene: LUCENE, - }, - }), - getSortFieldTitle: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.sortFieldTitle', { - defaultMessage: 'Sort Field', - }), - getSortFieldLabel: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.sortFieldLabel', { - defaultMessage: 'Document sort field', - }), - getSortOrderTitle: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.sortOrderTitle', { - defaultMessage: 'Sort Order', - }), - getSortOrderLabel: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.sortOrderLabel', { - defaultMessage: 'Document sort order', - }), - getFieldsTitle: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.fieldsTitle', { - defaultMessage: 'Fields', - }), - getFieldsLabel: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.fieldsLabel', { - defaultMessage: 'The fields to extract. Kibana scripted fields are not currently available', - }), - getFieldsWarningLabel: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.fieldsWarningLabel', { - defaultMessage: 'This datasource performs best with 10 or fewer fields', - }), - getAscendingOption: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.ascendingDropDown', { - defaultMessage: 'Ascending', - }), - getDescendingOption: () => - i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.descendingDropDown', { - defaultMessage: 'Descending', - }), - }, -}; diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/demodata.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/demodata.ts index 20c7a88ea4f4d5..caedbfdec5be4c 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/demodata.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/demodata.ts @@ -13,7 +13,7 @@ import { DemoRows } from '../../../canvas_plugin_src/functions/server/demodata/g export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.demodataHelpText', { defaultMessage: - 'A mock data set that includes project {ci} times with usernames, countries, and run phases.', + 'A sample data set that includes project {ci} times with usernames, countries, and run phases.', values: { ci: 'CI', }, diff --git a/x-pack/legacy/plugins/canvas/i18n/ui.ts b/x-pack/legacy/plugins/canvas/i18n/ui.ts index 5b94cb0435b31f..1abe56c99dc89e 100644 --- a/x-pack/legacy/plugins/canvas/i18n/ui.ts +++ b/x-pack/legacy/plugins/canvas/i18n/ui.ts @@ -308,6 +308,7 @@ export const ArgumentStrings = { }; export const DataSourceStrings = { + // Demo data source DemoData: { getDisplayName: () => i18n.translate('xpack.canvas.uis.dataSources.demoDataTitle', { @@ -319,7 +320,7 @@ export const DataSourceStrings = { }), getHelp: () => i18n.translate('xpack.canvas.uis.dataSources.demoDataLabel', { - defaultMessage: 'Mock data set with usernames, prices, projects, countries, and phases', + defaultMessage: 'Sample data set used to populate default elements', }), getDescription: () => i18n.translate('xpack.canvas.uis.dataSources.demoDataDescription', { @@ -330,6 +331,88 @@ export const DataSourceStrings = { }, }), }, + // Elasticsearch documents datasource + ESDocs: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocsTitle', { + defaultMessage: '{elasticsearch} documents', + values: { + elasticsearch: ELASTICSEARCH, + }, + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocsLabel', { + defaultMessage: 'Pull data directly from {elasticsearch} without the use of aggregations', + values: { + elasticsearch: ELASTICSEARCH, + }, + }), + getWarningTitle: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.warningTitle', { + defaultMessage: 'Query with caution', + }), + getWarning: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.warningDescription', { + defaultMessage: ` + Using this data source with larger data sets can result in slower performance. Use this source only when you need exact values.`, + }), + getIndexTitle: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.indexTitle', { + defaultMessage: 'Index', + }), + getIndexLabel: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.indexLabel', { + defaultMessage: 'Enter an index name or select an index pattern', + }), + getQueryTitle: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.queryTitle', { + defaultMessage: 'Query', + }), + getQueryLabel: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.queryLabel', { + defaultMessage: '{lucene} query string syntax', + values: { + lucene: LUCENE, + }, + }), + getSortFieldTitle: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.sortFieldTitle', { + defaultMessage: 'Sort field', + }), + getSortFieldLabel: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.sortFieldLabel', { + defaultMessage: 'Document sort field', + }), + getSortOrderTitle: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.sortOrderTitle', { + defaultMessage: 'Sort order', + }), + getSortOrderLabel: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.sortOrderLabel', { + defaultMessage: 'Document sort order', + }), + getFieldsTitle: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.fieldsTitle', { + defaultMessage: 'Fields', + }), + getFieldsLabel: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.fieldsLabel', { + defaultMessage: 'Scripted fields are unavailable', + }), + getFieldsWarningLabel: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.fieldsWarningLabel', { + defaultMessage: 'This datasource performs best with 10 or fewer fields', + }), + getAscendingOption: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.ascendingDropDown', { + defaultMessage: 'Ascending', + }), + getDescendingOption: () => + i18n.translate('xpack.canvas.uis.dataSources.esdocs.descendingDropDown', { + defaultMessage: 'Descending', + }), + }, + // Elasticsearch SQL data source Essql: { getDisplayName: () => i18n.translate('xpack.canvas.uis.dataSources.essqlTitle', { @@ -341,7 +424,7 @@ export const DataSourceStrings = { }), getHelp: () => i18n.translate('xpack.canvas.uis.dataSources.essqlLabel', { - defaultMessage: 'Use {elasticsearch} {sql} to get a data table', + defaultMessage: 'Write an {elasticsearch} {sql} query to retrieve data', values: { elasticsearch: ELASTICSEARCH, sql: SQL, @@ -353,18 +436,18 @@ export const DataSourceStrings = { }), getLabelAppend: () => i18n.translate('xpack.canvas.uis.dataSources.essql.queryTitleAppend', { - defaultMessage: 'Learn {elasticsearchShort} {sql} syntax', + defaultMessage: 'Learn {elasticsearchShort} {sql} query syntax', values: { elasticsearchShort: ELASTICSEARCH_SHORT, sql: SQL, }, }), }, + // Timelion datasource Timelion: { getAbout: () => i18n.translate('xpack.canvas.uis.dataSources.timelion.aboutDetail', { - defaultMessage: - 'Use {timelion} queries to pull back timeseries data that can be used with {canvas} elements.', + defaultMessage: 'Use {timelion} syntax in {canvas} to retrieve timeseries data', values: { timelion: TIMELION, canvas: CANVAS, @@ -372,7 +455,7 @@ export const DataSourceStrings = { }), getHelp: () => i18n.translate('xpack.canvas.uis.dataSources.timelionLabel', { - defaultMessage: 'Use {timelion} syntax to retrieve a timeseries', + defaultMessage: 'Use {timelion} syntax to retrieve timeseries data', values: { timelion: TIMELION, }, @@ -392,11 +475,11 @@ export const DataSourceStrings = { i18n.translate('xpack.canvas.uis.dataSources.timelion.intervalTitle', { defaultMessage: 'Interval', }), - getQueryHelp: () => + queryLabel: () => i18n.translate('xpack.canvas.uis.dataSources.timelion.queryLabel', { - defaultMessage: '{lucene} Query String syntax', + defaultMessage: '{timelion} Query String syntax', values: { - lucene: LUCENE, + timelion: TIMELION, }, }), getQueryLabel: () => diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss index 2407dcbbce5939..52c473ac2dd387 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss @@ -6,8 +6,13 @@ padding: 0 $euiSizeS; } -.canvasDataSource__section { - padding: $euiSizeM; +.canvasDataSource__section, +.canvasDataSource__list { + padding: $euiSizeM $euiSizeM 0; +} + +.canvasDataSource__sectionFooter { + padding: 0 $euiSizeM; } .canvasDataSource__triggerButton { @@ -19,10 +24,6 @@ margin-right: $euiSizeS; } -.canvasDataSource__list { - padding: $euiSizeM; -} - .canvasDataSource__card .euiCard__content { padding-top: 0 !important; // sass-lint:disable-line no-important } diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js index 8b0061e047f33f..285b69f057cd8f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js @@ -153,7 +153,7 @@ export class DatasourceComponent extends PureComponent { flush="left" size="s" > - + {stateDatasource.displayName}
diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js index 92f9b92cb1f06e..153a8a7ef75e62 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js @@ -15,6 +15,7 @@ export const DatasourceSelector = ({ onSelect, datasources, current }) => ( key={d.name} title={d.displayName} titleElement="h5" + titleSize="xs" icon={} description={d.help} layout="horizontal" diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/datasources/index.js b/x-pack/legacy/plugins/canvas/public/expression_types/datasources/index.js deleted file mode 100644 index 91dca7d275f8bf..00000000000000 --- a/x-pack/legacy/plugins/canvas/public/expression_types/datasources/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { esdocs } from './esdocs'; - -export const datasourceSpecs = [esdocs]; diff --git a/x-pack/legacy/plugins/canvas/public/lib/find_expression_type.js b/x-pack/legacy/plugins/canvas/public/lib/find_expression_type.js index d6d395feade8b9..2cd7c5efb74e95 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/find_expression_type.js +++ b/x-pack/legacy/plugins/canvas/public/lib/find_expression_type.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { datasourceRegistry } from '../expression_types/datasource'; +//import { datasourceRegistry } from '../expression_types/datasource'; import { transformRegistry } from '../expression_types/transform'; import { modelRegistry } from '../expression_types/model'; import { viewRegistry } from '../expression_types/view'; @@ -28,9 +28,6 @@ export function findExpressionType(name, type) { case 'transform': expression = transformRegistry.get(name); return !expression ? acc : acc.concat(expression); - case 'datasource': - expression = datasourceRegistry.get(name); - return !expression ? acc : acc.concat(expression); default: return acc; } diff --git a/x-pack/legacy/plugins/canvas/public/lib/load_expression_types.js b/x-pack/legacy/plugins/canvas/public/lib/load_expression_types.js index fb23f9459d30b9..82699eb5b88fa4 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/load_expression_types.js +++ b/x-pack/legacy/plugins/canvas/public/lib/load_expression_types.js @@ -5,11 +5,9 @@ */ import { argTypeSpecs } from '../expression_types/arg_types'; -import { datasourceSpecs } from '../expression_types/datasources'; -import { argTypeRegistry, datasourceRegistry } from '../expression_types'; +import { argTypeRegistry } from '../expression_types'; export function loadExpressionTypes() { // register default args, arg types, and expression types argTypeSpecs.forEach(expFn => argTypeRegistry.register(expFn)); - datasourceSpecs.forEach(expFn => datasourceRegistry.register(expFn)); } diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index 0a3faca1a25224..f4a3aed28a0a45 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -11,8 +11,6 @@ import { initLoadingIndicator } from './lib/loading_indicator'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; // @ts-ignore untyped local -import { datasourceSpecs } from './expression_types/datasources'; -// @ts-ignore untyped local import { argTypeSpecs } from './expression_types/arg_types'; import { transitions } from './transitions'; import { legacyRegistries } from './legacy_plugin_support'; @@ -90,7 +88,6 @@ export class CanvasPlugin // Register core canvas stuff canvasApi.addFunctions(initFunctions({ typesRegistry: plugins.expressions.__LEGACY.types })); - canvasApi.addDatasourceUIs(datasourceSpecs); canvasApi.addArgumentUIs(argTypeSpecs); canvasApi.addTransitions(transitions); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 86a6f958cf2580..fe0740849a9389 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4464,22 +4464,6 @@ "xpack.canvas.expressionTypes.argTypes.seriesStyle.styleLabel": "スタイル", "xpack.canvas.expressionTypes.argTypes.seriesStyleLabel": "選択された名前付きの数列のスタイルを設定", "xpack.canvas.expressionTypes.argTypes.seriesStyleTitle": "数列スタイル", - "xpack.canvas.expressionTypes.datasources.esdocs.ascendingDropDown": "昇順", - "xpack.canvas.expressionTypes.datasources.esdocs.descendingDropDown": "降順", - "xpack.canvas.expressionTypes.datasources.esdocs.fieldsLabel": "抽出するフィールドです。Kibana スクリプトフィールドは現在利用できません", - "xpack.canvas.expressionTypes.datasources.esdocs.fieldsTitle": "フィールド", - "xpack.canvas.expressionTypes.datasources.esdocs.fieldsWarningLabel": "このデータソースは、10 個以下のフィールドで最も高い性能を発揮します", - "xpack.canvas.expressionTypes.datasources.esdocs.indexLabel": "インデックス名を入力するか、インデックスパターンを選択してください", - "xpack.canvas.expressionTypes.datasources.esdocs.indexTitle": "インデックス", - "xpack.canvas.expressionTypes.datasources.esdocs.queryLabel": "{lucene} クエリ文字列の構文", - "xpack.canvas.expressionTypes.datasources.esdocs.queryTitle": "クエリ", - "xpack.canvas.expressionTypes.datasources.esdocs.sortFieldLabel": "ドキュメントソートフィールド", - "xpack.canvas.expressionTypes.datasources.esdocs.sortFieldTitle": "ソートフィールド", - "xpack.canvas.expressionTypes.datasources.esdocs.sortOrderLabel": "ドキュメントの並べ替え順", - "xpack.canvas.expressionTypes.datasources.esdocs.sortOrderTitle": "並べ替え順", - "xpack.canvas.expressionTypes.datasources.esdocs.warningTitle": "ご注意ください", - "xpack.canvas.expressionTypes.datasources.esdocsLabel": "Elasticsearch から未加工のドキュメントを読み込みます", - "xpack.canvas.expressionTypes.datasources.esdocsTitle": "Elasticsearch 未加工ドキュメント", "xpack.canvas.functionForm.contextError": "エラー: {errorMessage}", "xpack.canvas.functionForm.functionUnknown.unknownArgumentTypeError": "未知の表現タイプ「{expressionType}」", "xpack.canvas.functions.all.args.conditionHelpText": "確認する条件です。", @@ -4990,13 +4974,29 @@ "xpack.canvas.uis.arguments.textareaTitle": "テキストエリア", "xpack.canvas.uis.arguments.toggleLabel": "true/false トグルスイッチ", "xpack.canvas.uis.arguments.toggleTitle": "切り替え", + "xpack.canvas.uis.dataSources.esdocs.ascendingDropDown": "昇順", + "xpack.canvas.uis.dataSources.esdocs.descendingDropDown": "降順", + "xpack.canvas.uis.dataSources.esdocs.fieldsLabel": "抽出するフィールドです。Kibana スクリプトフィールドは現在利用できません", + "xpack.canvas.uis.dataSources.esdocs.fieldsTitle": "フィールド", + "xpack.canvas.uis.dataSources.esdocs.fieldsWarningLabel": "このデータソースは、10 個以下のフィールドで最も高い性能を発揮します", + "xpack.canvas.uis.dataSources.esdocs.indexLabel": "インデックス名を入力するか、インデックスパターンを選択してください", + "xpack.canvas.uis.dataSources.esdocs.indexTitle": "インデックス", + "xpack.canvas.uis.dataSources.esdocs.queryLabel": "{lucene} クエリ文字列の構文", + "xpack.canvas.uis.dataSources.esdocs.queryTitle": "クエリ", + "xpack.canvas.uis.dataSources.esdocs.sortFieldLabel": "ドキュメントソートフィールド", + "xpack.canvas.uis.dataSources.esdocs.sortFieldTitle": "ソートフィールド", + "xpack.canvas.uis.dataSources.esdocs.sortOrderLabel": "ドキュメントの並べ替え順", + "xpack.canvas.uis.dataSources.esdocs.sortOrderTitle": "並べ替え順", + "xpack.canvas.uis.dataSources.esdocs.warningTitle": "ご注意ください", + "xpack.canvas.uis.dataSources.esdocsLabel": "{elasticsearch} から未加工のドキュメントを読み込みます", + "xpack.canvas.uis.dataSources.esdocsTitle": "{elasticsearch} 未加工ドキュメント", "xpack.canvas.uis.dataSources.demoData.headingTitle": "デモデータを使用中です", "xpack.canvas.uis.dataSources.demoDataLabel": "ユーザー名、価格、プロジェクト、国、フェーズを含む模擬データセット", "xpack.canvas.uis.dataSources.demoDataTitle": "デモデータ", "xpack.canvas.uis.dataSources.essqlLabel": "{elasticsearch} {sql} でデータ表を取得します", "xpack.canvas.uis.dataSources.essqlTitle": "{elasticsearch} {sql}", "xpack.canvas.uis.dataSources.timelion.intervalTitle": "間隔", - "xpack.canvas.uis.dataSources.timelion.queryLabel": "{lucene} クエリ文字列の構文", + "xpack.canvas.uis.dataSources.timelion.queryLabel": "{timelion} クエリ文字列の構文", "xpack.canvas.uis.dataSources.timelion.queryTitle": "クエリ", "xpack.canvas.uis.dataSources.timelion.tips.functions": "{functionExample} などの一部 {timelion} 関数は {canvas} データ表に変換できません。データ操作に関する機能は正常に動作するはずです。", "xpack.canvas.uis.dataSources.timelion.tips.time": "{timelion} には時間範囲が必要です。ページのどこかに時間フィルターを追加するか、コードエディターで時間フィルターを渡す必要があります。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c580eb533feb94..7d67ec4e742831 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4465,22 +4465,6 @@ "xpack.canvas.expressionTypes.argTypes.seriesStyle.styleLabel": "样式", "xpack.canvas.expressionTypes.argTypes.seriesStyleLabel": "设置选定已命名序列的样式", "xpack.canvas.expressionTypes.argTypes.seriesStyleTitle": "序列样式", - "xpack.canvas.expressionTypes.datasources.esdocs.ascendingDropDown": "升序", - "xpack.canvas.expressionTypes.datasources.esdocs.descendingDropDown": "降序", - "xpack.canvas.expressionTypes.datasources.esdocs.fieldsLabel": "要提取的字段。Kibana 脚本字段当前不可用", - "xpack.canvas.expressionTypes.datasources.esdocs.fieldsTitle": "字段", - "xpack.canvas.expressionTypes.datasources.esdocs.fieldsWarningLabel": "字段不超过 10 个时,此数据源性能最佳", - "xpack.canvas.expressionTypes.datasources.esdocs.indexLabel": "输入索引名称或选择索引模式", - "xpack.canvas.expressionTypes.datasources.esdocs.indexTitle": "索引", - "xpack.canvas.expressionTypes.datasources.esdocs.queryLabel": "{lucene} 查询字符串语法", - "xpack.canvas.expressionTypes.datasources.esdocs.queryTitle": "查询", - "xpack.canvas.expressionTypes.datasources.esdocs.sortFieldLabel": "文档排序字段", - "xpack.canvas.expressionTypes.datasources.esdocs.sortFieldTitle": "排序字段", - "xpack.canvas.expressionTypes.datasources.esdocs.sortOrderLabel": "文档排序顺序", - "xpack.canvas.expressionTypes.datasources.esdocs.sortOrderTitle": "排序顺序", - "xpack.canvas.expressionTypes.datasources.esdocs.warningTitle": "务必谨慎操作", - "xpack.canvas.expressionTypes.datasources.esdocsLabel": "从 Elasticsearch 拉取原始文档", - "xpack.canvas.expressionTypes.datasources.esdocsTitle": "Elasticsearch 原始文档", "xpack.canvas.functionForm.contextError": "错误:{errorMessage}", "xpack.canvas.functionForm.functionUnknown.unknownArgumentTypeError": "表达式类型“{expressionType}”未知", "xpack.canvas.functions.all.args.conditionHelpText": "要检查的条件。", @@ -4991,13 +4975,29 @@ "xpack.canvas.uis.arguments.textareaTitle": "文本区域", "xpack.canvas.uis.arguments.toggleLabel": "True/False 切换开关", "xpack.canvas.uis.arguments.toggleTitle": "切换", + "xpack.canvas.uis.dataSources.esdocs.ascendingDropDown": "升序", + "xpack.canvas.uis.dataSources.esdocs.descendingDropDown": "降序", + "xpack.canvas.uis.dataSources.esdocs.fieldsLabel": "要提取的字段。Kibana 脚本字段当前不可用", + "xpack.canvas.uis.dataSources.esdocs.fieldsTitle": "字段", + "xpack.canvas.uis.dataSources.esdocs.fieldsWarningLabel": "字段不超过 10 个时,此数据源性能最佳", + "xpack.canvas.uis.dataSources.esdocs.indexLabel": "输入索引名称或选择索引模式", + "xpack.canvas.uis.dataSources.esdocs.indexTitle": "索引", + "xpack.canvas.uis.dataSources.esdocs.queryLabel": "{lucene} 查询字符串语法", + "xpack.canvas.uis.dataSources.esdocs.queryTitle": "查询", + "xpack.canvas.uis.dataSources.esdocs.sortFieldLabel": "文档排序字段", + "xpack.canvas.uis.dataSources.esdocs.sortFieldTitle": "排序字段", + "xpack.canvas.uis.dataSources.esdocs.sortOrderLabel": "文档排序顺序", + "xpack.canvas.uis.dataSources.esdocs.sortOrderTitle": "排序顺序", + "xpack.canvas.uis.dataSources.esdocs.warningTitle": "务必谨慎操作", + "xpack.canvas.uis.dataSources.esdocsLabel": "从 {elasticsearch} 拉取原始文档", + "xpack.canvas.uis.dataSources.esdocsTitle": "{elasticsearch} 原始文档", "xpack.canvas.uis.dataSources.demoData.headingTitle": "您正在使用演示数据", "xpack.canvas.uis.dataSources.demoDataLabel": "使用用户名、价格、项目、国家/地区和阶段模拟数据集", "xpack.canvas.uis.dataSources.demoDataTitle": "演示数据", "xpack.canvas.uis.dataSources.essqlLabel": "使用 {elasticsearch} {sql} 以获取数据表", "xpack.canvas.uis.dataSources.essqlTitle": "{elasticsearch} {sql}", "xpack.canvas.uis.dataSources.timelion.intervalTitle": "时间间隔", - "xpack.canvas.uis.dataSources.timelion.queryLabel": "{lucene} 查询字符串语法", + "xpack.canvas.uis.dataSources.timelion.queryLabel": "{timelion} 查询字符串语法", "xpack.canvas.uis.dataSources.timelion.queryTitle": "查询", "xpack.canvas.uis.dataSources.timelion.tips.functions": "一些 {timelion} 函数(如 {functionExample})不转换成 {canvas} 数据表。任何与数据操作有关的内容都适用。", "xpack.canvas.uis.dataSources.timelion.tips.time": "{timelion} 需要时间范围,您应将时间筛选元素添加到页面上的某个位置,或使用代码编辑器传入时间筛选。", From 01571b67394552da3c73e9dbdf75eaca245d3aff Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Wed, 18 Mar 2020 23:57:36 -0600 Subject: [PATCH 28/42] [SIEM][Detection Engine] Adds lists feature flag and list values to the REST interfaces ## Summary * https://github.com/elastic/kibana/issues/60022 * Adds the feature flag for simple list values * Adds the boolean filters of "and", "and not" to further filter based on simple values * Adds unit tests and e2e tests for the values. * Most tests can include the simple list values but some have to be skipped until we move those to more functions or just enable simple list values as a permanent feature. * DOES NOT FILTER ON THE VALUES JUST YET (That will be a follow on PR) ## Testing: To turn on/off the feature flag do this with an env variable (set this in your .bashrc/.zshrc): ```ts export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true ``` Expect to see this error in the console when the environment variable is set: ```ts server log [11:41:16.245] [error][plugins][siem] You have activated the lists feature flag which is NOT currently supported for SIEM! You should turn this feature flag off immediately by un-setting the environment variable: ELASTIC_XPACK_SIEM_LISTS_FEATURE and restarting Kibana ``` Expect create and update to work when the environment variable is set and look like this: ```ts ./update_rule.sh ./rules/updates/update_list.json { "created_at": "2020-03-15T17:42:37.074Z", "updated_at": "2020-03-15T17:54:22.427Z", "created_by": "yo", "description": "Query with a list", "enabled": true, "false_positives": [], "from": "now-6m", "id": "c602e3f6-713b-4f43-9bdd-b60fbfead1c5", "immutable": false, "interval": "5m", "rule_id": "query-with-list", "language": "kuery", "output_index": ".siem-signals-hassanabad-frank-default", "max_signals": 100, "risk_score": 1, "name": "Query with a list", "query": "user.name: root or user.name: admin", "references": [], "severity": "high", "updated_by": "yo", "tags": [], "to": "now", "type": "query", "threat": [], "version": 6, "lists": [ { "field": "source.ip", "boolean_operator": "and", "values": [ { "name": "127.0.0.1", "type": "value" } ] }, { "field": "host.name", "boolean_operator": "and not", "values": [ { "name": "rock01", "type": "value" } ] } ], "status": "succeeded", "status_date": "2020-03-15T17:42:40.718Z", "last_success_at": "2020-03-15T17:42:40.718Z", "last_success_message": "succeeded" } ``` ```ts ./post_rule.sh ./rules/queries/query_with_list.json { "created_at": "2020-03-15T17:42:37.074Z", "updated_at": "2020-03-15T17:42:37.116Z", "created_by": "yo", "description": "Query with a list", "enabled": true, "false_positives": [], "from": "now-6m", "id": "c602e3f6-713b-4f43-9bdd-b60fbfead1c5", "immutable": false, "interval": "5m", "rule_id": "query-with-list", "language": "kuery", "output_index": ".siem-signals-hassanabad-frank-default", "max_signals": 100, "risk_score": 1, "name": "Query with a list", "query": "user.name: root or user.name: admin", "references": [], "severity": "high", "updated_by": "yo", "tags": [], "to": "now", "type": "query", "threat": [], "version": 1, "lists": [ { "field": "source.ip", "boolean_operator": "and", "values": [ { "name": "127.0.0.1", "type": "value" } ] }, { "field": "host.name", "boolean_operator": "and not", "values": [ { "name": "rock01", "type": "value" }, { "name": "mothra", "type": "value" } ] } ] } ``` ```ts ./patch_rule.sh ./rules/patches/update_list.json { "created_at": "2020-03-15T18:02:52.434Z", "updated_at": "2020-03-15T18:02:57.675Z", "created_by": "yo", "description": "Query with a list", "enabled": true, "false_positives": [], "from": "now-6m", "id": "40b7c2fb-83b4-4820-bf7c-056f3a631126", "immutable": false, "interval": "5m", "rule_id": "query-with-list", "language": "kuery", "output_index": ".siem-signals-hassanabad-frank-default", "max_signals": 100, "risk_score": 1, "name": "Query with a list", "query": "user.name: root or user.name: admin", "references": [], "severity": "high", "updated_by": "yo", "tags": [], "to": "now", "type": "query", "threat": [], "version": 1, "lists": [ { "field": "source.ip", "boolean_operator": "and", "values": [ { "name": "127.0.0.1", "type": "value" } ] }, { "field": "host.name", "boolean_operator": "and not", "values": [ { "name": "rock01", "type": "value" }, { "name": "mothra", "type": "value" } ] } ], "status": "succeeded", "status_date": "2020-03-15T18:02:56.426Z", "last_success_at": "2020-03-15T18:02:56.426Z", "last_success_message": "succeeded" } ``` ```ts ./get_rule_by_rule_id.sh query-with-list { "created_at": "2020-03-15T18:10:07.657Z", "updated_at": "2020-03-15T18:10:08.479Z", "created_by": "yo", "description": "Query with a list", "enabled": true, "false_positives": [], "from": "now-6m", "id": "9854162b-003c-47be-af59-8c3c9545aafa", "immutable": false, "interval": "5m", "rule_id": "query-with-list", "language": "kuery", "output_index": ".siem-signals-hassanabad-frank-default", "max_signals": 100, "risk_score": 1, "name": "Query with a list", "query": "user.name: root or user.name: admin", "references": [], "severity": "high", "updated_by": "yo", "tags": [], "to": "now", "type": "query", "threat": [], "version": 1, "lists": [ { "field": "source.ip", "boolean_operator": "and", "values": [ { "name": "127.0.0.1", "type": "value" } ] }, { "field": "host.name", "boolean_operator": "and not", "values": [ { "name": "rock01", "type": "value" }, { "name": "mothra", "type": "value" } ] } ], "status": "going to run", "status_date": "2020-03-15T18:10:10.738Z" } ``` Expect these errors when the environment variable is not set: ```ts ./post_rule.sh ./rules/queries/query_with_list.json { "statusCode": 400, "error": "Bad Request", "message": "[request body]: child \"lists\" fails because [\"lists\" is not allowed]" } ``` ```ts ./update_rule.sh ./rules/queries/query_with_list.json { "statusCode": 400, "error": "Bad Request", "message": "[request body]: child \"lists\" fails because [\"lists\" is not allowed]" } ``` ```ts ./patch_rule.sh ./rules/patches/update_list.json { "statusCode": 400, "error": "Bad Request", "message": "[request body]: child \"lists\" fails because [\"lists\" is not allowed]" } ``` Expect that this is _backwards_ compatible with the feature flag but not necessarily _forwards_ compatible. This means: * You can have older data that never had lists and it will show up as an empty list when you query it. (backwards compatible) * You _might_ have lists and remove the env. variable and get back items as if the list was not there for (forwards compatible) * You can export without lists, flip on the env flag and import with newer lists feature (backwards compatible) * You can export lists and it will _not_ work with an older system (not forwards compatible) ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../detection_engine/feature_flags.test.ts | 97 ++ .../lib/detection_engine/feature_flags.ts | 49 + .../index/get_index_exists.test.ts | 9 + .../routes/__mocks__/request_responses.ts | 26 + .../routes/__mocks__/utils.ts | 89 ++ .../rules/add_prepackaged_rules_route.test.ts | 9 + .../rules/create_rules_bulk_route.test.ts | 9 + .../routes/rules/create_rules_bulk_route.ts | 2 + .../routes/rules/create_rules_route.test.ts | 9 + .../routes/rules/create_rules_route.ts | 2 + .../rules/delete_rules_bulk_route.test.ts | 9 + .../routes/rules/delete_rules_route.test.ts | 9 + .../routes/rules/find_rules_route.test.ts | 9 + .../rules/find_rules_status_route.test.ts | 9 + ...get_prepackaged_rules_status_route.test.ts | 9 + .../routes/rules/import_rules_route.test.ts | 9 + .../routes/rules/import_rules_route.ts | 2 + .../rules/patch_rules_bulk_route.test.ts | 9 + .../routes/rules/patch_rules_route.test.ts | 9 + .../routes/rules/read_rules_route.test.ts | 9 + .../rules/update_rules_bulk_route.test.ts | 9 + .../routes/rules/update_rules_bulk_route.ts | 2 + .../routes/rules/update_rules_route.test.ts | 9 + .../routes/rules/update_rules_route.ts | 2 + .../routes/rules/utils.test.ts | 854 ++---------------- .../detection_engine/routes/rules/utils.ts | 3 + .../routes/rules/validate.test.ts | 35 + .../add_prepackaged_rules_schema.test.ts | 121 +++ .../schemas/add_prepackaged_rules_schema.ts | 5 + .../schemas/create_rules_bulk_schema.test.ts | 9 + .../schemas/create_rules_schema.test.ts | 117 +++ .../routes/schemas/create_rules_schema.ts | 5 + .../schemas/export_rules_schema.test.ts | 9 + .../routes/schemas/find_rules_schema.test.ts | 9 + .../schemas/import_rules_schema.test.ts | 117 +++ .../routes/schemas/import_rules_schema.ts | 5 + .../schemas/patch_rules_bulk_schema.test.ts | 9 + .../routes/schemas/patch_rules_schema.test.ts | 151 ++++ .../routes/schemas/patch_rules_schema.ts | 5 + .../schemas/query_rules_bulk_schema.test.ts | 9 + .../routes/schemas/query_rules_schema.test.ts | 9 + .../query_signals_index_schema.test.ts | 9 + .../schemas/response/__mocks__/utils.ts | 26 + .../response/check_type_dependents.test.ts | 9 + .../schemas/response/error_schema.test.ts | 9 + .../schemas/response/exact_check.test.ts | 9 + .../response/find_rules_schema.test.ts | 9 + .../response/import_rules_schema.test.ts | 9 + .../response/prepackaged_rules_schema.test.ts | 9 + .../prepackaged_rules_status_schema.test.ts | 9 + .../response/rules_bulk_schema.test.ts | 9 + .../schemas/response/rules_schema.test.ts | 91 +- .../routes/schemas/response/rules_schema.ts | 27 +- .../routes/schemas/response/schemas.ts | 13 + .../type_timeline_only_schema.test.ts | 9 + .../routes/schemas/response/utils.test.ts | 9 + .../routes/schemas/schemas.ts | 12 + .../schemas/set_signal_status_schema.test.ts | 9 + .../schemas/types/lists_default_array.test.ts | 85 ++ .../schemas/types/lists_default_array.ts | 27 + .../schemas/update_rules_bulk_schema.test.ts | 9 + .../schemas/update_rules_schema.test.ts | 117 +++ .../routes/schemas/update_rules_schema.ts | 5 + .../routes/signals/open_close_signals.test.ts | 9 + .../signals/query_signals_route.test.ts | 9 + .../lib/detection_engine/routes/utils.test.ts | 9 + .../detection_engine/rules/create_rules.ts | 5 + .../create_rules_stream_from_ndjson.test.ts | 10 + .../rules/get_export_all.test.ts | 92 +- .../rules/get_export_by_object_ids.test.ts | 118 ++- .../rules/install_prepacked_rules.ts | 2 + .../lib/detection_engine/rules/patch_rules.ts | 3 + .../detection_engine/rules/update_rules.ts | 6 + .../scripts/rules/patches/update_list.json | 25 + .../rules/queries/query_with_list.json | 35 + .../scripts/rules/updates/update_list.json | 31 + .../signals/__mocks__/es_results.ts | 26 + .../signals/build_bulk_body.test.ts | 104 +++ .../signals/build_rule.test.ts | 78 ++ .../detection_engine/signals/build_rule.ts | 1 + .../signals/signal_params_schema.ts | 1 + .../siem/server/lib/detection_engine/types.ts | 6 + x-pack/legacy/plugins/siem/server/plugin.ts | 7 + .../common/config.ts | 5 + .../security_and_spaces/tests/utils.ts | 2 + 85 files changed, 2172 insertions(+), 810 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.test.ts new file mode 100644 index 00000000000000..920064f9a1b779 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.test.ts @@ -0,0 +1,97 @@ +/* + * 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 { + listsEnvFeatureFlagName, + hasListsFeature, + unSetFeatureFlagsForTestsOnly, + setFeatureFlagsForTestsOnly, +} from './feature_flags'; + +describe('feature_flags', () => { + beforeAll(() => { + delete process.env[listsEnvFeatureFlagName]; + }); + + afterEach(() => { + delete process.env[listsEnvFeatureFlagName]; + }); + + describe('hasListsFeature', () => { + test('hasListsFeature should return false if process.env is not set', () => { + expect(hasListsFeature()).toEqual(false); + }); + + test('hasListsFeature should return true if process.env is set to true', () => { + process.env[listsEnvFeatureFlagName] = 'true'; + expect(hasListsFeature()).toEqual(true); + }); + + test('hasListsFeature should return false if process.env is set to false', () => { + process.env[listsEnvFeatureFlagName] = 'false'; + expect(hasListsFeature()).toEqual(false); + }); + + test('hasListsFeature should return false if process.env is set to a non true value', () => { + process.env[listsEnvFeatureFlagName] = 'something else'; + expect(hasListsFeature()).toEqual(false); + }); + }); + + describe('setFeatureFlagsForTestsOnly', () => { + test('it can be called once and sets the environment variable for tests', () => { + setFeatureFlagsForTestsOnly(); + expect(process.env[listsEnvFeatureFlagName]).toEqual('true'); + unSetFeatureFlagsForTestsOnly(); // This is needed to not pollute other tests since this has to be paired + }); + + test('if it is called twice it throws an exception', () => { + setFeatureFlagsForTestsOnly(); + expect(() => setFeatureFlagsForTestsOnly()).toThrow( + 'In your tests you need to ensure in your afterEach/afterAll blocks you are calling unSetFeatureFlagsForTestsOnly' + ); + unSetFeatureFlagsForTestsOnly(); // This is needed to not pollute other tests since this has to be paired + }); + + test('it can be called twice as long as unSetFeatureFlagsForTestsOnly is called in-between', () => { + setFeatureFlagsForTestsOnly(); + unSetFeatureFlagsForTestsOnly(); + setFeatureFlagsForTestsOnly(); + expect(process.env[listsEnvFeatureFlagName]).toEqual('true'); + unSetFeatureFlagsForTestsOnly(); // This is needed to not pollute other tests since this has to be paired + }); + }); + + describe('unSetFeatureFlagsForTestsOnly', () => { + test('it can sets the value to undefined', () => { + setFeatureFlagsForTestsOnly(); + unSetFeatureFlagsForTestsOnly(); + expect(process.env[listsEnvFeatureFlagName]).toEqual(undefined); + }); + + test('it can not be be called before setFeatureFlagsForTestsOnly without throwing', () => { + expect(() => unSetFeatureFlagsForTestsOnly()).toThrow( + 'In your tests you need to ensure in your beforeEach/beforeAll blocks you are calling setFeatureFlagsForTestsOnly' + ); + }); + + test('if it is called twice it throws an exception', () => { + setFeatureFlagsForTestsOnly(); + unSetFeatureFlagsForTestsOnly(); + expect(() => unSetFeatureFlagsForTestsOnly()).toThrow( + 'In your tests you need to ensure in your beforeEach/beforeAll blocks you are calling setFeatureFlagsForTestsOnly' + ); + }); + + test('it can be called twice as long as setFeatureFlagsForTestsOnly is called in-between', () => { + setFeatureFlagsForTestsOnly(); + unSetFeatureFlagsForTestsOnly(); + setFeatureFlagsForTestsOnly(); + unSetFeatureFlagsForTestsOnly(); + expect(process.env[listsEnvFeatureFlagName]).toEqual(undefined); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.ts new file mode 100644 index 00000000000000..4e309faa46e1b4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.ts @@ -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. + */ + +// TODO: (LIST-FEATURE) Delete this file once the lists features are within the product and in a particular version + +// Very temporary file where we put our feature flags for detection lists. +// We need to use an environment variable and CANNOT use a kibana.dev.yml setting because some definitions +// of things are global in the modules are are initialized before the init of the server has a chance to start. +// Set this in your .bashrc/.zshrc to turn on lists feature, export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true + +// NOTE: This feature is forwards and backwards compatible but forwards compatible is not guaranteed. +// Once you enable this and begin using it you might not be able to easily go back back. +// So it's best to not turn it on unless you are developing code. +export const listsEnvFeatureFlagName = 'ELASTIC_XPACK_SIEM_LISTS_FEATURE'; + +// This is for setFeatureFlagsForTestsOnly and unSetFeatureFlagsForTestsOnly only to use +let setFeatureFlagsForTestsOnlyCalled = false; + +// Use this to detect if the lists feature is enabled or not +export const hasListsFeature = (): boolean => { + return process.env[listsEnvFeatureFlagName]?.trim().toLowerCase() === 'true'; +}; + +// This is for tests only to use in your beforeAll() calls +export const setFeatureFlagsForTestsOnly = (): void => { + if (setFeatureFlagsForTestsOnlyCalled) { + throw new Error( + 'In your tests you need to ensure in your afterEach/afterAll blocks you are calling unSetFeatureFlagsForTestsOnly' + ); + } else { + setFeatureFlagsForTestsOnlyCalled = true; + process.env[listsEnvFeatureFlagName] = 'true'; + } +}; + +// This is for tests only to use in your afterAll() calls +export const unSetFeatureFlagsForTestsOnly = (): void => { + if (!setFeatureFlagsForTestsOnlyCalled) { + throw new Error( + 'In your tests you need to ensure in your beforeEach/beforeAll blocks you are calling setFeatureFlagsForTestsOnly' + ); + } else { + delete process.env[listsEnvFeatureFlagName]; + setFeatureFlagsForTestsOnlyCalled = false; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts index cb358c15e5fad8..25945e72ff1797 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts @@ -5,6 +5,7 @@ */ import { getIndexExists } from './get_index_exists'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags'; class StatusCode extends Error { status: number = -1; @@ -15,6 +16,14 @@ class StatusCode extends Error { } describe('get_index_exists', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should return a true if you have _shards', async () => { const callWithRequest = jest.fn().mockResolvedValue({ _shards: { total: 1 } }); const indexExists = await getIndexExists(callWithRequest, 'some-index'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index d90c8ea49a53f9..01f5c364ae4206 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -412,6 +412,32 @@ export const getResult = (): RuleAlertType => ({ references: ['http://www.example.com', 'https://ww.example.com'], note: '# Investigative notes', version: 1, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }, createdAt: new Date('2019-12-13T16:40:33.400Z'), updatedAt: new Date('2019-12-13T16:40:33.400Z'), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts index f59370ce481b6a..aa9b05eb379a63 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -77,3 +77,92 @@ export const buildHapiStream = (string: string, filename = 'file.ndjson'): HapiR return stream; }; + +export const getOutputRuleAlertForRest = (): Omit< + OutputRuleAlertRest, + 'machine_learning_job_id' | 'anomaly_threshold' +> => ({ + created_by: 'elastic', + created_at: '2019-12-13T16:40:33.400Z', + updated_at: '2019-12-13T16:40:33.400Z', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + filters: [ + { + query: { + match_phrase: { + 'host.name': 'some-host', + }, + }, + }, + ], + meta: { + someMeta: 'someField', + }, + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + to: 'now', + type: 'query', + note: '# Investigative notes', + version: 1, +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 2b4fb8fa08a60c..f53efc8a3234db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -14,6 +14,7 @@ import { import { requestContextMock, serverMock } from '../__mocks__'; import { addPrepackedRulesRoute } from './add_prepackaged_rules_route'; import { PrepackagedRules } from '../../types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -44,6 +45,14 @@ describe('add_prepackaged_rules_route', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 2b31d37dddddb5..e2af678c828e66 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -16,11 +16,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesBulkRoute } from './create_rules_bulk_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('create_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index b819bc69192745..e8b1162b06182a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -84,6 +84,7 @@ export const createRulesBulkRoute = (router: IRouter) => { timeline_id: timelineId, timeline_title: timelineTitle, version, + lists, } = payloadRule; const ruleIdOrUuid = ruleId ?? uuid.v4(); try { @@ -138,6 +139,7 @@ export const createRulesBulkRoute = (router: IRouter) => { references, note, version, + lists, }); return transformValidateBulkError(ruleIdOrUuid, createdRule); } catch (err) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 976f371c6b1a69..1a4e19c2047b5d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -18,11 +18,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesRoute } from './create_rules_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('create_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 42bade1ba08553..3a440178344da6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -58,6 +58,7 @@ export const createRulesRoute = (router: IRouter): void => { type, references, note, + lists, } = request.body; const siemResponse = buildSiemResponse(response); @@ -124,6 +125,7 @@ export const createRulesRoute = (router: IRouter): void => { references, note, version: 1, + lists, }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index 16f9a9524df55e..f2da3ab4be8f63 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -17,11 +17,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { deleteRulesBulkRoute } from './delete_rules_bulk_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('delete_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index 0519addb275d68..e30f332ecd1cac 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -15,11 +15,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { deleteRulesRoute } from './delete_rules_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('delete_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index 57759844c100d0..b4591a8141f7bf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -13,11 +13,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { findRulesRoute } from './find_rules_route'; +import { unSetFeatureFlagsForTestsOnly, setFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('find_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts index 9c86b70b882709..89c9f34027120a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts @@ -8,11 +8,20 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { getFindResultStatus, ruleStatusRequest } from '../__mocks__/request_responses'; import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { findRulesStatusesRoute } from './find_rules_status_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('find_statuses', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts index 03059ed5ec5ccf..2bbd4f78afae1b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts @@ -13,6 +13,7 @@ import { getNonEmptyIndex, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock } from '../__mocks__'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -38,6 +39,14 @@ jest.mock('../../rules/get_prepackaged_rules', () => { }); describe('get_prepackaged_rule_status_route', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + let server: ReturnType; let { clients, context } = requestContextMock.createTools(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index c224e0f055b856..f6e1cf6e2420c6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -23,8 +23,17 @@ import { createMockConfig, requestContextMock, serverMock, requestMock } from '. import { importRulesRoute } from './import_rules_route'; import { DEFAULT_SIGNALS_INDEX } from '../../../../../common/constants'; import * as createRulesStreamFromNdJson from '../../rules/create_rules_stream_from_ndjson'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('import_rules_route', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + let config = createMockConfig(); let server: ReturnType; let request: ReturnType; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index d92ef316aef0cd..920cf97d32a7a2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -140,6 +140,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config timeline_id: timelineId, timeline_title: timelineTitle, version, + lists, } = parsedRule; try { @@ -191,6 +192,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config references, note, version, + lists, }); resolve({ rule_id: ruleId, status_code: 200 }); } else if (rule != null && request.query.overwrite) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index 967fd46f7e3da5..4c980c8cc60d2b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -14,11 +14,20 @@ import { } from '../__mocks__/request_responses'; import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { patchRulesBulkRoute } from './patch_rules_bulk_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('patch_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index 0c2ca882a55907..b92c18827557cb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -16,11 +16,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { patchRulesRoute } from './patch_rules_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('patch_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 7ebac9b785c82b..982e1bb47a53a7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -14,11 +14,20 @@ import { getFindResultStatusEmpty, } from '../__mocks__/request_responses'; import { requestMock, requestContextMock, serverMock } from '../__mocks__'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('read_signals', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 46639e1fe33808..d530866edaf0d9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -16,11 +16,20 @@ import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { updateRulesBulkRoute } from './update_rules_bulk_route'; import { BulkError } from '../utils'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('update_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 859935d8511264..deb319492258cd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -76,6 +76,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { references, note, version, + lists, } = payloadRule; const finalIndex = outputIndex ?? siemClient.signalsIndex; const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; @@ -114,6 +115,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { references, note, version, + lists, }); if (rule != null) { const ruleStatuses = await savedObjectsClient.find< diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index a6da8cd56ec17f..a15f1ca9b044e6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -16,11 +16,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('update_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index a9982a9896633e..c47a412c2e9df1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -59,6 +59,7 @@ export const updateRulesRoute = (router: IRouter) => { references, note, version, + lists, } = request.body; const siemResponse = buildSiemResponse(response); @@ -110,6 +111,7 @@ export const updateRulesRoute = (router: IRouter) => { references, note, version, + lists, }); if (rule != null) { const ruleStatuses = await savedObjectsClient.find< diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index 3243ccb14f89c2..3a8d068cad38df 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -20,403 +20,88 @@ import { } from './utils'; import { getResult } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; -import { OutputRuleAlertRest, ImportRuleAlertRest, RuleAlertParamsRest } from '../../types'; +import { ImportRuleAlertRest, RuleAlertParamsRest, RuleTypeParams } from '../../types'; import { BulkError, ImportSuccessError } from '../utils'; import { sampleRule } from '../../signals/__mocks__/es_results'; -import { getSimpleRule } from '../__mocks__/utils'; +import { getSimpleRule, getOutputRuleAlertForRest } from '../__mocks__/utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; import { PartialAlert } from '../../../../../../../../plugins/alerting/server'; import { SanitizedAlert } from '../../../../../../../../plugins/alerting/server/types'; +import { RuleAlertType } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; type PromiseFromStreams = ImportRuleAlertRest | Error; describe('utils', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + describe('transformAlertToRule', () => { test('should work with a full data set', () => { const fullRule = getResult(); const rule = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; - expect(rule).toEqual(expected); + expect(rule).toEqual(getOutputRuleAlertForRest()); }); test('should work with a partial data set missing data', () => { const fullRule = getResult(); - const { from, language, ...omitData } = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - output_index: '.siem-signals', - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; - expect(omitData).toEqual(expected); + const { from, language, ...omitParams } = fullRule.params; + fullRule.params = omitParams as RuleTypeParams; + const rule = transformAlertToRule(fullRule); + const { + from: from2, + language: language2, + ...expectedWithoutFromWithoutLanguage + } = getOutputRuleAlertForRest(); + expect(rule).toEqual(expectedWithoutFromWithoutLanguage); }); test('should omit query if query is null', () => { const fullRule = getResult(); fullRule.params.query = null; const rule = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - output_index: '.siem-signals', - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; - expect(rule).toEqual(expected); + const { query, ...expectedWithoutQuery } = getOutputRuleAlertForRest(); + expect(rule).toEqual(expectedWithoutQuery); }); test('should omit query if query is undefined', () => { const fullRule = getResult(); fullRule.params.query = undefined; const rule = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - output_index: '.siem-signals', - interval: '5m', - rule_id: 'rule-1', - risk_score: 50, - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; - expect(rule).toEqual(expected); + const { query, ...expectedWithoutQuery } = getOutputRuleAlertForRest(); + expect(rule).toEqual(expectedWithoutQuery); }); test('should omit a mix of undefined, null, and missing fields', () => { const fullRule = getResult(); fullRule.params.query = undefined; fullRule.params.language = null; - const { from, enabled, ...omitData } = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - false_positives: [], - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - risk_score: 50, - max_signals: 100, - name: 'Detect Root/Admin Users', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; - expect(omitData).toEqual(expected); + const { from, ...omitParams } = fullRule.params; + fullRule.params = omitParams as RuleTypeParams; + const { enabled, ...omitEnabled } = fullRule; + const rule = transformAlertToRule(omitEnabled as RuleAlertType); + const { + from: from2, + enabled: enabled2, + language, + query, + ...expectedWithoutFromEnabledLanguageQuery + } = getOutputRuleAlertForRest(); + expect(rule).toEqual(expectedWithoutFromEnabledLanguageQuery); }); test('should return enabled is equal to false', () => { const fullRule = getResult(); fullRule.enabled = false; const ruleWithEnabledFalse = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: false, - from: 'now-6m', - false_positives: [], - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - risk_score: 50, - rule_id: 'rule-1', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; + const expected = getOutputRuleAlertForRest(); + expected.enabled = false; expect(ruleWithEnabledFalse).toEqual(expected); }); @@ -424,65 +109,7 @@ describe('utils', () => { const fullRule = getResult(); fullRule.params.immutable = false; const ruleWithEnabledFalse = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - from: 'now-6m', - false_positives: [], - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - risk_score: 50, - rule_id: 'rule-1', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; + const expected = getOutputRuleAlertForRest(); expect(ruleWithEnabledFalse).toEqual(expected); }); @@ -490,65 +117,8 @@ describe('utils', () => { const fullRule = getResult(); fullRule.tags = ['tag 1', 'tag 2', `${INTERNAL_IDENTIFIER}_some_other_value`]; const rule = transformAlertToRule(fullRule); - const expected: Partial = { - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: ['tag 1', 'tag 2'], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; + const expected = getOutputRuleAlertForRest(); + expected.tags = ['tag 1', 'tag 2']; expect(rule).toEqual(expected); }); @@ -656,65 +226,7 @@ describe('utils', () => { total: 0, data: [getResult()], }); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - note: '# Investigative notes', - version: 1, - }; + const expected = getOutputRuleAlertForRest(); expect(output).toEqual({ page: 1, perPage: 0, @@ -738,65 +250,7 @@ describe('utils', () => { describe('transform', () => { test('outputs 200 if the data is of type siem alert', () => { const output = transform(getResult()); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - risk_score: 50, - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - note: '# Investigative notes', - version: 1, - }; + const expected = getOutputRuleAlertForRest(); expect(output).toEqual(expected); }); @@ -911,65 +365,7 @@ describe('utils', () => { describe('transformOrBulkError', () => { test('outputs 200 if the data is of type siem alert', () => { const output = transformOrBulkError('rule-1', getResult()); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - risk_score: 50, - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - note: '# Investigative notes', - version: 1, - }; + const expected = getOutputRuleAlertForRest(); expect(output).toEqual(expected); }); @@ -1033,57 +429,8 @@ describe('utils', () => { test('given single alert will return the alert transformed', () => { const result1 = getResult(); const transformed = transformAlertsToRules([result1]); - expect(transformed).toEqual([ - { - created_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - max_signals: 100, - meta: { someMeta: 'someField' }, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - risk_score: 50, - rule_id: 'rule-1', - severity: 'high', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - updated_at: '2019-12-13T16:40:33.400Z', - updated_by: 'elastic', - note: '# Investigative notes', - version: 1, - }, - ]); + const expected = getOutputRuleAlertForRest(); + expect(transformed).toEqual([expected]); }); test('given two alerts will return the two alerts transformed', () => { @@ -1093,106 +440,11 @@ describe('utils', () => { result2.params.ruleId = 'some other id'; const transformed = transformAlertsToRules([result1, result2]); - expect(transformed).toEqual([ - { - created_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - max_signals: 100, - meta: { someMeta: 'someField' }, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - risk_score: 50, - rule_id: 'rule-1', - severity: 'high', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - updated_at: '2019-12-13T16:40:33.400Z', - updated_by: 'elastic', - note: '# Investigative notes', - version: 1, - }, - { - created_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], - from: 'now-6m', - id: 'some other id', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - max_signals: 100, - meta: { someMeta: 'someField' }, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - risk_score: 50, - rule_id: 'some other id', - severity: 'high', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - updated_at: '2019-12-13T16:40:33.400Z', - updated_by: 'elastic', - note: '# Investigative notes', - version: 1, - }, - ]); + const expected1 = getOutputRuleAlertForRest(); + const expected2 = getOutputRuleAlertForRest(); + expected2.id = 'some other id'; + expected2.rule_id = 'some other id'; + expect(transformed).toEqual([expected1, expected2]); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index abd8dd7e87f033..fe7618bca0c75b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -28,6 +28,7 @@ import { createImportErrorObject, OutputError, } from '../utils'; +import { hasListsFeature } from '../../feature_flags'; type PromiseFromStreams = ImportRuleAlertRest | Error; @@ -141,6 +142,8 @@ export const transformAlertToRule = ( last_success_at: ruleStatus?.attributes.lastSuccessAt, last_failure_message: ruleStatus?.attributes.lastFailureMessage, last_success_message: ruleStatus?.attributes.lastSuccessMessage, + // TODO: (LIST-FEATURE) Remove hasListsFeature() check once we have lists available for a release + lists: hasListsFeature() ? alert.params.lists : null, }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts index ba6c702e9601b7..1dce602f3fcac8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts @@ -16,6 +16,7 @@ import { getResult } from '../__mocks__/request_responses'; import { FindResult } from '../../../../../../../../plugins/alerting/server'; import { RulesSchema } from '../schemas/response/rules_schema'; import { BulkError } from '../utils'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; export const ruleOutput: RulesSchema = { created_at: '2019-12-13T16:40:33.400Z', @@ -68,6 +69,32 @@ export const ruleOutput: RulesSchema = { }, }, ], + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], meta: { someMeta: 'someField', @@ -78,6 +105,14 @@ export const ruleOutput: RulesSchema = { }; describe('validate', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + describe('validate', () => { test('it should do a validation correctly', () => { const schema = t.exact(t.type({ a: t.number })); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index a002cc93240129..171a34f0d05922 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -6,8 +6,17 @@ import { ThreatParams, PrepackagedRules } from '../../types'; import { addPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('add prepackaged rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('empty objects do not validate', () => { expect(addPrepackagedRulesSchema.validate>({}).error).toBeTruthy(); }); @@ -1332,4 +1341,116 @@ describe('add prepackaged rules schema', () => { ).toEqual('child "note" fails because ["note" must be a string]'); }); }); + + // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema + // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the + // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, + // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally + describe.skip('lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + version: 1, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [], + version: 1, + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate', () => { + expect( + addPrepackagedRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [{ invalid_value: 'invalid value' }], + version: 1, + }).error.message + ).toEqual( + 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + ); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate with empty lists', () => { + expect( + addPrepackagedRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + version: 1, + }).value.lists + ).toEqual([]); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts index ec0a8e7871b5ba..4c60a66141250a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts @@ -34,12 +34,14 @@ import { references, note, version, + lists, anomaly_threshold, machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; +import { hasListsFeature } from '../../feature_flags'; /** * Big differences between this schema and the createRulesSchema @@ -102,4 +104,7 @@ export const addPrepackagedRulesSchema = Joi.object({ references: references.default([]), note: note.allow(''), version: version.required(), + + // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release + lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts index 6512bfdc4361f3..fa007bba6551a1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts @@ -6,11 +6,20 @@ import { createRulesBulkSchema } from './create_rules_bulk_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; // only the basics of testing are here. // see: create_rules_schema.test.ts for the bulk of the validation tests // this just wraps createRulesSchema in an array describe('create_rules_bulk_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('can take an empty array and validate it', () => { expect( createRulesBulkSchema.validate>>([]).error diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index 3bad87dc1a9ad5..db5097a6f25dbe 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -7,8 +7,17 @@ import { createRulesSchema } from './create_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { ThreatParams, RuleAlertParamsRest } from '../../types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('create rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('empty objects do not validate', () => { expect(createRulesSchema.validate>({}).error).toBeTruthy(); }); @@ -1314,5 +1323,113 @@ describe('create rules schema', () => { }).error ).toBeFalsy(); }); + + // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema + // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the + // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, + // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally + describe.skip('lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate', () => { + expect( + createRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [{ invalid_value: 'invalid value' }], + }).error.message + ).toEqual( + 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + ); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate with empty lists', () => { + expect( + createRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + }).value.lists + ).toEqual([]); + }); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts index e86963fd4594c7..0aa7317dd8cdc5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts @@ -35,11 +35,13 @@ import { references, note, version, + lists, machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; +import { hasListsFeature } from '../../feature_flags'; export const createRulesSchema = Joi.object({ anomaly_threshold: anomaly_threshold.when('type', { @@ -90,4 +92,7 @@ export const createRulesSchema = Joi.object({ references: references.default([]), note: note.allow(''), version: version.default(1), + + // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release + lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts index 621dcd8fa8ed40..0e71237f752324 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts @@ -6,8 +6,17 @@ import { exportRulesSchema, exportRulesQuerySchema } from './export_rules_schema'; import { ExportRulesRequestParams } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('create rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + describe('exportRulesSchema', () => { test('null value or absent values validate', () => { expect(exportRulesSchema.validate(null).error).toBeFalsy(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts index 339874e19c33a2..ffbfd193873a89 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts @@ -6,8 +6,17 @@ import { findRulesSchema } from './find_rules_schema'; import { FindParamsRest } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('find rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('empty objects do validate', () => { expect(findRulesSchema.validate>({}).error).toBeFalsy(); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts index 9c80ddde9e7b77..bcb24268fc6c7a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -11,8 +11,17 @@ import { } from './import_rules_schema'; import { ThreatParams, ImportRuleAlertRest } from '../../types'; import { ImportRulesRequestParams } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('import rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + describe('importRulesSchema', () => { test('empty objects do not validate', () => { expect(importRulesSchema.validate>({}).error).toBeTruthy(); @@ -1535,4 +1544,112 @@ describe('import rules schema', () => { ).toEqual('child "note" fails because ["note" must be a string]'); }); }); + + // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema + // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the + // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, + // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally + describe.skip('lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate and lists is empty', () => { + expect( + importRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [{ invalid_value: 'invalid value' }], + }).error.message + ).toEqual( + 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + ); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate', () => { + expect( + importRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + }).value.lists + ).toEqual([]); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts index 92718b7ae71ba1..469b59a8e08ad2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts @@ -40,12 +40,14 @@ import { references, note, version, + lists, anomaly_threshold, machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; +import { hasListsFeature } from '../../feature_flags'; /** * Differences from this and the createRulesSchema are @@ -111,6 +113,9 @@ export const importRulesSchema = Joi.object({ updated_at, created_by, updated_by, + + // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release + lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }); export const importRulesQuerySchema = Joi.object({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts index 43d1e7ab2aa3b5..e87c732e8a2f79 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts @@ -6,11 +6,20 @@ import { patchRulesBulkSchema } from './patch_rules_bulk_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; // only the basics of testing are here. // see: patch_rules_schema.test.ts for the bulk of the validation tests // this just wraps patchRulesSchema in an array describe('patch_rules_bulk_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('can take an empty array and validate it', () => { expect( patchRulesBulkSchema.validate>>([]).error diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts index ecdba7ccc0091a..6fc1a0c3caa9c6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts @@ -7,8 +7,17 @@ import { patchRulesSchema } from './patch_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { ThreatParams } from '../../types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('patch rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('empty objects do not validate as they require at least id or rule_id', () => { expect(patchRulesSchema.validate>({}).error).toBeTruthy(); }); @@ -1053,4 +1062,146 @@ describe('patch rules schema', () => { ).toEqual('child "note" fails because ["note" must be a string]'); }); }); + + // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema + // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the + // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, + // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally + describe.skip('lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('lists can be patched', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'some id', + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate', () => { + expect( + patchRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [{ invalid_value: 'invalid value' }], + }).error.message + ).toEqual( + 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + ); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate with empty lists', () => { + expect( + patchRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + }).value.lists + ).toEqual([]); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts index 4496a808f68694..8bb155d83cf44f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts @@ -35,9 +35,11 @@ import { note, id, version, + lists, anomaly_threshold, machine_learning_job_id, } from './schemas'; +import { hasListsFeature } from '../../feature_flags'; /* eslint-enable @typescript-eslint/camelcase */ export const patchRulesSchema = Joi.object({ @@ -70,4 +72,7 @@ export const patchRulesSchema = Joi.object({ references, note: note.allow(''), version, + + // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release + lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts index 7ea7fcbd1d86b3..389c5ff7ea6171 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts @@ -6,11 +6,20 @@ import { queryRulesBulkSchema } from './query_rules_bulk_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; // only the basics of testing are here. // see: query_rules_bulk_schema.test.ts for the bulk of the validation tests // this just wraps queryRulesSchema in an array describe('query_rules_bulk_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('can take an empty array and validate it', () => { expect( queryRulesBulkSchema.validate>>([]).error diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts index 0f392e399f36c5..68be4c627780c5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts @@ -6,8 +6,17 @@ import { queryRulesSchema } from './query_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('queryRulesSchema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('empty objects do not validate', () => { expect(queryRulesSchema.validate>({}).error).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts index 5c293f4825b95b..4752d1794ff28a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts @@ -6,8 +6,17 @@ import { querySignalsSchema } from './query_signals_index_schema'; import { SignalsQueryRestParams } from '../../signals/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('query, aggs, size, _source and track_total_hits on signals index', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('query, aggs, size, _source and track_total_hits simultaneously', () => { expect( querySignalsSchema.validate>({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts index dd88bd80d57879..46cd1b653b5b44 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts @@ -63,6 +63,32 @@ export const getBaseResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesS language: 'kuery', rule_id: 'query-rule-id', interval: '5m', + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }); export const getRulesBulkPayload = (): RulesBulkSchema => [getBaseResponsePayload()]; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts index 1a5ee793a25da6..0eda2a7a13d96f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts @@ -25,8 +25,17 @@ import { left } from 'fp-ts/lib/Either'; import { exactCheck } from './exact_check'; import { RulesSchema } from './rules_schema'; import { TypeAndTimelineOnly } from './type_timeline_only_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('check_type_dependents', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + describe('checkTypeDependents', () => { test('it should validate a type of "query" without anything extra', () => { const payload = getBaseResponsePayload(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts index 9708c928870f5a..11d8b85f259206 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts @@ -10,8 +10,17 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { exactCheck } from './exact_check'; import { foldLeftRight, getErrorPayload, getPaths } from './__mocks__/utils'; import { errorSchema, ErrorSchema } from './error_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('error_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate an error with a UUID given for id', () => { const error = getErrorPayload(); const decoded = errorSchema.decode(getErrorPayload()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts index d01c5e19d43220..cae4365d06856f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts @@ -10,8 +10,17 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { foldLeftRight, getPaths } from './__mocks__/utils'; import { exactCheck, findDifferencesRecursive } from './exact_check'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('exact_check', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it returns an error if given extra object properties', () => { const someType = t.exact( t.type({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts index 937af223b91ab5..f5c1970ee8c55c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts @@ -15,8 +15,17 @@ import { } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { RulesSchema } from './rules_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('find_rules_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate a typical single find rules response', () => { const payload = getFindResponseSingle(); const decoded = findRulesSchema.decode(payload); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts index 62ffcd527eea87..ce4bbf420a634b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts @@ -10,8 +10,17 @@ import { foldLeftRight, getPaths } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { ImportRulesSchema, importRulesSchema } from './import_rules_schema'; import { ErrorSchema } from './error_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('import_rules_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate an empty import response with no errors', () => { const payload: ImportRulesSchema = { success: true, success_count: 0, errors: [] }; const decoded = importRulesSchema.decode(payload); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts index 7f9b296e2d466c..46667826416e1f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts @@ -9,8 +9,17 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { foldLeftRight, getPaths } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { PrePackagedRulesSchema, prePackagedRulesSchema } from './prepackaged_rules_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('prepackaged_rules_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate an empty prepackaged response with defaults', () => { const payload: PrePackagedRulesSchema = { rules_installed: 0, rules_updated: 0 }; const decoded = prePackagedRulesSchema.decode(payload); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts index 9d44e09e847a0f..1c270ff402f75e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts @@ -12,8 +12,17 @@ import { PrePackagedRulesStatusSchema, prePackagedRulesStatusSchema, } from './prepackaged_rules_status_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('prepackaged_rules_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate an empty prepackaged response with defaults', () => { const payload: PrePackagedRulesStatusSchema = { rules_installed: 0, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts index c2f346cacc43ef..8dc97d727c4d1d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts @@ -17,8 +17,17 @@ import { import { RulesBulkSchema, rulesBulkSchema } from './rules_bulk_schema'; import { RulesSchema } from './rules_schema'; import { ErrorSchema } from './error_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('prepackaged_rule_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate a regular message and and error together with a uuid', () => { const payload: RulesBulkSchema = [getBaseResponsePayload(), getErrorPayload()]; const decoded = rulesBulkSchema.decode(payload); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts index a2594ffa21c459..fb9ff2c28dc44e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts @@ -8,12 +8,21 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { exactCheck } from './exact_check'; -import { rulesSchema, RulesSchema } from './rules_schema'; +import { rulesSchema, RulesSchema, removeList } from './rules_schema'; import { foldLeftRight, getBaseResponsePayload, getPaths } from './__mocks__/utils'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; describe('rules_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate a type of "query" without anything extra', () => { const payload = getBaseResponsePayload(); @@ -196,4 +205,84 @@ describe('rules_schema', () => { ]); expect(message.schema).toEqual({}); }); + + // TODO: (LIST-FEATURE) Remove this test once the feature flag is deployed + test('it should remove lists when we need it to be removed because the feature is off but there exists a list in the data', () => { + const payload = getBaseResponsePayload(); + const decoded = rulesSchema.decode(payload); + const listRemoved = removeList(decoded); + const message = pipe(listRemoved, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', + created_at: '2020-02-20T03:57:54.037Z', + updated_at: '2020-02-20T03:57:54.037Z', + created_by: 'elastic', + description: 'some description', + enabled: true, + false_positives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + immutable: false, + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + references: ['test 1', 'test 2'], + severity: 'high', + updated_by: 'elastic_kibana', + tags: [], + to: 'now', + type: 'query', + threat: [], + version: 1, + output_index: '.siem-signals-hassanabad-frank-default', + max_signals: 100, + risk_score: 55, + language: 'kuery', + rule_id: 'query-rule-id', + interval: '5m', + status: 'succeeded', + status_date: '2020-02-22T16:47:50.047Z', + last_success_at: '2020-02-22T16:47:50.047Z', + last_success_message: 'succeeded', + }); + }); + + test('it should work with lists that are not there and not cause invalidation or errors', () => { + const payload = getBaseResponsePayload(); + const { lists, ...payloadWithoutLists } = payload; + const decoded = rulesSchema.decode(payloadWithoutLists); + const listRemoved = removeList(decoded); + const message = pipe(listRemoved, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', + created_at: '2020-02-20T03:57:54.037Z', + updated_at: '2020-02-20T03:57:54.037Z', + created_by: 'elastic', + description: 'some description', + enabled: true, + false_positives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + immutable: false, + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + references: ['test 1', 'test 2'], + severity: 'high', + updated_by: 'elastic_kibana', + tags: [], + to: 'now', + type: 'query', + threat: [], + version: 1, + output_index: '.siem-signals-hassanabad-frank-default', + max_signals: 100, + risk_score: 55, + language: 'kuery', + rule_id: 'query-rule-id', + interval: '5m', + status: 'succeeded', + status_date: '2020-02-22T16:47:50.047Z', + last_success_at: '2020-02-22T16:47:50.047Z', + last_success_message: 'succeeded', + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts index 28b588a86aeb0d..75de97a55534b4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts @@ -7,8 +7,9 @@ /* eslint-disable @typescript-eslint/camelcase */ import * as t from 'io-ts'; import { isObject } from 'lodash/fp'; -import { Either } from 'fp-ts/lib/Either'; +import { Either, fold, right, left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; import { checkTypeDependents } from './check_type_dependents'; import { anomaly_threshold, @@ -52,6 +53,8 @@ import { meta, note, } from './schemas'; +import { ListsDefaultArray } from '../types/lists_default_array'; +import { hasListsFeature } from '../../../feature_flags'; /** * This is the required fields for the rules schema response. Put all required properties on @@ -82,6 +85,7 @@ export const requiredRulesSchema = t.type({ updated_at, created_by, version, + lists: ListsDefaultArray, }); export type RequiredRulesSchema = t.TypeOf; @@ -147,11 +151,30 @@ export const rulesSchema = new t.Type< 'RulesSchema', (input: unknown): input is RulesWithoutTypeDependentsSchema => isObject(input), (input): Either => { - return checkTypeDependents(input); + const output = checkTypeDependents(input); + if (!hasListsFeature()) { + // TODO: (LIST-FEATURE) Remove this after the lists feature is an accepted feature for a particular release + return removeList(output); + } else { + return output; + } }, t.identity ); +// TODO: (LIST-FEATURE) Remove this after the lists feature is an accepted feature for a particular release +export const removeList = ( + decoded: Either +): Either => { + const onLeft = (errors: t.Errors): Either => left(errors); + const onRight = (decodedValue: RequiredRulesSchema): Either => { + delete decodedValue.lists; + return right(decodedValue); + }; + const folded = fold(onLeft, onRight); + return pipe(decoded, folded); +}; + /** * This is the correct type you want to use for Rules that are outputted from the * REST interface. This has all base and all optional properties merged together. diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts index 072e3f5beefe2c..d90cb7b1f0829f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts @@ -131,3 +131,16 @@ export const rules_custom_installed = PositiveInteger; export const rules_not_installed = PositiveInteger; export const rules_not_updated = PositiveInteger; export const note = t.string; + +// NOTE: Experimental list support not being shipped currently and behind a feature flag +// TODO: Remove this comment once we lists have passed testing and is ready for the release +export const boolean_operator = t.keyof({ and: null, 'and not': null }); +export const list_type = t.keyof({ value: null }); // TODO: (LIST-FEATURE) Eventually this can include "list" when we support lists CRUD +export const list_value = t.exact(t.type({ name: t.string, type: list_type })); +export const list = t.exact( + t.type({ + field: t.string, + boolean_operator, + values: t.array(list_value), + }) +); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts index 219cd68d3a2a12..68a3c8b3038239 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts @@ -10,8 +10,17 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { exactCheck } from './exact_check'; import { foldLeftRight, getPaths } from './__mocks__/utils'; import { TypeAndTimelineOnly, typeAndTimelineOnlySchema } from './type_timeline_only_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('prepackaged_rule_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate a a type and timeline_id together', () => { const payload: TypeAndTimelineOnly = { type: 'query', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts index cd223c24792bf2..c1eb32be4895c2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts @@ -6,8 +6,17 @@ import * as t from 'io-ts'; import { formatErrors } from './utils'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('utils', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('returns an empty error message string if there are no errors', () => { const errors: t.Errors = []; const output = formatErrors(errors); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts index ad7050e8dd65c6..007294293f59bd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts @@ -111,3 +111,15 @@ export const version = Joi.number() .integer() .min(1); export const note = Joi.string(); + +// NOTE: Experimental list support not being shipped currently and behind a feature flag +// TODO: (LIST-FEATURE) Remove this comment once we lists have passed testing and is ready for the release +export const boolean_operator = Joi.string().valid('and', 'and not'); +export const list_type = Joi.string().valid('value'); // TODO: (LIST-FEATURE) Eventually this can be "list" when we support list types +export const list_value = Joi.object({ name: Joi.string().required(), type: list_type.required() }); +export const list = Joi.object({ + field: Joi.string().required(), + boolean_operator: boolean_operator.required(), + values: Joi.array().items(list_value), +}); +export const lists = Joi.array().items(list); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts index a6ba9b19a9d7d7..953532a6e1c266 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts @@ -6,8 +6,17 @@ import { setSignalsStatusSchema } from './set_signal_status_schema'; import { SignalsStatusRestParams } from '../../signals/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('set signal status schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('signal_ids and status is valid', () => { expect( setSignalsStatusSchema.validate>({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts new file mode 100644 index 00000000000000..14df1c3d8cd55a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts @@ -0,0 +1,85 @@ +/* + * 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 { ListsDefaultArray } from './lists_default_array'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; + +describe('lists_default_array', () => { + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of lists', () => { + const payload = [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + 5, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts new file mode 100644 index 00000000000000..0e0944a11b4166 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +import { list } from '../response/schemas'; + +export type ListsDefaultArrayC = t.Type; +type List = t.TypeOf; + +/** + * Types the ListsDefaultArray as: + * - If null or undefined, then a default array will be set for the list + */ +export const ListsDefaultArray: ListsDefaultArrayC = new t.Type( + 'listsWithDefaultArray', + t.array(list).is, + (input): Either => + input == null ? t.success([]) : t.array(list).decode(input), + t.identity +); + +export type ListsDefaultArraySchema = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts index e866260662ad7b..d329070eaaa0ac 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts @@ -6,11 +6,20 @@ import { updateRulesBulkSchema } from './update_rules_bulk_schema'; import { UpdateRuleAlertParamsRest } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; // only the basics of testing are here. // see: update_rules_schema.test.ts for the bulk of the validation tests // this just wraps updateRulesSchema in an array describe('update_rules_bulk_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('can take an empty array and validate it', () => { expect( updateRulesBulkSchema.validate>>([]).error diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index e37abf3746ae60..a0689966a86948 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -7,8 +7,17 @@ import { updateRulesSchema } from './update_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { ThreatParams, RuleAlertParamsRest } from '../../types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('create rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('empty objects do not validate as they require at least id or rule_id', () => { expect(updateRulesSchema.validate>({}).error).toBeTruthy(); }); @@ -1340,4 +1349,112 @@ describe('create rules schema', () => { ).toEqual('child "note" fails because ["note" must be a string]'); }); }); + + // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema + // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the + // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, + // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally + describe.skip('lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate', () => { + expect( + updateRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [{ invalid_value: 'invalid value' }], + }).error.message + ).toEqual( + 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + ); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate with empty lists', () => { + expect( + updateRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + }).value.lists + ).toEqual([]); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts index f7a53385200dfa..421172cf0b1a11 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts @@ -35,12 +35,14 @@ import { id, note, version, + lists, anomaly_threshold, machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; +import { hasListsFeature } from '../../feature_flags'; /** * This almost identical to the create_rules_schema except for a few details. @@ -99,4 +101,7 @@ export const updateRulesSchema = Joi.object({ references: references.default([]), note: note.allow(''), version, + + // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release + lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index b189eac186a78d..612d08c09785aa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -15,8 +15,17 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { setSignalsStatusRoute } from './open_close_signals_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('set signal status', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + let server: ReturnType; let { clients, context } = requestContextMock.createTools(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts index dcbb7b8e1fe44e..8d7b171a8537b4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -15,8 +15,17 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { querySignalsRoute } from './query_signals_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('query for signal', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + let server: ReturnType; let { clients, context } = requestContextMock.createTools(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 6768e9534a87ed..fdb1cd148c7fa0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -21,8 +21,17 @@ import { SiemResponseFactory, } from './utils'; import { responseMock } from './__mocks__'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags'; describe('utils', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + describe('transformError', () => { test('returns transformed output error from boom object with a 500 and payload of internal server error', () => { const boom = new Boom('some boom message'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index 1b4c06fb5d8287..0bf9d17d70fdc0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -8,6 +8,7 @@ import { Alert } from '../../../../../../../plugins/alerting/common'; import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { CreateRuleParams } from './types'; import { addTags } from './add_tags'; +import { hasListsFeature } from '../feature_flags'; export const createRules = ({ alertsClient, @@ -41,7 +42,10 @@ export const createRules = ({ references, note, version, + lists, }: CreateRuleParams): Promise => { + // TODO: Remove this and use regular lists once the feature is stable for a release + const listsParam = hasListsFeature() ? { lists } : {}; return alertsClient.create({ data: { name, @@ -74,6 +78,7 @@ export const createRules = ({ references, note, version, + ...listsParam, }, schedule: { interval }, enabled, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts index 8705682f61bcc1..3ed44081388336 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -65,6 +65,7 @@ describe('create_rules_stream_from_ndjson', () => { immutable: false, query: '', language: 'kuery', + lists: [], max_signals: 100, tags: [], threat: [], @@ -88,6 +89,7 @@ describe('create_rules_stream_from_ndjson', () => { immutable: false, query: '', language: 'kuery', + lists: [], max_signals: 100, tags: [], threat: [], @@ -151,6 +153,7 @@ describe('create_rules_stream_from_ndjson', () => { language: 'kuery', max_signals: 100, tags: [], + lists: [], threat: [], references: [], version: 1, @@ -173,6 +176,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], @@ -217,6 +221,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], @@ -240,6 +245,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], @@ -284,6 +290,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], @@ -308,6 +315,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], @@ -351,6 +359,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], @@ -377,6 +386,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index 39b596dfed855d..532bfbaf469ff8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -11,8 +11,17 @@ import { } from '../routes/__mocks__/request_responses'; import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; import { getExportAll } from './get_export_all'; +import { unSetFeatureFlagsForTestsOnly, setFeatureFlagsForTestsOnly } from '../feature_flags'; describe('getExportAll', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it exports everything from the alerts client', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); @@ -20,9 +29,86 @@ describe('getExportAll', () => { const exports = await getExportAll(alertsClient); expect(exports).toEqual({ - rulesNdjson: - '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"note":"# Investigative notes","version":1}\n', - exportDetails: '{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n', + rulesNdjson: `${JSON.stringify({ + created_at: '2019-12-13T16:40:33.400Z', + updated_at: '2019-12-13T16:40:33.400Z', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + output_index: '.siem-signals', + max_signals: 100, + risk_score: 50, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + meta: { someMeta: 'someField' }, + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + note: '# Investigative notes', + version: 1, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + })}\n`, + exportDetails: `${JSON.stringify({ + exported_count: 1, + missing_rules: [], + missing_rules_count: 0, + })}\n`, }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 1406c7c9000b27..f27299436c702c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -12,8 +12,17 @@ import { } from '../routes/__mocks__/request_responses'; import * as readRules from './read_rules'; import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags'; describe('get_export_by_object_ids', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { jest.resetAllMocks(); jest.restoreAllMocks(); @@ -28,9 +37,86 @@ describe('get_export_by_object_ids', () => { const objects = [{ rule_id: 'rule-1' }]; const exports = await getExportByObjectIds(alertsClient, objects); expect(exports).toEqual({ - rulesNdjson: - '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"note":"# Investigative notes","version":1}\n', - exportDetails: '{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n', + rulesNdjson: `${JSON.stringify({ + created_at: '2019-12-13T16:40:33.400Z', + updated_at: '2019-12-13T16:40:33.400Z', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + output_index: '.siem-signals', + max_signals: 100, + risk_score: 50, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + meta: { someMeta: 'someField' }, + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + note: '# Investigative notes', + version: 1, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + })}\n`, + exportDetails: `${JSON.stringify({ + exported_count: 1, + missing_rules: [], + missing_rules_count: 0, + })}\n`, }); }); @@ -119,6 +205,32 @@ describe('get_export_by_object_ids', () => { ], note: '# Investigative notes', version: 1, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }, ], }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index dc71ae3678f2ee..bcbe460fb6a66c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -46,6 +46,7 @@ export const installPrepackagedRules = ( references, note, version, + lists, } = rule; return [ ...acc, @@ -81,6 +82,7 @@ export const installPrepackagedRules = ( references, note, version, + lists, }), ]; }, []); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index 628f4033d5665d..4fb73235854c0c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -45,6 +45,7 @@ export const patchRules = async ({ note, version, throttle, + lists, }: PatchRuleParams): Promise => { const rule = await readRules({ alertsClient, ruleId, id }); if (rule == null) { @@ -77,6 +78,7 @@ export const patchRules = async ({ version, throttle, note, + lists, }); const nextParams = defaults( @@ -106,6 +108,7 @@ export const patchRules = async ({ references, note, version: calculatedVersion, + lists, } ); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 3987654589bdd9..b2a1d2a6307d26 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -10,6 +10,7 @@ import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './t import { addTags } from './add_tags'; import { ruleStatusSavedObjectType } from './saved_object_mappings'; import { calculateVersion } from './utils'; +import { hasListsFeature } from '../feature_flags'; export const updateRules = async ({ alertsClient, @@ -44,6 +45,7 @@ export const updateRules = async ({ version, throttle, note, + lists, }: UpdateRuleParams): Promise => { const rule = await readRules({ alertsClient, ruleId, id }); if (rule == null) { @@ -78,6 +80,9 @@ export const updateRules = async ({ note, }); + // TODO: Remove this and use regular lists once the feature is stable for a release + const listsParam = hasListsFeature() ? { lists } : {}; + const update = await alertsClient.update({ id: rule.id, data: { @@ -110,6 +115,7 @@ export const updateRules = async ({ references, note, version: calculatedVersion, + ...listsParam, }, }, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json new file mode 100644 index 00000000000000..8c86f4c85af1d8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json @@ -0,0 +1,25 @@ +{ + "rule_id": "query-with-list", + "lists": [ + { + "field": "source.ip", + "boolean_operator": "and", + "values": [ + { + "name": "127.0.0.1", + "type": "value" + } + ] + }, + { + "field": "host.name", + "boolean_operator": "and not", + "values": [ + { + "name": "rock01", + "type": "value" + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json new file mode 100644 index 00000000000000..f6856eec59966d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json @@ -0,0 +1,35 @@ +{ + "name": "Query with a list", + "description": "Query with a list", + "rule_id": "query-with-list", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin", + "lists": [ + { + "field": "source.ip", + "boolean_operator": "and", + "values": [ + { + "name": "127.0.0.1", + "type": "value" + } + ] + }, + { + "field": "host.name", + "boolean_operator": "and not", + "values": [ + { + "name": "rock01", + "type": "value" + }, + { + "name": "mothra", + "type": "value" + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json new file mode 100644 index 00000000000000..6704c9676fa562 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json @@ -0,0 +1,31 @@ +{ + "name": "Query with a list", + "description": "Query with a list", + "rule_id": "query-with-list", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin", + "lists": [ + { + "field": "source.ip", + "boolean_operator": "and", + "values": [ + { + "name": "127.0.0.1", + "type": "value" + } + ] + }, + { + "field": "host.name", + "boolean_operator": "and not", + "values": [ + { + "name": "rock01", + "type": "value" + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 010f6b2ee98ff2..31b922e0067cd5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -38,6 +38,32 @@ export const sampleRuleAlertParams = ( meta: undefined, threat: undefined, version: 1, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }); export const sampleDocNoSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts index 30dac114ac5060..c30635c9d14901 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -86,6 +86,32 @@ describe('buildBulkBody', () => { version: 1, created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }, }, }; @@ -176,6 +202,32 @@ describe('buildBulkBody', () => { version: 1, created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }, }, }; @@ -264,6 +316,32 @@ describe('buildBulkBody', () => { version: 1, created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }, }, }; @@ -345,6 +423,32 @@ describe('buildBulkBody', () => { version: 1, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, created_at: fakeSignalSourceHit.signal.rule?.created_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }, }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts index c2900782ed6769..499e3e9c88a851 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts @@ -75,6 +75,32 @@ describe('buildRule', () => { query: 'host.name: Braden', }, ], + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], version: 1, }; expect(rule).toEqual(expected); @@ -122,6 +148,32 @@ describe('buildRule', () => { version: 1, updated_at: rule.updated_at, created_at: rule.created_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }; expect(rule).toEqual(expected); }); @@ -168,6 +220,32 @@ describe('buildRule', () => { version: 1, updated_at: rule.updated_at, created_at: rule.created_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }; expect(rule).toEqual(expected); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts index a9ccda2efe99c9..a1bee162c92805 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts @@ -65,6 +65,7 @@ export const buildRule = ({ version: ruleParams.version, created_at: createdAt, updated_at: updatedAt, + lists: ruleParams.lists, machine_learning_job_id: ruleParams.machineLearningJobId, anomaly_threshold: ruleParams.anomalyThreshold, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts index 7b0546f56dd157..58dd53b6447c51 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts @@ -39,4 +39,5 @@ export const signalParamsSchema = () => type: schema.string(), references: schema.arrayOf(schema.string(), { defaultValue: [] }), version: schema.number({ defaultValue: 1 }), + lists: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index f77924aafadf86..5973a1dbe5f18d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -7,6 +7,7 @@ import { CallAPIOptions } from '../../../../../../../src/core/server'; import { Filter } from '../../../../../../../src/plugins/data/server'; import { IRuleStatusAttributes } from './rules/types'; +import { ListsDefaultArraySchema } from './routes/schemas/types/lists_default_array'; export type PartialFilter = Partial; @@ -22,6 +23,10 @@ export interface ThreatParams { technique: IMitreAttack[]; } +// Notice below we are using lists: ListsDefaultArraySchema[]; which is coming directly from the response output section. +// TODO: Eventually this whole RuleAlertParams will be replaced with io-ts. For now we can slowly strangle it out and reduce duplicate types +// We don't have the input types defined through io-ts just yet but as we being introducing types from there we will more and more remove +// types and share them between input and output schema but have an input Rule Schema and an output Rule Schema. export type RuleType = 'query' | 'saved_query' | 'machine_learning'; export interface RuleAlertParams { @@ -55,6 +60,7 @@ export interface RuleAlertParams { type: RuleType; version: number; throttle?: string; + lists: ListsDefaultArraySchema | null | undefined; } export type RuleTypeParams = Omit; diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index d9d381498fb56a..c505edc79bc760 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -34,6 +34,7 @@ import { ruleStatusSavedObjectType, } from './saved_objects'; import { SiemClientFactory } from './client'; +import { hasListsFeature, listsEnvFeatureFlagName } from './lib/detection_engine/feature_flags'; export { CoreSetup, CoreStart }; @@ -66,6 +67,12 @@ export class Plugin { public setup(core: CoreSetup, plugins: SetupPlugins, __legacy: LegacyServices) { this.logger.debug('Shim plugin setup'); + if (hasListsFeature()) { + // TODO: Remove this once we have the lists feature supported + this.logger.error( + `You have activated the lists feature flag which is NOT currently supported for SIEM! You should turn this feature flag off immediately by un-setting the environment variable: ${listsEnvFeatureFlagName} and restarting Kibana` + ); + } const router = core.http.createRouter(); core.http.registerRouteHandlerContext(this.name, (context, request, response) => ({ diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index d2bfeeb6433d38..89ebd902834b95 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -8,6 +8,7 @@ import path from 'path'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; +import { listsEnvFeatureFlagName } from '../../../legacy/plugins/siem/server/lib/detection_engine/feature_flags'; interface CreateTestConfigOptions { license: string; @@ -31,6 +32,10 @@ const enabledActionTypes = [ 'test.rate-limit', ]; +// Temporary feature flag for the lists feature +// TODO: Remove this once lists land in a Kibana version +process.env[listsEnvFeatureFlagName] = 'true'; + // eslint-disable-next-line import/no-default-export export function createTestConfig(name: string, options: CreateTestConfigOptions) { const { license = 'trial', disabledPlugins = [], ssl = false } = options; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts index 8847a2fdb21af2..6e2a391ec14e1f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts @@ -150,6 +150,7 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial Date: Thu, 19 Mar 2020 08:37:58 +0100 Subject: [PATCH 29/42] [APM] Optimize service map query (#60412) * [APM] Optimize service map query Closes #60411. - Chunk trace lookup - Remove pagination, move dedupe logic to server * Fix imports * Fix imports again Co-authored-by: Nathan L Smith --- .../app/ServiceMap/Cytoscape.stories.tsx | 2 +- .../app/ServiceMap/get_cytoscape_elements.ts | 179 ++++-------------- .../components/app/ServiceMap/index.tsx | 124 +++--------- x-pack/plugins/apm/server/index.ts | 17 ++ .../apm/server/lib/helpers/setup_request.ts | 1 + .../lib/service_map/dedupe_connections.ts | 123 ++++++++++++ .../server/lib/service_map/get_service_map.ts | 61 +++--- .../lib/service_map/get_trace_sample_ids.ts | 89 ++++----- .../plugins/apm/server/routes/service_map.ts | 12 +- 9 files changed, 293 insertions(+), 315 deletions(-) create mode 100644 x-pack/plugins/apm/server/lib/service_map/dedupe_connections.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx index 6f7b743d8b7795..b18f462b541710 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx @@ -13,7 +13,7 @@ import { getCytoscapeElements } from './get_cytoscape_elements'; import serviceMapResponse from './cytoscape-layout-test-response.json'; import { iconForNode } from './icons'; -const elementsFromResponses = getCytoscapeElements([serviceMapResponse], ''); +const elementsFromResponses = getCytoscapeElements(serviceMapResponse, ''); storiesOf('app/ServiceMap/Cytoscape', module).add( 'example', diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts index 9ba70646598fc1..4017aa2e3cdd90 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts @@ -4,166 +4,63 @@ * you may not use this file except in compliance with the Elastic License. */ import { ValuesType } from 'utility-types'; -import { sortBy, isEqual } from 'lodash'; -import { - Connection, - ConnectionNode -} from '../../../../../../../plugins/apm/common/service_map'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceMapAPIResponse } from '../../../../../../../plugins/apm/server/lib/service_map/get_service_map'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; -function getConnectionNodeId(node: ConnectionNode): string { - if ('destination.address' in node) { - // use a prefix to distinguish exernal destination ids from services - return `>${node['destination.address']}`; - } - return node['service.name']; -} - -function getConnectionId(connection: Connection) { - return `${getConnectionNodeId(connection.source)}~${getConnectionNodeId( - connection.destination - )}`; -} export function getCytoscapeElements( - responses: ServiceMapAPIResponse[], + response: ServiceMapAPIResponse, search: string ) { - const discoveredServices = responses.flatMap( - response => response.discoveredServices - ); - - const serviceNodes = responses - .flatMap(response => response.services) - .map(service => ({ - ...service, - id: service['service.name'] - })); - - // maps destination.address to service.name if possible - function getConnectionNode(node: ConnectionNode) { - let mappedNode: ConnectionNode | undefined; - - if ('destination.address' in node) { - mappedNode = discoveredServices.find(map => isEqual(map.from, node))?.to; - } - - if (!mappedNode) { - mappedNode = node; - } - - return { - ...mappedNode, - id: getConnectionNodeId(mappedNode) - }; - } - - // build connections with mapped nodes - const connections = responses - .flatMap(response => response.connections) - .map(connection => { - const source = getConnectionNode(connection.source); - const destination = getConnectionNode(connection.destination); - - return { - source, - destination, - id: getConnectionId({ source, destination }) - }; - }) - .filter(connection => connection.source.id !== connection.destination.id); - - const nodes = connections - .flatMap(connection => [connection.source, connection.destination]) - .concat(serviceNodes); - - type ConnectionWithId = ValuesType; - type ConnectionNodeWithId = ValuesType; - - const connectionsById = connections.reduce((connectionMap, connection) => { - return { - ...connectionMap, - [connection.id]: connection - }; - }, {} as Record); + const { nodes, connections } = response; const nodesById = nodes.reduce((nodeMap, node) => { return { ...nodeMap, [node.id]: node }; - }, {} as Record); - - const cyNodes = (Object.values(nodesById) as ConnectionNodeWithId[]).map( - node => { - let data = {}; - - if ('service.name' in node) { - data = { - href: getAPMHref( - `/services/${node['service.name']}/service-map`, - search - ), - agentName: node['agent.name'], - frameworkName: node['service.framework.name'], - type: 'service' - }; - } - - if ('span.type' in node) { - data = { - // For nodes with span.type "db", convert it to "database". Otherwise leave it as-is. - type: node['span.type'] === 'db' ? 'database' : node['span.type'], - // Externals should not have a subtype so make it undefined if the type is external. - subtype: node['span.type'] !== 'external' && node['span.subtype'] - }; - } - - return { - group: 'nodes' as const, - data: { - id: node.id, - label: - 'service.name' in node - ? node['service.name'] - : node['destination.address'], - ...data - } + }, {} as Record>); + + const cyNodes = (Object.values(nodesById) as Array< + ValuesType + >).map(node => { + let data = {}; + + if ('service.name' in node) { + data = { + href: getAPMHref( + `/services/${node['service.name']}/service-map`, + search + ), + agentName: node['agent.name'], + frameworkName: node['service.framework.name'], + type: 'service' }; } - ); - - // instead of adding connections in two directions, - // we add a `bidirectional` flag to use in styling - // and hide the inverse edge when rendering - const dedupedConnections = (sortBy( - Object.values(connectionsById), - // make sure that order is stable - 'id' - ) as ConnectionWithId[]).reduce< - Array< - ConnectionWithId & { bidirectional?: boolean; isInverseEdge?: boolean } - > - >((prev, connection) => { - const reversedConnection = prev.find( - c => - c.destination.id === connection.source.id && - c.source.id === connection.destination.id - ); - if (reversedConnection) { - reversedConnection.bidirectional = true; - return prev.concat({ - ...connection, - isInverseEdge: true - }); + if ('span.type' in node) { + data = { + // For nodes with span.type "db", convert it to "database". Otherwise leave it as-is. + type: node['span.type'] === 'db' ? 'database' : node['span.type'], + // Externals should not have a subtype so make it undefined if the type is external. + subtype: node['span.type'] !== 'external' && node['span.subtype'] + }; } - return prev.concat(connection); - }, []); + return { + group: 'nodes' as const, + data: { + id: node.id, + label: + 'service.name' in node + ? node['service.name'] + : node['destination.address'], + ...data + } + }; + }); - const cyEdges = dedupedConnections.map(connection => { + const cyEdges = connections.map(connection => { return { group: 'edges' as const, classes: connection.isInverseEdge ? 'invisible' : undefined, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx index 93aa3d406028c2..6222a00a9e8885 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -4,26 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiBetaBadge } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import { ElementDefinition } from 'cytoscape'; -import { find, isEqual } from 'lodash'; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState -} from 'react'; -import { EuiBetaBadge } from '@elastic/eui'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import { isValidPlatinumLicense } from '../../../../../../../plugins/apm/common/service_map'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceMapAPIResponse } from '../../../../../../../plugins/apm/server/lib/service_map/get_service_map'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useDeepObjectIdentity } from '../../../hooks/useDeepObjectIdentity'; +import { useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; -import { useLoadingIndicator } from '../../../hooks/useLoadingIndicator'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { callApmApi } from '../../../services/rest/createCallApmApi'; @@ -64,13 +53,11 @@ const BetaBadgeContainer = styled.div` top: ${theme.gutterTypes.gutterSmall}; z-index: 1; /* The element containing the cytoscape canvas has z-index = 0. */ `; -const MAX_REQUESTS = 5; export function ServiceMap({ serviceName }: ServiceMapProps) { const license = useLicense(); const { search } = useLocation(); const { urlParams, uiFilters } = useUrlParams(); - const { notifications } = useApmPluginContext().core; const params = useDeepObjectIdentity({ start: urlParams.start, end: urlParams.end, @@ -82,95 +69,28 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { } }); - const renderedElements = useRef([]); - - const [responses, setResponses] = useState([]); - - const { setIsLoading } = useLoadingIndicator(); - - const [, _setUnusedState] = useState(false); - - const elements = useMemo(() => getCytoscapeElements(responses, search), [ - responses, - search - ]); - - const forceUpdate = useCallback(() => _setUnusedState(value => !value), []); - - const getNext = useCallback( - async (input: { reset?: boolean; after?: string | undefined }) => { - const { start, end, uiFilters: strippedUiFilters, ...query } = params; - - if (input.reset) { - renderedElements.current = []; - setResponses([]); - } - - if (start && end) { - setIsLoading(true); - try { - const data = await callApmApi({ - pathname: '/api/apm/service-map', - params: { - query: { - ...query, - start, - end, - uiFilters: JSON.stringify(strippedUiFilters), - after: input.after - } - } - }); - setResponses(resp => resp.concat(data)); - - const shouldGetNext = - responses.length + 1 < MAX_REQUESTS && data.after; - - if (shouldGetNext) { - await getNext({ after: data.after }); - } else { - setIsLoading(false); + const { data } = useFetcher(() => { + const { start, end } = params; + if (start && end) { + return callApmApi({ + pathname: '/api/apm/service-map', + params: { + query: { + ...params, + start, + end, + uiFilters: JSON.stringify(params.uiFilters) } - } catch (error) { - setIsLoading(false); - notifications.toasts.addError(error, { - title: i18n.translate('xpack.apm.errorServiceMapData', { - defaultMessage: `Error loading service connections` - }) - }); } - } - }, - [params, setIsLoading, responses.length, notifications.toasts] - ); - - useEffect(() => { - const loadServiceMaps = async () => { - await getNext({ reset: true }); - }; - - loadServiceMaps(); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [params]); - - useEffect(() => { - if (renderedElements.current.length === 0) { - renderedElements.current = elements; - return; + }); } + }, [params]); - const newElements = elements.filter(element => { - return !find(renderedElements.current, el => isEqual(el, element)); - }); - - if (newElements.length > 0 && renderedElements.current.length > 0) { - renderedElements.current = elements; - forceUpdate(); - } - }, [elements, forceUpdate]); + const elements = useMemo(() => { + return data ? getCytoscapeElements(data as any, search) : []; + }, [data, search]); - const { ref: wrapperRef, width, height } = useRefDimensions(); + const { ref, height, width } = useRefDimensions(); if (!license) { return null; @@ -179,10 +99,10 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { return isValidPlatinumLicense(license) ? (
${node['destination.address']}`; + } + return node['service.name']; +} + +function getConnectionId(connection: Connection) { + return `${getConnectionNodeId(connection.source)}~${getConnectionNodeId( + connection.destination + )}`; +} + +type ServiceMapResponse = ConnectionsResponse & { services: ServicesResponse }; + +export function dedupeConnections(response: ServiceMapResponse) { + const { discoveredServices, services, connections } = response; + + const serviceNodes = services.map(service => ({ + ...service, + id: service['service.name'] + })); + + // maps destination.address to service.name if possible + function getConnectionNode(node: ConnectionNode) { + let mappedNode: ConnectionNode | undefined; + + if ('destination.address' in node) { + mappedNode = discoveredServices.find(map => isEqual(map.from, node))?.to; + } + + if (!mappedNode) { + mappedNode = node; + } + + return { + ...mappedNode, + id: getConnectionNodeId(mappedNode) + }; + } + + // build connections with mapped nodes + const mappedConnections = connections + .map(connection => { + const source = getConnectionNode(connection.source); + const destination = getConnectionNode(connection.destination); + + return { + source, + destination, + id: getConnectionId({ source, destination }) + }; + }) + .filter(connection => connection.source.id !== connection.destination.id); + + const nodes = mappedConnections + .flatMap(connection => [connection.source, connection.destination]) + .concat(serviceNodes); + + const dedupedNodes: typeof nodes = []; + + nodes.forEach(node => { + if (!dedupedNodes.find(dedupedNode => isEqual(node, dedupedNode))) { + dedupedNodes.push(node); + } + }); + + type ConnectionWithId = ValuesType; + + const connectionsById = mappedConnections.reduce( + (connectionMap, connection) => { + return { + ...connectionMap, + [connection.id]: connection + }; + }, + {} as Record + ); + + // instead of adding connections in two directions, + // we add a `bidirectional` flag to use in styling + const dedupedConnections = (sortBy( + Object.values(connectionsById), + // make sure that order is stable + 'id' + ) as ConnectionWithId[]).reduce< + Array< + ConnectionWithId & { bidirectional?: boolean; isInverseEdge?: boolean } + > + >((prev, connection) => { + const reversedConnection = prev.find( + c => + c.destination.id === connection.source.id && + c.source.id === connection.destination.id + ); + + if (reversedConnection) { + reversedConnection.bidirectional = true; + return prev.concat({ + ...connection, + isInverseEdge: true + }); + } + + return prev.concat(connection); + }, []); + + return { + nodes: dedupedNodes, + connections: dedupedConnections + }; +} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 85d71784b55c70..96acfb7986c68e 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { chunk } from 'lodash'; import { PromiseReturnType } from '../../../typings/common'; import { Setup, @@ -19,48 +19,61 @@ import { SERVICE_NAME, SERVICE_FRAMEWORK_NAME } from '../../../common/elasticsearch_fieldnames'; +import { dedupeConnections } from './dedupe_connections'; export interface IEnvOptions { setup: Setup & SetupTimeRange & SetupUIFilters; serviceName?: string; environment?: string; - after?: string; } async function getConnectionData({ setup, serviceName, - environment, - after + environment }: IEnvOptions) { - const { traceIds, after: nextAfter } = await getTraceSampleIds({ + const { traceIds } = await getTraceSampleIds({ setup, serviceName, - environment, - after + environment }); - const serviceMapData = traceIds.length - ? await getServiceMapFromTraceIds({ + const chunks = chunk( + traceIds, + setup.config['xpack.apm.serviceMapMaxTracesPerRequest'] + ); + + const init = { + connections: [], + discoveredServices: [] + }; + + if (!traceIds.length) { + return init; + } + + const chunkedResponses = await Promise.all( + chunks.map(traceIdsChunk => + getServiceMapFromTraceIds({ setup, serviceName, environment, - traceIds + traceIds: traceIdsChunk }) - : { connections: [], discoveredServices: [] }; + ) + ); - return { - after: nextAfter, - ...serviceMapData - }; + return chunkedResponses.reduce((prev, current) => { + return { + connections: prev.connections.concat(current.connections), + discoveredServices: prev.discoveredServices.concat( + current.discoveredServices + ) + }; + }); } async function getServicesData(options: IEnvOptions) { - // only return services on the first request for the global service map - if (options.after) { - return []; - } - const { setup } = options; const projection = getServicesProjection({ setup }); @@ -125,15 +138,19 @@ async function getServicesData(options: IEnvOptions) { ); } +export type ConnectionsResponse = PromiseReturnType; +export type ServicesResponse = PromiseReturnType; + export type ServiceMapAPIResponse = PromiseReturnType; + export async function getServiceMap(options: IEnvOptions) { const [connectionData, servicesData] = await Promise.all([ getConnectionData(options), getServicesData(options) ]); - return { + return dedupeConnections({ ...connectionData, services: servicesData - }; + }); } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 463fe7f2cf6402..f4e12df5d6a665 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -15,27 +15,24 @@ import { PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, - SPAN_TYPE, - SPAN_SUBTYPE, + TRACE_ID, DESTINATION_ADDRESS, - TRACE_ID + SPAN_TYPE, + SPAN_SUBTYPE } from '../../../common/elasticsearch_fieldnames'; -const MAX_CONNECTIONS_PER_REQUEST = 1000; const MAX_TRACES_TO_INSPECT = 1000; export async function getTraceSampleIds({ - after, serviceName, environment, setup }: { - after?: string; serviceName?: string; environment?: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, client, indices } = setup; + const { start, end, client, indices, config } = setup; const rangeQuery = { range: rangeFilter(start, end) }; @@ -65,9 +62,15 @@ export async function getTraceSampleIds({ query.bool.filter.push({ term: { [SERVICE_ENVIRONMENT]: environment } }); } - const afterObj = after - ? { after: JSON.parse(Buffer.from(after, 'base64').toString()) } - : {}; + const fingerprintBucketSize = serviceName + ? config['xpack.apm.serviceMapFingerprintBucketSize'] + : config['xpack.apm.serviceMapFingerprintGlobalBucketSize']; + + const traceIdBucketSize = serviceName + ? config['xpack.apm.serviceMapTraceIdBucketSize'] + : config['xpack.apm.serviceMapTraceIdGlobalBucketSize']; + + const samplerShardSize = traceIdBucketSize * 10; const params = { index: [indices['apm_oss.spanIndices']], @@ -77,42 +80,57 @@ export async function getTraceSampleIds({ aggs: { connections: { composite: { - size: MAX_CONNECTIONS_PER_REQUEST, - ...afterObj, sources: [ - { [SERVICE_NAME]: { terms: { field: SERVICE_NAME } } }, { - [SERVICE_ENVIRONMENT]: { - terms: { field: SERVICE_ENVIRONMENT, missing_bucket: true } + [DESTINATION_ADDRESS]: { + terms: { + field: DESTINATION_ADDRESS + } } }, { - [SPAN_TYPE]: { - terms: { field: SPAN_TYPE, missing_bucket: true } + [SERVICE_NAME]: { + terms: { + field: SERVICE_NAME + } } }, { - [SPAN_SUBTYPE]: { - terms: { field: SPAN_SUBTYPE, missing_bucket: true } + [SERVICE_ENVIRONMENT]: { + terms: { + field: SERVICE_ENVIRONMENT, + missing_bucket: true + } } }, { - [DESTINATION_ADDRESS]: { - terms: { field: DESTINATION_ADDRESS } + [SPAN_TYPE]: { + terms: { + field: SPAN_TYPE + } + } + }, + { + [SPAN_SUBTYPE]: { + terms: { + field: SPAN_SUBTYPE, + missing_bucket: true + } } } - ] + ], + size: fingerprintBucketSize }, aggs: { sample: { sampler: { - shard_size: 30 + shard_size: samplerShardSize }, aggs: { trace_ids: { terms: { field: TRACE_ID, - size: 10, + size: traceIdBucketSize, execution_hint: 'map' as const, // remove bias towards large traces by sorting on trace.id // which will be random-esque @@ -129,25 +147,9 @@ export async function getTraceSampleIds({ } }; - const tracesSampleResponse = await client.search< - { trace: { id: string } }, - typeof params - >(params); - - let nextAfter: string | undefined; - - const receivedAfterKey = - tracesSampleResponse.aggregations?.connections.after_key; - - if ( - receivedAfterKey && - (tracesSampleResponse.aggregations?.connections.buckets.length ?? 0) >= - MAX_CONNECTIONS_PER_REQUEST - ) { - nextAfter = Buffer.from(JSON.stringify(receivedAfterKey)).toString( - 'base64' - ); - } + const tracesSampleResponse = await client.search( + params + ); // make sure at least one trace per composite/connection bucket // is queried @@ -167,7 +169,6 @@ export async function getTraceSampleIds({ ); return { - after: nextAfter, traceIds }; } diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index bead0445d6ccce..a61a61e3ccaacf 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -20,10 +20,12 @@ export const serviceMapRoute = createRoute(() => ({ path: '/api/apm/service-map', params: { query: t.intersection([ - t.partial({ environment: t.string, serviceName: t.string }), + t.partial({ + environment: t.string, + serviceName: t.string + }), uiFiltersRt, - rangeRt, - t.partial({ after: t.string }) + rangeRt ]) }, handler: async ({ context, request }) => { @@ -36,9 +38,9 @@ export const serviceMapRoute = createRoute(() => ({ const setup = await setupRequest(context, request); const { - query: { serviceName, environment, after } + query: { serviceName, environment } } = context.params; - return getServiceMap({ setup, serviceName, environment, after }); + return getServiceMap({ setup, serviceName, environment }); } })); From 836b3d00eff70cb645347d82041139bebd2c602a Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 19 Mar 2020 09:08:43 +0100 Subject: [PATCH 30/42] [ML] Add functional tests for file data visualizer (#60413) This PR adds basic functional tests for the file data visualizer, covering a file import and error messages for non-log files. It also moves the file input path handling to a common location in order to avoid code duplication. --- test/functional/page_objects/common_page.ts | 6 + test/functional/page_objects/settings_page.ts | 4 +- .../components/about_panel/about_panel.js | 4 +- .../components/bottom_bar/bottom_bar.tsx | 1 + .../file_error_callouts.js | 2 + .../components/import_settings/simple.js | 2 + .../import_summary/import_summary.js | 1 + .../components/import_view/import_view.js | 5 +- .../components/results_view/results_view.js | 10 +- .../data_visualizer/file_data_visualizer.ts | 104 +++++++++++++++++ .../files_to_import/artificial_server_log | 19 +++ .../files_to_import/not_a_log_file | 8 ++ .../machine_learning/data_visualizer/index.ts | 1 + .../test/functional/page_objects/gis_page.js | 4 +- .../machine_learning/data_visualizer.ts | 5 + .../data_visualizer_file_based.ts | 110 ++++++++++++++++++ .../services/machine_learning/index.ts | 1 + x-pack/test/functional/services/ml.ts | 3 + 18 files changed, 275 insertions(+), 15 deletions(-) create mode 100644 x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts create mode 100644 x-pack/test/functional/apps/machine_learning/data_visualizer/files_to_import/artificial_server_log create mode 100644 x-pack/test/functional/apps/machine_learning/data_visualizer/files_to_import/not_a_log_file create mode 100644 x-pack/test/functional/services/machine_learning/data_visualizer_file_based.ts diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 5ee3726ddb44fe..6895034f22ed54 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -514,6 +514,12 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } }); } + + async setFileInputPath(path: string) { + log.debug(`Setting the path '${path}' on the file input`); + const input = await find.byCssSelector('.euiFilePicker__input'); + await input.type(path); + } } return new CommonPage(); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index c244deba5f17ea..0ad1a1dc513213 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -612,9 +612,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider log.debug(`Clicking importObjects`); await testSubjects.click('importObjects'); - log.debug(`Setting the path on the file input`); - const input = await find.byCssSelector('.euiFilePicker__input'); - await input.type(path); + await PageObjects.common.setFileInputPath(path); if (!overwriteAll) { log.debug(`Toggling overwriteAll`); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/about_panel.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/about_panel.js index 99cdc816dfe3da..edecc925591d37 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/about_panel.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/about_panel.js @@ -25,7 +25,7 @@ import { WelcomeContent } from './welcome_content'; export function AboutPanel({ onFilePickerChange }) { return ( - + @@ -58,7 +58,7 @@ export function AboutPanel({ onFilePickerChange }) { export function LoadingPanel() { return ( - +
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx index 72e4f88062789f..e28386093abe00 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx @@ -48,6 +48,7 @@ export const BottomBar: FC = ({ mode, onChangeMode, onCancel, di fill isDisabled={disableImport} onClick={() => onChangeMode(DATAVISUALIZER_MODE.IMPORT)} + data-test-subj="mlFileDataVisOpenImportPageButton" > {errorText} @@ -79,6 +80,7 @@ export function FileCouldNotBeRead({ error, loaded }) { } color="danger" iconType="cross" + data-test-subj="mlFileUploadErrorCallout fileCouldNotBeRead" > {error !== undefined &&

{error}

} {loaded && ( diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js index 8c6f569bf86054..271b9493aa1f35 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js @@ -47,6 +47,7 @@ export const SimpleSettings = ({ defaultMessage: 'Index name, required field', } )} + data-test-subj="mlFileDataVisIndexNameInput" /> @@ -63,6 +64,7 @@ export const SimpleSettings = ({ checked={createIndexPattern === true} disabled={initialized === true} onChange={onCreateIndexPatternChange} + data-test-subj="mlFileDataVisCreateIndexPatternCheckbox" /> ); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/import_summary.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/import_summary.js index d79ce020c8071b..0e67807a39fd92 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/import_summary.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/import_summary.js @@ -39,6 +39,7 @@ export function ImportSummary({ } color="success" iconType="check" + data-test-subj="mlFileImportSuccessCallout" > diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js index bdfc27099a1853..94627b688b03a3 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js @@ -462,7 +462,7 @@ export class ImportView extends Component { initialized === true; return ( - + @@ -470,7 +470,7 @@ export class ImportView extends Component { - +

{ ]; return ( - + -

{fileName}

+

{fileName}

- + { - + @@ -70,7 +70,7 @@ export const ResultsView = ({ data, fileName, results, showEditFlyout }) => { - + {}} />
diff --git a/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts new file mode 100644 index 00000000000000..94b28e5035edfe --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts @@ -0,0 +1,104 @@ +/* + * 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 path from 'path'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: FtrProviderContext) { + const ml = getService('ml'); + + const testDataListPositive = [ + { + suiteSuffix: 'with an artificial server log', + filePath: path.join(__dirname, 'files_to_import', 'artificial_server_log'), + indexName: 'user-import_1', + createIndexPattern: false, + expected: { + results: { + title: 'artificial_server_log', + }, + }, + }, + ]; + + const testDataListNegative = [ + { + suiteSuffix: 'with a non-log file', + filePath: path.join(__dirname, 'files_to_import', 'not_a_log_file'), + }, + ]; + + describe('file based', function() { + this.tags(['smoke', 'mlqa']); + before(async () => { + await ml.securityUI.loginAsMlPowerUser(); + await ml.navigation.navigateToMl(); + }); + + for (const testData of testDataListPositive) { + describe(testData.suiteSuffix, function() { + after(async () => { + await ml.api.deleteIndices(testData.indexName); + }); + + it('loads the data visualizer selector page', async () => { + await ml.navigation.navigateToDataVisualizer(); + }); + + it('loads the file upload page', async () => { + await ml.dataVisualizer.navigateToFileUpload(); + }); + + it('selects a file and loads visualizer results', async () => { + await ml.dataVisualizerFileBased.selectFile(testData.filePath); + }); + + it('displays the components of the file details page', async () => { + await ml.dataVisualizerFileBased.assertFileTitle(testData.expected.results.title); + await ml.dataVisualizerFileBased.assertFileContentPanelExists(); + await ml.dataVisualizerFileBased.assertSummaryPanelExists(); + await ml.dataVisualizerFileBased.assertFileStatsPanelExists(); + }); + + it('loads the import settings page', async () => { + await ml.dataVisualizerFileBased.navigateToFileImport(); + }); + + it('sets the index name', async () => { + await ml.dataVisualizerFileBased.setIndexName(testData.indexName); + }); + + it('sets the create index pattern checkbox', async () => { + await ml.dataVisualizerFileBased.setCreateIndexPatternCheckboxState( + testData.createIndexPattern + ); + }); + + it('imports the file', async () => { + await ml.dataVisualizerFileBased.startImportAndWaitForProcessing(); + }); + }); + } + + for (const testData of testDataListNegative) { + describe(testData.suiteSuffix, function() { + it('loads the data visualizer selector page', async () => { + await ml.navigation.navigateToDataVisualizer(); + }); + + it('loads the file upload page', async () => { + await ml.dataVisualizer.navigateToFileUpload(); + }); + + it('selects a file and displays an error', async () => { + await ml.dataVisualizerFileBased.selectFile(testData.filePath, true); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/data_visualizer/files_to_import/artificial_server_log b/x-pack/test/functional/apps/machine_learning/data_visualizer/files_to_import/artificial_server_log new file mode 100644 index 00000000000000..3571d3c9b5e429 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/data_visualizer/files_to_import/artificial_server_log @@ -0,0 +1,19 @@ +2018-01-06 16:56:14.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.0 +2018-01-06 16:56:15.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.1 +2018-01-06 16:56:16.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.2 +2018-01-06 16:56:17.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.3 +2018-01-06 16:56:18.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.0 +2018-01-06 16:56:19.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.2 +2018-01-06 16:56:20.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.3 +2018-01-06 16:56:21.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.4 +2018-01-06 16:56:22.295748 WARN host:'Server A' Disk watermark 80% +2018-01-06 17:16:23.295748 WARN host:'Server A' Disk watermark 90% +2018-01-06 17:36:10.295748 ERROR host:'Server A' Main process crashed +2018-01-06 17:36:14.295748 INFO host:'Server A' Connection from ip 123.456.789.0 closed +2018-01-06 17:36:15.295748 INFO host:'Server A' Connection from ip 123.456.789.1 closed +2018-01-06 17:36:16.295748 INFO host:'Server A' Connection from ip 123.456.789.2 closed +2018-01-06 17:36:17.295748 INFO host:'Server A' Connection from ip 123.456.789.3 closed +2018-01-06 17:46:11.295748 INFO host:'Server B' Some special characters °!"§$%&/()=?`'^²³{[]}\+*~#'-_.:,;µ|<>äöüß +2018-01-06 17:46:12.295748 INFO host:'Server B' Shutting down + + diff --git a/x-pack/test/functional/apps/machine_learning/data_visualizer/files_to_import/not_a_log_file b/x-pack/test/functional/apps/machine_learning/data_visualizer/files_to_import/not_a_log_file new file mode 100644 index 00000000000000..a580466c5c2a75 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/data_visualizer/files_to_import/not_a_log_file @@ -0,0 +1,8 @@ +This +is +not +a +log +file + + diff --git a/x-pack/test/functional/apps/machine_learning/data_visualizer/index.ts b/x-pack/test/functional/apps/machine_learning/data_visualizer/index.ts index fa4b5e76ae7282..4a1eb06d7a4877 100644 --- a/x-pack/test/functional/apps/machine_learning/data_visualizer/index.ts +++ b/x-pack/test/functional/apps/machine_learning/data_visualizer/index.ts @@ -10,5 +10,6 @@ export default function({ loadTestFile }: FtrProviderContext) { this.tags(['skipFirefox']); loadTestFile(require.resolve('./index_data_visualizer')); + loadTestFile(require.resolve('./file_data_visualizer')); }); } diff --git a/x-pack/test/functional/page_objects/gis_page.js b/x-pack/test/functional/page_objects/gis_page.js index b78ec6a477e1f8..8d0c649d75dd62 100644 --- a/x-pack/test/functional/page_objects/gis_page.js +++ b/x-pack/test/functional/page_objects/gis_page.js @@ -515,9 +515,7 @@ export function GisPageProvider({ getService, getPageObjects }) { } async uploadJsonFileForIndexing(path) { - log.debug(`Setting the path on the file input`); - const input = await find.byCssSelector('.euiFilePicker__input'); - await input.type(path); + await PageObjects.common.setFileInputPath(path); log.debug(`File selected`); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional/services/machine_learning/data_visualizer.ts b/x-pack/test/functional/services/machine_learning/data_visualizer.ts index dec854130624fa..c60ae29b6b3f40 100644 --- a/x-pack/test/functional/services/machine_learning/data_visualizer.ts +++ b/x-pack/test/functional/services/machine_learning/data_visualizer.ts @@ -22,5 +22,10 @@ export function MachineLearningDataVisualizerProvider({ getService }: FtrProvide await testSubjects.click('mlDataVisualizerSelectIndexButton'); await testSubjects.existOrFail('mlPageSourceSelection'); }, + + async navigateToFileUpload() { + await testSubjects.click('mlDataVisualizerUploadFileButton'); + await testSubjects.existOrFail('mlPageFileDataVisualizerUpload'); + }, }; } diff --git a/x-pack/test/functional/services/machine_learning/data_visualizer_file_based.ts b/x-pack/test/functional/services/machine_learning/data_visualizer_file_based.ts new file mode 100644 index 00000000000000..eea0a83879ea76 --- /dev/null +++ b/x-pack/test/functional/services/machine_learning/data_visualizer_file_based.ts @@ -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. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlCommon } from './common'; + +export function MachineLearningDataVisualizerFileBasedProvider( + { getService, getPageObjects }: FtrProviderContext, + mlCommon: MlCommon +) { + const log = getService('log'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + + const PageObjects = getPageObjects(['common']); + + return { + async selectFile(path: string, expectError: boolean = false) { + log.debug(`Importing file '${path}' ...`); + await PageObjects.common.setFileInputPath(path); + + await testSubjects.waitForDeleted('mlPageFileDataVisLoading'); + + if (expectError) { + await testSubjects.existOrFail('~mlFileUploadErrorCallout'); + } else { + await testSubjects.missingOrFail('~mlFileUploadErrorCallout'); + await testSubjects.existOrFail('mlPageFileDataVisResults'); + } + }, + + async assertFileTitle(expectedTitle: string) { + const actualTitle = await testSubjects.getVisibleText('mlFileDataVisResultsTitle'); + expect(actualTitle).to.eql( + expectedTitle, + `Expected file title to be '${expectedTitle}' (got '${actualTitle}')` + ); + }, + + async assertFileContentPanelExists() { + await testSubjects.existOrFail('mlFileDataVisFileContentPanel'); + }, + + async assertSummaryPanelExists() { + await testSubjects.existOrFail('mlFileDataVisSummaryPanel'); + }, + + async assertFileStatsPanelExists() { + await testSubjects.existOrFail('mlFileDataVisFileStatsPanel'); + }, + + async navigateToFileImport() { + await testSubjects.click('mlFileDataVisOpenImportPageButton'); + await testSubjects.existOrFail('mlPageFileDataVisImport'); + }, + + async assertImportSettingsPanelExists() { + await testSubjects.existOrFail('mlFileDataVisImportSettingsPanel'); + }, + + async assertIndexNameValue(expectedValue: string) { + const actualIndexName = await testSubjects.getAttribute( + 'mlFileDataVisIndexNameInput', + 'value' + ); + expect(actualIndexName).to.eql( + expectedValue, + `Expected index name to be '${expectedValue}' (got '${actualIndexName}')` + ); + }, + + async setIndexName(indexName: string) { + await mlCommon.setValueWithChecks('mlFileDataVisIndexNameInput', indexName, { + clearWithKeyboard: true, + }); + await this.assertIndexNameValue(indexName); + }, + + async assertCreateIndexPatternCheckboxValue(expectedValue: boolean) { + const isChecked = await testSubjects.isChecked('mlFileDataVisCreateIndexPatternCheckbox'); + expect(isChecked).to.eql( + expectedValue, + `Expected create index pattern checkbox to be ${expectedValue ? 'checked' : 'unchecked'}` + ); + }, + + async setCreateIndexPatternCheckboxState(newState: boolean) { + const isChecked = await testSubjects.isChecked('mlFileDataVisCreateIndexPatternCheckbox'); + if (isChecked !== newState) { + // this checkbox can't be clicked directly, instead click the corresponding label + const panel = await testSubjects.find('mlFileDataVisImportSettingsPanel'); + const label = await panel.findByCssSelector('[for="createIndexPattern"]'); + await label.click(); + } + await this.assertCreateIndexPatternCheckboxValue(newState); + }, + + async startImportAndWaitForProcessing() { + await testSubjects.click('mlFileDataVisImportButton'); + await retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail('mlFileImportSuccessCallout'); + }); + }, + }; +} diff --git a/x-pack/test/functional/services/machine_learning/index.ts b/x-pack/test/functional/services/machine_learning/index.ts index b916c1e7909b1c..f5adf63825163b 100644 --- a/x-pack/test/functional/services/machine_learning/index.ts +++ b/x-pack/test/functional/services/machine_learning/index.ts @@ -13,6 +13,7 @@ export { MachineLearningDataFrameAnalyticsProvider } from './data_frame_analytic export { MachineLearningDataFrameAnalyticsCreationProvider } from './data_frame_analytics_creation'; export { MachineLearningDataFrameAnalyticsTableProvider } from './data_frame_analytics_table'; export { MachineLearningDataVisualizerProvider } from './data_visualizer'; +export { MachineLearningDataVisualizerFileBasedProvider } from './data_visualizer_file_based'; export { MachineLearningDataVisualizerIndexBasedProvider } from './data_visualizer_index_based'; export { MachineLearningJobManagementProvider } from './job_management'; export { MachineLearningJobSelectionProvider } from './job_selection'; diff --git a/x-pack/test/functional/services/ml.ts b/x-pack/test/functional/services/ml.ts index 354e0907375ca4..f3981c9edf92f0 100644 --- a/x-pack/test/functional/services/ml.ts +++ b/x-pack/test/functional/services/ml.ts @@ -16,6 +16,7 @@ import { MachineLearningDataFrameAnalyticsCreationProvider, MachineLearningDataFrameAnalyticsTableProvider, MachineLearningDataVisualizerProvider, + MachineLearningDataVisualizerFileBasedProvider, MachineLearningDataVisualizerIndexBasedProvider, MachineLearningJobManagementProvider, MachineLearningJobSelectionProvider, @@ -48,6 +49,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { ); const dataFrameAnalyticsTable = MachineLearningDataFrameAnalyticsTableProvider(context); const dataVisualizer = MachineLearningDataVisualizerProvider(context); + const dataVisualizerFileBased = MachineLearningDataVisualizerFileBasedProvider(context, common); const dataVisualizerIndexBased = MachineLearningDataVisualizerIndexBasedProvider(context); const jobManagement = MachineLearningJobManagementProvider(context, api); const jobSelection = MachineLearningJobSelectionProvider(context); @@ -75,6 +77,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { dataFrameAnalyticsCreation, dataFrameAnalyticsTable, dataVisualizer, + dataVisualizerFileBased, dataVisualizerIndexBased, jobManagement, jobSelection, From 2eda06e7701ca41c6c03d8e3c9285116175d56ad Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Thu, 19 Mar 2020 08:28:43 +0000 Subject: [PATCH 31/42] Introduce search interceptor (#60523) * Add async search strategy * Add async search * Fix async strategy and add tests * Move types to separate file * Revert changes to demo search * Update demo search strategy to use async * Add async es search strategy * Return response as rawResponse * Poll after initial request * Add cancellation to search strategies * Add tests * Simplify async search strategy * Move loadingCount to search strategy * Update abort controller library * Bootstrap * Abort when the request is aborted * Add utility and update value suggestions route * Fix bad merge conflict * Update tests * Move to data_enhanced plugin * Remove bad merge * Revert switching abort controller libraries * Revert package.json in lib * Move to previous abort controller * Add support for frozen indices * Fix test to use fake timers to run debounced handlers * Revert changes to example plugin * Fix loading bar not going away when cancelling * Call getSearchStrategy instead of passing directly * Add async demo search strategy * Fix error with setting state * Update how aborting works * Fix type checks * Add test for loading count * Attempt to fix broken example test * Revert changes to test * Fix test * Update name to camelCase * Fix failing test * Don't require data_enhanced in example plugin * Actually send DELETE request * Use waitForCompletion parameter * Use default search params * Add support for rollups * Only make changes needed for frozen indices/rollups * Only make changes needed for frozen indices/rollups * Add back in async functionality * Fix tests/types * Fix issue with sending empty body in GET * Don't include skipped in loaded/total * Don't wait before polling the next time * Add search interceptor for bulk managing searches * Simplify search logic * Fix merge error * Review feedback * Add service for running beyond timeout * Refactor abort utils * Remove unneeded changes * Add tests * cleanup mocks * Update src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.html Co-Authored-By: Lukas Olson Co-authored-by: Lukas Olson Co-authored-by: Elastic Machine --- .../kibana-plugin-plugins-data-server.md | 2 - src/plugins/data/common/index.ts | 1 + .../data/common/utils/abort_utils.test.ts | 114 +++++++++++++ src/plugins/data/common/utils/abort_utils.ts | 56 +++++++ src/plugins/data/common/utils/index.ts | 1 + src/plugins/data/public/mocks.ts | 42 +---- src/plugins/data/public/search/index.ts | 2 + src/plugins/data/public/search/mocks.ts | 25 ++- .../public/search/request_timeout_error.ts | 30 ++++ .../public/search/search_interceptor.test.ts | 157 ++++++++++++++++++ .../data/public/search/search_interceptor.ts | 121 ++++++++++++++ .../data/public/search/search_service.ts | 16 +- .../search_source/search_source.test.ts | 2 +- .../default_search_strategy.test.ts | 18 +- src/plugins/data/public/search/types.ts | 4 + .../expressions/common/execution/execution.ts | 6 +- .../public/search/async_search_strategy.ts | 6 +- .../server/search/es_search_strategy.ts | 7 +- 18 files changed, 543 insertions(+), 67 deletions(-) create mode 100644 src/plugins/data/common/utils/abort_utils.test.ts create mode 100644 src/plugins/data/common/utils/abort_utils.ts create mode 100644 src/plugins/data/public/search/request_timeout_error.ts create mode 100644 src/plugins/data/public/search/search_interceptor.test.ts create mode 100644 src/plugins/data/public/search/search_interceptor.ts diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index e756eb9b729050..d179b9d9dcd824 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -60,7 +60,6 @@ | [esQuery](./kibana-plugin-plugins-data-server.esquery.md) | | | [fieldFormats](./kibana-plugin-plugins-data-server.fieldformats.md) | | | [indexPatterns](./kibana-plugin-plugins-data-server.indexpatterns.md) | | -| [search](./kibana-plugin-plugins-data-server.search.md) | | ## Type Aliases @@ -70,6 +69,5 @@ | [IFieldFormatsRegistry](./kibana-plugin-plugins-data-server.ifieldformatsregistry.md) | | | [ISearch](./kibana-plugin-plugins-data-server.isearch.md) | | | [ISearchCancel](./kibana-plugin-plugins-data-server.isearchcancel.md) | | -| [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | | | [TSearchStrategyProvider](./kibana-plugin-plugins-data-server.tsearchstrategyprovider.md) | Search strategy provider creates an instance of a search strategy with the request handler context bound to it. This way every search strategy can use whatever information they require from the request context. | diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index cf8c0bfe3d4345..e4a663a1599f12 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -26,3 +26,4 @@ export * from './query'; export * from './search'; export * from './search/aggs'; export * from './types'; +export * from './utils'; diff --git a/src/plugins/data/common/utils/abort_utils.test.ts b/src/plugins/data/common/utils/abort_utils.test.ts new file mode 100644 index 00000000000000..d2a25f2c2dd527 --- /dev/null +++ b/src/plugins/data/common/utils/abort_utils.test.ts @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AbortError, toPromise, getCombinedSignal } from './abort_utils'; + +jest.useFakeTimers(); + +const flushPromises = () => new Promise(resolve => setImmediate(resolve)); + +describe('AbortUtils', () => { + describe('AbortError', () => { + test('should preserve `message`', () => { + const message = 'my error message'; + const error = new AbortError(message); + expect(error.message).toBe(message); + }); + + test('should have a name of "AbortError"', () => { + const error = new AbortError(); + expect(error.name).toBe('AbortError'); + }); + }); + + describe('toPromise', () => { + describe('resolves', () => { + test('should not resolve if the signal does not abort', async () => { + const controller = new AbortController(); + const promise = toPromise(controller.signal); + const whenResolved = jest.fn(); + promise.then(whenResolved); + await flushPromises(); + expect(whenResolved).not.toBeCalled(); + }); + + test('should resolve if the signal does abort', async () => { + const controller = new AbortController(); + const promise = toPromise(controller.signal); + const whenResolved = jest.fn(); + promise.then(whenResolved); + controller.abort(); + await flushPromises(); + expect(whenResolved).toBeCalled(); + }); + }); + + describe('rejects', () => { + test('should not reject if the signal does not abort', async () => { + const controller = new AbortController(); + const promise = toPromise(controller.signal, true); + const whenRejected = jest.fn(); + promise.catch(whenRejected); + await flushPromises(); + expect(whenRejected).not.toBeCalled(); + }); + + test('should reject if the signal does abort', async () => { + const controller = new AbortController(); + const promise = toPromise(controller.signal, true); + const whenRejected = jest.fn(); + promise.catch(whenRejected); + controller.abort(); + await flushPromises(); + expect(whenRejected).toBeCalled(); + }); + }); + }); + + describe('getCombinedSignal', () => { + test('should return an AbortSignal', () => { + const signal = getCombinedSignal([]); + expect(signal instanceof AbortSignal).toBe(true); + }); + + test('should not abort if none of the signals abort', async () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + setTimeout(() => controller1.abort(), 2000); + setTimeout(() => controller2.abort(), 1000); + const signal = getCombinedSignal([controller1.signal, controller2.signal]); + expect(signal.aborted).toBe(false); + jest.advanceTimersByTime(500); + await flushPromises(); + expect(signal.aborted).toBe(false); + }); + + test('should abort when the first signal aborts', async () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + setTimeout(() => controller1.abort(), 2000); + setTimeout(() => controller2.abort(), 1000); + const signal = getCombinedSignal([controller1.signal, controller2.signal]); + expect(signal.aborted).toBe(false); + jest.advanceTimersByTime(1000); + await flushPromises(); + expect(signal.aborted).toBe(true); + }); + }); +}); diff --git a/src/plugins/data/common/utils/abort_utils.ts b/src/plugins/data/common/utils/abort_utils.ts new file mode 100644 index 00000000000000..5051515f3a8265 --- /dev/null +++ b/src/plugins/data/common/utils/abort_utils.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Class used to signify that something was aborted. Useful for applications to conditionally handle + * this type of error differently than other errors. + */ +export class AbortError extends Error { + constructor(message = 'Aborted') { + super(message); + this.message = message; + this.name = 'AbortError'; + } +} + +/** + * Returns a `Promise` corresponding with when the given `AbortSignal` is aborted. Useful for + * situations when you might need to `Promise.race` multiple `AbortSignal`s, or an `AbortSignal` + * with any other expected errors (or completions). + * @param signal The `AbortSignal` to generate the `Promise` from + * @param shouldReject If `false`, the promise will be resolved, otherwise it will be rejected + */ +export function toPromise(signal: AbortSignal, shouldReject = false) { + return new Promise((resolve, reject) => { + const action = shouldReject ? reject : resolve; + if (signal.aborted) action(); + signal.addEventListener('abort', action); + }); +} + +/** + * Returns an `AbortSignal` that will be aborted when the first of the given signals aborts. + * @param signals + */ +export function getCombinedSignal(signals: AbortSignal[]) { + const promises = signals.map(signal => toPromise(signal)); + const controller = new AbortController(); + Promise.race(promises).then(() => controller.abort()); + return controller.signal; +} diff --git a/src/plugins/data/common/utils/index.ts b/src/plugins/data/common/utils/index.ts index 8b8686c51b9c19..33989f3ad50a7a 100644 --- a/src/plugins/data/common/utils/index.ts +++ b/src/plugins/data/common/utils/index.ts @@ -19,3 +19,4 @@ /** @internal */ export { shortenDottedString } from './shorten_dotted_string'; +export { AbortError, toPromise, getCombinedSignal } from './abort_utils'; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index c5cff1c5c68d91..e3fc0e97af09b3 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -19,9 +19,7 @@ import { Plugin, DataPublicPluginSetup, DataPublicPluginStart, IndexPatternsContract } from '.'; import { fieldFormatsMock } from '../common/field_formats/mocks'; -import { searchSetupMock } from './search/mocks'; -import { AggTypeFieldFilters } from './search/aggs'; -import { searchAggsStartMock } from './search/aggs/mocks'; +import { searchSetupMock, searchStartMock } from './search/mocks'; import { queryServiceMock } from './query/mocks'; export type Setup = jest.Mocked>; @@ -35,59 +33,28 @@ const autocompleteMock: any = { const createSetupContract = (): Setup => { const querySetupMock = queryServiceMock.createSetupContract(); - const setupContract = { + return { autocomplete: autocompleteMock, search: searchSetupMock, fieldFormats: fieldFormatsMock as DataPublicPluginSetup['fieldFormats'], query: querySetupMock, - __LEGACY: { - esClient: { - search: jest.fn(), - msearch: jest.fn(), - }, - }, }; - - return setupContract; }; const createStartContract = (): Start => { const queryStartMock = queryServiceMock.createStartContract(); - const startContract = { + return { actions: { createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']), }, autocomplete: autocompleteMock, - getSuggestions: jest.fn(), - search: { - aggs: searchAggsStartMock(), - search: jest.fn(), - __LEGACY: { - AggConfig: jest.fn() as any, - AggType: jest.fn(), - aggTypeFieldFilters: new AggTypeFieldFilters(), - FieldParamType: jest.fn(), - MetricAggType: jest.fn(), - parentPipelineAggHelper: jest.fn() as any, - siblingPipelineAggHelper: jest.fn() as any, - esClient: { - search: jest.fn(), - msearch: jest.fn(), - }, - }, - }, + search: searchStartMock, fieldFormats: fieldFormatsMock as DataPublicPluginStart['fieldFormats'], query: queryStartMock, ui: { IndexPatternSelect: jest.fn(), SearchBar: jest.fn(), }, - __LEGACY: { - esClient: { - search: jest.fn(), - msearch: jest.fn(), - }, - }, indexPatterns: ({ make: () => ({ fieldsFetcher: { @@ -97,7 +64,6 @@ const createStartContract = (): Start => { get: jest.fn().mockReturnValue(Promise.resolve({})), } as unknown) as IndexPatternsContract, }; - return startContract; }; export { searchSourceMock } from './search/mocks'; diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index ac72cfd6f62ca9..f3d2d99af59989 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -56,4 +56,6 @@ export { SortDirection, } from './search_source'; +export { SearchInterceptor } from './search_interceptor'; + export { FetchOptions } from './fetch'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index 71b4eece91cefe..12cf258759a99c 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -17,7 +17,9 @@ * under the License. */ -import { searchAggsSetupMock } from './aggs/mocks'; +import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; +import { AggTypeFieldFilters } from './aggs/param_types/filter'; +import { ISearchStart } from './types'; export * from './search_source/mocks'; @@ -26,3 +28,24 @@ export const searchSetupMock = { registerSearchStrategyContext: jest.fn(), registerSearchStrategyProvider: jest.fn(), }; + +export const searchStartMock: jest.Mocked = { + aggs: searchAggsStartMock(), + search: jest.fn(), + cancel: jest.fn(), + getPendingCount$: jest.fn(), + runBeyondTimeout: jest.fn(), + __LEGACY: { + AggConfig: jest.fn() as any, + AggType: jest.fn(), + aggTypeFieldFilters: new AggTypeFieldFilters(), + FieldParamType: jest.fn(), + MetricAggType: jest.fn(), + parentPipelineAggHelper: jest.fn() as any, + siblingPipelineAggHelper: jest.fn() as any, + esClient: { + search: jest.fn(), + msearch: jest.fn(), + }, + }, +}; diff --git a/src/plugins/data/public/search/request_timeout_error.ts b/src/plugins/data/public/search/request_timeout_error.ts new file mode 100644 index 00000000000000..92894deb4f0ffa --- /dev/null +++ b/src/plugins/data/public/search/request_timeout_error.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Class used to signify that a request timed out. Useful for applications to conditionally handle + * this type of error differently than other errors. + */ +export class RequestTimeoutError extends Error { + constructor(message = 'Request timed out') { + super(message); + this.message = message; + this.name = 'RequestTimeoutError'; + } +} diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts new file mode 100644 index 00000000000000..a89d17464b9e0c --- /dev/null +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -0,0 +1,157 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable, Subject } from 'rxjs'; +import { IKibanaSearchRequest } from '../../common/search'; +import { RequestTimeoutError } from './request_timeout_error'; +import { SearchInterceptor } from './search_interceptor'; + +jest.useFakeTimers(); + +const flushPromises = () => new Promise(resolve => setImmediate(resolve)); +const mockSearch = jest.fn(); +let searchInterceptor: SearchInterceptor; + +describe('SearchInterceptor', () => { + beforeEach(() => { + mockSearch.mockClear(); + searchInterceptor = new SearchInterceptor(1000); + }); + + describe('search', () => { + test('should invoke `search` with the request', () => { + mockSearch.mockReturnValue(new Observable()); + const mockRequest: IKibanaSearchRequest = {}; + searchInterceptor.search(mockSearch, mockRequest); + expect(mockSearch.mock.calls[0][0]).toBe(mockRequest); + }); + + test('should mirror the observable to completion if the request does not time out', () => { + const mockResponse = new Subject(); + mockSearch.mockReturnValue(mockResponse.asObservable()); + const response = searchInterceptor.search(mockSearch, {}); + + setTimeout(() => mockResponse.next('hi'), 250); + setTimeout(() => mockResponse.complete(), 500); + + const next = jest.fn(); + const complete = jest.fn(); + response.subscribe({ next, complete }); + + jest.advanceTimersByTime(1000); + + expect(next).toHaveBeenCalledWith('hi'); + expect(complete).toHaveBeenCalled(); + }); + + test('should mirror the observable to error if the request does not time out', () => { + const mockResponse = new Subject(); + mockSearch.mockReturnValue(mockResponse.asObservable()); + const response = searchInterceptor.search(mockSearch, {}); + + setTimeout(() => mockResponse.next('hi'), 250); + setTimeout(() => mockResponse.error('error'), 500); + + const next = jest.fn(); + const error = jest.fn(); + response.subscribe({ next, error }); + + jest.advanceTimersByTime(1000); + + expect(next).toHaveBeenCalledWith('hi'); + expect(error).toHaveBeenCalledWith('error'); + }); + + test('should return a `RequestTimeoutError` if the request times out', () => { + mockSearch.mockReturnValue(new Observable()); + const response = searchInterceptor.search(mockSearch, {}); + + const error = jest.fn(); + response.subscribe({ error }); + + jest.advanceTimersByTime(1000); + + expect(error).toHaveBeenCalled(); + expect(error.mock.calls[0][0] instanceof RequestTimeoutError).toBe(true); + }); + }); + + describe('cancelPending', () => { + test('should abort all pending requests', async () => { + mockSearch.mockReturnValue(new Observable()); + + searchInterceptor.search(mockSearch, {}); + searchInterceptor.search(mockSearch, {}); + searchInterceptor.cancelPending(); + + await flushPromises(); + + const areAllRequestsAborted = mockSearch.mock.calls.every(([, { signal }]) => signal.aborted); + expect(areAllRequestsAborted).toBe(true); + }); + }); + + describe('runBeyondTimeout', () => { + test('should prevent the request from timing out', () => { + const mockResponse = new Subject(); + mockSearch.mockReturnValue(mockResponse.asObservable()); + const response = searchInterceptor.search(mockSearch, {}); + + setTimeout(searchInterceptor.runBeyondTimeout, 500); + setTimeout(() => mockResponse.next('hi'), 250); + setTimeout(() => mockResponse.complete(), 2000); + + const next = jest.fn(); + const complete = jest.fn(); + const error = jest.fn(); + response.subscribe({ next, error, complete }); + + jest.advanceTimersByTime(2000); + + expect(next).toHaveBeenCalledWith('hi'); + expect(error).not.toHaveBeenCalled(); + expect(complete).toHaveBeenCalled(); + }); + }); + + describe('getPendingCount$', () => { + test('should observe the number of pending requests', () => { + let i = 0; + const mockResponses = [new Subject(), new Subject()]; + mockSearch.mockImplementation(() => mockResponses[i++]); + + const pendingCount$ = searchInterceptor.getPendingCount$(); + + const next = jest.fn(); + pendingCount$.subscribe(next); + + const error = jest.fn(); + searchInterceptor.search(mockSearch, {}).subscribe({ error }); + searchInterceptor.search(mockSearch, {}).subscribe({ error }); + + setTimeout(() => mockResponses[0].complete(), 250); + setTimeout(() => mockResponses[1].error('error'), 500); + + jest.advanceTimersByTime(500); + + expect(next).toHaveBeenCalled(); + expect(next.mock.calls).toEqual([[0], [1], [2], [1], [0]]); + }); + }); +}); diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts new file mode 100644 index 00000000000000..3f83214f6050c3 --- /dev/null +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject, fromEvent, throwError } from 'rxjs'; +import { mergeMap, takeUntil, finalize } from 'rxjs/operators'; +import { getCombinedSignal } from '../../common/utils'; +import { IKibanaSearchRequest } from '../../common/search'; +import { ISearchGeneric, ISearchOptions } from './i_search'; +import { RequestTimeoutError } from './request_timeout_error'; + +export class SearchInterceptor { + /** + * `abortController` used to signal all searches to abort. + */ + private abortController = new AbortController(); + + /** + * Observable that emits when the number of pending requests changes. + */ + private pendingCount$ = new BehaviorSubject(0); + + /** + * The IDs from `setTimeout` when scheduling the automatic timeout for each request. + */ + private timeoutIds: Set = new Set(); + + /** + * This class should be instantiated with a `requestTimeout` corresponding with how many ms after + * requests are initiated that they should automatically cancel. + * @param requestTimeout Usually config value `elasticsearch.requestTimeout` + */ + constructor(private readonly requestTimeout?: number) {} + + /** + * Abort our `AbortController`, which in turn aborts any intercepted searches. + */ + public cancelPending = () => { + this.abortController.abort(); + this.abortController = new AbortController(); + }; + + /** + * Un-schedule timing out all of the searches intercepted. + */ + public runBeyondTimeout = () => { + this.timeoutIds.forEach(clearTimeout); + this.timeoutIds.clear(); + }; + + /** + * Returns an `Observable` over the current number of pending searches. This could mean that one + * of the search requests is still in flight, or that it has only received partial responses. + */ + public getPendingCount$ = () => { + return this.pendingCount$.asObservable(); + }; + + /** + * Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort + * either when `cancelPending` is called, when the request times out, or when the original + * `AbortSignal` is aborted. Updates the `pendingCount` when the request is started/finalized. + */ + public search = ( + search: ISearchGeneric, + request: IKibanaSearchRequest, + options?: ISearchOptions + ) => { + // Schedule this request to automatically timeout after some interval + const timeoutController = new AbortController(); + const { signal: timeoutSignal } = timeoutController; + const timeoutId = window.setTimeout(() => { + timeoutController.abort(); + }, this.requestTimeout); + this.addTimeoutId(timeoutId); + + // Get a combined `AbortSignal` that will be aborted whenever the first of the following occurs: + // 1. The user manually aborts (via `cancelPending`) + // 2. The request times out + // 3. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines) + const signals = [this.abortController.signal, timeoutSignal, options?.signal].filter( + Boolean + ) as AbortSignal[]; + const combinedSignal = getCombinedSignal(signals); + + // If the request timed out, throw a `RequestTimeoutError` + const timeoutError$ = fromEvent(timeoutSignal, 'abort').pipe( + mergeMap(() => throwError(new RequestTimeoutError())) + ); + + return search(request as any, { ...options, signal: combinedSignal }).pipe( + takeUntil(timeoutError$), + finalize(() => this.removeTimeoutId(timeoutId)) + ); + }; + + private addTimeoutId(id: number) { + this.timeoutIds.add(id); + this.pendingCount$.next(this.timeoutIds.size); + } + + private removeTimeoutId(id: number) { + this.timeoutIds.delete(id); + this.pendingCount$.next(this.timeoutIds.size); + } +} diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 691c8aa0e984db..62c7e0468bb886 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -25,6 +25,7 @@ import { TStrategyTypes } from './strategy_types'; import { getEsClient, LegacyApiCaller } from './es_client'; import { ES_SEARCH_STRATEGY, DEFAULT_SEARCH_STRATEGY } from '../../common/search'; import { esSearchStrategyProvider } from './es_search/es_search_strategy'; +import { SearchInterceptor } from './search_interceptor'; import { getAggTypes, AggType, @@ -91,6 +92,16 @@ export class SearchService implements Plugin { } public start(core: CoreStart): ISearchStart { + /** + * A global object that intercepts all searches and provides convenience methods for cancelling + * all pending search requests, as well as getting the number of pending search requests. + * TODO: Make this modular so that apps can opt in/out of search collection, or even provide + * their own search collector instances + */ + const searchInterceptor = new SearchInterceptor( + core.injectedMetadata.getInjectedVar('esRequestTimeout') as number + ); + const aggTypesStart = this.aggTypesRegistry.start(); return { @@ -103,13 +114,16 @@ export class SearchService implements Plugin { }, types: aggTypesStart, }, + cancel: () => searchInterceptor.cancelPending(), + getPendingCount$: () => searchInterceptor.getPendingCount$(), + runBeyondTimeout: () => searchInterceptor.runBeyondTimeout(), search: (request, options, strategyName) => { const strategyProvider = this.getSearchStrategy(strategyName || DEFAULT_SEARCH_STRATEGY); const { search } = strategyProvider({ core, getSearchStrategy: this.getSearchStrategy, }); - return search(request as any, options); + return searchInterceptor.search(search as any, request, options); }, __LEGACY: { esClient: this.esClient!, diff --git a/src/plugins/data/public/search/search_source/search_source.test.ts b/src/plugins/data/public/search/search_source/search_source.test.ts index d2b8308bfb258f..fcd116a3f41211 100644 --- a/src/plugins/data/public/search/search_source/search_source.test.ts +++ b/src/plugins/data/public/search/search_source/search_source.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SearchSource } from '../search_source'; +import { SearchSource } from './search_source'; import { IndexPattern } from '../..'; import { mockDataServices } from '../aggs/test_helpers'; diff --git a/src/plugins/data/public/search/search_strategy/default_search_strategy.test.ts b/src/plugins/data/public/search/search_strategy/default_search_strategy.test.ts index e4f492c89e0efd..210a0e5fd1ac78 100644 --- a/src/plugins/data/public/search/search_strategy/default_search_strategy.test.ts +++ b/src/plugins/data/public/search/search_strategy/default_search_strategy.test.ts @@ -18,9 +18,9 @@ */ import { IUiSettingsClient } from '../../../../../core/public'; -import { ISearchStart } from '../types'; import { SearchStrategySearchParams } from './types'; import { defaultSearchStrategy } from './default_search_strategy'; +import { searchStartMock } from '../mocks'; const { search } = defaultSearchStrategy; @@ -56,6 +56,12 @@ describe('defaultSearchStrategy', function() { searchMockResponse.abort.mockClear(); searchMock.mockClear(); + const searchService = searchStartMock; + searchService.aggs.calculateAutoTimeExpression = jest.fn().mockReturnValue('1d'); + searchService.search = newSearchMock; + searchService.__LEGACY.esClient.search = searchMock; + searchService.__LEGACY.esClient.msearch = msearchMock; + searchArgs = { searchRequests: [ { @@ -63,15 +69,7 @@ describe('defaultSearchStrategy', function() { }, ], esShardTimeout: 0, - searchService: ({ - search: newSearchMock, - __LEGACY: { - esClient: { - search: searchMock, - msearch: msearchMock, - }, - }, - } as unknown) as jest.Mocked, + searchService, }; es = searchArgs.searchService.__LEGACY.esClient; diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 1732c384b1a856..1b551f978b9716 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Observable } from 'rxjs'; import { CoreStart } from 'kibana/public'; import { SearchAggsSetup, SearchAggsStart, SearchAggsStartLegacy } from './aggs'; import { ISearch, ISearchGeneric } from './i_search'; @@ -86,6 +87,9 @@ export interface ISearchSetup { export interface ISearchStart { aggs: SearchAggsStart; + cancel: () => void; + getPendingCount$: () => Observable; + runBeyondTimeout: () => void; search: ISearchGeneric; __LEGACY: ISearchStartLegacy & SearchAggsStartLegacy; } diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index f70a32f2f09c12..d0ab178296408a 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -22,6 +22,7 @@ import { Executor } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; import { Defer, now } from '../../../kibana_utils/common'; +import { AbortError } from '../../../data/common'; import { RequestAdapter, DataAdapter } from '../../../inspector/common'; import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { @@ -190,10 +191,7 @@ export class Execution< for (const link of chainArr) { // if execution was aborted return error if (this.context.abortSignal && this.context.abortSignal.aborted) { - return createError({ - message: 'The expression was aborted.', - name: 'AbortError', - }); + return createError(new AbortError('The expression was aborted.')); } const { function: fnName, arguments: fnArgs } = link; diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts index fa5d677a53b2a7..6271d7fcbeaace 100644 --- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts @@ -6,6 +6,7 @@ import { EMPTY, fromEvent, NEVER, Observable, throwError, timer } from 'rxjs'; import { mergeMap, expand, takeUntil } from 'rxjs/operators'; +import { AbortError } from '../../../../../src/plugins/data/common'; import { IKibanaSearchResponse, ISearchContext, @@ -45,10 +46,7 @@ export const asyncSearchStrategyProvider: TSearchStrategyProvider { const config = await context.config$.pipe(first()).toPromise(); const defaultParams = getDefaultSearchParams(config); - const params = { ...defaultParams, trackTotalHits: true, ...request.params }; + const params = { ...defaultParams, ...request.params }; const response = await (request.indexType === 'rollup' ? rollupSearch(caller, { ...request, params }, options) @@ -45,11 +45,6 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider) : (response as AsyncSearchResponse).response; - if (typeof rawResponse.hits.total !== 'number') { - // @ts-ignore This should be fixed as part of https://github.com/elastic/kibana/issues/26356 - rawResponse.hits.total = rawResponse.hits.total.value; - } - const id = (response as AsyncSearchResponse).id; const { total, failed, successful } = rawResponse._shards; const loaded = failed + successful; From 8fd317c55a5356a280017026d88212fd6d45da66 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 19 Mar 2020 09:49:05 +0000 Subject: [PATCH 32/42] [Alerting] Adds navigation by consumer and alert type to alerting (#58997) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Navigation APIs to Alerting. Parts to this PR: Adds a client side (Public) plugin to Alerting, including two APIs: registerNavigation & registerDefaultNavigation. These allow a plugin to register navigation handlers for any alerts which it is the consumer of- one for specific AlertTypes and one for a default handler for all AlertTypes created by the plugin. The Alert Details page now uses these navigation handlers for the View In App button. If there's an AlertType specific handler it uses that, otherwise it uses a default one and if the consumer has not registered a handler - it remains disabled. A generic Alerting Example plugin that demonstrates usage of these APIs including two AlertTypes - one that always fires, and another that checks how many people are in Outer Space and allows you to trigger based on that. 😉 To enable the plugin run yarn start --ssl --run-examples --- examples/alerting_example/README.md | 5 + examples/alerting_example/common/constants.ts | 34 +++ examples/alerting_example/kibana.json | 10 + examples/alerting_example/package.json | 17 ++ .../public/alert_types/always_firing.tsx | 82 ++++++ .../public/alert_types/astros.tsx | 277 ++++++++++++++++++ .../public/alert_types/index.ts | 33 +++ .../alerting_example/public/application.tsx | 108 +++++++ .../public/components/create_alert.tsx | 72 +++++ .../public/components/documentation.tsx | 67 +++++ .../public/components/page.tsx | 74 +++++ .../public/components/view_alert.tsx | 116 ++++++++ .../public/components/view_astros_alert.tsx | 123 ++++++++ examples/alerting_example/public/index.ts | 22 ++ examples/alerting_example/public/plugin.tsx | 70 +++++ .../server/alert_types/always_firing.ts | 46 +++ .../server/alert_types/astros.ts | 82 ++++++ examples/alerting_example/server/index.ts | 23 ++ examples/alerting_example/server/plugin.ts | 39 +++ examples/alerting_example/tsconfig.json | 17 ++ package.json | 3 +- packages/kbn-pm/dist/index.js | 1 + packages/kbn-pm/src/config.ts | 1 + test/scripts/jenkins_xpack_build_kibana.sh | 1 + x-pack/plugins/actions/common/index.ts | 2 + x-pack/plugins/alerting/README.md | 57 +++- .../alerting/common/alert_navigation.ts | 14 + x-pack/plugins/alerting/common/alert_type.ts | 18 ++ x-pack/plugins/alerting/common/index.ts | 7 +- x-pack/plugins/alerting/kibana.json | 6 +- .../plugins/alerting/public/alert_api.test.ts | 176 +++++++++++ x-pack/plugins/alerting/public/alert_api.ts | 74 +++++ .../alert_navigation_registry.mock.ts | 25 ++ .../alert_navigation_registry.test.ts | 184 ++++++++++++ .../alert_navigation_registry.ts | 92 ++++++ .../public/alert_navigation_registry/index.ts | 8 + .../public/alert_navigation_registry/types.ts | 13 + x-pack/plugins/alerting/public/index.ts | 12 + x-pack/plugins/alerting/public/mocks.ts | 24 ++ x-pack/plugins/alerting/public/plugin.ts | 64 ++++ x-pack/plugins/alerting/server/plugin.ts | 1 + .../plugins/triggers_actions_ui/kibana.json | 14 +- .../public/application/app.tsx | 4 + .../public/application/constants/index.ts | 5 +- .../actions_connectors_list.test.tsx | 17 +- .../components/alert_details.test.tsx | 10 +- .../components/alert_details.tsx | 8 +- .../components/view_in_app.test.tsx | 108 +++++++ .../alert_details/components/view_in_app.tsx | 90 ++++++ .../components/alerts_list.test.tsx | 17 +- .../triggers_actions_ui/public/index.ts | 2 +- .../triggers_actions_ui/public/plugin.ts | 5 + .../apps/triggers_actions_ui/details.ts | 28 ++ .../fixtures/plugins/alerts/kibana.json | 9 + .../fixtures/plugins/alerts/package.json | 8 + .../plugins/alerts/public/application.tsx | 45 +++ .../fixtures/plugins/alerts/public/index.ts | 9 + .../fixtures/plugins/alerts/public/plugin.ts | 39 +++ .../fixtures/plugins/alerts/server/index.ts | 10 + .../alerts/{index.ts => server/plugin.ts} | 39 ++- .../page_objects/alert_details.ts | 11 + .../services/alerting/alerts.ts | 25 ++ 62 files changed, 2546 insertions(+), 57 deletions(-) create mode 100644 examples/alerting_example/README.md create mode 100644 examples/alerting_example/common/constants.ts create mode 100644 examples/alerting_example/kibana.json create mode 100644 examples/alerting_example/package.json create mode 100644 examples/alerting_example/public/alert_types/always_firing.tsx create mode 100644 examples/alerting_example/public/alert_types/astros.tsx create mode 100644 examples/alerting_example/public/alert_types/index.ts create mode 100644 examples/alerting_example/public/application.tsx create mode 100644 examples/alerting_example/public/components/create_alert.tsx create mode 100644 examples/alerting_example/public/components/documentation.tsx create mode 100644 examples/alerting_example/public/components/page.tsx create mode 100644 examples/alerting_example/public/components/view_alert.tsx create mode 100644 examples/alerting_example/public/components/view_astros_alert.tsx create mode 100644 examples/alerting_example/public/index.ts create mode 100644 examples/alerting_example/public/plugin.tsx create mode 100644 examples/alerting_example/server/alert_types/always_firing.ts create mode 100644 examples/alerting_example/server/alert_types/astros.ts create mode 100644 examples/alerting_example/server/index.ts create mode 100644 examples/alerting_example/server/plugin.ts create mode 100644 examples/alerting_example/tsconfig.json create mode 100644 x-pack/plugins/alerting/common/alert_navigation.ts create mode 100644 x-pack/plugins/alerting/common/alert_type.ts create mode 100644 x-pack/plugins/alerting/public/alert_api.test.ts create mode 100644 x-pack/plugins/alerting/public/alert_api.ts create mode 100644 x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.mock.ts create mode 100644 x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts create mode 100644 x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.ts create mode 100644 x-pack/plugins/alerting/public/alert_navigation_registry/index.ts create mode 100644 x-pack/plugins/alerting/public/alert_navigation_registry/types.ts create mode 100644 x-pack/plugins/alerting/public/index.ts create mode 100644 x-pack/plugins/alerting/public/mocks.ts create mode 100644 x-pack/plugins/alerting/public/plugin.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx create mode 100644 x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json create mode 100644 x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/application.tsx create mode 100644 x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/index.ts create mode 100644 x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts create mode 100644 x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/index.ts rename x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/{index.ts => server/plugin.ts} (61%) diff --git a/examples/alerting_example/README.md b/examples/alerting_example/README.md new file mode 100644 index 00000000000000..bf963c64586d30 --- /dev/null +++ b/examples/alerting_example/README.md @@ -0,0 +1,5 @@ +## Alerting Example + +This example plugin shows you how to create a custom Alert Type, create alerts based on that type and corresponding UI for viewing the details of all the alerts within the custom plugin. + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/examples/alerting_example/common/constants.ts b/examples/alerting_example/common/constants.ts new file mode 100644 index 00000000000000..5884eb32205195 --- /dev/null +++ b/examples/alerting_example/common/constants.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample'; + +// always firing +export const DEFAULT_INSTANCES_TO_GENERATE = 5; + +// Astros +export enum Craft { + OuterSpace = 'Outer Space', + ISS = 'ISS', +} +export enum Operator { + AreAbove = 'Are above', + AreBelow = 'Are below', + AreExactly = 'Are exactly', +} diff --git a/examples/alerting_example/kibana.json b/examples/alerting_example/kibana.json new file mode 100644 index 00000000000000..bcdb7c2f14a9c9 --- /dev/null +++ b/examples/alerting_example/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "alertingExample", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["alerting_example"], + "server": true, + "ui": true, + "requiredPlugins": ["triggers_actions_ui", "charts", "data", "alerting", "actions"], + "optionalPlugins": [] +} diff --git a/examples/alerting_example/package.json b/examples/alerting_example/package.json new file mode 100644 index 00000000000000..96187d847c1c41 --- /dev/null +++ b/examples/alerting_example/package.json @@ -0,0 +1,17 @@ +{ + "name": "alerting_example", + "version": "1.0.0", + "main": "target/examples/alerting_example", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" + } +} diff --git a/examples/alerting_example/public/alert_types/always_firing.tsx b/examples/alerting_example/public/alert_types/always_firing.tsx new file mode 100644 index 00000000000000..a62a24365ea3f9 --- /dev/null +++ b/examples/alerting_example/public/alert_types/always_firing.tsx @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AlertTypeModel } from '../../../../x-pack/plugins/triggers_actions_ui/public'; +import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants'; + +interface AlwaysFiringParamsProps { + alertParams: { instances?: number }; + setAlertParams: (property: string, value: any) => void; + errors: { [key: string]: string[] }; +} + +export function getAlertType(): AlertTypeModel { + return { + id: 'example.always-firing', + name: 'Always Fires', + iconClass: 'bolt', + alertParamsExpression: AlwaysFiringExpression, + validate: (alertParams: AlwaysFiringParamsProps['alertParams']) => { + const { instances } = alertParams; + const validationResult = { + errors: { + instances: new Array(), + }, + }; + if (instances && instances < 0) { + validationResult.errors.instances.push( + i18n.translate('AlertingExample.addAlert.error.invalidRandomInstances', { + defaultMessage: 'instances must be equal or greater than zero.', + }) + ); + } + return validationResult; + }, + }; +} + +export const AlwaysFiringExpression: React.FunctionComponent = ({ + alertParams, + setAlertParams, +}) => { + const { instances = DEFAULT_INSTANCES_TO_GENERATE } = alertParams; + return ( + + + + + { + setAlertParams('instances', event.target.valueAsNumber); + }} + /> + + + + + ); +}; diff --git a/examples/alerting_example/public/alert_types/astros.tsx b/examples/alerting_example/public/alert_types/astros.tsx new file mode 100644 index 00000000000000..9bda7da6f140dd --- /dev/null +++ b/examples/alerting_example/public/alert_types/astros.tsx @@ -0,0 +1,277 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState, useEffect, Fragment } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiFieldNumber, + EuiPopoverTitle, + EuiSelect, + EuiCallOut, + EuiExpression, + EuiTextColor, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { flatten } from 'lodash'; +import { ALERTING_EXAMPLE_APP_ID, Craft, Operator } from '../../common/constants'; +import { SanitizedAlert } from '../../../../x-pack/plugins/alerting/common'; +import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerting/public'; +import { AlertTypeModel } from '../../../../x-pack/plugins/triggers_actions_ui/public'; + +export function registerNavigation(alerting: AlertingSetup) { + alerting.registerNavigation( + ALERTING_EXAMPLE_APP_ID, + 'example.people-in-space', + (alert: SanitizedAlert) => `/astros/${alert.id}` + ); +} + +interface PeopleinSpaceParamsProps { + alertParams: { outerSpaceCapacity?: number; craft?: string; op?: string }; + setAlertParams: (property: string, value: any) => void; + errors: { [key: string]: string[] }; +} + +function isValueInEnum(enumeratin: Record, value: any): boolean { + return !!Object.values(enumeratin).find(enumVal => enumVal === value); +} + +export function getAlertType(): AlertTypeModel { + return { + id: 'example.people-in-space', + name: 'People Are In Space Right Now', + iconClass: 'globe', + alertParamsExpression: PeopleinSpaceExpression, + validate: (alertParams: PeopleinSpaceParamsProps['alertParams']) => { + const { outerSpaceCapacity, craft, op } = alertParams; + + const validationResult = { + errors: { + outerSpaceCapacity: new Array(), + craft: new Array(), + }, + }; + if (!isValueInEnum(Craft, craft)) { + validationResult.errors.craft.push( + i18n.translate('AlertingExample.addAlert.error.invalidCraft', { + defaultMessage: 'You must choose one of the following Craft: {crafts}', + values: { + crafts: Object.values(Craft).join(', '), + }, + }) + ); + } + if (!(typeof outerSpaceCapacity === 'number' && outerSpaceCapacity >= 0)) { + validationResult.errors.outerSpaceCapacity.push( + i18n.translate('AlertingExample.addAlert.error.invalidOuterSpaceCapacity', { + defaultMessage: 'outerSpaceCapacity must be a number greater than or equal to zero.', + }) + ); + } + if (!isValueInEnum(Operator, op)) { + validationResult.errors.outerSpaceCapacity.push( + i18n.translate('AlertingExample.addAlert.error.invalidCraft', { + defaultMessage: 'You must choose one of the following Operator: {crafts}', + values: { + crafts: Object.values(Operator).join(', '), + }, + }) + ); + } + + return validationResult; + }, + }; +} + +export const PeopleinSpaceExpression: React.FunctionComponent = ({ + alertParams, + setAlertParams, + errors, +}) => { + const { outerSpaceCapacity = 0, craft = Craft.OuterSpace, op = Operator.AreAbove } = alertParams; + + // store defaults + useEffect(() => { + if (outerSpaceCapacity !== alertParams.outerSpaceCapacity) { + setAlertParams('outerSpaceCapacity', outerSpaceCapacity); + } + if (craft !== alertParams.craft) { + setAlertParams('craft', craft); + } + if (op !== alertParams.op) { + setAlertParams('op', op); + } + }, [alertParams, craft, op, outerSpaceCapacity, setAlertParams]); + + const [craftTrigger, setCraftTrigger] = useState<{ craft: string; isOpen: boolean }>({ + craft, + isOpen: false, + }); + const [outerSpaceCapacityTrigger, setOuterSpaceCapacity] = useState<{ + outerSpaceCapacity: number; + op: string; + isOpen: boolean; + }>({ + outerSpaceCapacity, + op, + isOpen: false, + }); + + const errorsCallout = flatten( + Object.entries(errors).map(([field, errs]: [string, string[]]) => + errs.map(e => ( +

+ {field}:`: ${errs}` +

+ )) + ) + ); + + return ( + + {errorsCallout.length ? ( + + {errorsCallout} + + ) : ( + + )} + + + { + setCraftTrigger({ + ...craftTrigger, + isOpen: true, + }); + }} + /> + } + isOpen={craftTrigger.isOpen} + closePopover={() => { + setCraftTrigger({ + ...craftTrigger, + isOpen: false, + }); + }} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > +
+ When the People in + { + setAlertParams('craft', event.target.value); + setCraftTrigger({ + craft: event.target.value, + isOpen: false, + }); + }} + options={[ + { value: Craft.OuterSpace, text: 'Outer Space' }, + { value: Craft.ISS, text: 'the International Space Station' }, + ]} + /> +
+
+
+ + + { + setOuterSpaceCapacity({ + ...outerSpaceCapacityTrigger, + isOpen: true, + }); + }} + /> + } + isOpen={outerSpaceCapacityTrigger.isOpen} + closePopover={() => { + setOuterSpaceCapacity({ + ...outerSpaceCapacityTrigger, + isOpen: false, + }); + }} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > +
+ + + { + setAlertParams('op', event.target.value); + setOuterSpaceCapacity({ + ...outerSpaceCapacityTrigger, + op: event.target.value, + isOpen: false, + }); + }} + options={[ + { value: Operator.AreAbove, text: 'Are above' }, + { value: Operator.AreBelow, text: 'Are below' }, + { value: Operator.AreExactly, text: 'Are exactly' }, + ]} + /> + + + + { + setAlertParams('outerSpaceCapacity', event.target.valueAsNumber); + setOuterSpaceCapacity({ + ...outerSpaceCapacityTrigger, + outerSpaceCapacity: event.target.valueAsNumber, + isOpen: false, + }); + }} + /> + + +
+
+
+
+
+ ); +}; diff --git a/examples/alerting_example/public/alert_types/index.ts b/examples/alerting_example/public/alert_types/index.ts new file mode 100644 index 00000000000000..96d9c09d15836f --- /dev/null +++ b/examples/alerting_example/public/alert_types/index.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { registerNavigation as registerPeopleInSpaceNavigation } from './astros'; +import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; +import { SanitizedAlert } from '../../../../x-pack/plugins/alerting/common'; +import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerting/public'; + +export function registerNavigation(alerting: AlertingSetup) { + // register default navigation + alerting.registerDefaultNavigation( + ALERTING_EXAMPLE_APP_ID, + (alert: SanitizedAlert) => `/alert/${alert.id}` + ); + + registerPeopleInSpaceNavigation(alerting); +} diff --git a/examples/alerting_example/public/application.tsx b/examples/alerting_example/public/application.tsx new file mode 100644 index 00000000000000..d71db92d3d421a --- /dev/null +++ b/examples/alerting_example/public/application.tsx @@ -0,0 +1,108 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter as Router, Route, RouteComponentProps } from 'react-router-dom'; +import { EuiPage } from '@elastic/eui'; +import { + AppMountParameters, + CoreStart, + IUiSettingsClient, + ToastsSetup, +} from '../../../src/core/public'; +import { DataPublicPluginStart } from '../../../src/plugins/data/public'; +import { ChartsPluginStart } from '../../../src/plugins/charts/public'; + +import { Page } from './components/page'; +import { DocumentationPage } from './components/documentation'; +import { ViewAlertPage } from './components/view_alert'; +import { TriggersAndActionsUIPublicPluginStart } from '../../../x-pack/plugins/triggers_actions_ui/public'; +import { AlertingExamplePublicStartDeps } from './plugin'; +import { ViewPeopleInSpaceAlertPage } from './components/view_astros_alert'; + +export interface AlertingExampleComponentParams { + application: CoreStart['application']; + http: CoreStart['http']; + basename: string; + triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; + data: DataPublicPluginStart; + charts: ChartsPluginStart; + uiSettings: IUiSettingsClient; + toastNotifications: ToastsSetup; +} + +const AlertingExampleApp = (deps: AlertingExampleComponentParams) => { + const { basename, http } = deps; + return ( + + + ( + + + + )} + /> + ) => { + return ( + + + + ); + }} + /> + ) => { + return ( + + + + ); + }} + /> + + + ); +}; + +export const renderApp = ( + { application, notifications, http, uiSettings }: CoreStart, + deps: AlertingExamplePublicStartDeps, + { appBasePath, element }: AppMountParameters +) => { + ReactDOM.render( + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/examples/alerting_example/public/components/create_alert.tsx b/examples/alerting_example/public/components/create_alert.tsx new file mode 100644 index 00000000000000..65b8a9412dcda6 --- /dev/null +++ b/examples/alerting_example/public/components/create_alert.tsx @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; + +import { EuiIcon, EuiFlexItem, EuiCard, EuiFlexGroup } from '@elastic/eui'; + +import { + AlertsContextProvider, + AlertAdd, +} from '../../../../x-pack/plugins/triggers_actions_ui/public'; +import { AlertingExampleComponentParams } from '../application'; +import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; + +export const CreateAlert = ({ + http, + triggers_actions_ui, + charts, + uiSettings, + data, + toastNotifications, +}: AlertingExampleComponentParams) => { + const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); + + return ( + + + } + title={`Create Alert`} + description="Create an new Alert based on one of our example Alert Types ." + onClick={() => setAlertFlyoutVisibility(true)} + /> + + + + + + + + ); +}; diff --git a/examples/alerting_example/public/components/documentation.tsx b/examples/alerting_example/public/components/documentation.tsx new file mode 100644 index 00000000000000..17cc34959b0108 --- /dev/null +++ b/examples/alerting_example/public/components/documentation.tsx @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; + +import { + EuiText, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; +import { CreateAlert } from './create_alert'; +import { AlertingExampleComponentParams } from '../application'; + +export const DocumentationPage = (deps: AlertingExampleComponentParams) => ( + + + + +

Welcome to the Alerting plugin example

+
+
+
+ + + + +

Documentation links

+
+
+
+ + +

Plugin Structure

+

+ This example solution has both `server` and a `public` plugins. The `server` handles + registration of example the AlertTypes, while the `public` handles creation of, and + navigation for, these alert types. +

+
+ + +
+
+
+); diff --git a/examples/alerting_example/public/components/page.tsx b/examples/alerting_example/public/components/page.tsx new file mode 100644 index 00000000000000..99076c7ddcedf5 --- /dev/null +++ b/examples/alerting_example/public/components/page.tsx @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; + +import { + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, + EuiBreadcrumbs, + EuiSpacer, +} from '@elastic/eui'; + +type PageProps = RouteComponentProps & { + title: string; + children: React.ReactNode; + crumb?: string; + isHome?: boolean; +}; + +export const Page = withRouter(({ title, crumb, children, isHome = false, history }: PageProps) => { + const breadcrumbs: Array<{ + text: string; + onClick?: () => void; + }> = [ + { + text: crumb ?? title, + }, + ]; + if (!isHome) { + breadcrumbs.splice(0, 0, { + text: 'Home', + onClick: () => { + history.push(`/`); + }, + }); + } + return ( + + + + +

{title}

+
+
+
+ + + + {children} + +
+ ); +}); diff --git a/examples/alerting_example/public/components/view_alert.tsx b/examples/alerting_example/public/components/view_alert.tsx new file mode 100644 index 00000000000000..c1b65eb92edc59 --- /dev/null +++ b/examples/alerting_example/public/components/view_alert.tsx @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useState, useEffect, Fragment } from 'react'; + +import { + EuiText, + EuiLoadingKibana, + EuiCallOut, + EuiTextColor, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiCodeBlock, + EuiSpacer, +} from '@elastic/eui'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { CoreStart } from 'kibana/public'; +import { isEmpty } from 'lodash'; +import { Alert, AlertTaskState } from '../../../../x-pack/plugins/alerting/common'; +import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; + +type Props = RouteComponentProps & { + http: CoreStart['http']; + id: string; +}; +export const ViewAlertPage = withRouter(({ http, id }: Props) => { + const [alert, setAlert] = useState(null); + const [alertState, setAlertState] = useState(null); + + useEffect(() => { + if (!alert) { + http.get(`/api/alert/${id}`).then(setAlert); + } + if (!alertState) { + http.get(`/api/alert/${id}/state`).then(setAlertState); + } + }, [alert, alertState, http, id]); + + return alert && alertState ? ( + + +

+ This is a generic view for all Alerts created by the + {ALERTING_EXAMPLE_APP_ID} + plugin. +

+

+ You are now viewing the {`${alert.name}`} + Alert, whose ID is {`${alert.id}`}. +

+

+ Its AlertType is {`${alert.alertTypeId}`} and + its scheduled to run at an interval of + {`${alert.schedule.interval}`}. +

+
+ + +

Alert Instances

+
+ {isEmpty(alertState.alertInstances) ? ( + +

This Alert doesn't have any active alert instances at the moment.

+
+ ) : ( + + +

+ Bellow are the active Alert Instances which were activated on the alerts last run. +
+ For each instance id you can see its current state in JSON format. +

+
+ + + {Object.entries(alertState.alertInstances ?? {}).map(([instance, { state }]) => ( + + {instance} + + + {`${JSON.stringify(state)}`} + + + + ))} + +
+ )} +
+ ) : ( + + ); +}); diff --git a/examples/alerting_example/public/components/view_astros_alert.tsx b/examples/alerting_example/public/components/view_astros_alert.tsx new file mode 100644 index 00000000000000..db93d8f54924dc --- /dev/null +++ b/examples/alerting_example/public/components/view_astros_alert.tsx @@ -0,0 +1,123 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useState, useEffect, Fragment } from 'react'; + +import { + EuiText, + EuiLoadingKibana, + EuiCallOut, + EuiTextColor, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiStat, +} from '@elastic/eui'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { CoreStart } from 'kibana/public'; +import { isEmpty } from 'lodash'; +import { Alert, AlertTaskState } from '../../../../x-pack/plugins/alerting/common'; +import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; + +type Props = RouteComponentProps & { + http: CoreStart['http']; + id: string; +}; + +function hasCraft(state: any): state is { craft: string } { + return state && state.craft; +} +export const ViewPeopleInSpaceAlertPage = withRouter(({ http, id }: Props) => { + const [alert, setAlert] = useState(null); + const [alertState, setAlertState] = useState(null); + + useEffect(() => { + if (!alert) { + http.get(`/api/alert/${id}`).then(setAlert); + } + if (!alertState) { + http.get(`/api/alert/${id}/state`).then(setAlertState); + } + }, [alert, alertState, http, id]); + + return alert && alertState ? ( + + +

+ This is a specific view for all + example.people-in-space Alerts created by + the + {ALERTING_EXAMPLE_APP_ID} + plugin. +

+
+ + +

Alert Instances

+
+ {isEmpty(alertState.alertInstances) ? ( + +

+ The people in {alert.params.craft} at the moment are not {alert.params.op}{' '} + {alert.params.outerSpaceCapacity} +

+
+ ) : ( + + +

+ The alert has been triggered because the people in {alert.params.craft} at the moment{' '} + {alert.params.op} {alert.params.outerSpaceCapacity} +

+
+ +
+ + + + + + + {Object.entries(alertState.alertInstances ?? {}).map( + ([instance, { state }], index) => ( + + {instance} + + {hasCraft(state) ? state.craft : 'Unknown Craft'} + + + ) + )} + + + +
+
+ )} +
+ ) : ( + + ); +}); diff --git a/examples/alerting_example/public/index.ts b/examples/alerting_example/public/index.ts new file mode 100644 index 00000000000000..4a2bfc79903c33 --- /dev/null +++ b/examples/alerting_example/public/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AlertingExamplePlugin } from './plugin'; + +export const plugin = () => new AlertingExamplePlugin(); diff --git a/examples/alerting_example/public/plugin.tsx b/examples/alerting_example/public/plugin.tsx new file mode 100644 index 00000000000000..299806d393446e --- /dev/null +++ b/examples/alerting_example/public/plugin.tsx @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Plugin, CoreSetup, AppMountParameters, CoreStart } from 'kibana/public'; +import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerting/public'; +import { ChartsPluginStart } from '../../../src/plugins/charts/public'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../../x-pack/plugins/triggers_actions_ui/public'; +import { DataPublicPluginStart } from '../../../src/plugins/data/public'; +import { getAlertType as getAlwaysFiringAlertType } from './alert_types/always_firing'; +import { getAlertType as getPeopleInSpaceAlertType } from './alert_types/astros'; +import { registerNavigation } from './alert_types'; + +export type Setup = void; +export type Start = void; + +export interface AlertingExamplePublicSetupDeps { + alerting: AlertingSetup; + triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; +} + +export interface AlertingExamplePublicStartDeps { + alerting: AlertingSetup; + triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; + charts: ChartsPluginStart; + data: DataPublicPluginStart; +} + +export class AlertingExamplePlugin implements Plugin { + public setup( + core: CoreSetup, + { alerting, triggers_actions_ui }: AlertingExamplePublicSetupDeps + ) { + core.application.register({ + id: 'AlertingExample', + title: 'Alerting Example', + async mount(params: AppMountParameters) { + const [coreStart, depsStart]: [ + CoreStart, + AlertingExamplePublicStartDeps + ] = await core.getStartServices(); + const { renderApp } = await import('./application'); + return renderApp(coreStart, depsStart, params); + }, + }); + + triggers_actions_ui.alertTypeRegistry.register(getAlwaysFiringAlertType()); + triggers_actions_ui.alertTypeRegistry.register(getPeopleInSpaceAlertType()); + + registerNavigation(alerting); + } + + public start() {} + public stop() {} +} diff --git a/examples/alerting_example/server/alert_types/always_firing.ts b/examples/alerting_example/server/alert_types/always_firing.ts new file mode 100644 index 00000000000000..f0553ad5ebebd1 --- /dev/null +++ b/examples/alerting_example/server/alert_types/always_firing.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import uuid from 'uuid'; +import { range } from 'lodash'; +import { AlertType } from '../../../../x-pack/plugins/alerting/server'; +import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants'; + +export const alertType: AlertType = { + id: 'example.always-firing', + name: 'Always firing', + actionGroups: [{ id: 'default', name: 'default' }], + defaultActionGroupId: 'default', + async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) { + const count = (state.count ?? 0) + 1; + + range(instances) + .map(() => ({ id: uuid.v4() })) + .forEach((instance: { id: string }) => { + services + .alertInstanceFactory(instance.id) + .replaceState({ triggerdOnCycle: count }) + .scheduleActions('default'); + }); + + return { + count, + }; + }, +}; diff --git a/examples/alerting_example/server/alert_types/astros.ts b/examples/alerting_example/server/alert_types/astros.ts new file mode 100644 index 00000000000000..3a53f85e6a266b --- /dev/null +++ b/examples/alerting_example/server/alert_types/astros.ts @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import axios from 'axios'; +import { AlertType } from '../../../../x-pack/plugins/alerting/server'; +import { Operator, Craft } from '../../common/constants'; + +interface PeopleInSpace { + people: Array<{ + craft: string; + name: string; + }>; + number: number; +} + +function getOperator(op: string) { + switch (op) { + case Operator.AreAbove: + return (left: number, right: number) => left > right; + case Operator.AreBelow: + return (left: number, right: number) => left < right; + case Operator.AreExactly: + return (left: number, right: number) => left === right; + default: + return () => { + throw new Error( + `Invalid Operator "${op}" [${Operator.AreAbove},${Operator.AreBelow},${Operator.AreExactly}]` + ); + }; + } +} + +function getCraftFilter(craft: string) { + return (person: { craft: string; name: string }) => + craft === Craft.OuterSpace ? true : craft === person.craft; +} + +export const alertType: AlertType = { + id: 'example.people-in-space', + name: 'People In Space Right Now', + actionGroups: [{ id: 'default', name: 'default' }], + defaultActionGroupId: 'default', + async executor({ services, params }) { + const { outerSpaceCapacity, craft: craftToTriggerBy, op } = params; + + const response = await axios.get('http://api.open-notify.org/astros.json'); + const { + data: { number: peopleInSpace, people = [] }, + } = response; + + const peopleInCraft = people.filter(getCraftFilter(craftToTriggerBy)); + + if (getOperator(op)(peopleInCraft.length, outerSpaceCapacity)) { + peopleInCraft.forEach(({ craft, name }) => { + services + .alertInstanceFactory(name) + .replaceState({ craft }) + .scheduleActions('default'); + }); + } + + return { + peopleInSpace, + }; + }, +}; diff --git a/examples/alerting_example/server/index.ts b/examples/alerting_example/server/index.ts new file mode 100644 index 00000000000000..32e9b181ebb544 --- /dev/null +++ b/examples/alerting_example/server/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/server'; +import { AlertingExamplePlugin } from './plugin'; + +export const plugin: PluginInitializer = () => new AlertingExamplePlugin(); diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts new file mode 100644 index 00000000000000..b5dabe51e86854 --- /dev/null +++ b/examples/alerting_example/server/plugin.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Plugin, CoreSetup } from 'kibana/server'; +import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerting/server'; + +import { alertType as alwaysFiringAlert } from './alert_types/always_firing'; +import { alertType as peopleInSpaceAlert } from './alert_types/astros'; + +// this plugin's dependendencies +export interface AlertingExampleDeps { + alerting: AlertingSetup; +} + +export class AlertingExamplePlugin implements Plugin { + public setup(core: CoreSetup, { alerting }: AlertingExampleDeps) { + alerting.registerType(alwaysFiringAlert); + alerting.registerType(peopleInSpaceAlert); + } + + public start() {} + public stop() {} +} diff --git a/examples/alerting_example/tsconfig.json b/examples/alerting_example/tsconfig.json new file mode 100644 index 00000000000000..078522b36cb125 --- /dev/null +++ b/examples/alerting_example/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "common/**/*.ts", + "../../typings/**/*", + ], + "exclude": [] +} diff --git a/package.json b/package.json index d4524f07aaaada..70d064fa2a8eb7 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,8 @@ "x-pack/legacy/plugins/*", "examples/*", "test/plugin_functional/plugins/*", - "test/interpreter_functional/plugins/*" + "test/interpreter_functional/plugins/*", + "x-pack/test/functional_with_es_ssl/fixtures/plugins/*" ], "nohoist": [ "**/@types/*", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 285a780ae053ed..62b12e8e38c875 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -56996,6 +56996,7 @@ function getProjectPaths({ projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack')); projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/plugins/*')); projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/legacy/plugins/*')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/test/functional_with_es_ssl/fixtures/plugins/*')); } if (!skipKibanaPlugins) { diff --git a/packages/kbn-pm/src/config.ts b/packages/kbn-pm/src/config.ts index 6ba8d58a26f88f..59b43b230e6034 100644 --- a/packages/kbn-pm/src/config.ts +++ b/packages/kbn-pm/src/config.ts @@ -48,6 +48,7 @@ export function getProjectPaths({ rootPath, ossOnly, skipKibanaPlugins }: Option projectPaths.push(resolve(rootPath, 'x-pack')); projectPaths.push(resolve(rootPath, 'x-pack/plugins/*')); projectPaths.push(resolve(rootPath, 'x-pack/legacy/plugins/*')); + projectPaths.push(resolve(rootPath, 'x-pack/test/functional_with_es_ssl/fixtures/plugins/*')); } if (!skipKibanaPlugins) { diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index f87d6e1102c45e..2bf9d2d9c158b5 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -6,6 +6,7 @@ source src/dev/ci_setup/setup_env.sh echo " -> building kibana platform plugins" node scripts/build_kibana_platform_plugins \ --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ + --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ --verbose; # doesn't persist, also set in kibanaPipeline.groovy diff --git a/x-pack/plugins/actions/common/index.ts b/x-pack/plugins/actions/common/index.ts index 9f4141dbcae7df..84eb64f6370ac8 100644 --- a/x-pack/plugins/actions/common/index.ts +++ b/x-pack/plugins/actions/common/index.ts @@ -5,3 +5,5 @@ */ export * from './types'; + +export const BASE_ACTION_API_PATH = '/api/action'; diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index fa2e5c8e2faa1f..177e42de5a95b6 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -18,6 +18,7 @@ Table of Contents - [Methods](#methods) - [Executor](#executor) - [Example](#example) + - [Alert Navigation](#alert-navigation) - [RESTful API](#restful-api) - [`POST /api/alert`: Create alert](#post-apialert-create-alert) - [`DELETE /api/alert/{id}`: Delete alert](#delete-apialertid-delete-alert) @@ -268,6 +269,61 @@ server.newPlatform.setup.plugins.alerting.registerType({ }); ``` +## Alert Navigation +When registering an Alert Type, you'll likely want to provide a way of viewing alerts of that type within your own plugin, or perhaps you want to provide a view for all alerts created from within your solution within your own UI. + +In order for the Alerting framework to know that your plugin has its own internal view for displaying an alert, you must resigter a navigation handler within the framework. + +A navigation handler is nothing more than a function that receives an Alert and its corresponding AlertType, and is expected to then return the path *within your plugin* which knows how to display this alert. + +The signature of such a handler is: + +``` +type AlertNavigationHandler = ( + alert: SanitizedAlert, + alertType: AlertType +) => string; +``` + +There are two ways to register this handler. +By specifying _alerting_ as a dependency of your *public* (client side) plugin, you'll gain access to two apis: _alerting.registerNavigation_ and _alerting.registerDefaultNavigation_. + +### registerNavigation +The _registerNavigation_ api allows you to register a handler for a specific alert type within your solution: + +``` +alerting.registerNavigation( + 'my-application-id', + 'my-application-id.my-alert-type', + (alert: SanitizedAlert, alertType: AlertType) => `/my-unique-alert/${alert.id}` +); +``` + +This tells the Alerting framework that, given an alert of the AlertType whose ID is `my-application-id.my-unique-alert-type`, if that Alert's `consumer` value (which is set when the alert is created by your plugin) is your application (whose id is `my-application-id`), then it will navigate to your application using the path `/my-unique-alert/${the id of the alert}`. + +The navigation is handled using the `navigateToApp` api, meaning that the path will be automatically picked up by your `react-router-dom` **Route** component, so all you have top do is configure a Route that handles the path `/my-unique-alert/:id`. + +You can look at the `alerting-example` plugin to see an example of using this API, which is enabled using the `--run-examples` flag when you run `yarn start`. + +### registerDefaultNavigation +The _registerDefaultNavigation_ api allows you to register a handler for any alert type within your solution: + +``` +alerting.registerDefaultNavigation( + 'my-application-id', + (alert: SanitizedAlert, alertType: AlertType) => `/my-other-alerts/${alert.id}` +); +``` + +This tells the Alerting framework that, given any alert whose `consumer` value is your application, as long as then it will navigate to your application using the path `/my-other-alerts/${the id of the alert}`. + +### balancing both APIs side by side +As we mentioned, using `registerDefaultNavigation` will tell the Alerting Framework that your application can handle any type of Alert we throw at it, as long as your application created it, using the handler you provide it. + +The only case in which this handler will not be used to evaluate the navigation for an alert (assuming your application is the `consumer`) is if you have also used `registerNavigation` api, along side your `registerDefaultNavigation` usage, to handle that alert's specific AlertType. + +You can use the `registerNavigation` api to specify as many AlertType specific handlers as you like, but you can only use it once per AlertType as we wouldn't know which handler to use if you specified two for the same AlertType. For the same reason, you can only use `registerDefaultNavigation` once per plugin, as it covers all cases for your specific plugin. + ## RESTful API Using an alert type requires you to create an alert that will contain parameters and actions for a given alert type. See below for CRUD operations using the API. @@ -480,4 +536,3 @@ The templating system will take the alert and alert type as described above and ``` There are limitations that we are aware of using only templates, and we are gathering feedback and use cases for these. (for example passing an array of strings to an action). - diff --git a/x-pack/plugins/alerting/common/alert_navigation.ts b/x-pack/plugins/alerting/common/alert_navigation.ts new file mode 100644 index 00000000000000..188764069e84f3 --- /dev/null +++ b/x-pack/plugins/alerting/common/alert_navigation.ts @@ -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. + */ + +import { JsonObject } from '../../infra/common/typed_json'; +export interface AlertUrlNavigation { + path: string; +} +export interface AlertStateNavigation { + state: JsonObject; +} +export type AlertNavigation = AlertUrlNavigation | AlertStateNavigation; diff --git a/x-pack/plugins/alerting/common/alert_type.ts b/x-pack/plugins/alerting/common/alert_type.ts new file mode 100644 index 00000000000000..b30cf3fa185189 --- /dev/null +++ b/x-pack/plugins/alerting/common/alert_type.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface AlertType { + id: string; + name: string; + actionGroups: ActionGroup[]; + actionVariables: string[]; + defaultActionGroupId: ActionGroup['id']; +} + +export interface ActionGroup { + id: string; + name: string; +} diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index 8c6969cded85ad..b705a334bc2b56 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -5,10 +5,9 @@ */ export * from './alert'; +export * from './alert_type'; export * from './alert_instance'; export * from './alert_task_instance'; +export * from './alert_navigation'; -export interface ActionGroup { - id: string; - name: string; -} +export const BASE_ALERT_API_PATH = '/api/alert'; diff --git a/x-pack/plugins/alerting/kibana.json b/x-pack/plugins/alerting/kibana.json index 12f48d98dbf587..02514511e75601 100644 --- a/x-pack/plugins/alerting/kibana.json +++ b/x-pack/plugins/alerting/kibana.json @@ -1,10 +1,10 @@ { "id": "alerting", "server": true, + "ui": true, "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "alerting"], "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "actions"], - "optionalPlugins": ["usageCollection", "spaces", "security"], - "ui": false -} \ No newline at end of file + "optionalPlugins": ["usageCollection", "spaces", "security"] +} diff --git a/x-pack/plugins/alerting/public/alert_api.test.ts b/x-pack/plugins/alerting/public/alert_api.test.ts new file mode 100644 index 00000000000000..a1a90f7c893a77 --- /dev/null +++ b/x-pack/plugins/alerting/public/alert_api.test.ts @@ -0,0 +1,176 @@ +/* + * 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 { AlertType } from '../common'; +import { httpServiceMock } from '../../../../src/core/public/mocks'; +import { loadAlert, loadAlertState, loadAlertType, loadAlertTypes } from './alert_api'; +import uuid from 'uuid'; + +const http = httpServiceMock.createStartContract(); + +beforeEach(() => jest.resetAllMocks()); + +describe('loadAlertTypes', () => { + test('should call get alert types API', async () => { + const resolvedValue: AlertType[] = [ + { + id: 'test', + name: 'Test', + actionVariables: ['var1'], + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + }, + ]; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertTypes({ http }); + expect(result).toEqual(resolvedValue); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alert/types", + ] + `); + }); +}); + +describe('loadAlertType', () => { + test('should call get alert types API', async () => { + const alertType: AlertType = { + id: 'test', + name: 'Test', + actionVariables: ['var1'], + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + }; + http.get.mockResolvedValueOnce([alertType]); + + await loadAlertType({ http, id: alertType.id }); + + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alert/types", + ] + `); + }); + + test('should find the required alertType', async () => { + const alertType: AlertType = { + id: 'test-another', + name: 'Test Another', + actionVariables: [], + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + }; + http.get.mockResolvedValueOnce([alertType]); + + expect(await loadAlertType({ http, id: 'test-another' })).toEqual(alertType); + }); + + test('should throw if required alertType is missing', async () => { + http.get.mockResolvedValueOnce([ + { + id: 'test-another', + name: 'Test Another', + actionVariables: [], + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + }, + ]); + + expect(loadAlertType({ http, id: 'test' })).rejects.toMatchInlineSnapshot( + `[Error: Alert type "test" is not registered.]` + ); + }); +}); + +describe('loadAlert', () => { + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + id: alertId, + name: 'name', + tags: [], + enabled: true, + alertTypeId: '.noop', + schedule: { interval: '1s' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + expect(await loadAlert({ http, alertId })).toEqual(resolvedValue); + expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}`); + }); +}); + +describe('loadAlertState', () => { + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: {}, + second_instance: {}, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue); + expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + }); + + test('should parse AlertInstances', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: '2020-02-09T23:15:41.941Z', + }, + }, + }, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + expect(await loadAlertState({ http, alertId })).toEqual({ + ...resolvedValue, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: new Date('2020-02-09T23:15:41.941Z'), + }, + }, + }, + }, + }); + expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + }); + + test('should handle empty response from api', async () => { + const alertId = uuid.v4(); + http.get.mockResolvedValueOnce(''); + + expect(await loadAlertState({ http, alertId })).toEqual({}); + expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + }); +}); diff --git a/x-pack/plugins/alerting/public/alert_api.ts b/x-pack/plugins/alerting/public/alert_api.ts new file mode 100644 index 00000000000000..1df39e9f38b1d8 --- /dev/null +++ b/x-pack/plugins/alerting/public/alert_api.ts @@ -0,0 +1,74 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; +import * as t from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { findFirst } from 'fp-ts/lib/Array'; +import { isNone } from 'fp-ts/lib/Option'; + +import { i18n } from '@kbn/i18n'; +import { BASE_ALERT_API_PATH, alertStateSchema } from '../common'; +import { Alert, AlertType, AlertTaskState } from '../common'; + +export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { + return await http.get(`${BASE_ALERT_API_PATH}/types`); +} + +export async function loadAlertType({ + http, + id, +}: { + http: HttpSetup; + id: AlertType['id']; +}): Promise { + const maybeAlertType = findFirst(type => type.id === id)( + await http.get(`${BASE_ALERT_API_PATH}/types`) + ); + if (isNone(maybeAlertType)) { + throw new Error( + i18n.translate('xpack.alerting.loadAlertType.missingAlertTypeError', { + defaultMessage: 'Alert type "{id}" is not registered.', + values: { + id, + }, + }) + ); + } + return maybeAlertType.value; +} + +export async function loadAlert({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`); +} + +type EmptyHttpResponse = ''; +export async function loadAlertState({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + return await http + .get(`${BASE_ALERT_API_PATH}/${alertId}/state`) + .then((state: AlertTaskState | EmptyHttpResponse) => (state ? state : {})) + .then((state: AlertTaskState) => { + return pipe( + alertStateSchema.decode(state), + fold((e: t.Errors) => { + throw new Error(`Alert "${alertId}" has invalid state`); + }, t.identity) + ); + }); +} diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.mock.ts b/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.mock.ts new file mode 100644 index 00000000000000..792bd8e885ea6e --- /dev/null +++ b/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.mock.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AlertNavigationRegistry } from './alert_navigation_registry'; + +type Schema = PublicMethodsOf; + +const createAlertNavigationRegistryMock = () => { + const mocked: jest.Mocked = { + has: jest.fn(), + hasDefaultHandler: jest.fn(), + hasTypedHandler: jest.fn(), + register: jest.fn(), + registerDefault: jest.fn(), + get: jest.fn(), + }; + return mocked; +}; + +export const alertNavigationRegistryMock = { + create: createAlertNavigationRegistryMock, +}; diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts b/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts new file mode 100644 index 00000000000000..439ee9e818ef4b --- /dev/null +++ b/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts @@ -0,0 +1,184 @@ +/* + * 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 { AlertNavigationRegistry } from './alert_navigation_registry'; +import { AlertType, SanitizedAlert } from '../../common'; +import uuid from 'uuid'; + +beforeEach(() => jest.resetAllMocks()); + +const mockAlertType = (id: string): AlertType => ({ + id, + name: id, + actionGroups: [], + actionVariables: [], + defaultActionGroupId: 'default', +}); + +describe('AlertNavigationRegistry', () => { + function handler(alert: SanitizedAlert, alertType: AlertType) { + return {}; + } + + describe('has()', () => { + test('returns false for unregistered consumer handlers', () => { + const registry = new AlertNavigationRegistry(); + expect(registry.has('siem', mockAlertType(uuid.v4()))).toEqual(false); + }); + + test('returns false for unregistered alert types handlers', () => { + const registry = new AlertNavigationRegistry(); + expect(registry.has('siem', mockAlertType('index_threshold'))).toEqual(false); + }); + + test('returns true for registered consumer & alert types handlers', () => { + const registry = new AlertNavigationRegistry(); + const alertType = mockAlertType('index_threshold'); + registry.register('siem', alertType, handler); + expect(registry.has('siem', alertType)).toEqual(true); + }); + + test('returns true for registered consumer with default handler', () => { + const registry = new AlertNavigationRegistry(); + const alertType = mockAlertType('index_threshold'); + registry.registerDefault('siem', handler); + expect(registry.has('siem', alertType)).toEqual(true); + }); + }); + + describe('hasDefaultHandler()', () => { + test('returns false for unregistered consumer handlers', () => { + const registry = new AlertNavigationRegistry(); + expect(registry.hasDefaultHandler('siem')).toEqual(false); + }); + + test('returns true for registered consumer handlers', () => { + const registry = new AlertNavigationRegistry(); + + registry.registerDefault('siem', handler); + expect(registry.hasDefaultHandler('siem')).toEqual(true); + }); + }); + + describe('register()', () => { + test('registers a handler by consumer & Alert Type', () => { + const registry = new AlertNavigationRegistry(); + const alertType = mockAlertType('index_threshold'); + registry.register('siem', alertType, handler); + expect(registry.has('siem', alertType)).toEqual(true); + }); + + test('allows registeration of multiple handlers for the same consumer', () => { + const registry = new AlertNavigationRegistry(); + + const indexThresholdAlertType = mockAlertType('index_threshold'); + registry.register('siem', indexThresholdAlertType, handler); + expect(registry.has('siem', indexThresholdAlertType)).toEqual(true); + + const geoAlertType = mockAlertType('geogrid'); + registry.register('siem', geoAlertType, handler); + expect(registry.has('siem', geoAlertType)).toEqual(true); + }); + + test('allows registeration of multiple handlers for the same Alert Type', () => { + const registry = new AlertNavigationRegistry(); + + const indexThresholdAlertType = mockAlertType('geogrid'); + registry.register('siem', indexThresholdAlertType, handler); + expect(registry.has('siem', indexThresholdAlertType)).toEqual(true); + + registry.register('apm', indexThresholdAlertType, handler); + expect(registry.has('apm', indexThresholdAlertType)).toEqual(true); + }); + + test('throws if an existing handler is registered', () => { + const registry = new AlertNavigationRegistry(); + const alertType = mockAlertType('index_threshold'); + registry.register('siem', alertType, handler); + expect(() => { + registry.register('siem', alertType, handler); + }).toThrowErrorMatchingInlineSnapshot( + `"Navigation for Alert type \\"index_threshold\\" within \\"siem\\" is already registered."` + ); + }); + }); + + describe('registerDefault()', () => { + test('registers a handler by consumer', () => { + const registry = new AlertNavigationRegistry(); + registry.registerDefault('siem', handler); + expect(registry.hasDefaultHandler('siem')).toEqual(true); + }); + + test('allows registeration of default and typed handlers for the same consumer', () => { + const registry = new AlertNavigationRegistry(); + + registry.registerDefault('siem', handler); + expect(registry.hasDefaultHandler('siem')).toEqual(true); + + const geoAlertType = mockAlertType('geogrid'); + registry.register('siem', geoAlertType, handler); + expect(registry.has('siem', geoAlertType)).toEqual(true); + }); + + test('throws if an existing handler is registered', () => { + const registry = new AlertNavigationRegistry(); + registry.registerDefault('siem', handler); + expect(() => { + registry.registerDefault('siem', handler); + }).toThrowErrorMatchingInlineSnapshot( + `"Default Navigation within \\"siem\\" is already registered."` + ); + }); + }); + + describe('get()', () => { + test('returns registered handlers by consumer & Alert Type', () => { + const registry = new AlertNavigationRegistry(); + + function indexThresholdHandler(alert: SanitizedAlert, alertType: AlertType) { + return {}; + } + + const indexThresholdAlertType = mockAlertType('indexThreshold'); + registry.register('siem', indexThresholdAlertType, indexThresholdHandler); + expect(registry.get('siem', indexThresholdAlertType)).toEqual(indexThresholdHandler); + }); + + test('returns default handlers by consumer when there is no handler for requested alert type', () => { + const registry = new AlertNavigationRegistry(); + + function defaultHandler(alert: SanitizedAlert, alertType: AlertType) { + return {}; + } + + registry.registerDefault('siem', defaultHandler); + expect(registry.get('siem', mockAlertType('geogrid'))).toEqual(defaultHandler); + }); + + test('returns default handlers by consumer when there are other alert type handler', () => { + const registry = new AlertNavigationRegistry(); + + registry.register('siem', mockAlertType('indexThreshold'), () => ({})); + + function defaultHandler(alert: SanitizedAlert, alertType: AlertType) { + return {}; + } + + registry.registerDefault('siem', defaultHandler); + expect(registry.get('siem', mockAlertType('geogrid'))).toEqual(defaultHandler); + }); + + test('throws if a handler isnt registered', () => { + const registry = new AlertNavigationRegistry(); + const alertType = mockAlertType('index_threshold'); + + expect(() => registry.get('siem', alertType)).toThrowErrorMatchingInlineSnapshot( + `"Navigation for Alert type \\"index_threshold\\" within \\"siem\\" is not registered."` + ); + }); + }); +}); diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.ts b/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.ts new file mode 100644 index 00000000000000..7f1919fbea6845 --- /dev/null +++ b/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.ts @@ -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 Boom from 'boom'; +import { i18n } from '@kbn/i18n'; +import { AlertType } from '../../common'; +import { AlertNavigationHandler } from './types'; + +const DEFAULT_HANDLER = Symbol('*'); +export class AlertNavigationRegistry { + private readonly alertNavigations: Map< + string, + Map + > = new Map(); + + public has(consumer: string, alertType: AlertType) { + return this.hasTypedHandler(consumer, alertType) || this.hasDefaultHandler(consumer); + } + + public hasTypedHandler(consumer: string, alertType: AlertType) { + return this.alertNavigations.get(consumer)?.has(alertType.id) ?? false; + } + + public hasDefaultHandler(consumer: string) { + return this.alertNavigations.get(consumer)?.has(DEFAULT_HANDLER) ?? false; + } + + private createConsumerNavigation(consumer: string) { + const consumerNavigations = new Map(); + this.alertNavigations.set(consumer, consumerNavigations); + return consumerNavigations; + } + + public registerDefault(consumer: string, handler: AlertNavigationHandler) { + if (this.hasDefaultHandler(consumer)) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.alertNavigationRegistry.register.duplicateDefaultError', { + defaultMessage: 'Default Navigation within "{consumer}" is already registered.', + values: { + consumer, + }, + }) + ); + } + + const consumerNavigations = + this.alertNavigations.get(consumer) ?? this.createConsumerNavigation(consumer); + + consumerNavigations.set(DEFAULT_HANDLER, handler); + } + + public register(consumer: string, alertType: AlertType, handler: AlertNavigationHandler) { + if (this.hasTypedHandler(consumer, alertType)) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.alertNavigationRegistry.register.duplicateNavigationError', { + defaultMessage: + 'Navigation for Alert type "{alertType}" within "{consumer}" is already registered.', + values: { + alertType: alertType.id, + consumer, + }, + }) + ); + } + + const consumerNavigations = + this.alertNavigations.get(consumer) ?? this.createConsumerNavigation(consumer); + + consumerNavigations.set(alertType.id, handler); + } + + public get(consumer: string, alertType: AlertType): AlertNavigationHandler { + if (this.has(consumer, alertType)) { + const consumerHandlers = this.alertNavigations.get(consumer)!; + return (consumerHandlers.get(alertType.id) ?? consumerHandlers.get(DEFAULT_HANDLER))!; + } + + throw Boom.badRequest( + i18n.translate('xpack.alerting.alertNavigationRegistry.get.missingNavigationError', { + defaultMessage: + 'Navigation for Alert type "{alertType}" within "{consumer}" is not registered.', + values: { + alertType: alertType.id, + consumer, + }, + }) + ); + } +} diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/index.ts b/x-pack/plugins/alerting/public/alert_navigation_registry/index.ts new file mode 100644 index 00000000000000..1d8b3ffce6bcf3 --- /dev/null +++ b/x-pack/plugins/alerting/public/alert_navigation_registry/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './types'; +export * from './alert_navigation_registry'; diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts b/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts new file mode 100644 index 00000000000000..0038652f47f129 --- /dev/null +++ b/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts @@ -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 { JsonObject } from '../../../infra/common/typed_json'; +import { AlertType, SanitizedAlert } from '../../common'; + +export type AlertNavigationHandler = ( + alert: SanitizedAlert, + alertType: AlertType +) => JsonObject | string; diff --git a/x-pack/plugins/alerting/public/index.ts b/x-pack/plugins/alerting/public/index.ts new file mode 100644 index 00000000000000..2c3ec2fcc33c8f --- /dev/null +++ b/x-pack/plugins/alerting/public/index.ts @@ -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 { AlertingPublicPlugin } from './plugin'; +export { PluginSetupContract, PluginStartContract } from './plugin'; + +export function plugin() { + return new AlertingPublicPlugin(); +} diff --git a/x-pack/plugins/alerting/public/mocks.ts b/x-pack/plugins/alerting/public/mocks.ts new file mode 100644 index 00000000000000..5b99b86c1b7c5a --- /dev/null +++ b/x-pack/plugins/alerting/public/mocks.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AlertingPublicPlugin } from './plugin'; + +export type Setup = jest.Mocked>; +export type Start = jest.Mocked>; + +const createSetupContract = (): Setup => ({ + registerNavigation: jest.fn(), + registerDefaultNavigation: jest.fn(), +}); + +const createStartContract = (): Start => ({ + getNavigation: jest.fn(), +}); + +export const alertingPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/x-pack/plugins/alerting/public/plugin.ts b/x-pack/plugins/alerting/public/plugin.ts new file mode 100644 index 00000000000000..43f84b190f4100 --- /dev/null +++ b/x-pack/plugins/alerting/public/plugin.ts @@ -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 { CoreSetup, Plugin, CoreStart } from 'src/core/public'; + +import { AlertNavigationRegistry, AlertNavigationHandler } from './alert_navigation_registry'; +import { loadAlert, loadAlertType } from './alert_api'; +import { Alert, AlertNavigation } from '../common'; + +export interface PluginSetupContract { + registerNavigation: ( + consumer: string, + alertType: string, + handler: AlertNavigationHandler + ) => void; + registerDefaultNavigation: (consumer: string, handler: AlertNavigationHandler) => void; +} +export interface PluginStartContract { + getNavigation: (alertId: Alert['id']) => Promise; +} + +export class AlertingPublicPlugin implements Plugin { + private alertNavigationRegistry?: AlertNavigationRegistry; + public setup(core: CoreSetup) { + this.alertNavigationRegistry = new AlertNavigationRegistry(); + + const registerNavigation = async ( + consumer: string, + alertType: string, + handler: AlertNavigationHandler + ) => + this.alertNavigationRegistry!.register( + consumer, + await loadAlertType({ http: core.http, id: alertType }), + handler + ); + + const registerDefaultNavigation = async (consumer: string, handler: AlertNavigationHandler) => + this.alertNavigationRegistry!.registerDefault(consumer, handler); + + return { + registerNavigation, + registerDefaultNavigation, + }; + } + + public start(core: CoreStart) { + return { + getNavigation: async (alertId: Alert['id']) => { + const alert = await loadAlert({ http: core.http, alertId }); + const alertType = await loadAlertType({ http: core.http, id: alert.alertTypeId }); + + if (this.alertNavigationRegistry!.has(alert.consumer, alertType)) { + const navigationHandler = this.alertNavigationRegistry!.get(alert.consumer, alertType); + const state = navigationHandler(alert, alertType); + return typeof state === 'string' ? { path: state } : { state }; + } + }, + }; + } +} diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index b4b2de19ef24f0..8d54432f7d9c35 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -137,6 +137,7 @@ export class AlertingPlugin { taskRunnerFactory: this.taskRunnerFactory, }); this.alertTypeRegistry = alertTypeRegistry; + this.serverBasePath = core.http.basePath.serverBasePath; const usageCollection = plugins.usageCollection; diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index 6883faa5ee2302..d11f2b3e51c9d8 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -1,8 +1,8 @@ { - "id": "triggers_actions_ui", - "version": "kibana", - "server": false, - "ui": true, - "optionalPlugins": ["alerting", "alertingBuiltins"], - "requiredPlugins": ["management", "charts", "data"] - } + "id": "triggers_actions_ui", + "version": "kibana", + "server": false, + "ui": true, + "optionalPlugins": ["alerting", "alertingBuiltins"], + "requiredPlugins": ["management", "charts", "data"] +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 70945350c3cfa3..0593940a0d105d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -13,6 +13,7 @@ import { IUiSettingsClient, ApplicationStart, ChromeBreadcrumb, + CoreStart, } from 'kibana/public'; import { BASE_PATH, Section, routeToAlertDetails } from './constants'; import { TriggersActionsUIHome } from './home'; @@ -23,11 +24,14 @@ import { TypeRegistry } from './type_registry'; import { AlertDetailsRouteWithApi as AlertDetailsRoute } from './sections/alert_details/components/alert_details_route'; import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { PluginStartContract as AlertingStart } from '../../../alerting/public'; export interface AppDeps { dataPlugin: DataPublicPluginStart; charts: ChartsPluginStart; chrome: ChromeStart; + alerting?: AlertingStart; + navigateToApp: CoreStart['application']['navigateToApp']; docLinks: DocLinksStart; toastNotifications: ToastsSetup; http: HttpSetup; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index d469651b48b04a..2f5172e8b386a4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +export { BASE_ALERT_API_PATH } from '../../../../alerting/common'; +export { BASE_ACTION_API_PATH } from '../../../../actions/common'; + export const BASE_PATH = '/management/kibana/triggersActions'; -export const BASE_ACTION_API_PATH = '/api/action'; -export const BASE_ALERT_API_PATH = '/api/alert'; export type Section = 'connectors' | 'alerts'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index f94efc0d06729f..9187836d52462e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -13,6 +13,7 @@ import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; import { AppContextProvider } from '../../../app_context'; import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; +import { alertingPluginMock } from '../../../../../../alerting/public/mocks'; jest.mock('../../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), @@ -49,7 +50,7 @@ describe('actions_connectors_list component empty', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -57,9 +58,11 @@ describe('actions_connectors_list component empty', () => { docLinks, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), + alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { @@ -145,7 +148,7 @@ describe('actions_connectors_list component with items', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -153,9 +156,11 @@ describe('actions_connectors_list component with items', () => { docLinks, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), + alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { @@ -228,7 +233,7 @@ describe('actions_connectors_list component empty with show only capability', () { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -236,9 +241,11 @@ describe('actions_connectors_list component empty with show only capability', () docLinks, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), + alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { @@ -316,7 +323,7 @@ describe('actions_connectors_list with show only capability', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -324,9 +331,11 @@ describe('actions_connectors_list with show only capability', () => { docLinks, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), + alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index c142f0c6d3a504..92b3e4eb9679fe 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -19,6 +19,7 @@ import { import { times, random } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { ViewInApp } from './view_in_app'; jest.mock('../../../app_context', () => ({ useAppDependencies: jest.fn(() => ({ @@ -247,14 +248,7 @@ describe('alert_details', () => { expect( shallow( - ).containsMatchingElement( - - - - ) + ).containsMatchingElement() ).toBeTruthy(); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 30016637dc1824..49e818ebc7ee40 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -33,6 +33,7 @@ import { withBulkAlertOperations, } from '../../common/components/with_bulk_alert_api_operations'; import { AlertInstancesRouteWithApi } from './alert_instances_route'; +import { ViewInApp } from './view_in_app'; type AlertDetailsProps = { alert: Alert; @@ -95,12 +96,7 @@ export const AlertDetails: React.FunctionComponent = ({ - - - + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx new file mode 100644 index 00000000000000..18825d58aa055a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx @@ -0,0 +1,108 @@ +/* + * 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 * as React from 'react'; +import uuid from 'uuid'; +import { mount, ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + +import { Alert } from '../../../../types'; +import { ViewInApp } from './view_in_app'; +import { useAppDependencies } from '../../../app_context'; + +jest.mock('../../../app_context', () => { + const alerting = { + getNavigation: jest.fn(async id => (id === 'alert-with-nav' ? { path: '/alert' } : undefined)), + }; + const navigateToApp = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ + http: jest.fn(), + navigateToApp, + alerting, + legacy: { + capabilities: { + get: jest.fn(() => ({})), + }, + }, + })), + }; +}); + +jest.mock('../../../lib/capabilities', () => ({ + hasSaveAlertsCapability: jest.fn(() => true), +})); + +describe('alert_details', () => { + describe('link to the app that created the alert', () => { + it('is disabled when there is no navigation', async () => { + const alert = mockAlert(); + const { alerting } = useAppDependencies(); + + let component: ReactWrapper; + await act(async () => { + // use mount as we need useEffect to run + component = mount(); + + await waitForUseEffect(); + + expect(component!.find('button').prop('disabled')).toBe(true); + expect(component!.text()).toBe('View in app'); + + expect(alerting!.getNavigation).toBeCalledWith(alert.id); + }); + }); + + it('enabled when there is navigation', async () => { + const alert = mockAlert({ id: 'alert-with-nav', consumer: 'siem' }); + const { navigateToApp } = useAppDependencies(); + + let component: ReactWrapper; + act(async () => { + // use mount as we need useEffect to run + component = mount(); + + await waitForUseEffect(); + + expect(component!.find('button').prop('disabled')).toBe(undefined); + + component!.find('button').prop('onClick')!({ + currentTarget: {}, + } as React.MouseEvent<{}, MouseEvent>); + + expect(navigateToApp).toBeCalledWith('siem', '/alert'); + }); + }); + }); +}); + +function waitForUseEffect() { + return new Promise(resolve => { + setTimeout(resolve, 0); + }); +} + +function mockAlert(overloads: Partial = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx new file mode 100644 index 00000000000000..337b355ce129c4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CoreStart } from 'kibana/public'; +import { fromNullable, fold } from 'fp-ts/lib/Option'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { useAppDependencies } from '../../../app_context'; + +import { + AlertNavigation, + AlertStateNavigation, + AlertUrlNavigation, +} from '../../../../../../alerting/common'; +import { Alert } from '../../../../types'; + +export interface ViewInAppProps { + alert: Alert; +} + +const NO_NAVIGATION = false; + +type AlertNavigationLoadingState = AlertNavigation | false | null; + +export const ViewInApp: React.FunctionComponent = ({ alert }) => { + const { navigateToApp, alerting: maybeAlerting } = useAppDependencies(); + + const [alertNavigation, setAlertNavigation] = useState(null); + useEffect(() => { + pipe( + fromNullable(maybeAlerting), + fold( + /** + * If the alerting plugin is disabled, + * navigation isn't supported + */ + () => setAlertNavigation(NO_NAVIGATION), + alerting => + alerting + .getNavigation(alert.id) + .then(nav => (nav ? setAlertNavigation(nav) : setAlertNavigation(NO_NAVIGATION))) + .catch(() => { + setAlertNavigation(NO_NAVIGATION); + }) + ) + ); + }, [alert.id, maybeAlerting]); + + return ( + + + + ); +}; + +function hasNavigation( + alertNavigation: AlertNavigationLoadingState +): alertNavigation is AlertStateNavigation | AlertUrlNavigation { + return alertNavigation + ? alertNavigation.hasOwnProperty('state') || alertNavigation.hasOwnProperty('path') + : NO_NAVIGATION; +} + +function getNavigationHandler( + alertNavigation: AlertNavigationLoadingState, + alert: Alert, + navigateToApp: CoreStart['application']['navigateToApp'] +): object { + return hasNavigation(alertNavigation) + ? { + onClick: () => { + navigateToApp(alert.consumer, alertNavigation); + }, + } + : {}; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index a80daf544f34ed..108cc724aa4074 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -15,6 +15,7 @@ import { ValidationResult } from '../../../../types'; import { AppContextProvider } from '../../../app_context'; import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; +import { alertingPluginMock } from '../../../../../../alerting/public/mocks'; jest.mock('../../../lib/action_connector_api', () => ({ loadActionTypes: jest.fn(), @@ -83,7 +84,7 @@ describe('alerts_list component empty', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -91,9 +92,11 @@ describe('alerts_list component empty', () => { docLinks, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), + alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { @@ -204,7 +207,7 @@ describe('alerts_list component with items', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -212,9 +215,11 @@ describe('alerts_list component with items', () => { docLinks, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), + alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { @@ -292,7 +297,7 @@ describe('alerts_list component empty with show only capability', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -300,9 +305,11 @@ describe('alerts_list component empty with show only capability', () => { docLinks, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), + alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { @@ -409,7 +416,7 @@ describe('alerts_list with show only capability', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -417,9 +424,11 @@ describe('alerts_list with show only capability', () => { docLinks, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), + alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index fbffd5c2f999d9..668a8802d14617 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -11,7 +11,7 @@ export { AlertsContextProvider } from './application/context/alerts_context'; export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context'; export { AlertAdd } from './application/sections/alert_form'; export { ActionForm } from './application/sections/action_connector_form'; -export { AlertAction, Alert } from './types'; +export { AlertAction, Alert, AlertTypeModel } from './types'; export { ConnectorAddFlyout, ConnectorEditFlyout, diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index f4d8c478efaf28..99a3d65589e8e9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -15,6 +15,7 @@ import { TypeRegistry } from './application/type_registry'; import { ManagementStart } from '../../../../src/plugins/management/public'; import { boot } from './application/boot'; import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; +import { PluginStartContract as AlertingStart } from '../../alerting/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; export interface TriggersAndActionsUIPublicPluginSetup { @@ -31,6 +32,8 @@ interface PluginsStart { data: DataPublicPluginStart; charts: ChartsPluginStart; management: ManagementStart; + alerting?: AlertingStart; + navigateToApp: CoreStart['application']['navigateToApp']; } export class Plugin @@ -80,6 +83,7 @@ export class Plugin boot({ dataPlugin: plugins.data, charts: plugins.charts, + alerting: plugins.alerting, element: params.element, toastNotifications: core.notifications.toasts, http: core.http, @@ -89,6 +93,7 @@ export class Plugin savedObjects: core.savedObjects.client, I18nContext: core.i18n.Context, capabilities: core.application.capabilities, + navigateToApp: core.application.navigateToApp, setBreadcrumbs: params.setBreadcrumbs, actionTypeRegistry: this.actionTypeRegistry, alertTypeRegistry: this.alertTypeRegistry, diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 74a267c6e0a8e0..64655e5b45a2b7 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -148,6 +148,34 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); + describe.skip('View In App', function() { + const testRunUuid = uuid.v4(); + before(async () => { + await pageObjects.common.navigateToApp('triggersActions'); + }); + + it('renders the alert details view in app button', async () => { + const alert = await alerting.alerts.createNoOp(`test-alert-${testRunUuid}`); + + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + + expect(await pageObjects.alertDetailsUI.isViewInAppEnabled()).to.be(true); + + await pageObjects.alertDetailsUI.clickViewInAppEnabled(); + + expect(await pageObjects.alertDetailsUI.getNoOpAppTitle()).to.be(`View Alert ${alert.id}`); + }); + }); + describe('Alert Instances', function() { const testRunUuid = uuid.v4(); let alert: any; diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json new file mode 100644 index 00000000000000..f072937c4b128e --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "alerting_fixture", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack"], + "requiredPlugins": ["alerting"], + "server": true, + "ui": true +} diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/package.json b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/package.json index 836fa09855d8fd..7f7463f4815e7f 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/package.json +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/package.json @@ -3,5 +3,13 @@ "version": "0.0.0", "kibana": { "version": "kibana" + }, + "main": "target/test/functional_with_es_ssl/fixtures/plugins/alerts", + "scripts": { + "kbn": "node ../../../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" } } diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/application.tsx b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/application.tsx new file mode 100644 index 00000000000000..2301a39187801b --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/application.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter as Router, Route, RouteComponentProps } from 'react-router-dom'; +import { EuiPage, EuiText } from '@elastic/eui'; +import { AppMountParameters, CoreStart } from '../../../../../../../src/core/public'; + +export interface AlertingExampleComponentParams { + basename: string; +} + +const AlertingExampleApp = (deps: AlertingExampleComponentParams) => { + const { basename } = deps; + return ( + + + ) => { + return ( + +

View Alert {props.match.params.id}

+
+ ); + }} + /> +
+
+ ); +}; + +export const renderApp = ( + core: CoreStart, + deps: any, + { appBasePath, element }: AppMountParameters +) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/index.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/index.ts new file mode 100644 index 00000000000000..095769cccb8fbe --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/index.ts @@ -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. + */ + +import { AlertingFixturePlugin } from './plugin'; + +export const plugin = () => new AlertingFixturePlugin(); diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts new file mode 100644 index 00000000000000..2bf353f79985cd --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { PluginSetupContract as AlertingSetup } from '../../../../../../plugins/alerting/public'; +import { AlertType, SanitizedAlert } from '../../../../../../plugins/alerting/common'; + +export type Setup = void; +export type Start = void; + +export interface AlertingExamplePublicSetupDeps { + alerting: AlertingSetup; +} + +export class AlertingFixturePlugin implements Plugin { + public setup(core: CoreSetup, { alerting }: AlertingExamplePublicSetupDeps) { + alerting.registerNavigation( + 'consumer-noop', + 'test.noop', + (alert: SanitizedAlert, alertType: AlertType) => `/alert/${alert.id}` + ); + + core.application.register({ + id: 'consumer-noop', + title: 'No Op App', + async mount(params: AppMountParameters) { + const [coreStart, depsStart] = await core.getStartServices(); + const { renderApp } = await import('./application'); + return renderApp(coreStart, depsStart, params); + }, + }); + } + + public start() {} + public stop() {} +} diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/index.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/index.ts new file mode 100644 index 00000000000000..2b02d9ff0f681b --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/index.ts @@ -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. + */ + +import { PluginInitializer } from 'kibana/server'; +import { AlertingFixturePlugin } from './plugin'; + +export const plugin: PluginInitializer = () => new AlertingFixturePlugin(); diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts similarity index 61% rename from x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts rename to x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index 9069044b83ed9f..d4ae6d3557c3be 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -4,21 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertType } from '../../../../../plugins/alerting/server'; - -// eslint-disable-next-line import/no-default-export -export default function(kibana: any) { - return new kibana.Plugin({ - require: ['alerting'], - name: 'alerts', - init(server: any) { - createNoopAlertType(server.newPlatform.setup.plugins.alerting); - createAlwaysFiringAlertType(server.newPlatform.setup.plugins.alerting); - }, - }); +import { Plugin, CoreSetup } from 'kibana/server'; +import { + PluginSetupContract as AlertingSetup, + AlertType, +} from '../../../../../../plugins/alerting/server'; + +// this plugin's dependendencies +export interface AlertingExampleDeps { + alerting: AlertingSetup; +} + +export class AlertingFixturePlugin implements Plugin { + public setup(core: CoreSetup, { alerting }: AlertingExampleDeps) { + createNoopAlertType(alerting); + createAlwaysFiringAlertType(alerting); + } + + public start() {} + public stop() {} } -function createNoopAlertType(setupContract: any) { +function createNoopAlertType(alerting: AlertingSetup) { const noopAlertType: AlertType = { id: 'test.noop', name: 'Test: Noop', @@ -26,10 +33,10 @@ function createNoopAlertType(setupContract: any) { defaultActionGroupId: 'default', async executor() {}, }; - setupContract.registerType(noopAlertType); + alerting.registerType(noopAlertType); } -function createAlwaysFiringAlertType(setupContract: any) { +function createAlwaysFiringAlertType(alerting: AlertingSetup) { // Alert types const alwaysFiringAlertType: any = { id: 'test.always-firing', @@ -54,5 +61,5 @@ function createAlwaysFiringAlertType(setupContract: any) { }; }, }; - setupContract.registerType(alwaysFiringAlertType); + alerting.registerType(alwaysFiringAlertType); } diff --git a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts index ddd88cb8885342..03f0056670311d 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts @@ -102,5 +102,16 @@ export function AlertDetailsPageProvider({ getService }: FtrProviderContext) { const nextButton = await testSubjects.find(`pagination-button-next`); nextButton.click(); }, + async isViewInAppEnabled() { + const viewInAppButton = await testSubjects.find(`alertDetails-viewInApp`); + return (await viewInAppButton.getAttribute('disabled')) !== 'disabled'; + }, + async clickViewInAppEnabled() { + const viewInAppButton = await testSubjects.find(`alertDetails-viewInApp`); + return viewInAppButton.click(); + }, + async getNoOpAppTitle() { + return await testSubjects.getVisibleText('noop-title'); + }, }; } diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts index 695751cf5ac490..5b506c20e029c0 100644 --- a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -22,6 +22,31 @@ export class Alerts { }); } + public async createNoOp(name: string) { + this.log.debug(`creating alert ${name}`); + + const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + enabled: true, + name, + tags: ['foo'], + alertTypeId: 'test.noop', + consumer: 'consumer-noop', + schedule: { interval: '1m' }, + throttle: '1m', + actions: [], + params: {}, + }); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + + this.log.debug(`created alert ${alert.id}`); + + return alert; + } + public async createAlwaysFiringWithActions( name: string, actions: Array<{ From 27045e09427ee413cd4374b22b15975d714001a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 19 Mar 2020 08:02:07 -0400 Subject: [PATCH 33/42] Make slack param validation handle empty messages (#60468) --- .../actions/server/builtin_action_types/slack.ts | 2 +- .../tests/actions/builtin_action_types/slack.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index 042853796695d6..3a351853c1e462 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -35,7 +35,7 @@ const SecretsSchema = schema.object(secretsSchemaProps); export type ActionParamsType = TypeOf; const ParamsSchema = schema.object({ - message: schema.string(), + message: schema.string({ minLength: 1 }), }); // action type definition diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index 5dcff8712a28d6..8afa43bfea21e6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -159,6 +159,20 @@ export default function slackTest({ getService }: FtrProviderContext) { expect(result.status).to.eql('ok'); }); + it('should handle an empty message error', async () => { + const { body: result } = await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + message: '', + }, + }) + .expect(200); + expect(result.status).to.eql('error'); + expect(result.message).to.match(/error validating action params: \[message\]: /); + }); + it('should handle a 40x slack error', async () => { const { body: result } = await supertest .post(`/api/action/${simulatedActionId}/_execute`) From 4efeeac560abfda3637e103d38763c860197c6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 19 Mar 2020 08:06:51 -0400 Subject: [PATCH 34/42] Sort by name when fetching alerts and connectors (#60506) * Sort by name when fetching alerts and connectors * Fix jest tests * Add functional test * Fix failing jest test --- .../plugins/actions/server/mappings.json | 7 +- .../plugins/alerting/server/mappings.json | 7 +- .../actions/server/routes/find.test.ts | 1 + x-pack/plugins/actions/server/routes/find.ts | 2 + .../alerting/server/routes/find.test.ts | 1 + x-pack/plugins/alerting/server/routes/find.ts | 2 + .../lib/action_connector_api.test.ts | 2 + .../application/lib/action_connector_api.ts | 2 + .../public/application/lib/alert_api.test.ts | 152 ++++++++++-------- .../public/application/lib/alert_api.ts | 2 + .../apps/triggers_actions_ui/alerts.ts | 49 ++++-- 11 files changed, 141 insertions(+), 86 deletions(-) diff --git a/x-pack/legacy/plugins/actions/server/mappings.json b/x-pack/legacy/plugins/actions/server/mappings.json index a9c4d80b00af10..ef6a0c9919920f 100644 --- a/x-pack/legacy/plugins/actions/server/mappings.json +++ b/x-pack/legacy/plugins/actions/server/mappings.json @@ -2,7 +2,12 @@ "action": { "properties": { "name": { - "type": "text" + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } }, "actionTypeId": { "type": "keyword" diff --git a/x-pack/legacy/plugins/alerting/server/mappings.json b/x-pack/legacy/plugins/alerting/server/mappings.json index 31733f44e7ce6e..a7e85febf2446b 100644 --- a/x-pack/legacy/plugins/alerting/server/mappings.json +++ b/x-pack/legacy/plugins/alerting/server/mappings.json @@ -5,7 +5,12 @@ "type": "boolean" }, "name": { - "type": "text" + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } }, "tags": { "type": "keyword" diff --git a/x-pack/plugins/actions/server/routes/find.test.ts b/x-pack/plugins/actions/server/routes/find.test.ts index 862e26132fdc35..b51130b2640aa4 100644 --- a/x-pack/plugins/actions/server/routes/find.test.ts +++ b/x-pack/plugins/actions/server/routes/find.test.ts @@ -81,6 +81,7 @@ describe('findActionRoute', () => { "perPage": 1, "search": undefined, "sortField": undefined, + "sortOrder": undefined, }, }, ] diff --git a/x-pack/plugins/actions/server/routes/find.ts b/x-pack/plugins/actions/server/routes/find.ts index 71d4274980fcc3..820dd32d710aed 100644 --- a/x-pack/plugins/actions/server/routes/find.ts +++ b/x-pack/plugins/actions/server/routes/find.ts @@ -26,6 +26,7 @@ const querySchema = schema.object({ }), search_fields: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), sort_field: schema.maybe(schema.string()), + sort_order: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])), has_reference: schema.maybe( // use nullable as maybe is currently broken // in config-schema @@ -70,6 +71,7 @@ export const findActionRoute = (router: IRouter, licenseState: LicenseState) => sortField: query.sort_field, fields: query.fields, filter: query.filter, + sortOrder: query.sort_order, }; if (query.search_fields) { diff --git a/x-pack/plugins/alerting/server/routes/find.test.ts b/x-pack/plugins/alerting/server/routes/find.test.ts index ba0114c99a9bdd..391d6df3f99317 100644 --- a/x-pack/plugins/alerting/server/routes/find.test.ts +++ b/x-pack/plugins/alerting/server/routes/find.test.ts @@ -82,6 +82,7 @@ describe('findAlertRoute', () => { "perPage": 1, "search": undefined, "sortField": undefined, + "sortOrder": undefined, }, }, ] diff --git a/x-pack/plugins/alerting/server/routes/find.ts b/x-pack/plugins/alerting/server/routes/find.ts index efc5c3ea97183d..1f8f161cf30285 100644 --- a/x-pack/plugins/alerting/server/routes/find.ts +++ b/x-pack/plugins/alerting/server/routes/find.ts @@ -26,6 +26,7 @@ const querySchema = schema.object({ }), search_fields: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), sort_field: schema.maybe(schema.string()), + sort_order: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])), has_reference: schema.maybe( // use nullable as maybe is currently broken // in config-schema @@ -70,6 +71,7 @@ export const findAlertRoute = (router: IRouter, licenseState: LicenseState) => { sortField: query.sort_field, fields: query.fields, filter: query.filter, + sortOrder: query.sort_order, }; if (query.search_fields) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts index f568e0a71d0cf4..62e7b1cf022bba 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts @@ -57,6 +57,8 @@ describe('loadAllActions', () => { Object { "query": Object { "per_page": 10000, + "sort_field": "name.keyword", + "sort_order": "asc", }, }, ] diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts index 5b2b59603d281d..26ad97f05849d4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts @@ -30,6 +30,8 @@ export async function loadAllActions({ return await http.get(`${BASE_ACTION_API_PATH}/_find`, { query: { per_page: MAX_ACTIONS_RETURNED, + sort_field: 'name.keyword', + sort_order: 'asc', }, }); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index 0b06982828446c..0555823d0245e6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -170,6 +170,8 @@ describe('loadAlerts', () => { "per_page": 10, "search": undefined, "search_fields": undefined, + "sort_field": "name.keyword", + "sort_order": "asc", }, }, ] @@ -188,20 +190,22 @@ describe('loadAlerts', () => { const result = await loadAlerts({ http, searchText: 'apples', page: { index: 0, size: 10 } }); expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alert/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "page": 1, - "per_page": 10, - "search": "apples", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); + Array [ + "/api/alert/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "page": 1, + "per_page": 10, + "search": "apples", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name.keyword", + "sort_order": "asc", + }, + }, + ] + `); }); test('should call find API with actionTypesFilter', async () => { @@ -220,20 +224,22 @@ describe('loadAlerts', () => { }); expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alert/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "page": 1, - "per_page": 10, - "search": "foo", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); + Array [ + "/api/alert/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "page": 1, + "per_page": 10, + "search": "foo", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name.keyword", + "sort_order": "asc", + }, + }, + ] + `); }); test('should call find API with typesFilter', async () => { @@ -252,20 +258,22 @@ describe('loadAlerts', () => { }); expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alert/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "page": 1, - "per_page": 10, - "search": undefined, - "search_fields": undefined, - }, - }, - ] - `); + Array [ + "/api/alert/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "page": 1, + "per_page": 10, + "search": undefined, + "search_fields": undefined, + "sort_field": "name.keyword", + "sort_order": "asc", + }, + }, + ] + `); }); test('should call find API with actionTypesFilter and typesFilter', async () => { @@ -285,20 +293,22 @@ describe('loadAlerts', () => { }); expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alert/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "page": 1, - "per_page": 10, - "search": "baz", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); + Array [ + "/api/alert/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "page": 1, + "per_page": 10, + "search": "baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name.keyword", + "sort_order": "asc", + }, + }, + ] + `); }); test('should call find API with searchText and tagsFilter and typesFilter', async () => { @@ -318,20 +328,22 @@ describe('loadAlerts', () => { }); expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alert/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "page": 1, - "per_page": 10, - "search": "apples, foo, baz", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); + Array [ + "/api/alert/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "page": 1, + "per_page": 10, + "search": "apples, foo, baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name.keyword", + "sort_order": "asc", + }, + }, + ] + `); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts index ff6b4ba17c6d98..1b18460ba11cb8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts @@ -87,6 +87,8 @@ export async function loadAlerts({ search: searchText, filter: filters.length ? filters.join(' and ') : undefined, default_search_operator: 'AND', + sort_field: 'name.keyword', + sort_order: 'asc', }, }); } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 791712fa244893..4354b19da24aca 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -18,20 +18,21 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const supertest = getService('supertest'); const find = getService('find'); - async function createAlert(alertTypeId?: string, name?: string, params?: any) { + async function createAlert(overwrites: Record = {}) { const { body: createdAlert } = await supertest .post(`/api/alert`) .set('kbn-xsrf', 'foo') .send({ enabled: true, - name: name ?? generateUniqueKey(), + name: generateUniqueKey(), tags: ['foo', 'bar'], - alertTypeId: alertTypeId ?? 'test.noop', + alertTypeId: 'test.noop', consumer: 'test', schedule: { interval: '1m' }, throttle: '1m', actions: [], - params: params ?? {}, + params: {}, + ...overwrites, }) .expect(200); return createdAlert; @@ -98,6 +99,22 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ]); }); + it('should display alerts in alphabetical order', async () => { + const uniqueKey = generateUniqueKey(); + await createAlert({ name: 'b', tags: [uniqueKey] }); + await createAlert({ name: 'c', tags: [uniqueKey] }); + await createAlert({ name: 'a', tags: [uniqueKey] }); + + await pageObjects.common.navigateToApp('triggersActions'); + await pageObjects.triggersActionsUI.searchAlerts(uniqueKey); + + const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); + expect(searchResults).to.have.length(3); + expect(searchResults[0].name).to.eql('a'); + expect(searchResults[1].name).to.eql('b'); + expect(searchResults[2].name).to.eql('c'); + }); + it('should search for alert', async () => { const createdAlert = await createAlert(); await pageObjects.common.navigateToApp('triggersActions'); @@ -115,16 +132,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should edit an alert', async () => { - const createdAlert = await createAlert('.index-threshold', 'new alert', { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', + const createdAlert = await createAlert({ + alertTypeId: '.index-threshold', + name: 'new alert', + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000, 5000], + index: ['.kibana_1'], + timeField: 'alert', + }, }); await pageObjects.common.navigateToApp('triggersActions'); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); From ee6bb64f1330fd8539a642affdf00db15fc2ff56 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 19 Mar 2020 08:23:20 -0400 Subject: [PATCH 35/42] [Remote clusters] Update copy (#60382) --- .../remote_cluster_form.test.js.snap | 196 ++++++------------ .../remote_cluster_form.js | 51 ++--- .../remote_cluster_page_title.js | 24 ++- .../remote_cluster_add/remote_cluster_add.js | 6 + .../remote_cluster_edit.js | 2 +- .../detail_panel/detail_panel.js | 60 +++--- .../remote_cluster_table.js | 2 +- .../application/services/documentation.ts | 2 + 8 files changed, 152 insertions(+), 191 deletions(-) diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap index 88b869b1d1d8fc..b5bf4057f0e36d 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap @@ -126,7 +126,7 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u @@ -209,11 +209,11 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u className="euiTextColor euiTextColor--subdued" > - A unique name for the remote cluster. + A unique name for the cluster.

@@ -364,7 +364,7 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u description={ @@ -374,25 +374,6 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u fullWidth={true} hasChildLabel={true} hasEmptyLabelSpace={true} - helpText={ - - - , - } - } - /> - } labelType="label" > @@ -488,11 +469,11 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u className="euiTextColor euiTextColor--subdued" > - Remote cluster connections work by configuring a remote cluster and connecting only to a limited number of nodes in that remote cluster. + Use seed nodes by default, or switch to a single proxy address. - - , - } - } - /> - } labelType="label" >
@@ -549,7 +510,6 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u >
- -
- - - , - } - } - > - Configure a remote cluster with a single proxy address. - - - - -
-
@@ -685,7 +600,7 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u hasEmptyLabelSpace={false} helpText={ @@ -786,11 +701,11 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u id="mockId-help" > - The address used for all remote connections. + The address to use for remote connections. @@ -920,14 +835,26 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u hasEmptyLabelSpace={false} helpText={ + + , + } + } /> } label={ @@ -953,11 +880,11 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u htmlFor="mockId" > - Server name + Server name (optional) @@ -1010,11 +937,39 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u id="mockId-help" > + + , + } + } > - An optional hostname string which will be sent in the server_name field of the TLS Server Name Indication extension if TLS is enabled. + A string sent in the server_name field of the TLS Server Name Indication extension if TLS is enabled. + + + @@ -1033,7 +988,7 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u

- By default, a request fails if any of the queried remote clusters are unavailable. To continue sending a request to other remote clusters if this cluster is unavailable, enable + A request fails if any of the queried remote clusters are unavailable. To send requests to other remote clusters if this cluster is unavailable, enable - A unique name for the remote cluster. + A unique name for the cluster. @@ -1534,7 +1489,7 @@ Array [

- Remote cluster connections work by configuring a remote cluster and connecting only to a limited number of nodes in that remote cluster. + Use seed nodes by default, or switch to a single proxy address.
-
- Configure a remote cluster with a single proxy address. - -
@@ -1717,7 +1659,7 @@ Array [ class="euiFormHelpText euiFormRow__text" id="mockId-help" > - The number of gateway nodes to connect to. + The number of gateway nodes to connect to for this cluster. @@ -1751,7 +1693,7 @@ Array [ class="euiTextColor euiTextColor--subdued" >

- By default, a request fails if any of the queried remote clusters are unavailable. To continue sending a request to other remote clusters if this cluster is unavailable, enable + A request fails if any of the queried remote clusters are unavailable. To send requests to other remote clusters if this cluster is unavailable, enable Skip if unavailable diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js index 358ffc03da783b..94d6ca4ebb6481 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js @@ -37,7 +37,7 @@ import { import { skippingDisconnectedClustersUrl, transportPortUrl, - proxyModeUrl, + proxySettingsUrl, } from '../../../services/documentation'; import { RequestFlyout } from './request_flyout'; @@ -328,7 +328,7 @@ export class RemoteClusterForm extends Component { helpText={ } fullWidth @@ -363,7 +363,7 @@ export class RemoteClusterForm extends Component { helpText={ } isInvalid={Boolean(areErrorsVisible && errorProxyAddress)} @@ -414,13 +414,23 @@ export class RemoteClusterForm extends Component { label={ } helpText={ + + + ), + }} /> } fullWidth @@ -456,33 +466,14 @@ export class RemoteClusterForm extends Component { <> - - - - ), - }} - /> - } - > + } checked={mode === PROXY_MODE} @@ -523,9 +514,7 @@ export class RemoteClusterForm extends Component {

@@ -839,7 +828,7 @@ export class RemoteClusterForm extends Component { description={ } fullWidth diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js index 82d8a7b0cfa8b4..5a3b1faedad2ba 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js @@ -7,23 +7,22 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; +import { remoteClustersUrl } from '../../../services/documentation'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, - EuiPageContentHeader, EuiSpacer, + EuiText, EuiTitle, } from '@elastic/eui'; -import { remoteClustersUrl } from '../../../services/documentation'; - -export const RemoteClusterPageTitle = ({ title }) => ( +export const RemoteClusterPageTitle = ({ title, description }) => ( - + @@ -47,10 +46,23 @@ export const RemoteClusterPageTitle = ({ title }) => ( - + + + {description ? ( + <> + + + + {description} + + + ) : null} + + ); RemoteClusterPageTitle.propTypes = { title: PropTypes.node.isRequired, + description: PropTypes.node, }; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js index 4a861695c0eb37..0531310bd097b2 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js @@ -65,6 +65,12 @@ export class RemoteClusterAdd extends PureComponent { defaultMessage="Add remote cluster" /> } + description={ + + } /> diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js index 89a48927f68338..4006422d3df508 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js @@ -125,7 +125,7 @@ export class DetailPanel extends Component { title={ - - - - ) : ( - - - - ), - }} - /> + {/* A remote cluster is not editable if configured in elasticsearch.yml, so we direct the user to documentation instead */} + {isConfiguredByNode ? ( + + + + ), + }} + /> + ) : ( + + + + ), + }} + /> + )} @@ -249,7 +259,7 @@ export class DetailPanel extends Component { @@ -363,7 +373,7 @@ export class DetailPanel extends Component { diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js index ec20805ccd9192..73f32fe8bca5b6 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js @@ -141,7 +141,7 @@ export class RemoteClusterTable extends Component { content={ } /> diff --git a/x-pack/plugins/remote_clusters/public/application/services/documentation.ts b/x-pack/plugins/remote_clusters/public/application/services/documentation.ts index f6f5dc987c2eb7..76744e90096dae 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/documentation.ts +++ b/x-pack/plugins/remote_clusters/public/application/services/documentation.ts @@ -10,6 +10,7 @@ export let skippingDisconnectedClustersUrl: string; export let remoteClustersUrl: string; export let transportPortUrl: string; export let proxyModeUrl: string; +export let proxySettingsUrl: string; export function init(docLinks: DocLinksStart): void { const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; @@ -19,4 +20,5 @@ export function init(docLinks: DocLinksStart): void { remoteClustersUrl = `${esDocBasePath}/modules-remote-clusters.html`; transportPortUrl = `${esDocBasePath}/modules-transport.html`; proxyModeUrl = `${esDocBasePath}/modules-remote-clusters.html#proxy-mode`; + proxySettingsUrl = `${esDocBasePath}/modules-remote-clusters.html#remote-cluster-proxy-settings`; } From 6ed2918b6c76077af87ed0941d1cb457992a35e8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 19 Mar 2020 15:06:33 +0200 Subject: [PATCH 36/42] [SIEM][CASE] Configuration page action bar (#60608) * Add bottom bar * Add listeners --- .../case/components/configure_cases/index.tsx | 97 ++++++++++++------- 1 file changed, 62 insertions(+), 35 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx index b3c424bef6a7af..cbc3be6d144a2c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -7,8 +7,15 @@ import React, { useReducer, useCallback, useEffect, useState } from 'react'; import styled, { css } from 'styled-components'; -import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer, EuiCallOut } from '@elastic/eui'; -import { noop, isEmpty } from 'lodash/fp'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiCallOut, + EuiBottomBar, + EuiButtonEmpty, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; import { useKibana } from '../../../../lib/kibana'; import { useConnectors } from '../../../../containers/case/configure/use_connectors'; import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; @@ -32,6 +39,9 @@ import { Mapping } from '../configure_cases/mapping'; import { SectionWrapper } from '../wrappers'; import { configureCasesReducer, State } from './reducer'; import * as i18n from './translations'; +import { getCaseUrl } from '../../../../components/link_to'; + +const CASE_URL = getCaseUrl(); const FormWrapper = styled.div` ${({ theme }) => css` @@ -68,6 +78,8 @@ const ConfigureCasesComponent: React.FC = () => { null ); + const [actionBarVisible, setActionBarVisible] = useState(false); + const handleShowAddFlyout = useCallback(() => setAddFlyoutVisibility(true), []); const [{ connectorId, closureType, mapping }, dispatch] = useReducer( @@ -111,11 +123,22 @@ const ConfigureCasesComponent: React.FC = () => { const handleSubmit = useCallback( // TO DO give a warning/error to user when field are not mapped so they have chance to do it () => { + setActionBarVisible(false); persistCaseConfigure({ connectorId, closureType }); }, [connectorId, closureType, mapping] ); + const onChangeConnector = useCallback((newConnectorId: string) => { + setActionBarVisible(true); + setConnectorId(newConnectorId); + }, []); + + const onChangeClosureType = useCallback((newClosureType: ClosureType) => { + setActionBarVisible(true); + setClosureType(newClosureType); + }, []); + useEffect(() => { if ( !isEmpty(connectors) && @@ -171,7 +194,7 @@ const ConfigureCasesComponent: React.FC = () => { connectors={connectors ?? []} disabled={persistLoading || isLoadingConnectors} isLoading={isLoadingConnectors} - onChangeConnector={setConnectorId} + onChangeConnector={onChangeConnector} handleShowAddFlyout={handleShowAddFlyout} selectedConnector={connectorId} /> @@ -180,7 +203,7 @@ const ConfigureCasesComponent: React.FC = () => { @@ -192,37 +215,41 @@ const ConfigureCasesComponent: React.FC = () => { setEditFlyoutVisibility={setEditFlyoutVisibility} /> - - - - - - {i18n.CANCEL} - - - - - {i18n.SAVE_CHANGES} - - - - + {actionBarVisible && ( + + + + + + + {i18n.CANCEL} + + + + + {i18n.SAVE_CHANGES} + + + + + + + )} Date: Thu, 19 Mar 2020 14:09:44 +0100 Subject: [PATCH 37/42] migrate saved objects management edition view to react/typescript/eui (#59490) * migrate so management edition view to react * fix bundle name + add forgotten data-test-subj * add FTR tests for edition page * EUIfy react components * wrap form with EuiPanel + caps btns labels * Wrapping whole view in page content panel and removing legacy classes * improve delete confirmation modal * update translations * improve delete popin * add unit test on view components * remove kui classes & address comments * extract createFieldList and add tests * disable form submit during submition Co-authored-by: cchaos --- .../public/overlays/modal/modal_service.tsx | 1 + .../management/saved_object_registry.ts | 8 +- .../management/sections/objects/_objects.js | 1 - .../management/sections/objects/_view.html | 204 +------- .../management/sections/objects/_view.js | 309 ++---------- .../__snapshots__/header.test.tsx.snap | 165 +++++++ .../__snapshots__/intro.test.tsx.snap | 67 +++ .../not_found_errors.test.tsx.snap | 301 ++++++++++++ .../components/object_view/field.test.tsx | 95 ++++ .../objects/components/object_view/field.tsx | 162 +++++++ .../objects/components/object_view/form.tsx | 186 +++++++ .../components/object_view/header.test.tsx | 125 +++++ .../objects/components/object_view/header.tsx | 110 +++++ .../objects/components/object_view/index.ts | 23 + .../components/object_view/intro.test.tsx | 34 ++ .../objects/components/object_view/intro.tsx | 44 ++ .../object_view/not_found_errors.test.tsx | 64 +++ .../object_view/not_found_errors.tsx | 77 +++ .../objects/lib/create_field_list.test.ts | 132 +++++ .../sections/objects/lib/create_field_list.ts | 135 ++++++ .../lib/{in_app_url.js => in_app_url.ts} | 14 +- .../sections/objects/saved_object_view.tsx | 176 +++++++ .../management/sections/objects/types.ts | 38 ++ .../edit_saved_object.ts | 103 ++++ .../apps/saved_objects_management/index.ts | 27 ++ test/functional/config.js | 1 + .../edit_saved_object/data.json | 85 ++++ .../edit_saved_object/mappings.json | 459 ++++++++++++++++++ .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- 30 files changed, 2675 insertions(+), 475 deletions(-) create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/intro.test.tsx.snap create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.test.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/form.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.test.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/index.ts create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.test.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.test.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/create_field_list.test.ts create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/create_field_list.ts rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{in_app_url.js => in_app_url.ts} (71%) create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/saved_object_view.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/types.ts create mode 100644 test/functional/apps/saved_objects_management/edit_saved_object.ts create mode 100644 test/functional/apps/saved_objects_management/index.ts create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/data.json create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/mappings.json diff --git a/src/core/public/overlays/modal/modal_service.tsx b/src/core/public/overlays/modal/modal_service.tsx index 3cf1fe745be8e7..f3bbd5c94bdb4f 100644 --- a/src/core/public/overlays/modal/modal_service.tsx +++ b/src/core/public/overlays/modal/modal_service.tsx @@ -69,6 +69,7 @@ export interface OverlayModalConfirmOptions { closeButtonAriaLabel?: string; 'data-test-subj'?: string; defaultFocusedButton?: EuiConfirmModalProps['defaultFocusedButton']; + buttonColor?: EuiConfirmModalProps['buttonColor']; } /** diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts index 8e73a09480c41b..cb9ac0e01bb7f8 100644 --- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts +++ b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts @@ -35,9 +35,15 @@ interface SavedObjectRegistryEntry { title: string; } +export interface ISavedObjectsManagementRegistry { + register(service: SavedObjectRegistryEntry): void; + all(): SavedObjectRegistryEntry[]; + get(id: string): SavedObjectRegistryEntry | undefined; +} + const registry: SavedObjectRegistryEntry[] = []; -export const savedObjectManagementRegistry = { +export const savedObjectManagementRegistry: ISavedObjectsManagementRegistry = { register: (service: SavedObjectRegistryEntry) => { registry.push(service); }, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js index e3ab862cd84b7b..c5901ca6ee6bf4 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js @@ -28,7 +28,6 @@ import { ObjectsTable } from './components/objects_table'; import { I18nContext } from 'ui/i18n'; import { get } from 'lodash'; import { npStart } from 'ui/new_platform'; - import { getIndexBreadcrumbs } from './breadcrumbs'; const REACT_OBJECTS_TABLE_DOM_ELEMENT_ID = 'reactSavedObjectsTable'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html index 6efef7b48fa0e8..8bce0aabcd64a9 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html @@ -1,203 +1,5 @@ - - - -

- - -
-
-
- - -
- -
-
- -
- -
- -
-
-
-
- - -
-
-
- - -
- -
-
-
-
-
-
- -
- - -
- - - - - - - - -
-
- - - -
- - - -
-
+ + +
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js index d1a8d6a1b14af5..a847055b40015a 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js @@ -17,26 +17,20 @@ * under the License. */ -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import angular from 'angular'; +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import 'angular'; import 'angular-elastic/elastic'; -import rison from 'rison-node'; -import { savedObjectManagementRegistry } from '../../saved_object_registry'; -import objectViewHTML from './_view.html'; import uiRoutes from 'ui/routes'; import { uiModules } from 'ui/modules'; -import { fatalError, toastNotifications } from 'ui/notify'; -import 'ui/accessibility/kbn_ui_ace_keyboard_mode'; -import { isNumeric } from './lib/numeric'; -import { canViewInApp } from './lib/in_app_url'; +import { I18nContext } from 'ui/i18n'; import { npStart } from 'ui/new_platform'; - -import { castEsToKbnFieldTypeName } from '../../../../../../../plugins/data/public'; - +import objectViewHTML from './_view.html'; import { getViewBreadcrumbs } from './breadcrumbs'; +import { savedObjectManagementRegistry } from '../../saved_object_registry'; +import { SavedObjectEdition } from './saved_object_view'; -const location = 'SavedObject view'; +const REACT_OBJECTS_VIEW_DOM_ELEMENT_ID = 'reactSavedObjectsView'; uiRoutes.when('/management/kibana/objects/:service/:id', { template: objectViewHTML, @@ -44,261 +38,48 @@ uiRoutes.when('/management/kibana/objects/:service/:id', { requireUICapability: 'management.kibana.objects', }); +function createReactView($scope, $routeParams) { + const { service: serviceName, id: objectId, notFound } = $routeParams; + + const { savedObjects, overlays, notifications, application } = npStart.core; + + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_OBJECTS_VIEW_DOM_ELEMENT_ID); + if (!node) { + return; + } + + render( + + + , + node + ); + }); +} + +function destroyReactView() { + const node = document.getElementById(REACT_OBJECTS_VIEW_DOM_ELEMENT_ID); + node && unmountComponentAtNode(node); +} + uiModules .get('apps/management', ['monospaced.elastic']) .directive('kbnManagementObjectsView', function() { return { restrict: 'E', - controller: function($scope, $routeParams, $location, $window, $rootScope, uiCapabilities) { - const serviceObj = savedObjectManagementRegistry.get($routeParams.service); - const service = serviceObj.service; - const savedObjectsClient = npStart.core.savedObjects.client; - const { overlays } = npStart.core; - - /** - * Creates a field definition and pushes it to the memo stack. This function - * is designed to be used in conjunction with _.reduce(). If the - * values is plain object it will recurse through all the keys till it hits - * a string, number or an array. - * - * @param {array} memo The stack of fields - * @param {mixed} value The value of the field - * @param {string} key The key of the field - * @param {object} collection This is a reference the collection being reduced - * @param {array} parents The parent keys to the field - * @returns {array} - */ - const createField = function(memo, val, key, collection, parents) { - if (Array.isArray(parents)) { - parents.push(key); - } else { - parents = [key]; - } - - const field = { type: 'text', name: parents.join('.'), value: val }; - - if (_.isString(field.value)) { - try { - field.value = angular.toJson(JSON.parse(field.value), true); - field.type = 'json'; - } catch (err) { - field.value = field.value; - } - } else if (isNumeric(field.value)) { - field.type = 'number'; - } else if (Array.isArray(field.value)) { - field.type = 'array'; - field.value = angular.toJson(field.value, true); - } else if (_.isBoolean(field.value)) { - field.type = 'boolean'; - field.value = field.value; - } else if (_.isPlainObject(field.value)) { - // do something recursive - return _.reduce(field.value, _.partialRight(createField, parents), memo); - } - - memo.push(field); - - // once the field is added to the object you need to pop the parents - // to remove it since we've hit the end of the branch. - parents.pop(); - return memo; - }; - - const readObjectClass = function(fields, Class) { - const fieldMap = _.indexBy(fields, 'name'); - - _.forOwn(Class.mapping, function(esType, name) { - if (fieldMap[name]) return; - - fields.push({ - name: name, - type: (function() { - switch (castEsToKbnFieldTypeName(esType)) { - case 'string': - return 'text'; - case 'number': - return 'number'; - case 'boolean': - return 'boolean'; - default: - return 'json'; - } - })(), - }); - }); - - if (Class.searchSource && !fieldMap['kibanaSavedObjectMeta.searchSourceJSON']) { - fields.push({ - name: 'kibanaSavedObjectMeta.searchSourceJSON', - type: 'json', - value: '{}', - }); - } - - if (!fieldMap.references) { - fields.push({ - name: 'references', - type: 'array', - value: '[]', - }); - } - }; - - const { edit: canEdit, delete: canDelete } = uiCapabilities.savedObjectsManagement; - $scope.canEdit = canEdit; - $scope.canDelete = canDelete; - $scope.canViewInApp = canViewInApp(uiCapabilities, service.type); - - $scope.notFound = $routeParams.notFound; - - $scope.title = service.type; - - savedObjectsClient - .get(service.type, $routeParams.id) - .then(function(obj) { - $scope.obj = obj; - $scope.link = service.urlFor(obj.id); - - const fields = _.reduce(obj.attributes, createField, []); - // Special handling for references which isn't within "attributes" - createField(fields, obj.references, 'references'); - - if (service.Class) readObjectClass(fields, service.Class); - - // sorts twice since we want numerical sort to prioritize over name, - // and sortBy will do string comparison if trying to match against strings - const nameSortedFields = _.sortBy(fields, 'name'); - $scope.$evalAsync(() => { - $scope.fields = _.sortBy(nameSortedFields, field => { - const orderIndex = service.Class.fieldOrder - ? service.Class.fieldOrder.indexOf(field.name) - : -1; - return orderIndex > -1 ? orderIndex : Infinity; - }); - }); - $scope.$digest(); - }) - .catch(error => fatalError(error, location)); - - // This handles the validation of the Ace Editor. Since we don't have any - // other hooks into the editors to tell us if the content is valid or not - // we need to use the annotations to see if they have any errors. If they - // do then we push the field.name to aceInvalidEditor variable. - // Otherwise we remove it. - const loadedEditors = []; - $scope.aceInvalidEditors = []; - - $scope.aceLoaded = function(editor) { - if (_.contains(loadedEditors, editor)) return; - loadedEditors.push(editor); - - editor.$blockScrolling = Infinity; - - const session = editor.getSession(); - const fieldName = editor.container.id; - - session.setTabSize(2); - session.setUseSoftTabs(true); - session.on('changeAnnotation', function() { - const annotations = session.getAnnotations(); - if (_.some(annotations, { type: 'error' })) { - if (!_.contains($scope.aceInvalidEditors, fieldName)) { - $scope.aceInvalidEditors.push(fieldName); - } - } else { - $scope.aceInvalidEditors = _.without($scope.aceInvalidEditors, fieldName); - } - - if (!$rootScope.$$phase) $scope.$apply(); - }); - }; - - $scope.cancel = function() { - $window.history.back(); - return false; - }; - - /** - * Deletes an object and sets the notification - * @param {type} name description - * @returns {type} description - */ - $scope.delete = function() { - function doDelete() { - savedObjectsClient - .delete(service.type, $routeParams.id) - .then(function() { - return redirectHandler('deleted'); - }) - .catch(error => fatalError(error, location)); - } - const confirmModalOptions = { - confirmButtonText: i18n.translate( - 'kbn.management.objects.confirmModalOptions.deleteButtonLabel', - { - defaultMessage: 'Delete', - } - ), - title: i18n.translate('kbn.management.objects.confirmModalOptions.modalTitle', { - defaultMessage: 'Delete saved Kibana object?', - }), - }; - - overlays - .openConfirm( - i18n.translate('kbn.management.objects.confirmModalOptions.modalDescription', { - defaultMessage: "You can't recover deleted objects", - }), - confirmModalOptions - ) - .then(isConfirmed => { - if (isConfirmed) { - doDelete(); - } - }); - }; - - $scope.submit = function() { - const source = _.cloneDeep($scope.obj.attributes); - - _.each($scope.fields, function(field) { - let value = field.value; - - if (field.type === 'number') { - value = Number(field.value); - } - - if (field.type === 'array') { - value = JSON.parse(field.value); - } - - _.set(source, field.name, value); - }); - - const { references, ...attributes } = source; - - savedObjectsClient - .update(service.type, $routeParams.id, attributes, { references }) - .then(function() { - return redirectHandler('updated'); - }) - .catch(error => fatalError(error, location)); - }; - - function redirectHandler(action) { - $location.path('/management/kibana/objects').search({ - _a: rison.encode({ - tab: serviceObj.title, - }), - }); - - toastNotifications.addSuccess( - `${_.capitalize(action)} '${ - $scope.obj.attributes.title - }' ${$scope.title.toLowerCase()} object` - ); - } + controller: function($scope, $routeParams) { + createReactView($scope, $routeParams); + $scope.$on('$destroy', destroyReactView); }, }; }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap new file mode 100644 index 00000000000000..7e1f7ea12b0147 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap @@ -0,0 +1,165 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Intro component renders correctly 1`] = ` +
+ +
+ +
+ +

+ + Edit search + +

+
+
+
+ +
+ +
+ +
+ + + + + + +
+ + + +
+
+
+ +
+ +
+ +
+`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/intro.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/intro.test.tsx.snap new file mode 100644 index 00000000000000..812031b4b363c7 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/intro.test.tsx.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Intro component renders correctly 1`] = ` + + + } + > +
+
+
+ + +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap new file mode 100644 index 00000000000000..ac565a000813e0 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap @@ -0,0 +1,301 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NotFoundErrors component renders correctly for index-pattern type 1`] = ` + + + } + > +
+
+
+ + +`; + +exports[`NotFoundErrors component renders correctly for index-pattern-field type 1`] = ` + + + } + > +
+
+
+ + +`; + +exports[`NotFoundErrors component renders correctly for search type 1`] = ` + + + } + > +
+
+
+ + +`; + +exports[`NotFoundErrors component renders correctly for unknown type 1`] = ` + + + } + > +
+
+