From 746e236869927812c0d66906fe1abecb63e9ca36 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 16 Mar 2020 10:44:28 +0100 Subject: [PATCH 001/115] [SIEM] Adds 'Closes and opens signals' Cypress test (#59950) * adds signals data * adds 'closes and opens signals' * refactors test * adds extra check to see that the selected number of signals is correct Co-authored-by: Elastic Machine --- .../cypress/integration/detections.spec.ts | 114 + .../siem/cypress/screens/detections.ts | 16 + .../plugins/siem/cypress/tasks/detections.ts | 46 +- .../plugins/siem/cypress/tasks/es_archiver.ts | 8 + .../utility_bar/utility_bar_action.tsx | 16 +- .../utility_bar/utility_bar_text.tsx | 5 +- .../signals/signals_filter_group/index.tsx | 2 + .../signals/signals_utility_bar/index.tsx | 7 +- .../es_archives/signals/data.json.gz | Bin 0 -> 58602 bytes .../es_archives/signals/mappings.json | 7602 +++++++++++++++++ 10 files changed, 7809 insertions(+), 7 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts create mode 100644 x-pack/test/siem_cypress/es_archives/signals/data.json.gz create mode 100644 x-pack/test/siem_cypress/es_archives/signals/mappings.json diff --git a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts new file mode 100644 index 000000000000..1624586d4ca1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts @@ -0,0 +1,114 @@ +/* + * 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 { + NUMBER_OF_SIGNALS, + SELECTED_SIGNALS, + SHOWING_SIGNALS, + SIGNALS, +} from '../screens/detections'; + +import { + closeSignals, + goToClosedSignals, + goToOpenedSignals, + openSignals, + selectNumberOfSignals, + waitForSignalsPanelToBeLoaded, + waitForSignals, + waitForSignalsToBeLoaded, +} from '../tasks/detections'; +import { esArchiverLoad } from '../tasks/es_archiver'; +import { loginAndWaitForPage } from '../tasks/login'; + +import { DETECTIONS } from '../urls/navigation'; + +describe('Detections', () => { + before(() => { + esArchiverLoad('signals'); + loginAndWaitForPage(DETECTIONS); + }); + + it('Closes and opens signals', () => { + waitForSignalsPanelToBeLoaded(); + waitForSignalsToBeLoaded(); + + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .then(numberOfSignals => { + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${numberOfSignals} signals`); + + const numberOfSignalsToBeClosed = 3; + selectNumberOfSignals(numberOfSignalsToBeClosed); + + cy.get(SELECTED_SIGNALS) + .invoke('text') + .should('eql', `Selected ${numberOfSignalsToBeClosed} signals`); + + closeSignals(); + waitForSignals(); + cy.reload(); + waitForSignals(); + + const expectedNumberOfSignalsAfterClosing = +numberOfSignals - numberOfSignalsToBeClosed; + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eq', expectedNumberOfSignalsAfterClosing.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${expectedNumberOfSignalsAfterClosing.toString()} signals`); + + goToClosedSignals(); + waitForSignals(); + + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eql', numberOfSignalsToBeClosed.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${numberOfSignalsToBeClosed.toString()} signals`); + cy.get(SIGNALS).should('have.length', numberOfSignalsToBeClosed); + + const numberOfSignalsToBeOpened = 1; + selectNumberOfSignals(numberOfSignalsToBeOpened); + + cy.get(SELECTED_SIGNALS) + .invoke('text') + .should('eql', `Selected ${numberOfSignalsToBeOpened} signal`); + + openSignals(); + waitForSignals(); + cy.reload(); + waitForSignalsToBeLoaded(); + waitForSignals(); + goToClosedSignals(); + waitForSignals(); + + const expectedNumberOfClosedSignalsAfterOpened = 2; + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eql', expectedNumberOfClosedSignalsAfterOpened.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${expectedNumberOfClosedSignalsAfterOpened.toString()} signals`); + cy.get(SIGNALS).should('have.length', expectedNumberOfClosedSignalsAfterOpened); + + goToOpenedSignals(); + waitForSignals(); + + const expectedNumberOfOpenedSignals = + +numberOfSignals - expectedNumberOfClosedSignalsAfterOpened; + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${expectedNumberOfOpenedSignals.toString()} signals`); + + cy.get('[data-test-subj="server-side-event-count"]') + .invoke('text') + .should('eql', expectedNumberOfOpenedSignals.toString()); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts index 8089b028a10d..8b5ba2357880 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts @@ -4,6 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +export const CLOSED_SIGNALS_BTN = '[data-test-subj="closedSignals"]'; + export const LOADING_SIGNALS_PANEL = '[data-test-subj="loading-signals-panel"]'; export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal-detection-rules"]'; + +export const NUMBER_OF_SIGNALS = '[data-test-subj="server-side-event-count"]'; + +export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] .siemLinkIcon__label'; + +export const OPENED_SIGNALS_BTN = '[data-test-subj="openSignals"]'; + +export const SELECTED_SIGNALS = '[data-test-subj="selectedSignals"]'; + +export const SHOWING_SIGNALS = '[data-test-subj="showingSignals"]'; + +export const SIGNALS = '[data-test-subj="event"]'; + +export const SIGNAL_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts index 4a0a565a74e2..21a0c136b90d 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts @@ -4,7 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LOADING_SIGNALS_PANEL, MANAGE_SIGNAL_DETECTION_RULES_BTN } from '../screens/detections'; +import { + CLOSED_SIGNALS_BTN, + LOADING_SIGNALS_PANEL, + MANAGE_SIGNAL_DETECTION_RULES_BTN, + OPEN_CLOSE_SIGNALS_BTN, + OPENED_SIGNALS_BTN, + SIGNALS, + SIGNAL_CHECKBOX, +} from '../screens/detections'; +import { REFRESH_BUTTON } from '../screens/siem_header'; + +export const closeSignals = () => { + cy.get(OPEN_CLOSE_SIGNALS_BTN).click({ force: true }); +}; + +export const goToClosedSignals = () => { + cy.get(CLOSED_SIGNALS_BTN).click({ force: true }); +}; export const goToManageSignalDetectionRules = () => { cy.get(MANAGE_SIGNAL_DETECTION_RULES_BTN) @@ -12,6 +29,28 @@ export const goToManageSignalDetectionRules = () => { .click({ force: true }); }; +export const goToOpenedSignals = () => { + cy.get(OPENED_SIGNALS_BTN).click({ force: true }); +}; + +export const openSignals = () => { + cy.get(OPEN_CLOSE_SIGNALS_BTN).click({ force: true }); +}; + +export const selectNumberOfSignals = (numberOfSignals: number) => { + for (let i = 0; i < numberOfSignals; i++) { + cy.get(SIGNAL_CHECKBOX) + .eq(i) + .click({ force: true }); + } +}; + +export const waitForSignals = () => { + cy.get(REFRESH_BUTTON) + .invoke('text') + .should('not.equal', 'Updating'); +}; + export const waitForSignalsIndexToBeCreated = () => { cy.request({ url: '/api/detection_engine/index', retryOnStatusCodeFailure: true }).then( response => { @@ -26,3 +65,8 @@ export const waitForSignalsPanelToBeLoaded = () => { cy.get(LOADING_SIGNALS_PANEL).should('exist'); cy.get(LOADING_SIGNALS_PANEL).should('not.exist'); }; + +export const waitForSignalsToBeLoaded = () => { + const expectedNumberOfDisplayedSignals = 25; + cy.get(SIGNALS).should('have.length', expectedNumberOfDisplayedSignals); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/es_archiver.ts b/x-pack/legacy/plugins/siem/cypress/tasks/es_archiver.ts index 72c95cba2361..1743fcb56106 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/es_archiver.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/es_archiver.ts @@ -12,6 +12,14 @@ export const esArchiverLoadEmptyKibana = () => { ); }; +export const esArchiverLoad = (folder: string) => { + cy.exec( + `node ../../../../scripts/es_archiver load ${folder} --dir ../../../test/siem_cypress/es_archives --config ../../../../test/functional/config.js --es-url ${Cypress.env( + 'ELASTICSEARCH_URL' + )} --kibana-url ${Cypress.config().baseUrl}` + ); +}; + export const esArchiverUnloadEmptyKibana = () => { cy.exec( `node ../../../../scripts/es_archiver empty_kibana unload empty--dir ../../../test/siem_cypress/es_archives --config ../../../../test/functional/config.js --es-url ${Cypress.env( diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx index d3e2be0e8f81..19e884e32639 100644 --- a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx @@ -42,11 +42,23 @@ Popover.displayName = 'Popover'; export interface UtilityBarActionProps extends LinkIconProps { popoverContent?: (closePopover: () => void) => React.ReactNode; + dataTestSubj?: string; } export const UtilityBarAction = React.memo( - ({ children, color, disabled, href, iconSide, iconSize, iconType, onClick, popoverContent }) => ( - + ({ + children, + color, + dataTestSubj, + disabled, + href, + iconSide, + iconSize, + iconType, + onClick, + popoverContent, + }) => ( + {popoverContent ? ( (({ children }) => ( - {children} +export const UtilityBarText = React.memo(({ children, dataTestSubj }) => ( + {children} )); UtilityBarText.displayName = 'UtilityBarText'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx index 3c1317d463f8..a8dd22863e3c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx @@ -32,6 +32,7 @@ const SignalsTableFilterGroupComponent: React.FC = ({ onFilterGroupChange return ( = ({ onFilterGroupChange diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx index 25c0424cadf1..2000a699ab18 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -65,13 +65,15 @@ const SignalsUtilityBarComponent: React.FC = ({ - {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} + + {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} + {canUserCRUD && hasIndexWrite && ( <> - + {i18n.SELECTED_SIGNALS( showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, showClearSelection ? totalCount : Object.keys(selectedEventIds).length @@ -79,6 +81,7 @@ const SignalsUtilityBarComponent: React.FC = ({ +#yKt5Ih8T7#Ii;oPprM-zGWd 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

<@<3bF$Vx!=P?#9{ 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 literal 0 HcmV?d00001 diff --git a/x-pack/test/siem_cypress/es_archives/signals/mappings.json b/x-pack/test/siem_cypress/es_archives/signals/mappings.json new file mode 100644 index 000000000000..114faa0dae33 --- /dev/null +++ b/x-pack/test/siem_cypress/es_archives/signals/mappings.json @@ -0,0 +1,7602 @@ +{ + "type": "index", + "value": { + "aliases": { + ".siem-signals-default": { + "is_write_index": true + } + }, + "index": ".siem-signals-default-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "doc_values": false, + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "observer": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "signal": { + "properties": { + "ancestors": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_event": { + "properties": { + "action": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + }, + "module": { + "type": "keyword" + }, + "original": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "outcome": { + "type": "keyword" + }, + "provider": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_time": { + "type": "date" + }, + "parent": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "false_positives": { + "type": "keyword" + }, + "filters": { + "type": "object" + }, + "from": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "immutable": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "language": { + "type": "keyword" + }, + "max_signals": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "output_index": { + "type": "keyword" + }, + "query": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "risk_score": { + "type": "keyword" + }, + "rule_id": { + "type": "keyword" + }, + "saved_id": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "size": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + } + } + }, + "timeline_id": { + "type": "keyword" + }, + "timeline_title": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": ".siem-signals-default", + "rollover_alias": ".siem-signals-default" + }, + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + "auditbeat-7.6.0": { + "is_write_index": true + } + }, + "index": "auditbeat-7.6.0-2020.03.11-000001", + "mappings": { + "_meta": { + "beat": "auditbeat", + "version": "7.6.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "dns.answers": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "dns.answers.*" + } + }, + { + "log.syslog": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "log.syslog.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "kubernetes.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.labels.*" + } + }, + { + "kubernetes.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.annotations.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "auditd": { + "properties": { + "data": { + "properties": { + "a0": { + "ignore_above": 1024, + "type": "keyword" + }, + "a1": { + "ignore_above": 1024, + "type": "keyword" + }, + "a2": { + "ignore_above": 1024, + "type": "keyword" + }, + "a3": { + "ignore_above": 1024, + "type": "keyword" + }, + "a[0-3]": { + "ignore_above": 1024, + "type": "keyword" + }, + "acct": { + "ignore_above": 1024, + "type": "keyword" + }, + "acl": { + "ignore_above": 1024, + "type": "keyword" + }, + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "added": { + "ignore_above": 1024, + "type": "keyword" + }, + "addr": { + "ignore_above": 1024, + "type": "keyword" + }, + "apparmor": { + "ignore_above": 1024, + "type": "keyword" + }, + "arch": { + "ignore_above": 1024, + "type": "keyword" + }, + "argc": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_backlog_limit": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_backlog_wait_time": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_failure": { + "ignore_above": 1024, + "type": "keyword" + }, + "banners": { + "ignore_above": 1024, + "type": "keyword" + }, + "bool": { + "ignore_above": 1024, + "type": "keyword" + }, + "bus": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fe": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fi": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fp": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fver": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "capability": { + "ignore_above": 1024, + "type": "keyword" + }, + "cgroup": { + "ignore_above": 1024, + "type": "keyword" + }, + "changed": { + "ignore_above": 1024, + "type": "keyword" + }, + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "cmd": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "compat": { + "ignore_above": 1024, + "type": "keyword" + }, + "daddr": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "default-context": { + "ignore_above": 1024, + "type": "keyword" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "dir": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "dmac": { + "ignore_above": 1024, + "type": "keyword" + }, + "dport": { + "ignore_above": 1024, + "type": "keyword" + }, + "enforcing": { + "ignore_above": 1024, + "type": "keyword" + }, + "entries": { + "ignore_above": 1024, + "type": "keyword" + }, + "exit": { + "ignore_above": 1024, + "type": "keyword" + }, + "fam": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "fd": { + "ignore_above": 1024, + "type": "keyword" + }, + "fe": { + "ignore_above": 1024, + "type": "keyword" + }, + "feature": { + "ignore_above": 1024, + "type": "keyword" + }, + "fi": { + "ignore_above": 1024, + "type": "keyword" + }, + "file": { + "ignore_above": 1024, + "type": "keyword" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "format": { + "ignore_above": 1024, + "type": "keyword" + }, + "fp": { + "ignore_above": 1024, + "type": "keyword" + }, + "fver": { + "ignore_above": 1024, + "type": "keyword" + }, + "grantors": { + "ignore_above": 1024, + "type": "keyword" + }, + "grp": { + "ignore_above": 1024, + "type": "keyword" + }, + "hook": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "icmp_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "igid": { + "ignore_above": 1024, + "type": "keyword" + }, + "img-ctx": { + "ignore_above": 1024, + "type": "keyword" + }, + "inif": { + "ignore_above": 1024, + "type": "keyword" + }, + "ino": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode_gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode_uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "invalid_context": { + "ignore_above": 1024, + "type": "keyword" + }, + "ioctlcmd": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "ignore_above": 1024, + "type": "keyword" + }, + "ipid": { + "ignore_above": 1024, + "type": "keyword" + }, + "ipx-net": { + "ignore_above": 1024, + "type": "keyword" + }, + "items": { + "ignore_above": 1024, + "type": "keyword" + }, + "iuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "ksize": { + "ignore_above": 1024, + "type": "keyword" + }, + "laddr": { + "ignore_above": 1024, + "type": "keyword" + }, + "len": { + "ignore_above": 1024, + "type": "keyword" + }, + "list": { + "ignore_above": 1024, + "type": "keyword" + }, + "lport": { + "ignore_above": 1024, + "type": "keyword" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "macproto": { + "ignore_above": 1024, + "type": "keyword" + }, + "maj": { + "ignore_above": 1024, + "type": "keyword" + }, + "major": { + "ignore_above": 1024, + "type": "keyword" + }, + "minor": { + "ignore_above": 1024, + "type": "keyword" + }, + "model": { + "ignore_above": 1024, + "type": "keyword" + }, + "msg": { + "ignore_above": 1024, + "type": "keyword" + }, + "nargs": { + "ignore_above": 1024, + "type": "keyword" + }, + "net": { + "ignore_above": 1024, + "type": "keyword" + }, + "new": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-chardev": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-disk": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-fs": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-level": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-log_passwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-mem": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-net": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-range": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-rng": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-role": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-seuser": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-vcpu": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_lock": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "nlnk-fam": { + "ignore_above": 1024, + "type": "keyword" + }, + "nlnk-grp": { + "ignore_above": 1024, + "type": "keyword" + }, + "nlnk-pid": { + "ignore_above": 1024, + "type": "keyword" + }, + "oauid": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "ocomm": { + "ignore_above": 1024, + "type": "keyword" + }, + "oflag": { + "ignore_above": 1024, + "type": "keyword" + }, + "old": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-auid": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-chardev": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-disk": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-fs": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-level": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-log_passwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-mem": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-net": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-range": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-rng": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-role": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-ses": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-seuser": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-vcpu": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_enforcing": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_lock": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_prom": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_val": { + "ignore_above": 1024, + "type": "keyword" + }, + "op": { + "ignore_above": 1024, + "type": "keyword" + }, + "opid": { + "ignore_above": 1024, + "type": "keyword" + }, + "oses": { + "ignore_above": 1024, + "type": "keyword" + }, + "outif": { + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "ignore_above": 1024, + "type": "keyword" + }, + "per": { + "ignore_above": 1024, + "type": "keyword" + }, + "perm": { + "ignore_above": 1024, + "type": "keyword" + }, + "perm_mask": { + "ignore_above": 1024, + "type": "keyword" + }, + "permissive": { + "ignore_above": 1024, + "type": "keyword" + }, + "pfs": { + "ignore_above": 1024, + "type": "keyword" + }, + "printer": { + "ignore_above": 1024, + "type": "keyword" + }, + "prom": { + "ignore_above": 1024, + "type": "keyword" + }, + "proto": { + "ignore_above": 1024, + "type": "keyword" + }, + "qbytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "range": { + "ignore_above": 1024, + "type": "keyword" + }, + "reason": { + "ignore_above": 1024, + "type": "keyword" + }, + "removed": { + "ignore_above": 1024, + "type": "keyword" + }, + "res": { + "ignore_above": 1024, + "type": "keyword" + }, + "resrc": { + "ignore_above": 1024, + "type": "keyword" + }, + "rport": { + "ignore_above": 1024, + "type": "keyword" + }, + "sauid": { + "ignore_above": 1024, + "type": "keyword" + }, + "scontext": { + "ignore_above": 1024, + "type": "keyword" + }, + "selected-context": { + "ignore_above": 1024, + "type": "keyword" + }, + "seperm": { + "ignore_above": 1024, + "type": "keyword" + }, + "seperms": { + "ignore_above": 1024, + "type": "keyword" + }, + "seqno": { + "ignore_above": 1024, + "type": "keyword" + }, + "seresult": { + "ignore_above": 1024, + "type": "keyword" + }, + "ses": { + "ignore_above": 1024, + "type": "keyword" + }, + "seuser": { + "ignore_above": 1024, + "type": "keyword" + }, + "sig": { + "ignore_above": 1024, + "type": "keyword" + }, + "sigev_signo": { + "ignore_above": 1024, + "type": "keyword" + }, + "smac": { + "ignore_above": 1024, + "type": "keyword" + }, + "socket": { + "properties": { + "addr": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "ignore_above": 1024, + "type": "keyword" + }, + "saddr": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "spid": { + "ignore_above": 1024, + "type": "keyword" + }, + "sport": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "subj": { + "ignore_above": 1024, + "type": "keyword" + }, + "success": { + "ignore_above": 1024, + "type": "keyword" + }, + "syscall": { + "ignore_above": 1024, + "type": "keyword" + }, + "table": { + "ignore_above": 1024, + "type": "keyword" + }, + "tclass": { + "ignore_above": 1024, + "type": "keyword" + }, + "tcontext": { + "ignore_above": 1024, + "type": "keyword" + }, + "terminal": { + "ignore_above": 1024, + "type": "keyword" + }, + "tty": { + "ignore_above": 1024, + "type": "keyword" + }, + "unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "uri": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "val": { + "ignore_above": 1024, + "type": "keyword" + }, + "ver": { + "ignore_above": 1024, + "type": "keyword" + }, + "virt": { + "ignore_above": 1024, + "type": "keyword" + }, + "vm": { + "ignore_above": 1024, + "type": "keyword" + }, + "vm-ctx": { + "ignore_above": 1024, + "type": "keyword" + }, + "vm-pid": { + "ignore_above": 1024, + "type": "keyword" + }, + "watch": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "paths": { + "properties": { + "dev": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "item": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "nametype": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_role": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_user": { + "ignore_above": 1024, + "type": "keyword" + }, + "objtype": { + "ignore_above": 1024, + "type": "keyword" + }, + "ogid": { + "ignore_above": 1024, + "type": "keyword" + }, + "ouid": { + "ignore_above": 1024, + "type": "keyword" + }, + "rdev": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "session": { + "ignore_above": 1024, + "type": "keyword" + }, + "summary": { + "properties": { + "actor": { + "properties": { + "primary": { + "ignore_above": 1024, + "type": "keyword" + }, + "secondary": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "how": { + "ignore_above": 1024, + "type": "keyword" + }, + "object": { + "properties": { + "primary": { + "ignore_above": 1024, + "type": "keyword" + }, + "secondary": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "docker": { + "properties": { + "container": { + "properties": { + "labels": { + "type": "object" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "fields": { + "type": "object" + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "fields": { + "raw": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "selinux": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "role": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "setgid": { + "type": "boolean" + }, + "setuid": { + "type": "boolean" + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geoip": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "blake2b_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "blake2b_384": { + "ignore_above": 1024, + "type": "keyword" + }, + "blake2b_512": { + "ignore_above": 1024, + "type": "keyword" + }, + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha384": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_384": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_512": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512_224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "xxh64": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "containerized": { + "type": "boolean" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "build": { + "ignore_above": 1024, + "type": "keyword" + }, + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "jolokia": { + "properties": { + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "secured": { + "type": "boolean" + }, + "server": { + "properties": { + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "kubernetes": { + "properties": { + "annotations": { + "properties": { + "*": { + "type": "object" + } + } + }, + "container": { + "properties": { + "image": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "properties": { + "*": { + "type": "object" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "statefulset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "observer": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "blake2b_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "blake2b_384": { + "ignore_above": 1024, + "type": "keyword" + }, + "blake2b_512": { + "ignore_above": 1024, + "type": "keyword" + }, + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha384": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_384": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_512": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512_224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "xxh64": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "socket": { + "properties": { + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "system": { + "properties": { + "audit": { + "properties": { + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "boottime": { + "type": "date" + }, + "containerized": { + "type": "boolean" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "timezone": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "offset": { + "properties": { + "sec": { + "type": "long" + } + } + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "package": { + "properties": { + "arch": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "installtime": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "release": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "summary": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "dir": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "properties": { + "last_changed": { + "type": "date" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "shell": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "user_information": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "timeseries": { + "properties": { + "instance": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tracing": { + "properties": { + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "audit": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "effective": { + "properties": { + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "filesystem": { + "properties": { + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "name_map": { + "type": "object" + }, + "saved": { + "properties": { + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "selinux": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "role": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "terminal": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": "auditbeat", + "rollover_alias": "auditbeat-7.6.0" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "1", + "query": { + "default_field": [ + "message", + "tags", + "agent.ephemeral_id", + "agent.id", + "agent.name", + "agent.type", + "agent.version", + "as.organization.name", + "client.address", + "client.as.organization.name", + "client.domain", + "client.geo.city_name", + "client.geo.continent_name", + "client.geo.country_iso_code", + "client.geo.country_name", + "client.geo.name", + "client.geo.region_iso_code", + "client.geo.region_name", + "client.mac", + "client.registered_domain", + "client.top_level_domain", + "client.user.domain", + "client.user.email", + "client.user.full_name", + "client.user.group.domain", + "client.user.group.id", + "client.user.group.name", + "client.user.hash", + "client.user.id", + "client.user.name", + "cloud.account.id", + "cloud.availability_zone", + "cloud.instance.id", + "cloud.instance.name", + "cloud.machine.type", + "cloud.provider", + "cloud.region", + "container.id", + "container.image.name", + "container.image.tag", + "container.name", + "container.runtime", + "destination.address", + "destination.as.organization.name", + "destination.domain", + "destination.geo.city_name", + "destination.geo.continent_name", + "destination.geo.country_iso_code", + "destination.geo.country_name", + "destination.geo.name", + "destination.geo.region_iso_code", + "destination.geo.region_name", + "destination.mac", + "destination.registered_domain", + "destination.top_level_domain", + "destination.user.domain", + "destination.user.email", + "destination.user.full_name", + "destination.user.group.domain", + "destination.user.group.id", + "destination.user.group.name", + "destination.user.hash", + "destination.user.id", + "destination.user.name", + "dns.answers.class", + "dns.answers.data", + "dns.answers.name", + "dns.answers.type", + "dns.header_flags", + "dns.id", + "dns.op_code", + "dns.question.class", + "dns.question.name", + "dns.question.registered_domain", + "dns.question.subdomain", + "dns.question.top_level_domain", + "dns.question.type", + "dns.response_code", + "dns.type", + "ecs.version", + "error.code", + "error.id", + "error.message", + "error.stack_trace", + "error.type", + "event.action", + "event.category", + "event.code", + "event.dataset", + "event.hash", + "event.id", + "event.kind", + "event.module", + "event.original", + "event.outcome", + "event.provider", + "event.timezone", + "event.type", + "file.device", + "file.directory", + "file.extension", + "file.gid", + "file.group", + "file.hash.md5", + "file.hash.sha1", + "file.hash.sha256", + "file.hash.sha512", + "file.inode", + "file.mode", + "file.name", + "file.owner", + "file.path", + "file.target_path", + "file.type", + "file.uid", + "geo.city_name", + "geo.continent_name", + "geo.country_iso_code", + "geo.country_name", + "geo.name", + "geo.region_iso_code", + "geo.region_name", + "group.domain", + "group.id", + "group.name", + "hash.md5", + "hash.sha1", + "hash.sha256", + "hash.sha512", + "host.architecture", + "host.geo.city_name", + "host.geo.continent_name", + "host.geo.country_iso_code", + "host.geo.country_name", + "host.geo.name", + "host.geo.region_iso_code", + "host.geo.region_name", + "host.hostname", + "host.id", + "host.mac", + "host.name", + "host.os.family", + "host.os.full", + "host.os.kernel", + "host.os.name", + "host.os.platform", + "host.os.version", + "host.type", + "host.user.domain", + "host.user.email", + "host.user.full_name", + "host.user.group.domain", + "host.user.group.id", + "host.user.group.name", + "host.user.hash", + "host.user.id", + "host.user.name", + "http.request.body.content", + "http.request.method", + "http.request.referrer", + "http.response.body.content", + "http.version", + "log.level", + "log.logger", + "log.origin.file.name", + "log.origin.function", + "log.original", + "log.syslog.facility.name", + "log.syslog.severity.name", + "network.application", + "network.community_id", + "network.direction", + "network.iana_number", + "network.name", + "network.protocol", + "network.transport", + "network.type", + "observer.geo.city_name", + "observer.geo.continent_name", + "observer.geo.country_iso_code", + "observer.geo.country_name", + "observer.geo.name", + "observer.geo.region_iso_code", + "observer.geo.region_name", + "observer.hostname", + "observer.mac", + "observer.name", + "observer.os.family", + "observer.os.full", + "observer.os.kernel", + "observer.os.name", + "observer.os.platform", + "observer.os.version", + "observer.product", + "observer.serial_number", + "observer.type", + "observer.vendor", + "observer.version", + "organization.id", + "organization.name", + "os.family", + "os.full", + "os.kernel", + "os.name", + "os.platform", + "os.version", + "package.architecture", + "package.checksum", + "package.description", + "package.install_scope", + "package.license", + "package.name", + "package.path", + "package.version", + "process.args", + "text", + "process.executable", + "process.hash.md5", + "process.hash.sha1", + "process.hash.sha256", + "process.hash.sha512", + "process.name", + "text", + "text", + "text", + "text", + "text", + "process.thread.name", + "process.title", + "process.working_directory", + "server.address", + "server.as.organization.name", + "server.domain", + "server.geo.city_name", + "server.geo.continent_name", + "server.geo.country_iso_code", + "server.geo.country_name", + "server.geo.name", + "server.geo.region_iso_code", + "server.geo.region_name", + "server.mac", + "server.registered_domain", + "server.top_level_domain", + "server.user.domain", + "server.user.email", + "server.user.full_name", + "server.user.group.domain", + "server.user.group.id", + "server.user.group.name", + "server.user.hash", + "server.user.id", + "server.user.name", + "service.ephemeral_id", + "service.id", + "service.name", + "service.node.name", + "service.state", + "service.type", + "service.version", + "source.address", + "source.as.organization.name", + "source.domain", + "source.geo.city_name", + "source.geo.continent_name", + "source.geo.country_iso_code", + "source.geo.country_name", + "source.geo.name", + "source.geo.region_iso_code", + "source.geo.region_name", + "source.mac", + "source.registered_domain", + "source.top_level_domain", + "source.user.domain", + "source.user.email", + "source.user.full_name", + "source.user.group.domain", + "source.user.group.id", + "source.user.group.name", + "source.user.hash", + "source.user.id", + "source.user.name", + "threat.framework", + "threat.tactic.id", + "threat.tactic.name", + "threat.tactic.reference", + "threat.technique.id", + "threat.technique.name", + "threat.technique.reference", + "tracing.trace.id", + "tracing.transaction.id", + "url.domain", + "url.extension", + "url.fragment", + "url.full", + "url.original", + "url.password", + "url.path", + "url.query", + "url.registered_domain", + "url.scheme", + "url.top_level_domain", + "url.username", + "user.domain", + "user.email", + "user.full_name", + "user.group.domain", + "user.group.id", + "user.group.name", + "user.hash", + "user.id", + "user.name", + "user_agent.device.name", + "user_agent.name", + "text", + "user_agent.original", + "user_agent.os.family", + "user_agent.os.full", + "user_agent.os.kernel", + "user_agent.os.name", + "user_agent.os.platform", + "user_agent.os.version", + "user_agent.version", + "text", + "agent.hostname", + "timeseries.instance", + "cloud.project.id", + "cloud.image.id", + "host.os.build", + "host.os.codename", + "kubernetes.pod.name", + "kubernetes.pod.uid", + "kubernetes.namespace", + "kubernetes.node.name", + "kubernetes.replicaset.name", + "kubernetes.deployment.name", + "kubernetes.statefulset.name", + "kubernetes.container.name", + "kubernetes.container.image", + "jolokia.agent.version", + "jolokia.agent.id", + "jolokia.server.product", + "jolokia.server.version", + "jolokia.server.vendor", + "jolokia.url", + "raw", + "file.origin", + "file.selinux.user", + "file.selinux.role", + "file.selinux.domain", + "file.selinux.level", + "user.audit.id", + "user.audit.name", + "user.effective.id", + "user.effective.name", + "user.effective.group.id", + "user.effective.group.name", + "user.filesystem.id", + "user.filesystem.name", + "user.filesystem.group.id", + "user.filesystem.group.name", + "user.saved.id", + "user.saved.name", + "user.saved.group.id", + "user.saved.group.name", + "user.selinux.user", + "user.selinux.role", + "user.selinux.domain", + "user.selinux.level", + "user.selinux.category", + "source.path", + "destination.path", + "auditd.message_type", + "auditd.session", + "auditd.result", + "auditd.summary.actor.primary", + "auditd.summary.actor.secondary", + "auditd.summary.object.type", + "auditd.summary.object.primary", + "auditd.summary.object.secondary", + "auditd.summary.how", + "auditd.paths.inode", + "auditd.paths.dev", + "auditd.paths.obj_user", + "auditd.paths.obj_role", + "auditd.paths.obj_domain", + "auditd.paths.obj_level", + "auditd.paths.objtype", + "auditd.paths.ouid", + "auditd.paths.rdev", + "auditd.paths.nametype", + "auditd.paths.ogid", + "auditd.paths.item", + "auditd.paths.mode", + "auditd.paths.name", + "auditd.data.action", + "auditd.data.minor", + "auditd.data.acct", + "auditd.data.addr", + "auditd.data.cipher", + "auditd.data.id", + "auditd.data.entries", + "auditd.data.kind", + "auditd.data.ksize", + "auditd.data.spid", + "auditd.data.arch", + "auditd.data.argc", + "auditd.data.major", + "auditd.data.unit", + "auditd.data.table", + "auditd.data.terminal", + "auditd.data.grantors", + "auditd.data.direction", + "auditd.data.op", + "auditd.data.tty", + "auditd.data.syscall", + "auditd.data.data", + "auditd.data.family", + "auditd.data.mac", + "auditd.data.pfs", + "auditd.data.items", + "auditd.data.a0", + "auditd.data.a1", + "auditd.data.a2", + "auditd.data.a3", + "auditd.data.hostname", + "auditd.data.lport", + "auditd.data.rport", + "auditd.data.exit", + "auditd.data.fp", + "auditd.data.laddr", + "auditd.data.sport", + "auditd.data.capability", + "auditd.data.nargs", + "auditd.data.new-enabled", + "auditd.data.audit_backlog_limit", + "auditd.data.dir", + "auditd.data.cap_pe", + "auditd.data.model", + "auditd.data.new_pp", + "auditd.data.old-enabled", + "auditd.data.oauid", + "auditd.data.old", + "auditd.data.banners", + "auditd.data.feature", + "auditd.data.vm-ctx", + "auditd.data.opid", + "auditd.data.seperms", + "auditd.data.seresult", + "auditd.data.new-rng", + "auditd.data.old-net", + "auditd.data.sigev_signo", + "auditd.data.ino", + "auditd.data.old_enforcing", + "auditd.data.old-vcpu", + "auditd.data.range", + "auditd.data.res", + "auditd.data.added", + "auditd.data.fam", + "auditd.data.nlnk-pid", + "auditd.data.subj", + "auditd.data.a[0-3]", + "auditd.data.cgroup", + "auditd.data.kernel", + "auditd.data.ocomm", + "auditd.data.new-net", + "auditd.data.permissive", + "auditd.data.class", + "auditd.data.compat", + "auditd.data.fi", + "auditd.data.changed", + "auditd.data.msg", + "auditd.data.dport", + "auditd.data.new-seuser", + "auditd.data.invalid_context", + "auditd.data.dmac", + "auditd.data.ipx-net", + "auditd.data.iuid", + "auditd.data.macproto", + "auditd.data.obj", + "auditd.data.ipid", + "auditd.data.new-fs", + "auditd.data.vm-pid", + "auditd.data.cap_pi", + "auditd.data.old-auid", + "auditd.data.oses", + "auditd.data.fd", + "auditd.data.igid", + "auditd.data.new-disk", + "auditd.data.parent", + "auditd.data.len", + "auditd.data.oflag", + "auditd.data.uuid", + "auditd.data.code", + "auditd.data.nlnk-grp", + "auditd.data.cap_fp", + "auditd.data.new-mem", + "auditd.data.seperm", + "auditd.data.enforcing", + "auditd.data.new-chardev", + "auditd.data.old-rng", + "auditd.data.outif", + "auditd.data.cmd", + "auditd.data.hook", + "auditd.data.new-level", + "auditd.data.sauid", + "auditd.data.sig", + "auditd.data.audit_backlog_wait_time", + "auditd.data.printer", + "auditd.data.old-mem", + "auditd.data.perm", + "auditd.data.old_pi", + "auditd.data.state", + "auditd.data.format", + "auditd.data.new_gid", + "auditd.data.tcontext", + "auditd.data.maj", + "auditd.data.watch", + "auditd.data.device", + "auditd.data.grp", + "auditd.data.bool", + "auditd.data.icmp_type", + "auditd.data.new_lock", + "auditd.data.old_prom", + "auditd.data.acl", + "auditd.data.ip", + "auditd.data.new_pi", + "auditd.data.default-context", + "auditd.data.inode_gid", + "auditd.data.new-log_passwd", + "auditd.data.new_pe", + "auditd.data.selected-context", + "auditd.data.cap_fver", + "auditd.data.file", + "auditd.data.net", + "auditd.data.virt", + "auditd.data.cap_pp", + "auditd.data.old-range", + "auditd.data.resrc", + "auditd.data.new-range", + "auditd.data.obj_gid", + "auditd.data.proto", + "auditd.data.old-disk", + "auditd.data.audit_failure", + "auditd.data.inif", + "auditd.data.vm", + "auditd.data.flags", + "auditd.data.nlnk-fam", + "auditd.data.old-fs", + "auditd.data.old-ses", + "auditd.data.seqno", + "auditd.data.fver", + "auditd.data.qbytes", + "auditd.data.seuser", + "auditd.data.cap_fe", + "auditd.data.new-vcpu", + "auditd.data.old-level", + "auditd.data.old_pp", + "auditd.data.daddr", + "auditd.data.old-role", + "auditd.data.ioctlcmd", + "auditd.data.smac", + "auditd.data.apparmor", + "auditd.data.fe", + "auditd.data.perm_mask", + "auditd.data.ses", + "auditd.data.cap_fi", + "auditd.data.obj_uid", + "auditd.data.reason", + "auditd.data.list", + "auditd.data.old_lock", + "auditd.data.bus", + "auditd.data.old_pe", + "auditd.data.new-role", + "auditd.data.prom", + "auditd.data.uri", + "auditd.data.audit_enabled", + "auditd.data.old-log_passwd", + "auditd.data.old-seuser", + "auditd.data.per", + "auditd.data.scontext", + "auditd.data.tclass", + "auditd.data.ver", + "auditd.data.new", + "auditd.data.val", + "auditd.data.img-ctx", + "auditd.data.old-chardev", + "auditd.data.old_val", + "auditd.data.success", + "auditd.data.inode_uid", + "auditd.data.removed", + "auditd.data.socket.port", + "auditd.data.socket.saddr", + "auditd.data.socket.addr", + "auditd.data.socket.family", + "auditd.data.socket.path", + "geoip.continent_name", + "geoip.city_name", + "geoip.region_name", + "geoip.country_iso_code", + "hash.blake2b_256", + "hash.blake2b_384", + "hash.blake2b_512", + "hash.md5", + "hash.sha1", + "hash.sha224", + "hash.sha256", + "hash.sha384", + "hash.sha3_224", + "hash.sha3_256", + "hash.sha3_384", + "hash.sha3_512", + "hash.sha512", + "hash.sha512_224", + "hash.sha512_256", + "hash.xxh64", + "event.origin", + "user.entity_id", + "user.terminal", + "process.entity_id", + "process.hash.blake2b_256", + "process.hash.blake2b_384", + "process.hash.blake2b_512", + "process.hash.sha224", + "process.hash.sha384", + "process.hash.sha3_224", + "process.hash.sha3_256", + "process.hash.sha3_384", + "process.hash.sha3_512", + "process.hash.sha512_224", + "process.hash.sha512_256", + "process.hash.xxh64", + "socket.entity_id", + "system.audit.host.timezone.name", + "system.audit.host.hostname", + "system.audit.host.id", + "system.audit.host.architecture", + "system.audit.host.mac", + "system.audit.host.os.codename", + "system.audit.host.os.platform", + "system.audit.host.os.name", + "system.audit.host.os.family", + "system.audit.host.os.version", + "system.audit.host.os.kernel", + "system.audit.package.entity_id", + "system.audit.package.name", + "system.audit.package.version", + "system.audit.package.release", + "system.audit.package.arch", + "system.audit.package.license", + "system.audit.package.summary", + "system.audit.package.url", + "system.audit.user.name", + "system.audit.user.uid", + "system.audit.user.gid", + "system.audit.user.dir", + "system.audit.user.shell", + "system.audit.user.user_information", + "system.audit.user.password.type", + "fields.*" + ] + }, + "refresh_interval": "5s" + } + } + } +} \ No newline at end of file From 168239ca07d4a0d90e8fea04a7c4d63fa455cd45 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 16 Mar 2020 12:39:27 +0100 Subject: [PATCH 002/115] [Uptime] Index Status API to Rest (#59657) * gql to rest * update snap * fix api Co-authored-by: Elastic Machine --- .../plugins/uptime/common/constants/index.ts | 1 + .../uptime/common/constants/rest_api.ts | 9 ++++ .../plugins/uptime/common/graphql/types.ts | 12 +---- .../uptime/common/runtime_types/common.ts | 6 +++ .../connected/empty_state/empty_state.tsx | 30 ++++++++++++ .../public/components/connected/index.ts | 1 + .../kuerybar/kuery_bar_container.tsx | 2 +- .../connected/pages/overview_container.tsx | 2 +- .../__snapshots__/empty_state.test.tsx.snap | 37 +++++++-------- .../__tests__/empty_state.test.tsx | 24 ++++------ .../functional/empty_state/empty_state.tsx | 33 ++++++------- .../empty_state/empty_state_error.tsx | 7 ++- .../public/components/functional/index.ts | 1 - .../functional/kuery_bar/kuery_bar.tsx | 16 +++---- .../__tests__/monitor_list.test.tsx | 4 +- .../monitor_list_pagination.test.tsx | 4 +- .../monitor_list_drawer/__tests__/data.json | 2 +- .../public/hooks/update_kuery_string.ts | 2 +- .../plugins/uptime/public/pages/overview.tsx | 7 ++- .../uptime/public/queries/doc_count_query.ts | 22 --------- .../plugins/uptime/public/queries/index.ts | 1 - .../public/queries/monitor_states_query.ts | 4 +- .../uptime/public/state/actions/index.ts | 1 + .../public/state/actions/index_status.ts | 10 ++++ .../uptime/public/state/actions/types.ts | 8 ++++ .../uptime/public/state/actions/utils.ts | 16 +++++++ .../plugins/uptime/public/state/api/index.ts | 1 + .../uptime/public/state/api/index_status.ts | 31 +++++++++++++ .../public/state/effects/fetch_effect.ts | 4 -- .../uptime/public/state/effects/index.ts | 2 + .../public/state/effects/index_status.ts | 17 +++++++ .../uptime/public/state/reducers/index.ts | 2 + .../public/state/reducers/index_pattern.ts | 3 +- .../public/state/reducers/index_status.ts | 30 ++++++++++++ .../index.ts => state/reducers/types.ts} | 5 +- .../uptime/public/state/reducers/utils.ts | 33 +++++++++++++ .../state/selectors/__tests__/index.test.ts | 5 ++ .../uptime/public/state/selectors/index.ts | 6 ++- .../graphql/monitor_states/resolvers.ts | 16 +------ .../graphql/monitor_states/schema.gql.ts | 13 +----- .../server/lib/requests/get_index_status.ts | 6 +-- .../server/lib/requests/uptime_requests.ts | 7 +-- .../plugins/uptime/server/rest_api/index.ts | 3 +- .../get_index_pattern.ts | 0 .../rest_api/index_state/get_index_status.ts | 29 ++++++++++++ .../{index_pattern => index_state}/index.ts | 1 + .../apis/uptime/feature_controls.ts | 25 ++++------ .../apis/uptime/graphql/doc_count.js | 36 --------------- .../uptime/graphql/fixtures/doc_count.json | 8 ---- .../graphql/fixtures/monitor_states.json | 46 +++++-------------- .../fixtures/monitor_states_id_filtered.json | 10 ++-- .../fixtures/monitor_states_page_1.json | 46 +++++-------------- .../fixtures/monitor_states_page_10.json | 46 +++++-------------- .../monitor_states_page_10_previous.json | 46 +++++-------------- .../fixtures/monitor_states_page_2.json | 46 +++++-------------- .../monitor_states_page_2_previous.json | 46 +++++-------------- .../fixtures/monitor_states_page_3.json | 46 +++++-------------- .../monitor_states_page_3_previous.json | 46 +++++-------------- .../fixtures/monitor_states_page_4.json | 46 +++++-------------- .../monitor_states_page_4_previous.json | 46 +++++-------------- .../fixtures/monitor_states_page_5.json | 46 +++++-------------- .../monitor_states_page_5_previous.json | 46 +++++-------------- .../fixtures/monitor_states_page_6.json | 46 +++++-------------- .../monitor_states_page_6_previous.json | 46 +++++-------------- .../fixtures/monitor_states_page_7.json | 46 +++++-------------- .../monitor_states_page_7_previous.json | 46 +++++-------------- .../fixtures/monitor_states_page_8.json | 46 +++++-------------- .../monitor_states_page_8_previous.json | 46 +++++-------------- .../fixtures/monitor_states_page_9.json | 46 +++++-------------- .../monitor_states_page_9_previous.json | 46 +++++-------------- .../apis/uptime/graphql/index.js | 1 - .../apis/uptime/rest/doc_count.ts | 20 ++++++++ .../apis/uptime/rest/fixtures/doc_count.json | 4 ++ .../api_integration/apis/uptime/rest/index.ts | 1 + 74 files changed, 590 insertions(+), 911 deletions(-) create mode 100644 x-pack/legacy/plugins/uptime/common/constants/rest_api.ts create mode 100644 x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx delete mode 100644 x-pack/legacy/plugins/uptime/public/queries/doc_count_query.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/actions/index_status.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/actions/utils.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/api/index_status.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/effects/index_status.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/index_status.ts rename x-pack/legacy/plugins/uptime/public/{components/functional/empty_state/index.ts => state/reducers/types.ts} (76%) create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/utils.ts rename x-pack/plugins/uptime/server/rest_api/{index_pattern => index_state}/get_index_pattern.ts (100%) create mode 100644 x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts rename x-pack/plugins/uptime/server/rest_api/{index_pattern => index_state}/index.ts (82%) delete mode 100644 x-pack/test/api_integration/apis/uptime/graphql/doc_count.js delete mode 100644 x-pack/test/api_integration/apis/uptime/graphql/fixtures/doc_count.json create mode 100644 x-pack/test/api_integration/apis/uptime/rest/doc_count.ts create mode 100644 x-pack/test/api_integration/apis/uptime/rest/fixtures/doc_count.json diff --git a/x-pack/legacy/plugins/uptime/common/constants/index.ts b/x-pack/legacy/plugins/uptime/common/constants/index.ts index 9d5ad4607491..0425fc19a7b4 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/index.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/index.ts @@ -12,3 +12,4 @@ export * from './capabilities'; export { PLUGIN } from './plugin'; export { QUERY, STATES } from './query'; export * from './ui'; +export * from './rest_api'; diff --git a/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts b/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts new file mode 100644 index 000000000000..f09c79597783 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/constants/rest_api.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. + */ + +export enum REST_API_URLS { + INDEX_STATUS = '/api/uptime/index_status', +} diff --git a/x-pack/legacy/plugins/uptime/common/graphql/types.ts b/x-pack/legacy/plugins/uptime/common/graphql/types.ts index a33a69c22987..1a37ce0b18c7 100644 --- a/x-pack/legacy/plugins/uptime/common/graphql/types.ts +++ b/x-pack/legacy/plugins/uptime/common/graphql/types.ts @@ -21,8 +21,6 @@ export interface Query { /** Fetches the current state of Uptime monitors for the given parameters. */ getMonitorStates?: MonitorSummaryResult | null; - /** Fetches details about the uptime index. */ - getStatesIndexStatus: StatesIndexStatus; } export interface PingResults { @@ -392,7 +390,7 @@ export interface MonitorSummaryResult { /** The objects representing the state of a series of heartbeat monitors. */ summaries?: MonitorSummary[] | null; /** The number of summaries. */ - totalSummaryCount: DocCount; + totalSummaryCount: number; } /** Represents the current state and associated data for an Uptime monitor. */ export interface MonitorSummary { @@ -525,13 +523,7 @@ export interface SummaryHistogramPoint { /** The number of _down_ documents. */ down: number; } -/** Represents the current status of the uptime index. */ -export interface StatesIndexStatus { - /** Flag denoting whether the index exists. */ - indexExists: boolean; - /** The number of documents in the index. */ - docCount?: DocCount | null; -} + export interface AllPingsQueryArgs { /** Optional: the direction to sort by. Accepts 'asc' and 'desc'. Defaults to 'desc'. */ diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/common.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/common.ts index 84e3ae33294f..37101b5b46fd 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/common.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/common.ts @@ -22,6 +22,12 @@ export const SummaryType = t.partial({ geo: CheckGeoType, }); +export const StatesIndexStatusType = t.type({ + indexExists: t.boolean, + docCount: t.number, +}); + export type Summary = t.TypeOf; export type CheckGeo = t.TypeOf; export type Location = t.TypeOf; +export type StatesIndexStatus = t.TypeOf; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx new file mode 100644 index 000000000000..cac7042ca5b5 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { indexStatusAction } from '../../../state/actions'; +import { indexStatusSelector } from '../../../state/selectors'; +import { EmptyStateComponent } from '../../functional/empty_state/empty_state'; + +export const EmptyState: React.FC = ({ children }) => { + const { data, loading, errors } = useSelector(indexStatusSelector); + + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(indexStatusAction.get()); + }, [dispatch]); + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts index 2e30e5c3cb24..baa961ddc87d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts @@ -13,3 +13,4 @@ export { MonitorStatusBar } from './monitor/status_bar_container'; export { MonitorListDrawer } from './monitor/list_drawer_container'; export { MonitorListActionsPopover } from './monitor/drawer_popover_container'; export { DurationChart } from './charts/monitor_duration'; +export { EmptyState } from './empty_state/empty_state'; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/kuerybar/kuery_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/kuerybar/kuery_bar_container.tsx index d0f160b2c554..a42f96962b95 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/kuerybar/kuery_bar_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/kuerybar/kuery_bar_container.tsx @@ -10,7 +10,7 @@ import { selectIndexPattern } from '../../../state/selectors'; import { getIndexPattern } from '../../../state/actions'; import { KueryBarComponent } from '../../functional'; -const mapStateToProps = (state: AppState) => ({ indexPattern: selectIndexPattern(state) }); +const mapStateToProps = (state: AppState) => ({ ...selectIndexPattern(state) }); const mapDispatchToProps = (dispatch: any) => ({ loadIndexPattern: () => { diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/pages/overview_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/pages/overview_container.tsx index cbd1fae77c51..79aaa071507e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/pages/overview_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/pages/overview_container.tsx @@ -18,6 +18,6 @@ const mapDispatchToProps = (dispatch: any): DispatchProps => ({ setEsKueryFilters: (esFilters: string) => dispatch(setEsKueryString(esFilters)), }); -const mapStateToProps = (state: AppState) => ({ indexPattern: selectIndexPattern(state) }); +const mapStateToProps = (state: AppState) => ({ ...selectIndexPattern(state) }); export const OverviewPage = connect(mapStateToProps, mapDispatchToProps)(OverviewPageComponent); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap index 472d9c2be59e..a885cfe22ccd 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap @@ -2,16 +2,6 @@ exports[`EmptyState component does not render empty state with appropriate base path and no docs 1`] = ` `; -exports[`EmptyState component doesn't render child components when count is falsey 1`] = ` +exports[`EmptyState component doesn't render child components when count is falsy 1`] = ` { let statesIndexStatus: StatesIndexStatus; @@ -16,15 +16,13 @@ describe('EmptyState component', () => { beforeEach(() => { statesIndexStatus = { indexExists: true, - docCount: { - count: 1, - }, + docCount: 1, }; }); it('renders child components when count is truthy', () => { const component = shallowWithIntl( - +

Foo
Bar
Baz
@@ -33,9 +31,9 @@ describe('EmptyState component', () => { expect(component).toMatchSnapshot(); }); - it(`doesn't render child components when count is falsey`, () => { + it(`doesn't render child components when count is falsy`, () => { const component = mountWithIntl( - +
Shouldn't be rendered
); @@ -57,7 +55,7 @@ describe('EmptyState component', () => { }, ]; const component = mountWithIntl( - +
Shouldn't appear...
); @@ -66,7 +64,7 @@ describe('EmptyState component', () => { it('renders loading state if no errors or doc count', () => { const component = mountWithIntl( - +
Should appear even while loading...
); @@ -75,13 +73,11 @@ describe('EmptyState component', () => { it('does not render empty state with appropriate base path and no docs', () => { statesIndexStatus = { - docCount: { - count: 0, - }, + docCount: 0, indexExists: true, }; const component = mountWithIntl( - +
If this is in the snapshot the test should fail
); @@ -91,7 +87,7 @@ describe('EmptyState component', () => { it('notifies when index does not exist', () => { statesIndexStatus.indexExists = false; const component = mountWithIntl( - +
This text should not render
); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx index d2d46dff3b9f..80afc2894ea4 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx @@ -6,29 +6,29 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; -import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../../higher_order'; -import { docCountQuery } from '../../../queries'; import { EmptyStateError } from './empty_state_error'; import { EmptyStateLoading } from './empty_state_loading'; -import { StatesIndexStatus } from '../../../../common/graphql/types'; import { DataMissing } from './data_missing'; - -interface EmptyStateQueryResult { - statesIndexStatus?: StatesIndexStatus; -} +import { StatesIndexStatus } from '../../../../common/runtime_types'; interface EmptyStateProps { children: JSX.Element[] | JSX.Element; + statesIndexStatus: StatesIndexStatus | null; + loading: boolean; + errors?: Error[]; } -type Props = UptimeGraphQLQueryProps & EmptyStateProps; - -export const EmptyStateComponent = ({ children, data, errors }: Props) => { - if (errors) { +export const EmptyStateComponent = ({ + children, + statesIndexStatus, + loading, + errors, +}: EmptyStateProps) => { + if (errors?.length) { return ; } - if (data && data.statesIndexStatus) { - const { indexExists, docCount } = data.statesIndexStatus; + if (!loading && statesIndexStatus) { + const { indexExists, docCount } = statesIndexStatus; if (!indexExists) { return ( { })} /> ); - } else if (indexExists && docCount && docCount.count === 0) { + } else if (indexExists && docCount === 0) { return ( { } return ; }; - -export const EmptyState = withUptimeGraphQL( - EmptyStateComponent, - docCountQuery -); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state_error.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state_error.tsx index 745b185b57fa..c8e2bece1cb7 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state_error.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state_error.tsx @@ -7,15 +7,14 @@ import { EuiEmptyPrompt, EuiPanel, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; -import { GraphQLError } from 'graphql'; interface EmptyStateErrorProps { - errors: GraphQLError[]; + errors: Error[]; } export const EmptyStateError = ({ errors }: EmptyStateErrorProps) => { const unauthorized = errors.find( - (error: GraphQLError) => error.message && error.message.includes('unauthorized') + (error: Error) => error.message && error.message.includes('unauthorized') ); return ( @@ -46,7 +45,7 @@ export const EmptyStateError = ({ errors }: EmptyStateErrorProps) => { body={ {!unauthorized && - errors.map((error: GraphQLError) =>

{error.message}

)} + errors.map((error: Error) =>

{error.message}

)}
} /> diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/index.ts index e86ba548fb5d..daba13d8df64 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/index.ts @@ -5,7 +5,6 @@ */ export { DonutChart } from './charts/donut_chart'; -export { EmptyState } from './empty_state'; export { KueryBarComponent } from './kuery_bar/kuery_bar'; export { MonitorCharts } from './monitor_charts'; export { MonitorList } from './monitor_list'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx index 496e8d898df3..2f5ccc2adf31 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx @@ -34,14 +34,16 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { interface Props { autocomplete: DataPublicPluginSetup['autocomplete']; - loadIndexPattern: any; - indexPattern: any; + loadIndexPattern: () => void; + indexPattern: IIndexPattern | null; + loading: boolean; } export function KueryBarComponent({ autocomplete: autocompleteService, loadIndexPattern, indexPattern, + loading, }: Props) { useEffect(() => { if (!indexPattern) { @@ -53,19 +55,13 @@ export function KueryBarComponent({ suggestions: [], isLoadingIndexPattern: true, }); - const [isLoadingIndexPattern, setIsLoadingIndexPattern] = useState(true); const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); let currentRequestCheck: string; - useEffect(() => { - if (indexPattern !== undefined) { - setIsLoadingIndexPattern(false); - } - }, [indexPattern]); const [getUrlParams, updateUrlParams] = useUrlParams(); const { search: kuery } = getUrlParams(); - const indexPatternMissing = !isLoadingIndexPattern && !indexPattern; + const indexPatternMissing = loading && !indexPattern; async function onChange(inputValue: string, selectionStart: number) { if (!indexPattern) { @@ -124,7 +120,7 @@ export function KueryBarComponent({ { }, }, ], - totalSummaryCount: { - count: 2, - }, + totalSummaryCount: 2, }; }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx index ff54e6100615..1aef9281a306 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx @@ -89,9 +89,7 @@ describe('MonitorListPagination component', () => { }, }, ], - totalSummaryCount: { - count: 2, - }, + totalSummaryCount: 2, }; }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json index a45e974685b9..e8142f0480c4 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json @@ -3,7 +3,7 @@ "monitorStates": { "prevPagePagination": null, "nextPagePagination": null, - "totalSummaryCount": { "count": 147428, "__typename": "DocCount" }, + "totalSummaryCount": 147428, "summaries": [ { "monitor_id": "andrewvc-com", diff --git a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts b/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts index 5fcacf842466..ab4d6f75849e 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts @@ -24,7 +24,7 @@ const getKueryString = (urlFilters: string): string => { }; export const useUpdateKueryString = ( - indexPattern: IIndexPattern, + indexPattern: IIndexPattern | null, filterQueryString = '', urlFilters: string ): [string?, Error?] => { diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index 15e31d5e4462..af9b8bf04641 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -9,7 +9,6 @@ import React, { useContext, useEffect } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { - EmptyState, MonitorList, OverviewPageParsingErrorCallout, StatusPanel, @@ -19,13 +18,13 @@ import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../../../plugins/observability/public'; import { DataPublicPluginSetup, IIndexPattern } from '../../../../../../src/plugins/data/public'; import { UptimeThemeContext } from '../contexts'; -import { FilterGroup, KueryBar } from '../components/connected'; +import { EmptyState, FilterGroup, KueryBar } from '../components/connected'; import { useUpdateKueryString } from '../hooks'; import { PageHeader } from './page_header'; interface OverviewPageProps { autocomplete: DataPublicPluginSetup['autocomplete']; - indexPattern: IIndexPattern; + indexPattern: IIndexPattern | null; setEsKueryFilters: (esFilters: string) => void; } @@ -81,7 +80,7 @@ export const OverviewPageComponent = ({ autocomplete, indexPattern, setEsKueryFi return ( <> - + diff --git a/x-pack/legacy/plugins/uptime/public/queries/doc_count_query.ts b/x-pack/legacy/plugins/uptime/public/queries/doc_count_query.ts deleted file mode 100644 index 3067a9d16f05..000000000000 --- a/x-pack/legacy/plugins/uptime/public/queries/doc_count_query.ts +++ /dev/null @@ -1,22 +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 gql from 'graphql-tag'; - -export const docCountQueryString = ` -query GetStateIndexStatus { - statesIndexStatus: getStatesIndexStatus { - docCount { - count - } - indexExists - } -} -`; - -export const docCountQuery = gql` - ${docCountQueryString} -`; diff --git a/x-pack/legacy/plugins/uptime/public/queries/index.ts b/x-pack/legacy/plugins/uptime/public/queries/index.ts index f2fff9bc506d..283382ec1b7b 100644 --- a/x-pack/legacy/plugins/uptime/public/queries/index.ts +++ b/x-pack/legacy/plugins/uptime/public/queries/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { docCountQuery, docCountQueryString } from './doc_count_query'; export { pingsQuery, pingsQueryString } from './pings_query'; diff --git a/x-pack/legacy/plugins/uptime/public/queries/monitor_states_query.ts b/x-pack/legacy/plugins/uptime/public/queries/monitor_states_query.ts index 76f62ad453bd..9e609786094d 100644 --- a/x-pack/legacy/plugins/uptime/public/queries/monitor_states_query.ts +++ b/x-pack/legacy/plugins/uptime/public/queries/monitor_states_query.ts @@ -17,9 +17,7 @@ query MonitorStates($dateRangeStart: String!, $dateRangeEnd: String!, $paginatio ) { prevPagePagination nextPagePagination - totalSummaryCount { - count - } + totalSummaryCount summaries { monitor_id histogram { diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts b/x-pack/legacy/plugins/uptime/public/state/actions/index.ts index dfcea64bf9c0..b2ab73879a4a 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/index.ts @@ -11,3 +11,4 @@ export * from './monitor_status'; export * from './index_patternts'; export * from './ping'; export * from './monitor_duration'; +export * from './index_status'; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index_status.ts b/x-pack/legacy/plugins/uptime/public/state/actions/index_status.ts new file mode 100644 index 000000000000..336758a71ce6 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/actions/index_status.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 { createAsyncAction } from './utils'; +import { StatesIndexStatus } from '../../../common/runtime_types'; + +export const indexStatusAction = createAsyncAction('GET INDEX STATUS'); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/types.ts b/x-pack/legacy/plugins/uptime/public/state/actions/types.ts index dba70ed839ac..e9bf11256b0b 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/types.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/types.ts @@ -4,6 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Action } from 'redux-actions'; + +export interface AsyncAction { + get: (payload?: any) => Action; + success: (payload?: any) => Action; + fail: (payload?: any) => Action; +} + export interface QueryParams { monitorId: string; dateStart: string; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/utils.ts b/x-pack/legacy/plugins/uptime/public/state/actions/utils.ts new file mode 100644 index 000000000000..337c4bfb2fa4 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/actions/utils.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createAction } from 'redux-actions'; +import { AsyncAction } from './types'; + +export function createAsyncAction(actionStr: string): AsyncAction { + return { + get: createAction(actionStr), + success: createAction(`${actionStr}_SUCCESS`), + fail: createAction(`${actionStr}_FAIL`), + }; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index.ts b/x-pack/legacy/plugins/uptime/public/state/api/index.ts index 7d42c6ee46bd..518091cb36dd 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/index.ts @@ -9,5 +9,6 @@ export * from './overview_filters'; export * from './snapshot'; export * from './monitor_status'; export * from './index_pattern'; +export * from './index_status'; export * from './ping'; export * from './monitor_duration'; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index_status.ts b/x-pack/legacy/plugins/uptime/public/state/api/index_status.ts new file mode 100644 index 000000000000..9c531b3406a7 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/index_status.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 { PathReporter } from 'io-ts/lib/PathReporter'; +import { isRight } from 'fp-ts/lib/Either'; +import { getApiPath } from '../../lib/helper'; +import { REST_API_URLS } from '../../../common/constants/rest_api'; +import { StatesIndexStatus, StatesIndexStatusType } from '../../../common/runtime_types'; + +interface ApiRequest { + basePath: string; +} + +export const fetchIndexStatus = async ({ basePath }: ApiRequest): Promise => { + const url = getApiPath(REST_API_URLS.INDEX_STATUS, basePath); + + const response = await fetch(url); + if (!response.ok) { + throw new Error(response.statusText); + } + const responseData = await response.json(); + const decoded = StatesIndexStatusType.decode(responseData); + PathReporter.report(decoded); + if (isRight(decoded)) { + return decoded.right; + } + throw PathReporter.report(decoded); +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts b/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts index d293cdbe451b..ea389ff0a674 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts @@ -26,10 +26,6 @@ export function fetchEffectFactory( ) { return function*(action: Action) { try { - if (!action.payload) { - yield put(fail(new Error('Cannot fetch snapshot for undefined parameters.'))); - return; - } const { payload: { ...params }, } = action; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts index 43af88f4cc29..7c45aa142ecf 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts @@ -12,6 +12,7 @@ import { fetchMonitorStatusEffect } from './monitor_status'; import { fetchIndexPatternEffect } from './index_pattern'; import { fetchPingHistogramEffect } from './ping'; import { fetchMonitorDurationEffect } from './monitor_duration'; +import { fetchIndexStatusEffect } from './index_status'; export function* rootEffect() { yield fork(fetchMonitorDetailsEffect); @@ -21,4 +22,5 @@ export function* rootEffect() { yield fork(fetchIndexPatternEffect); yield fork(fetchPingHistogramEffect); yield fork(fetchMonitorDurationEffect); + yield fork(fetchIndexStatusEffect); } diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index_status.ts b/x-pack/legacy/plugins/uptime/public/state/effects/index_status.ts new file mode 100644 index 000000000000..793a671f5fed --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/effects/index_status.ts @@ -0,0 +1,17 @@ +/* + * 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 { takeLatest } from 'redux-saga/effects'; +import { indexStatusAction } from '../actions'; +import { fetchIndexStatus } from '../api'; +import { fetchEffectFactory } from './fetch_effect'; + +export function* fetchIndexStatusEffect() { + yield takeLatest( + indexStatusAction.get, + fetchEffectFactory(fetchIndexStatus, indexStatusAction.success, indexStatusAction.fail) + ); +} diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts index 32362afae42b..4a83b54504ca 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts @@ -13,6 +13,7 @@ import { monitorStatusReducer } from './monitor_status'; import { indexPatternReducer } from './index_pattern'; import { pingReducer } from './ping'; import { monitorDurationReducer } from './monitor_duration'; +import { indexStatusReducer } from './index_status'; export const rootReducer = combineReducers({ monitor: monitorReducer, @@ -23,4 +24,5 @@ export const rootReducer = combineReducers({ indexPattern: indexPatternReducer, ping: pingReducer, monitorDuration: monitorDurationReducer, + indexStatus: indexStatusReducer, }); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts index dff043f81b95..bc482e2f35c4 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts @@ -5,9 +5,10 @@ */ import { handleActions, Action } from 'redux-actions'; import { getIndexPattern, getIndexPatternSuccess, getIndexPatternFail } from '../actions'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; export interface IndexPatternState { - index_pattern: any; + index_pattern: IIndexPattern | null; errors: any[]; loading: boolean; } diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index_status.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/index_status.ts new file mode 100644 index 000000000000..50a02210fb5d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/index_status.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { handleActions } from 'redux-actions'; +import { indexStatusAction } from '../actions'; +import { handleAsyncAction } from './utils'; +import { IReducerState } from './types'; +import { StatesIndexStatus } from '../../../common/runtime_types'; + +export interface IndexStatusState extends IReducerState { + data: StatesIndexStatus | null; +} + +const initialState: IndexStatusState = { + data: null, + loading: false, + errors: [], +}; + +type PayLoad = StatesIndexStatus & Error; + +export const indexStatusReducer = handleActions( + { + ...handleAsyncAction('data', indexStatusAction), + }, + initialState +); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/index.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/types.ts similarity index 76% rename from x-pack/legacy/plugins/uptime/public/components/functional/empty_state/index.ts rename to x-pack/legacy/plugins/uptime/public/state/reducers/types.ts index 8ee70bf51f00..40fe4bddbf17 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/types.ts @@ -4,4 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EmptyState } from './empty_state'; +export interface IReducerState { + errors: Error[]; + loading: boolean; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/utils.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/utils.ts new file mode 100644 index 000000000000..773ec1068694 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/utils.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux-actions'; +import { AsyncAction } from '../actions/types'; +import { IReducerState } from './types'; + +export function handleAsyncAction( + storeKey: string, + asyncAction: AsyncAction +) { + return { + [String(asyncAction.get)]: (state: ReducerState) => ({ + ...state, + loading: true, + }), + + [String(asyncAction.success)]: (state: ReducerState, action: Action) => ({ + ...state, + loading: false, + [storeKey]: action.payload === null ? action.payload : { ...action.payload }, + }), + + [String(asyncAction.fail)]: (state: ReducerState, action: Action) => ({ + ...state, + errors: [...state.errors, action.payload], + loading: false, + }), + }; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts index 24d34b4d067c..de446418632b 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -60,6 +60,11 @@ describe('state selectors', () => { loading: false, errors: [], }, + indexStatus: { + loading: false, + data: null, + errors: [], + }, }; it('selects base path from state', () => { diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts index 0a914a14c372..adba288b8b14 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts @@ -30,7 +30,7 @@ export const selectMonitorStatus = (state: AppState) => { }; export const selectIndexPattern = ({ indexPattern }: AppState) => { - return indexPattern.index_pattern; + return { indexPattern: indexPattern.index_pattern, loading: indexPattern.loading }; }; export const selectPingHistogram = ({ ping, ui }: AppState) => { @@ -45,3 +45,7 @@ export const selectPingHistogram = ({ ping, ui }: AppState) => { export const selectDurationLines = ({ monitorDuration }: AppState) => { return monitorDuration; }; + +export const indexStatusSelector = ({ indexStatus }: AppState) => { + return indexStatus; +}; diff --git a/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts b/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts index 6ac42f771725..1560b78b3c05 100644 --- a/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts +++ b/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts @@ -10,9 +10,8 @@ import { UMResolver } from '../../../../../legacy/plugins/uptime/common/graphql/ import { GetMonitorStatesQueryArgs, MonitorSummaryResult, - StatesIndexStatus, } from '../../../../../legacy/plugins/uptime/common/graphql/types'; -import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants/context_defaults'; +import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; export type UMGetMonitorStatesResolver = UMResolver< MonitorSummaryResult | Promise, @@ -21,19 +20,11 @@ export type UMGetMonitorStatesResolver = UMResolver< UMContext >; -export type UMStatesIndexExistsResolver = UMResolver< - StatesIndexStatus | Promise, - any, - {}, - UMContext ->; - export const createMonitorStatesResolvers: CreateUMGraphQLResolvers = ( libs: UMServerLibs ): { Query: { getMonitorStates: UMGetMonitorStatesResolver; - getStatesIndexStatus: UMStatesIndexExistsResolver; }; } => { return { @@ -64,7 +55,7 @@ export const createMonitorStatesResolvers: CreateUMGraphQLResolvers = ( }), ]); - const totalSummaryCount = indexStatus?.docCount ?? { count: undefined }; + const totalSummaryCount = indexStatus?.docCount ?? 0; return { summaries, @@ -73,9 +64,6 @@ export const createMonitorStatesResolvers: CreateUMGraphQLResolvers = ( totalSummaryCount, }; }, - async getStatesIndexStatus(_resolver, {}, { APICaller }): Promise { - return await libs.requests.getIndexStatus({ callES: APICaller }); - }, }, }; }; diff --git a/x-pack/plugins/uptime/server/graphql/monitor_states/schema.gql.ts b/x-pack/plugins/uptime/server/graphql/monitor_states/schema.gql.ts index 198f97eab965..d088aed95120 100644 --- a/x-pack/plugins/uptime/server/graphql/monitor_states/schema.gql.ts +++ b/x-pack/plugins/uptime/server/graphql/monitor_states/schema.gql.ts @@ -156,15 +156,7 @@ export const monitorStatesSchema = gql` "The objects representing the state of a series of heartbeat monitors." summaries: [MonitorSummary!] "The number of summaries." - totalSummaryCount: DocCount! - } - - "Represents the current status of the uptime index." - type StatesIndexStatus { - "Flag denoting whether the index exists." - indexExists: Boolean! - "The number of documents in the index." - docCount: DocCount + totalSummaryCount: Int! } enum CursorDirection { @@ -186,8 +178,5 @@ export const monitorStatesSchema = gql` filters: String statusFilter: String ): MonitorSummaryResult - - "Fetches details about the uptime index." - getStatesIndexStatus: StatesIndexStatus! } `; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts index 95aa7eeef88e..d8a05c08b141 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts @@ -5,8 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { StatesIndexStatus } from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; +import { StatesIndexStatus } from '../../../../../legacy/plugins/uptime/common/runtime_types'; export const getIndexStatus: UMElasticsearchQueryFn<{}, StatesIndexStatus> = async ({ callES }) => { const { @@ -15,8 +15,6 @@ export const getIndexStatus: UMElasticsearchQueryFn<{}, StatesIndexStatus> = asy } = await callES('count', { index: INDEX_NAMES.HEARTBEAT }); return { indexExists: total > 0, - docCount: { - count, - }, + docCount: count, }; }; diff --git a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts index 6fd77afe711d..7f192994bd07 100644 --- a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts +++ b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts @@ -5,11 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { - Ping, - PingResults, - StatesIndexStatus, -} from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { Ping, PingResults } from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { GetFilterBarParams, GetLatestMonitorParams, @@ -26,6 +22,7 @@ import { MonitorDetails, MonitorLocations, Snapshot, + StatesIndexStatus, } from '../../../../../legacy/plugins/uptime/common/runtime_types'; import { GetMonitorStatesResult } from './get_monitor_states'; import { GetSnapshotCountParams } from './get_snapshot_counts'; diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index 69981b7860d5..b0cc38ebfb4b 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -6,7 +6,6 @@ import { createGetOverviewFilters } from './overview_filters'; import { createGetPingsRoute } from './pings'; -import { createGetIndexPatternRoute } from './index_pattern'; import { createLogMonitorPageRoute, createLogOverviewPageRoute } from './telemetry'; import { createGetSnapshotCount } from './snapshot'; import { UMRestApiRouteFactory } from './types'; @@ -18,6 +17,7 @@ import { } from './monitors'; import { createGetPingHistogramRoute } from './pings/get_ping_histogram'; import { createGetMonitorDurationRoute } from './monitors/monitors_durations'; +import { createGetIndexPatternRoute, createGetIndexStatusRoute } from './index_state'; export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; @@ -27,6 +27,7 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [ createGetOverviewFilters, createGetPingsRoute, createGetIndexPatternRoute, + createGetIndexStatusRoute, createGetMonitorRoute, createGetMonitorDetailsRoute, createGetMonitorLocationsRoute, diff --git a/x-pack/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts similarity index 100% rename from x-pack/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts rename to x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts new file mode 100644 index 000000000000..44799aa19c14 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.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 { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteFactory } from '../types'; +import { REST_API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; + +export const createGetIndexStatusRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: REST_API_URLS.INDEX_STATUS, + validate: false, + options: { + tags: ['access:uptime'], + }, + handler: async ({ callES }, _context, _request, response): Promise => { + try { + return response.ok({ + body: { + ...(await libs.requests.getIndexStatus({ callES })), + }, + }); + } catch (e) { + return response.internalError({ body: { message: e.message } }); + } + }, +}); diff --git a/x-pack/plugins/uptime/server/rest_api/index_pattern/index.ts b/x-pack/plugins/uptime/server/rest_api/index_state/index.ts similarity index 82% rename from x-pack/plugins/uptime/server/rest_api/index_pattern/index.ts rename to x-pack/plugins/uptime/server/rest_api/index_state/index.ts index b35e2e7b65a2..ff44794bfe7d 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_pattern/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/index.ts @@ -5,3 +5,4 @@ */ export { createGetIndexPatternRoute } from './get_index_pattern'; +export { createGetIndexStatusRoute } from './get_index_status'; diff --git a/x-pack/test/api_integration/apis/uptime/feature_controls.ts b/x-pack/test/api_integration/apis/uptime/feature_controls.ts index adbfacb014e9..15666acab233 100644 --- a/x-pack/test/api_integration/apis/uptime/feature_controls.ts +++ b/x-pack/test/api_integration/apis/uptime/feature_controls.ts @@ -5,9 +5,9 @@ */ import expect from '@kbn/expect'; -import { docCountQueryString } from '../../../../legacy/plugins/uptime/public/queries'; import { FtrProviderContext } from '../../ftr_provider_context'; import { PINGS_DATE_RANGE_END, PINGS_DATE_RANGE_START } from './constants'; +import { REST_API_URLS } from '../../../../legacy/plugins/uptime/common/constants'; export default function featureControlsTests({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); @@ -26,22 +26,13 @@ export default function featureControlsTests({ getService }: FtrProviderContext) expect(result.response).to.have.property('statusCode', 200); }; - const executeGraphQLQuery = async (username: string, password: string, spaceId?: string) => { + const executeRESTAPIQuery = async (username: string, password: string, spaceId?: string) => { const basePath = spaceId ? `/s/${spaceId}` : ''; - const getDocCountQuery = { - operationName: null, - query: docCountQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2019-01-28T19:00:16.078Z', - }, - }; return await supertest - .post(`${basePath}/api/uptime/graphql`) + .get(basePath + REST_API_URLS.INDEX_STATUS) .auth(username, password) .set('kbn-xsrf', 'foo') - .send({ ...getDocCountQuery }) .then((response: any) => ({ error: undefined, response })) .catch((error: any) => ({ error, response: undefined })); }; @@ -82,7 +73,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) full_name: 'a kibana user', }); - const graphQLResult = await executeGraphQLQuery(username, password); + const graphQLResult = await executeRESTAPIQuery(username, password); expect404(graphQLResult); const pingsResult = await executePingsRequest(username, password); @@ -121,7 +112,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) full_name: 'a kibana user', }); - const graphQLResult = await executeGraphQLQuery(username, password); + const graphQLResult = await executeRESTAPIQuery(username, password); expectResponse(graphQLResult); const pingsResult = await executePingsRequest(username, password); @@ -163,7 +154,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) full_name: 'a kibana user', }); - const graphQLResult = await executeGraphQLQuery(username, password); + const graphQLResult = await executeRESTAPIQuery(username, password); expect404(graphQLResult); const pingsResult = await executePingsRequest(username, password); @@ -232,7 +223,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }); it('user_1 can access APIs in space_1', async () => { - const graphQLResult = await executeGraphQLQuery(username, password, space1Id); + const graphQLResult = await executeRESTAPIQuery(username, password, space1Id); expectResponse(graphQLResult); const pingsResult = await executePingsRequest(username, password, space1Id); @@ -240,7 +231,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }); it(`user_1 can't access APIs in space_2`, async () => { - const graphQLResult = await executeGraphQLQuery(username, password); + const graphQLResult = await executeRESTAPIQuery(username, password); expect404(graphQLResult); const pingsResult = await executePingsRequest(username, password); diff --git a/x-pack/test/api_integration/apis/uptime/graphql/doc_count.js b/x-pack/test/api_integration/apis/uptime/graphql/doc_count.js deleted file mode 100644 index 1aa69faaab73..000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/doc_count.js +++ /dev/null @@ -1,36 +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 { docCountQueryString } from '../../../../../legacy/plugins/uptime/public/queries'; -import { expectFixtureEql } from './helpers/expect_fixture_eql'; - -export default function({ getService }) { - describe('docCount query', () => { - before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat')); - after('unload heartbeat index', () => getService('esArchiver').unload('uptime/full_heartbeat')); - - const supertest = getService('supertest'); - - it(`will fetch the index's count`, async () => { - const getDocCountQuery = { - operationName: null, - query: docCountQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getDocCountQuery }); - - expectFixtureEql(data, 'doc_count'); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/doc_count.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/doc_count.json deleted file mode 100644 index 4daf223a79a6..000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/doc_count.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "statesIndexStatus": { - "docCount": { - "count": 2000 - }, - "indexExists": true - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states.json index 59f5f95e7d84..05724f0716e8 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": null, "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0009-up\"}}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0000-intermittent", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_id_filtered.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_id_filtered.json index 9a1363f00578..6e62787069f4 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_id_filtered.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_id_filtered.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": null, "nextPagePagination": null, - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0002-up", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -198,4 +194,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_1.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_1.json index 59f5f95e7d84..05724f0716e8 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_1.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_1.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": null, "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0009-up\"}}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0000-intermittent", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10.json index 5c07b4daaf54..6cbe4ee3659a 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0090-intermittent\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": null, - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0090-intermittent", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10_previous.json index 71184093f431..9a3f781735cb 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10_previous.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10_previous.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0080-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0089-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0080-down", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2.json index 3b15ea3c24ee..4f4af9c2c601 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0010-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0019-up\"}}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0010-down", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2_previous.json index eb6512d2f75b..fe48ad49d13b 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2_previous.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2_previous.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": null, "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0009-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0000-intermittent", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3.json index aee4fa6946fc..70ca665704a7 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0020-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0029-up\"}}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0020-down", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3_previous.json index 03164f794a4d..3f09c951ec2f 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3_previous.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3_previous.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0010-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0019-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0010-down", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4.json index 488fdab14f1e..cdc0f32c9765 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0030-intermittent\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0039-up\"}}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0030-intermittent", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4_previous.json index 79ce05d86f53..9f6d004380c1 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4_previous.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4_previous.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0020-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0029-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0020-down", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5.json index ef62cdd86c2a..dedddb2a78ad 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0040-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0049-up\"}}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0040-down", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5_previous.json index 34e8269cb95d..fabcf7040495 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5_previous.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5_previous.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0030-intermittent\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0039-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0030-intermittent", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6.json index d733467c3f9b..943cc68249dc 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0050-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0059-up\"}}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0050-down", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6_previous.json index 12e27106bd53..564f58f59f37 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6_previous.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6_previous.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0040-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0049-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0040-down", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7.json index d0f2b820f832..cb94273e91fd 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0060-intermittent\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0069-up\"}}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0060-intermittent", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7_previous.json index cf0c8641cc87..7aac62bba84f 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7_previous.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7_previous.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0050-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0059-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0050-down", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8.json index 2801e94e034c..08cbd0d878b4 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0070-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0079-up\"}}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0070-down", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8_previous.json index 7fcbd4dd5965..8de639b705ee 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8_previous.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8_previous.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0060-intermittent\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0069-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0060-intermittent", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9.json index 0adb7ad0b0db..c38f5c801a26 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0080-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0089-up\"}}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0080-down", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9_previous.json index a796be38bd0d..5c2ec8512e32 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9_previous.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9_previous.json @@ -2,9 +2,7 @@ "monitorStates": { "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0070-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0079-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": { - "count": 2000 - }, + "totalSummaryCount": 2000, "summaries": [ { "monitor_id": "0070-down", @@ -172,9 +170,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -362,9 +358,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -552,9 +546,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -742,9 +734,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -932,9 +922,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1122,9 +1110,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1312,9 +1298,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1502,9 +1486,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1692,9 +1674,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1882,9 +1862,7 @@ "geo": null, "observer": { "geo": { - "name": [ - "mpls" - ], + "name": ["mpls"], "location": null } }, @@ -1908,4 +1886,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/index.js b/x-pack/test/api_integration/apis/uptime/graphql/index.js index c2fdc57edede..ee22974d4717 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/index.js +++ b/x-pack/test/api_integration/apis/uptime/graphql/index.js @@ -10,7 +10,6 @@ export default function({ loadTestFile }) { // the uptime app and runs it against the live HTTP server, // verifying the pre-loaded documents are returned in a way that // matches the snapshots contained in './fixtures' - loadTestFile(require.resolve('./doc_count')); loadTestFile(require.resolve('./monitor_states')); loadTestFile(require.resolve('./ping_list')); }); diff --git a/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts b/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts new file mode 100644 index 000000000000..1f5322f581b3 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts @@ -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 { FtrProviderContext } from '../../../ftr_provider_context'; +import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { REST_API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; + +export default function({ getService }: FtrProviderContext) { + describe('docCount query', () => { + const supertest = getService('supertest'); + + it(`will fetch the index's count`, async () => { + const apiResponse = await supertest.get(REST_API_URLS.INDEX_STATUS); + const data = apiResponse.body; + expectFixtureEql(data, 'doc_count'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/doc_count.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/doc_count.json new file mode 100644 index 000000000000..41b9af392dde --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/doc_count.json @@ -0,0 +1,4 @@ +{ + "docCount": 2000, + "indexExists": true +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index 5e26cb9216f4..67b94f19c638 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -21,6 +21,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./selected_monitor')); loadTestFile(require.resolve('./ping_histogram')); loadTestFile(require.resolve('./monitor_duration')); + loadTestFile(require.resolve('./doc_count')); }); }); } From a946adbf10b0148bf510b0588713dd7a2a04579f Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 16 Mar 2020 13:01:48 +0100 Subject: [PATCH 003/115] adds new test (#60064) --- .../cypress/integration/detections.spec.ts | 43 ++++++++++++++++++- .../siem/cypress/screens/detections.ts | 4 +- .../plugins/siem/cypress/tasks/detections.ts | 7 +++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts index 1624586d4ca1..c048510c50c3 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts @@ -5,12 +5,14 @@ */ import { NUMBER_OF_SIGNALS, + OPEN_CLOSE_SIGNALS_BTN, SELECTED_SIGNALS, SHOWING_SIGNALS, SIGNALS, } from '../screens/detections'; import { + closeFirstSignal, closeSignals, goToClosedSignals, goToOpenedSignals, @@ -26,7 +28,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS } from '../urls/navigation'; describe('Detections', () => { - before(() => { + beforeEach(() => { esArchiverLoad('signals'); loginAndWaitForPage(DETECTIONS); }); @@ -111,4 +113,43 @@ describe('Detections', () => { .should('eql', expectedNumberOfOpenedSignals.toString()); }); }); + + it('Closes one signal when more than one opened signals are selected', () => { + waitForSignalsToBeLoaded(); + + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .then(numberOfSignals => { + const numberOfSignalsToBeClosed = 1; + const numberOfSignalsToBeSelected = 3; + + cy.get(OPEN_CLOSE_SIGNALS_BTN).should('have.attr', 'disabled'); + selectNumberOfSignals(numberOfSignalsToBeSelected); + cy.get(OPEN_CLOSE_SIGNALS_BTN).should('not.have.attr', 'disabled'); + + closeFirstSignal(); + cy.reload(); + waitForSignalsToBeLoaded(); + waitForSignals(); + + const expectedNumberOfSignals = +numberOfSignals - numberOfSignalsToBeClosed; + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eq', expectedNumberOfSignals.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${expectedNumberOfSignals.toString()} signals`); + + goToClosedSignals(); + waitForSignals(); + + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eql', numberOfSignalsToBeClosed.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${numberOfSignalsToBeClosed.toString()} signal`); + cy.get(SIGNALS).should('have.length', numberOfSignalsToBeClosed); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts index 8b5ba2357880..f388ac1215d0 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts @@ -12,7 +12,9 @@ export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal export const NUMBER_OF_SIGNALS = '[data-test-subj="server-side-event-count"]'; -export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] .siemLinkIcon__label'; +export const OPEN_CLOSE_SIGNAL_BTN = '[data-test-subj="update-signal-status-button"]'; + +export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] button'; export const OPENED_SIGNALS_BTN = '[data-test-subj="openSignals"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts index 21a0c136b90d..3416e3eb81de 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts @@ -8,6 +8,7 @@ import { CLOSED_SIGNALS_BTN, LOADING_SIGNALS_PANEL, MANAGE_SIGNAL_DETECTION_RULES_BTN, + OPEN_CLOSE_SIGNAL_BTN, OPEN_CLOSE_SIGNALS_BTN, OPENED_SIGNALS_BTN, SIGNALS, @@ -15,6 +16,12 @@ import { } from '../screens/detections'; import { REFRESH_BUTTON } from '../screens/siem_header'; +export const closeFirstSignal = () => { + cy.get(OPEN_CLOSE_SIGNAL_BTN) + .first() + .click({ force: true }); +}; + export const closeSignals = () => { cy.get(OPEN_CLOSE_SIGNALS_BTN).click({ force: true }); }; From 4be60e54261be05eda4c5b4530c93d9450398d8d Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 16 Mar 2020 15:05:41 +0300 Subject: [PATCH 004/115] [Step 1][App Arch] Saved object migrations from kibana plugin to new platform (#59044) * [App Arch] Saved object migrations from kibana plugin to new platform Part of #55946 * search type -> data plugin * remove migrations folder * visualization type -> visualizations plugin * src/legacy/core_plugins/data/mappings-> np * savedObjectsManagement -> management section * move code into save_objects folder Co-authored-by: Elastic Machine --- .../saved_objects/serialization/types.ts | 2 +- src/legacy/core_plugins/data/index.ts | 19 - src/legacy/core_plugins/kibana/index.js | 51 - src/legacy/core_plugins/kibana/mappings.json | 89 - .../kibana/migrations/migrations.js | 564 +----- .../kibana/migrations/migrations.test.js | 1574 ----------------- .../index_patterns/index_patterns_service.ts | 7 +- src/plugins/data/server/plugin.ts | 3 + .../data/server/query/index.ts} | 32 +- .../data/server/query/query_service.ts | 29 + .../data/server/saved_objects/index.ts | 22 + .../index_pattern_migrations.test.ts | 97 + .../saved_objects/index_pattern_migrations.ts | 60 + .../server/saved_objects/index_patterns.ts | 58 + .../data/server/saved_objects/query.ts | 52 + .../data/server/saved_objects/search.ts | 60 + .../saved_objects/search_migrations.test.ts | 299 ++++ .../server/saved_objects/search_migrations.ts | 92 + .../data/server/search/search_service.ts | 4 + src/plugins/data/server/server.api.md | 2 +- src/plugins/visualizations/kibana.json | 2 +- src/plugins/visualizations/server/index.ts | 30 + src/plugins/visualizations/server/plugin.ts | 54 + .../server/saved_objects/index.ts | 20 + .../server/saved_objects/visualization.ts | 56 + .../visualization_migrations.test.ts | 1356 ++++++++++++++ .../saved_objects/visualization_migrations.ts | 574 ++++++ src/plugins/visualizations/server/types.ts | 23 + 28 files changed, 2899 insertions(+), 2332 deletions(-) rename src/{legacy/core_plugins/data/mappings.ts => plugins/data/server/query/index.ts} (60%) create mode 100644 src/plugins/data/server/query/query_service.ts create mode 100644 src/plugins/data/server/saved_objects/index.ts create mode 100644 src/plugins/data/server/saved_objects/index_pattern_migrations.test.ts create mode 100644 src/plugins/data/server/saved_objects/index_pattern_migrations.ts create mode 100644 src/plugins/data/server/saved_objects/index_patterns.ts create mode 100644 src/plugins/data/server/saved_objects/query.ts create mode 100644 src/plugins/data/server/saved_objects/search.ts create mode 100644 src/plugins/data/server/saved_objects/search_migrations.test.ts create mode 100644 src/plugins/data/server/saved_objects/search_migrations.ts create mode 100644 src/plugins/visualizations/server/index.ts create mode 100644 src/plugins/visualizations/server/plugin.ts create mode 100644 src/plugins/visualizations/server/saved_objects/index.ts create mode 100644 src/plugins/visualizations/server/saved_objects/visualization.ts create mode 100644 src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts create mode 100644 src/plugins/visualizations/server/saved_objects/visualization_migrations.ts create mode 100644 src/plugins/visualizations/server/types.ts diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index 524c2c8ffae7..dfaec127ba15 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -50,7 +50,7 @@ export interface SavedObjectsRawDocSource { * scenario out of the box. */ interface SavedObjectDoc { - attributes: unknown; + attributes: any; id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional type: string; namespace?: string; diff --git a/src/legacy/core_plugins/data/index.ts b/src/legacy/core_plugins/data/index.ts index 813eab00f725..10c8cf464b82 100644 --- a/src/legacy/core_plugins/data/index.ts +++ b/src/legacy/core_plugins/data/index.ts @@ -19,8 +19,6 @@ import { resolve } from 'path'; import { Legacy } from '../../../../kibana'; -import { mappings } from './mappings'; -import { SavedQuery } from '../../../plugins/data/public'; // eslint-disable-next-line import/no-default-export export default function DataPlugin(kibana: any) { @@ -36,23 +34,6 @@ export default function DataPlugin(kibana: any) { init: (server: Legacy.Server) => ({}), uiExports: { injectDefaultVars: () => ({}), - mappings, - savedObjectsManagement: { - query: { - icon: 'search', - defaultSearchField: 'title', - isImportableAndExportable: true, - getTitle(obj: SavedQuery) { - return obj.attributes.title; - }, - getInAppUrl(obj: SavedQuery) { - return { - path: `/app/kibana#/discover?_a=(savedQuery:'${encodeURIComponent(obj.id)}')`, - uiCapabilitiesPath: 'discover.show', - }; - }, - }, - }, }, }; diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 092eed924f33..1d772536fa1e 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -126,57 +126,6 @@ export default function(kibana) { ], savedObjectsManagement: { - 'index-pattern': { - icon: 'indexPatternApp', - defaultSearchField: 'title', - isImportableAndExportable: true, - getTitle(obj) { - return obj.attributes.title; - }, - getEditUrl(obj) { - return `/management/kibana/index_patterns/${encodeURIComponent(obj.id)}`; - }, - getInAppUrl(obj) { - return { - path: `/app/kibana#/management/kibana/index_patterns/${encodeURIComponent(obj.id)}`, - uiCapabilitiesPath: 'management.kibana.index_patterns', - }; - }, - }, - visualization: { - icon: 'visualizeApp', - defaultSearchField: 'title', - isImportableAndExportable: true, - getTitle(obj) { - return obj.attributes.title; - }, - getEditUrl(obj) { - return `/management/kibana/objects/savedVisualizations/${encodeURIComponent(obj.id)}`; - }, - getInAppUrl(obj) { - return { - path: `/app/kibana#/visualize/edit/${encodeURIComponent(obj.id)}`, - uiCapabilitiesPath: 'visualize.show', - }; - }, - }, - search: { - icon: 'discoverApp', - defaultSearchField: 'title', - isImportableAndExportable: true, - getTitle(obj) { - return obj.attributes.title; - }, - getEditUrl(obj) { - return `/management/kibana/objects/savedSearches/${encodeURIComponent(obj.id)}`; - }, - getInAppUrl(obj) { - return { - path: `/app/kibana#/discover/${encodeURIComponent(obj.id)}`, - uiCapabilitiesPath: 'discover.show', - }; - }, - }, dashboard: { icon: 'dashboardApp', defaultSearchField: 'title', diff --git a/src/legacy/core_plugins/kibana/mappings.json b/src/legacy/core_plugins/kibana/mappings.json index 4cf9ea1d301c..af3f79588552 100644 --- a/src/legacy/core_plugins/kibana/mappings.json +++ b/src/legacy/core_plugins/kibana/mappings.json @@ -1,93 +1,4 @@ { - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, "dashboard": { "properties": { "description": { diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.js b/src/legacy/core_plugins/kibana/migrations/migrations.js index 29b6e632d19f..d37887c640b9 100644 --- a/src/legacy/core_plugins/kibana/migrations/migrations.js +++ b/src/legacy/core_plugins/kibana/migrations/migrations.js @@ -17,7 +17,7 @@ * under the License. */ -import { cloneDeep, get, omit, has, flow } from 'lodash'; +import { get } from 'lodash'; import { migrations730 as dashboardMigrations730 } from '../public/dashboard/migrations'; function migrateIndexPattern(doc) { @@ -58,559 +58,7 @@ function migrateIndexPattern(doc) { doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); } -// [TSVB] Migrate percentile-rank aggregation (value -> values) -const migratePercentileRankAggregation = doc => { - const visStateJSON = get(doc, 'attributes.visState'); - let visState; - - if (visStateJSON) { - try { - visState = JSON.parse(visStateJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - } - if (visState && visState.type === 'metrics') { - const series = get(visState, 'params.series') || []; - - series.forEach(part => { - (part.metrics || []).forEach(metric => { - if (metric.type === 'percentile_rank' && has(metric, 'value')) { - metric.values = [metric.value]; - - delete metric.value; - } - }); - }); - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } - } - return doc; -}; - -// Migrate date histogram aggregation (remove customInterval) -const migrateDateHistogramAggregation = doc => { - const visStateJSON = get(doc, 'attributes.visState'); - let visState; - - if (visStateJSON) { - try { - visState = JSON.parse(visStateJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - } - - if (visState && visState.aggs) { - visState.aggs.forEach(agg => { - if (agg.type === 'date_histogram' && agg.params) { - if (agg.params.interval === 'custom') { - agg.params.interval = agg.params.customInterval; - } - delete agg.params.customInterval; - } - - if ( - get(agg, 'params.customBucket.type', null) === 'date_histogram' && - agg.params.customBucket.params - ) { - if (agg.params.customBucket.params.interval === 'custom') { - agg.params.customBucket.params.interval = agg.params.customBucket.params.customInterval; - } - delete agg.params.customBucket.params.customInterval; - } - }); - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } - } - return doc; -}; - -function removeDateHistogramTimeZones(doc) { - const visStateJSON = get(doc, 'attributes.visState'); - if (visStateJSON) { - let visState; - try { - visState = JSON.parse(visStateJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - } - if (visState && visState.aggs) { - visState.aggs.forEach(agg => { - // We're checking always for the existance of agg.params here. This should always exist, but better - // be safe then sorry during migrations. - if (agg.type === 'date_histogram' && agg.params) { - delete agg.params.time_zone; - } - - if ( - get(agg, 'params.customBucket.type', null) === 'date_histogram' && - agg.params.customBucket.params - ) { - delete agg.params.customBucket.params.time_zone; - } - }); - doc.attributes.visState = JSON.stringify(visState); - } - } - return doc; -} - -// migrate gauge verticalSplit to alignment -// https://github.com/elastic/kibana/issues/34636 -function migrateGaugeVerticalSplitToAlignment(doc, logger) { - const visStateJSON = get(doc, 'attributes.visState'); - - if (visStateJSON) { - try { - const visState = JSON.parse(visStateJSON); - if (visState && visState.type === 'gauge' && !visState.params.gauge.alignment) { - visState.params.gauge.alignment = visState.params.gauge.verticalSplit - ? 'vertical' - : 'horizontal'; - delete visState.params.gauge.verticalSplit; - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } - } catch (e) { - logger.warning(`Exception @ migrateGaugeVerticalSplitToAlignment! ${e}`); - logger.warning(`Exception @ migrateGaugeVerticalSplitToAlignment! Payload: ${visStateJSON}`); - } - } - return doc; -} -// Migrate filters (string -> { query: string, language: lucene }) -/* - Enabling KQL in TSVB causes problems with savedObject visualizations when these are saved with filters. - In a visualisation type of saved object, if the visState param is of type metric, the filter is saved as a string that is not interpretted correctly as a lucene query in the visualization itself. - We need to transform the filter string into an object containing the original string as a query and specify the query language as lucene. - For Metrics visualizations (param.type === "metric"), filters can be applied to each series object in the series array within the SavedObject.visState.params object. - Path to the series array is thus: - attributes.visState. -*/ -function transformFilterStringToQueryObject(doc) { - // Migrate filters - // If any filters exist and they are a string, we assume it to be lucene and transform the filter into an object accordingly - const newDoc = cloneDeep(doc); - const visStateJSON = get(doc, 'attributes.visState'); - if (visStateJSON) { - let visState; - try { - visState = JSON.parse(visStateJSON); - } catch (e) { - // let it go, the data is invalid and we'll leave it as is - } - if (visState) { - const visType = get(visState, 'params.type'); - const tsvbTypes = ['metric', 'markdown', 'top_n', 'gauge', 'table', 'timeseries']; - if (tsvbTypes.indexOf(visType) === -1) { - // skip - return doc; - } - // migrate the params fitler - const params = get(visState, 'params'); - if (params.filter && typeof params.filter === 'string') { - const paramsFilterObject = { - query: params.filter, - language: 'lucene', - }; - params.filter = paramsFilterObject; - } - - // migrate the annotations query string: - const annotations = get(visState, 'params.annotations') || []; - annotations.forEach(item => { - if (!item.query_string) { - // we don't need to transform anything if there isn't a filter at all - return; - } - if (typeof item.query_string === 'string') { - const itemQueryStringObject = { - query: item.query_string, - language: 'lucene', - }; - item.query_string = itemQueryStringObject; - } - }); - // migrate the series filters - const series = get(visState, 'params.series') || []; - series.forEach(item => { - if (!item.filter) { - // we don't need to transform anything if there isn't a filter at all - return; - } - // series item filter - if (typeof item.filter === 'string') { - const itemfilterObject = { - query: item.filter, - language: 'lucene', - }; - item.filter = itemfilterObject; - } - // series item split filters filter - if (item.split_filters) { - const splitFilters = get(item, 'split_filters') || []; - splitFilters.forEach(filter => { - if (!filter.filter) { - // we don't need to transform anything if there isn't a filter at all - return; - } - if (typeof filter.filter === 'string') { - const filterfilterObject = { - query: filter.filter, - language: 'lucene', - }; - filter.filter = filterfilterObject; - } - }); - } - }); - newDoc.attributes.visState = JSON.stringify(visState); - } - } - return newDoc; -} -function transformSplitFiltersStringToQueryObject(doc) { - // Migrate split_filters in TSVB objects that weren't migrated in 7.3 - // If any filters exist and they are a string, we assume them to be lucene syntax and transform the filter into an object accordingly - const newDoc = cloneDeep(doc); - const visStateJSON = get(doc, 'attributes.visState'); - if (visStateJSON) { - let visState; - try { - visState = JSON.parse(visStateJSON); - } catch (e) { - // let it go, the data is invalid and we'll leave it as is - } - if (visState) { - const visType = get(visState, 'params.type'); - const tsvbTypes = ['metric', 'markdown', 'top_n', 'gauge', 'table', 'timeseries']; - if (tsvbTypes.indexOf(visType) === -1) { - // skip - return doc; - } - // migrate the series split_filter filters - const series = get(visState, 'params.series') || []; - series.forEach(item => { - // series item split filters filter - if (item.split_filters) { - const splitFilters = get(item, 'split_filters') || []; - if (splitFilters.length > 0) { - // only transform split_filter filters if we have filters - splitFilters.forEach(filter => { - if (typeof filter.filter === 'string') { - const filterfilterObject = { - query: filter.filter, - language: 'lucene', - }; - filter.filter = filterfilterObject; - } - }); - } - } - }); - newDoc.attributes.visState = JSON.stringify(visState); - } - } - return newDoc; -} - -function migrateFiltersAggQuery(doc) { - const visStateJSON = get(doc, 'attributes.visState'); - - if (visStateJSON) { - try { - const visState = JSON.parse(visStateJSON); - if (visState && visState.aggs) { - visState.aggs.forEach(agg => { - if (agg.type !== 'filters') return; - - agg.params.filters.forEach(filter => { - if (filter.input.language) return filter; - filter.input.language = 'lucene'; - }); - }); - - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - } - } - return doc; -} - -function replaceMovAvgToMovFn(doc, logger) { - const visStateJSON = get(doc, 'attributes.visState'); - let visState; - - if (visStateJSON) { - try { - visState = JSON.parse(visStateJSON); - - if (visState && visState.type === 'metrics') { - const series = get(visState, 'params.series', []); - - series.forEach(part => { - if (part.metrics && Array.isArray(part.metrics)) { - part.metrics.forEach(metric => { - if (metric.type === 'moving_average') { - metric.model_type = metric.model; - metric.alpha = get(metric, 'settings.alpha', 0.3); - metric.beta = get(metric, 'settings.beta', 0.1); - metric.gamma = get(metric, 'settings.gamma', 0.3); - metric.period = get(metric, 'settings.period', 1); - metric.multiplicative = get(metric, 'settings.type') === 'mult'; - - delete metric.minimize; - delete metric.model; - delete metric.settings; - delete metric.predict; - } - }); - } - }); - - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } - } catch (e) { - logger.warning(`Exception @ replaceMovAvgToMovFn! ${e}`); - logger.warning(`Exception @ replaceMovAvgToMovFn! Payload: ${visStateJSON}`); - } - } - - return doc; -} - -function migrateSearchSortToNestedArray(doc) { - const sort = get(doc, 'attributes.sort'); - if (!sort) return doc; - - // Don't do anything if we already have a two dimensional array - if (Array.isArray(sort) && sort.length > 0 && Array.isArray(sort[0])) { - return doc; - } - - return { - ...doc, - attributes: { - ...doc.attributes, - sort: [doc.attributes.sort], - }, - }; -} - -function migrateFiltersAggQueryStringQueries(doc) { - const visStateJSON = get(doc, 'attributes.visState'); - - if (visStateJSON) { - try { - const visState = JSON.parse(visStateJSON); - if (visState && visState.aggs) { - visState.aggs.forEach(agg => { - if (agg.type !== 'filters') return doc; - - agg.params.filters.forEach(filter => { - if (filter.input.query.query_string) { - filter.input.query = filter.input.query.query_string.query; - } - }); - }); - - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - } - } - return doc; -} - -function migrateSubTypeAndParentFieldProperties(doc) { - if (!doc.attributes.fields) return doc; - - const fieldsString = doc.attributes.fields; - const fields = JSON.parse(fieldsString); - const migratedFields = fields.map(field => { - if (field.subType === 'multi') { - return { - ...omit(field, 'parent'), - subType: { multi: { parent: field.parent } }, - }; - } - - return field; - }); - - return { - ...doc, - attributes: { - ...doc.attributes, - fields: JSON.stringify(migratedFields), - }, - }; -} - -const executeMigrations720 = flow( - migratePercentileRankAggregation, - migrateDateHistogramAggregation -); -const executeMigrations730 = flow( - migrateGaugeVerticalSplitToAlignment, - transformFilterStringToQueryObject, - migrateFiltersAggQuery, - replaceMovAvgToMovFn -); - -const executeVisualizationMigrations731 = flow(migrateFiltersAggQueryStringQueries); - -const executeSearchMigrations740 = flow(migrateSearchSortToNestedArray); - -const executeMigrations742 = flow(transformSplitFiltersStringToQueryObject); - export const migrations = { - 'index-pattern': { - '6.5.0': doc => { - doc.attributes.type = doc.attributes.type || undefined; - doc.attributes.typeMeta = doc.attributes.typeMeta || undefined; - return doc; - }, - '7.6.0': flow(migrateSubTypeAndParentFieldProperties), - }, - visualization: { - /** - * We need to have this migration twice, once with a version prior to 7.0.0 once with a version - * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already - * released. Thus a user who already had 7.0.0 installed already got the 7.0.0 migrations below running, - * so we need a version higher than that. But this fix was backported to the 6.7 release, meaning if we - * would only have the 7.0.1 migration in here a user on the 6.7 release will migrate their saved objects - * to the 7.0.1 state, and thus when updating their Kibana to 7.0, will never run the 7.0.0 migrations introduced - * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 - * only contained the 6.7.2 migration and not the 7.0.1 migration. - */ - '6.7.2': removeDateHistogramTimeZones, - '7.0.0': doc => { - // Set new "references" attribute - doc.references = doc.references || []; - - // Migrate index pattern - migrateIndexPattern(doc); - - // Migrate saved search - const savedSearchId = get(doc, 'attributes.savedSearchId'); - if (savedSearchId) { - doc.references.push({ - type: 'search', - name: 'search_0', - id: savedSearchId, - }); - doc.attributes.savedSearchRefName = 'search_0'; - } - delete doc.attributes.savedSearchId; - - // Migrate controls - const visStateJSON = get(doc, 'attributes.visState'); - if (visStateJSON) { - let visState; - try { - visState = JSON.parse(visStateJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - } - if (visState) { - const controls = get(visState, 'params.controls') || []; - controls.forEach((control, i) => { - if (!control.indexPattern) { - return; - } - control.indexPatternRefName = `control_${i}_index_pattern`; - doc.references.push({ - name: control.indexPatternRefName, - type: 'index-pattern', - id: control.indexPattern, - }); - delete control.indexPattern; - }); - doc.attributes.visState = JSON.stringify(visState); - } - } - - // Migrate table splits - try { - const visState = JSON.parse(doc.attributes.visState); - if (get(visState, 'type') !== 'table') { - return doc; // do nothing; we only want to touch tables - } - - let splitCount = 0; - visState.aggs = visState.aggs.map(agg => { - if (agg.schema !== 'split') { - return agg; - } - - splitCount++; - if (splitCount === 1) { - return agg; // leave the first split agg unchanged - } - agg.schema = 'bucket'; - // the `row` param is exclusively used by split aggs, so we remove it - agg.params = omit(agg.params, ['row']); - return agg; - }); - - if (splitCount <= 1) { - return doc; // do nothing; we only want to touch tables with multiple split aggs - } - - const newDoc = cloneDeep(doc); - newDoc.attributes.visState = JSON.stringify(visState); - return newDoc; - } catch (e) { - throw new Error( - `Failure attempting to migrate saved object '${doc.attributes.title}' - ${e}` - ); - } - }, - '7.0.1': removeDateHistogramTimeZones, - '7.2.0': doc => executeMigrations720(doc), - '7.3.0': executeMigrations730, - '7.3.1': executeVisualizationMigrations731, - // migrate split_filters that were not migrated in 7.3.0 (transformFilterStringToQueryObject). - '7.4.2': executeMigrations742, - }, dashboard: { '7.0.0': doc => { // Set new "references" attribute @@ -651,14 +99,4 @@ export const migrations = { }, '7.3.0': dashboardMigrations730, }, - search: { - '7.0.0': doc => { - // Set new "references" attribute - doc.references = doc.references || []; - // Migrate index pattern - migrateIndexPattern(doc); - return doc; - }, - '7.4.0': executeSearchMigrations740, - }, }; diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.test.js b/src/legacy/core_plugins/kibana/migrations/migrations.test.js index e39bc59201e7..b02081128c85 100644 --- a/src/legacy/core_plugins/kibana/migrations/migrations.test.js +++ b/src/legacy/core_plugins/kibana/migrations/migrations.test.js @@ -19,1312 +19,6 @@ import { migrations } from './migrations'; -describe('index-pattern', () => { - describe('6.5.0', () => { - const migrate = doc => migrations['index-pattern']['6.5.0'](doc); - - it('adds "type" and "typeMeta" properties to object when not declared', () => { - expect( - migrate({ - attributes: {}, - }) - ).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "type": undefined, - "typeMeta": undefined, - }, -} -`); - }); - - it('keeps "type" and "typeMeta" properties as is when declared', () => { - expect( - migrate({ - attributes: { - type: '123', - typeMeta: '123', - }, - }) - ).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "type": "123", - "typeMeta": "123", - }, -} -`); - }); - }); - - describe('7.6.0', function() { - const migrate = doc => migrations['index-pattern']['7.6.0'](doc); - - it('should remove the parent property and update the subType prop on every field that has them', () => { - const input = { - attributes: { - title: 'test', - fields: - '[{"name":"customer_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":"multi","parent":"customer_name"}]', - }, - }; - const expected = { - attributes: { - title: 'test', - fields: - '[{"name":"customer_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_name"}}}]', - }, - }; - - expect(migrate(input)).toEqual(expected); - }); - }); -}); - -describe('visualization', () => { - describe('date histogram time zone removal', () => { - const migrate = doc => migrations.visualization['6.7.2'](doc); - let doc; - beforeEach(() => { - doc = { - attributes: { - visState: JSON.stringify({ - aggs: [ - { - enabled: true, - id: '1', - params: { - // Doesn't make much sense but we want to test it's not removing it from anything else - time_zone: 'Europe/Berlin', - }, - schema: 'metric', - type: 'count', - }, - { - enabled: true, - id: '2', - params: { - customInterval: '2h', - drop_partials: false, - extended_bounds: {}, - field: 'timestamp', - time_zone: 'Europe/Berlin', - interval: 'auto', - min_doc_count: 1, - useNormalizedEsInterval: true, - }, - schema: 'segment', - type: 'date_histogram', - }, - { - enabled: true, - id: '4', - params: { - customInterval: '2h', - drop_partials: false, - extended_bounds: {}, - field: 'timestamp', - interval: 'auto', - min_doc_count: 1, - useNormalizedEsInterval: true, - }, - schema: 'segment', - type: 'date_histogram', - }, - { - enabled: true, - id: '3', - params: { - customBucket: { - enabled: true, - id: '1-bucket', - params: { - customInterval: '2h', - drop_partials: false, - extended_bounds: {}, - field: 'timestamp', - interval: 'auto', - min_doc_count: 1, - time_zone: 'Europe/Berlin', - useNormalizedEsInterval: true, - }, - type: 'date_histogram', - }, - customMetric: { - enabled: true, - id: '1-metric', - params: {}, - type: 'count', - }, - }, - schema: 'metric', - type: 'max_bucket', - }, - ], - }), - }, - }; - }); - - it('should remove time_zone from date_histogram aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[1]).not.toHaveProperty('params.time_zone'); - }); - - it('should not remove time_zone from non date_histogram aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[0]).toHaveProperty('params.time_zone'); - }); - - it('should remove time_zone from nested aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[3]).not.toHaveProperty('params.customBucket.params.time_zone'); - }); - - it('should not fail on date histograms without a time_zone', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[2]).not.toHaveProperty('params.time_zone'); - }); - - it('should be able to apply the migration twice, since we need it for 6.7.2 and 7.0.1', () => { - const migratedDoc = migrate(migrate(doc)); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[1]).not.toHaveProperty('params.time_zone'); - expect(aggs[0]).toHaveProperty('params.time_zone'); - expect(aggs[3]).not.toHaveProperty('params.customBucket.params.time_zone'); - expect(aggs[2]).not.toHaveProperty('params.time_zone'); - }); - }); - - describe('7.0.0', () => { - const migrate = doc => migrations.visualization['7.0.0'](doc); - const generateDoc = ({ type, aggs }) => ({ - attributes: { - title: 'My Vis', - description: 'This is my super cool vis.', - visState: JSON.stringify({ type, aggs }), - uiStateJSON: '{}', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - }, - }); - - it('does not throw error on empty object', () => { - const migratedDoc = migrate({ - attributes: { - visState: '{}', - }, - }); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "visState": "{}", - }, - "references": Array [], -} -`); - }); - - it('skips errors when searchSourceJSON is null', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: null, - }, - savedSearchId: '123', - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": null, - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('skips errors when searchSourceJSON is undefined', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: undefined, - }, - savedSearchId: '123', - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": undefined, - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('skips error when searchSourceJSON is not a string', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: 123, - }, - savedSearchId: '123', - }, - }; - expect(migrate(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": 123, - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('skips error when searchSourceJSON is invalid json', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: '{abc123}', - }, - savedSearchId: '123', - }, - }; - expect(migrate(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{abc123}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('skips error when "index" and "filter" is missing from searchSourceJSON', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ bar: true }), - }, - savedSearchId: '123', - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('extracts "index" attribute from doc', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }), - }, - savedSearchId: '123', - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern", - }, - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('extracts index patterns from the filter', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - bar: true, - filter: [ - { - meta: { index: 'my-index', foo: true }, - }, - ], - }), - }, - savedSearchId: '123', - }, - }; - const migratedDoc = migrate(doc); - - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "my-index", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern", - }, - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('extracts index patterns from controls', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - foo: true, - visState: JSON.stringify({ - bar: false, - params: { - controls: [ - { - bar: true, - indexPattern: 'pattern*', - }, - { - foo: true, - }, - ], - }, - }), - }, - }; - const migratedDoc = migrate(doc); - - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "visState": "{\\"bar\\":false,\\"params\\":{\\"controls\\":[{\\"bar\\":true,\\"indexPatternRefName\\":\\"control_0_index_pattern\\"},{\\"foo\\":true}]}}", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "control_0_index_pattern", - "type": "index-pattern", - }, - ], - "type": "visualization", -} -`); - }); - - it('skips extracting savedSearchId when missing', () => { - const doc = { - id: '1', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{}", - }, - "visState": "{}", - }, - "id": "1", - "references": Array [], -} -`); - }); - - it('extract savedSearchId from doc', () => { - const doc = { - id: '1', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - savedSearchId: '123', - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], -} -`); - }); - - it('delete savedSearchId when empty string in doc', () => { - const doc = { - id: '1', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - savedSearchId: '', - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{}", - }, - "visState": "{}", - }, - "id": "1", - "references": Array [], -} -`); - }); - - it('should return a new object if vis is table and has multiple split aggs', () => { - const aggs = [ - { - id: '1', - schema: 'metric', - params: {}, - }, - { - id: '2', - schema: 'split', - params: { foo: 'bar', row: true }, - }, - { - id: '3', - schema: 'split', - params: { hey: 'ya', row: false }, - }, - ]; - const tableDoc = generateDoc({ type: 'table', aggs }); - const expected = tableDoc; - const actual = migrate(tableDoc); - expect(actual).not.toBe(expected); - }); - - it('should not touch any vis that is not table', () => { - const aggs = []; - const pieDoc = generateDoc({ type: 'pie', aggs }); - const expected = pieDoc; - const actual = migrate(pieDoc); - expect(actual).toBe(expected); - }); - - it('should not change values in any vis that is not table', () => { - const aggs = [ - { - id: '1', - schema: 'metric', - params: {}, - }, - { - id: '2', - schema: 'split', - params: { foo: 'bar', row: true }, - }, - { - id: '3', - schema: 'segment', - params: { hey: 'ya' }, - }, - ]; - const pieDoc = generateDoc({ type: 'pie', aggs }); - const expected = pieDoc; - const actual = migrate(pieDoc); - expect(actual).toEqual(expected); - }); - - it('should not touch table vis if there are not multiple split aggs', () => { - const aggs = [ - { - id: '1', - schema: 'metric', - params: {}, - }, - { - id: '2', - schema: 'split', - params: { foo: 'bar', row: true }, - }, - ]; - const tableDoc = generateDoc({ type: 'table', aggs }); - const expected = tableDoc; - const actual = migrate(tableDoc); - expect(actual).toBe(expected); - }); - - it('should change all split aggs to `bucket` except the first', () => { - const aggs = [ - { - id: '1', - schema: 'metric', - params: {}, - }, - { - id: '2', - schema: 'split', - params: { foo: 'bar', row: true }, - }, - { - id: '3', - schema: 'split', - params: { hey: 'ya', row: false }, - }, - { - id: '4', - schema: 'bucket', - params: { heyyy: 'yaaa' }, - }, - ]; - const expected = ['metric', 'split', 'bucket', 'bucket']; - const migrated = migrate(generateDoc({ type: 'table', aggs })); - const actual = JSON.parse(migrated.attributes.visState); - expect(actual.aggs.map(agg => agg.schema)).toEqual(expected); - }); - - it('should remove `rows` param from any aggs that are not `split`', () => { - const aggs = [ - { - id: '1', - schema: 'metric', - params: {}, - }, - { - id: '2', - schema: 'split', - params: { foo: 'bar', row: true }, - }, - { - id: '3', - schema: 'split', - params: { hey: 'ya', row: false }, - }, - ]; - const expected = [{}, { foo: 'bar', row: true }, { hey: 'ya' }]; - const migrated = migrate(generateDoc({ type: 'table', aggs })); - const actual = JSON.parse(migrated.attributes.visState); - expect(actual.aggs.map(agg => agg.params)).toEqual(expected); - }); - - it('should throw with a reference to the doc name if something goes wrong', () => { - const doc = { - attributes: { - title: 'My Vis', - description: 'This is my super cool vis.', - visState: '!/// Intentionally malformed JSON ///!', - uiStateJSON: '{}', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - }, - }; - expect(() => migrate(doc)).toThrowError(/My Vis/); - }); - }); - - describe('date histogram custom interval removal', () => { - const migrate = doc => migrations.visualization['7.2.0'](doc); - let doc; - beforeEach(() => { - doc = { - attributes: { - visState: JSON.stringify({ - aggs: [ - { - enabled: true, - id: '1', - params: { - customInterval: '1h', - }, - schema: 'metric', - type: 'count', - }, - { - enabled: true, - id: '2', - params: { - customInterval: '2h', - drop_partials: false, - extended_bounds: {}, - field: 'timestamp', - interval: 'auto', - min_doc_count: 1, - useNormalizedEsInterval: true, - }, - schema: 'segment', - type: 'date_histogram', - }, - { - enabled: true, - id: '4', - params: { - customInterval: '2h', - drop_partials: false, - extended_bounds: {}, - field: 'timestamp', - interval: 'custom', - min_doc_count: 1, - useNormalizedEsInterval: true, - }, - schema: 'segment', - type: 'date_histogram', - }, - { - enabled: true, - id: '3', - params: { - customBucket: { - enabled: true, - id: '1-bucket', - params: { - customInterval: '2h', - drop_partials: false, - extended_bounds: {}, - field: 'timestamp', - interval: 'custom', - min_doc_count: 1, - useNormalizedEsInterval: true, - }, - type: 'date_histogram', - }, - customMetric: { - enabled: true, - id: '1-metric', - params: {}, - type: 'count', - }, - }, - schema: 'metric', - type: 'max_bucket', - }, - ], - }), - }, - }; - }); - - it('should remove customInterval from date_histogram aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[1]).not.toHaveProperty('params.customInterval'); - }); - - it('should not change interval from date_histogram aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[1].params.interval).toBe( - JSON.parse(doc.attributes.visState).aggs[1].params.interval - ); - }); - - it('should not remove customInterval from non date_histogram aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[0]).toHaveProperty('params.customInterval'); - }); - - it('should set interval with customInterval value and remove customInterval when interval equals "custom"', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[2].params.interval).toBe( - JSON.parse(doc.attributes.visState).aggs[2].params.customInterval - ); - expect(aggs[2]).not.toHaveProperty('params.customInterval'); - }); - - it('should remove customInterval from nested aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[3]).not.toHaveProperty('params.customBucket.params.customInterval'); - }); - - it('should remove customInterval from nested aggregations and set interval with customInterval value', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[3].params.customBucket.params.interval).toBe( - JSON.parse(doc.attributes.visState).aggs[3].params.customBucket.params.customInterval - ); - expect(aggs[3]).not.toHaveProperty('params.customBucket.params.customInterval'); - }); - - it('should not fail on date histograms without a customInterval', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[3]).not.toHaveProperty('params.customInterval'); - }); - }); - describe('7.3.0', () => { - const logMsgArr = []; - const logger = { - warning: msg => logMsgArr.push(msg), - }; - const migrate = doc => migrations.visualization['7.3.0'](doc, logger); - - it('migrates type = gauge verticalSplit: false to alignment: vertical', () => { - const migratedDoc = migrate({ - attributes: { - visState: JSON.stringify({ type: 'gauge', params: { gauge: { verticalSplit: false } } }), - }, - }); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"horizontal\\"}}}", - }, -} -`); - }); - - it('migrates type = gauge verticalSplit: false to alignment: horizontal', () => { - const migratedDoc = migrate({ - attributes: { - visState: JSON.stringify({ type: 'gauge', params: { gauge: { verticalSplit: true } } }), - }, - }); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"vertical\\"}}}", - }, -} -`); - }); - - it('doesnt migrate type = gauge containing invalid visState object, adds message to log', () => { - const migratedDoc = migrate({ - attributes: { - visState: JSON.stringify({ type: 'gauge' }), - }, - }); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "visState": "{\\"type\\":\\"gauge\\"}", - }, -} -`); - expect(logMsgArr).toMatchInlineSnapshot(` -Array [ - "Exception @ migrateGaugeVerticalSplitToAlignment! TypeError: Cannot read property 'gauge' of undefined", - "Exception @ migrateGaugeVerticalSplitToAlignment! Payload: {\\"type\\":\\"gauge\\"}", -] -`); - }); - - describe('filters agg query migration', () => { - const doc = { - attributes: { - visState: JSON.stringify({ - aggs: [ - { - type: 'filters', - params: { - filters: [ - { - input: { - query: 'response:200', - }, - label: '', - }, - { - input: { - query: 'response:404', - }, - label: 'bad response', - }, - { - input: { - query: { - exists: { - field: 'phpmemory', - }, - }, - }, - label: '', - }, - ], - }, - }, - ], - }), - }, - }; - - it('should add language property to filters without one, assuming lucene', () => { - const migrationResult = migrate(doc); - expect(migrationResult).toEqual({ - attributes: { - visState: JSON.stringify({ - aggs: [ - { - type: 'filters', - params: { - filters: [ - { - input: { - query: 'response:200', - language: 'lucene', - }, - label: '', - }, - { - input: { - query: 'response:404', - language: 'lucene', - }, - label: 'bad response', - }, - { - input: { - query: { - exists: { - field: 'phpmemory', - }, - }, - language: 'lucene', - }, - label: '', - }, - ], - }, - }, - ], - }), - }, - }); - }); - }); - - describe('replaceMovAvgToMovFn()', () => { - let doc; - - beforeEach(() => { - doc = { - attributes: { - title: 'VIS', - visState: `{"title":"VIS","type":"metrics","params":{"id":"61ca57f0-469d-11e7-af02-69e470af7417", - "type":"timeseries","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(0,156,224,1)", - "split_mode":"terms","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"count", - "numerator":"FlightDelay:true"},{"settings":"","minimize":0,"window":5,"model": - "holt_winters","id":"23054fe0-8915-11e9-9b86-d3f94982620f","type":"moving_average","field": - "61ca57f2-469d-11e7-af02-69e470af7417","predict":1}],"separate_axis":0,"axis_position":"right", - "formatter":"number","chart_type":"line","line_width":"2","point_size":"0","fill":0.5,"stacked":"none", - "label":"Percent Delays","terms_size":"2","terms_field":"OriginCityName"}],"time_field":"timestamp", - "index_pattern":"kibana_sample_data_flights","interval":">=12h","axis_position":"left","axis_formatter": - "number","show_legend":1,"show_grid":1,"annotations":[{"fields":"FlightDelay,Cancelled,Carrier", - "template":"{{Carrier}}: Flight Delayed and Cancelled!","index_pattern":"kibana_sample_data_flights", - "query_string":"FlightDelay:true AND Cancelled:true","id":"53b7dff0-4c89-11e8-a66a-6989ad5a0a39", - "color":"rgba(0,98,177,1)","time_field":"timestamp","icon":"fa-exclamation-triangle", - "ignore_global_filters":1,"ignore_panel_filters":1,"hidden":true}],"legend_position":"bottom", - "axis_scale":"normal","default_index_pattern":"kibana_sample_data_flights","default_timefield":"timestamp"}, - "aggs":[]}`, - }, - migrationVersion: { - visualization: '7.2.0', - }, - type: 'visualization', - }; - }); - - test('should add some necessary moving_fn fields', () => { - const migratedDoc = migrate(doc); - const visState = JSON.parse(migratedDoc.attributes.visState); - const metric = visState.params.series[0].metrics[1]; - - expect(metric).toHaveProperty('model_type'); - expect(metric).toHaveProperty('alpha'); - expect(metric).toHaveProperty('beta'); - expect(metric).toHaveProperty('gamma'); - expect(metric).toHaveProperty('period'); - expect(metric).toHaveProperty('multiplicative'); - }); - }); - }); - describe('7.3.0 tsvb', () => { - const migrate = doc => migrations.visualization['7.3.0'](doc); - const generateDoc = ({ params }) => ({ - attributes: { - title: 'My Vis', - description: 'This is my super cool vis.', - visState: JSON.stringify({ params }), - uiStateJSON: '{}', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - }, - }); - it('should change series item filters from a string into an object', () => { - const params = { type: 'metric', series: [{ filter: 'Filter Bytes Test:>1000' }] }; - const testDoc1 = generateDoc({ params }); - const migratedTestDoc1 = migrate(testDoc1); - const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; - expect(series[0].filter).toHaveProperty('query'); - expect(series[0].filter).toHaveProperty('language'); - }); - it('should not change a series item filter string in the object after migration', () => { - const markdownParams = { - type: 'markdown', - series: [ - { - filter: 'Filter Bytes Test:>1000', - split_filters: [{ filter: 'bytes:>1000' }], - }, - ], - }; - const markdownDoc = generateDoc({ params: markdownParams }); - const migratedMarkdownDoc = migrate(markdownDoc); - const markdownSeries = JSON.parse(migratedMarkdownDoc.attributes.visState).params.series; - expect(markdownSeries[0].filter.query).toBe( - JSON.parse(markdownDoc.attributes.visState).params.series[0].filter - ); - expect(markdownSeries[0].split_filters[0].filter.query).toBe( - JSON.parse(markdownDoc.attributes.visState).params.series[0].split_filters[0].filter - ); - }); - it('should change series item filters from a string into an object for all filters', () => { - const params = { - type: 'timeseries', - filter: 'bytes:>1000', - series: [ - { - filter: 'Filter Bytes Test:>1000', - split_filters: [{ filter: 'bytes:>1000' }], - }, - ], - annotations: [{ query_string: 'bytes:>1000' }], - }; - const timeSeriesDoc = generateDoc({ params: params }); - const migratedtimeSeriesDoc = migrate(timeSeriesDoc); - const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; - expect(Object.keys(timeSeriesParams.series[0].filter)).toEqual( - expect.arrayContaining(['query', 'language']) - ); - expect(Object.keys(timeSeriesParams.series[0].split_filters[0].filter)).toEqual( - expect.arrayContaining(['query', 'language']) - ); - expect(Object.keys(timeSeriesParams.annotations[0].query_string)).toEqual( - expect.arrayContaining(['query', 'language']) - ); - }); - it('should not fail on a metric visualization without a filter in a series item', () => { - const params = { type: 'metric', series: [{}, {}, {}] }; - const testDoc1 = generateDoc({ params }); - const migratedTestDoc1 = migrate(testDoc1); - const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; - expect(series[2]).not.toHaveProperty('filter.query'); - }); - it('should not migrate a visualization of unknown type', () => { - const params = { type: 'unknown', series: [{ filter: 'foo:bar' }] }; - const doc = generateDoc({ params }); - const migratedDoc = migrate(doc); - const series = JSON.parse(migratedDoc.attributes.visState).params.series; - expect(series[0].filter).toEqual(params.series[0].filter); - }); - }); - - describe('7.3.1', () => { - const migrate = migrations.visualization['7.3.1']; - - it('should migrate filters agg query string queries', () => { - const state = { - aggs: [ - { type: 'count', params: {} }, - { - type: 'filters', - params: { - filters: [ - { - input: { - query: { - query_string: { query: 'machine.os.keyword:"win 8"' }, - }, - }, - }, - ], - }, - }, - ], - }; - const expected = { - aggs: [ - { type: 'count', params: {} }, - { - type: 'filters', - params: { - filters: [{ input: { query: 'machine.os.keyword:"win 8"' } }], - }, - }, - ], - }; - const migratedDoc = migrate({ attributes: { visState: JSON.stringify(state) } }); - expect(migratedDoc).toEqual({ attributes: { visState: JSON.stringify(expected) } }); - }); - }); - describe('7.4.2 tsvb split_filters migration', () => { - const migrate = doc => migrations.visualization['7.4.2'](doc); - const generateDoc = ({ params }) => ({ - attributes: { - title: 'My Vis', - description: 'This is my super cool vis.', - visState: JSON.stringify({ params }), - uiStateJSON: '{}', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - }, - }); - it('should change series item filters from a string into an object for all filters', () => { - const params = { - type: 'timeseries', - filter: { - query: 'bytes:>1000', - language: 'lucene', - }, - series: [ - { - split_filters: [{ filter: 'bytes:>1000' }], - }, - ], - }; - const timeSeriesDoc = generateDoc({ params: params }); - const migratedtimeSeriesDoc = migrate(timeSeriesDoc); - const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; - expect(Object.keys(timeSeriesParams.filter)).toEqual( - expect.arrayContaining(['query', 'language']) - ); - expect(timeSeriesParams.series[0].split_filters[0].filter).toEqual({ - query: 'bytes:>1000', - language: 'lucene', - }); - }); - it('should change series item split filters when there is no filter item', () => { - const params = { - type: 'timeseries', - filter: { - query: 'bytes:>1000', - language: 'lucene', - }, - series: [ - { - split_filters: [{ filter: 'bytes:>1000' }], - }, - ], - annotations: [ - { - query_string: { - query: 'bytes:>1000', - language: 'lucene', - }, - }, - ], - }; - const timeSeriesDoc = generateDoc({ params: params }); - const migratedtimeSeriesDoc = migrate(timeSeriesDoc); - const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; - expect(timeSeriesParams.series[0].split_filters[0].filter).toEqual({ - query: 'bytes:>1000', - language: 'lucene', - }); - }); - it('should not convert split_filters to objects if there are no split filter filters', () => { - const params = { - type: 'timeseries', - filter: { - query: 'bytes:>1000', - language: 'lucene', - }, - series: [ - { - split_filters: [], - }, - ], - }; - const timeSeriesDoc = generateDoc({ params: params }); - const migratedtimeSeriesDoc = migrate(timeSeriesDoc); - const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; - expect(timeSeriesParams.series[0].split_filters).not.toHaveProperty('query'); - }); - it('should do nothing if a split_filter is already a query:language object', () => { - const params = { - type: 'timeseries', - filter: { - query: 'bytes:>1000', - language: 'lucene', - }, - series: [ - { - split_filters: [ - { - filter: { - query: 'bytes:>1000', - language: 'lucene', - }, - }, - ], - }, - ], - annotations: [ - { - query_string: { - query: 'bytes:>1000', - language: 'lucene', - }, - }, - ], - }; - const timeSeriesDoc = generateDoc({ params: params }); - const migratedtimeSeriesDoc = migrate(timeSeriesDoc); - const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; - expect(timeSeriesParams.series[0].split_filters[0].filter.query).toEqual('bytes:>1000'); - expect(timeSeriesParams.series[0].split_filters[0].filter.language).toEqual('lucene'); - }); - }); -}); - describe('dashboard', () => { describe('7.0.0', () => { const migration = migrations.dashboard['7.0.0']; @@ -1751,271 +445,3 @@ Object { }); }); }); - -describe('search', () => { - describe('7.0.0', () => { - const migration = migrations.search['7.0.0']; - - test('skips errors when searchSourceJSON is null', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: null, - }, - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": null, - }, - }, - "id": "123", - "references": Array [], - "type": "search", -} -`); - }); - - test('skips errors when searchSourceJSON is undefined', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: undefined, - }, - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": undefined, - }, - }, - "id": "123", - "references": Array [], - "type": "search", -} -`); - }); - - test('skips error when searchSourceJSON is not a string', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: 123, - }, - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": 123, - }, - }, - "id": "123", - "references": Array [], - "type": "search", -} -`); - }); - - test('skips error when searchSourceJSON is invalid json', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: '{abc123}', - }, - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{abc123}", - }, - }, - "id": "123", - "references": Array [], - "type": "search", -} -`); - }); - - test('skips error when "index" and "filter" is missing from searchSourceJSON', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ bar: true }), - }, - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true}", - }, - }, - "id": "123", - "references": Array [], - "type": "search", -} -`); - }); - - test('extracts "index" attribute from doc', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }), - }, - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", - }, - }, - "id": "123", - "references": Array [ - Object { - "id": "pattern*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern", - }, - ], - "type": "search", -} -`); - }); - - test('extracts index patterns from filter', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - bar: true, - filter: [ - { - meta: { - foo: true, - index: 'my-index', - }, - }, - ], - }), - }, - }, - }; - const migratedDoc = migration(doc); - - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", - }, - }, - "id": "123", - "references": Array [ - Object { - "id": "my-index", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern", - }, - ], - "type": "search", -} -`); - }); - }); - - describe('7.4.0', function() { - const migration = migrations.search['7.4.0']; - - test('transforms one dimensional sort arrays into two dimensional arrays', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - sort: ['bytes', 'desc'], - }, - }; - - const expected = { - id: '123', - type: 'search', - attributes: { - sort: [['bytes', 'desc']], - }, - }; - - const migratedDoc = migration(doc); - - expect(migratedDoc).toEqual(expected); - }); - - test("doesn't modify search docs that already have two dimensional sort arrays", () => { - const doc = { - id: '123', - type: 'search', - attributes: { - sort: [['bytes', 'desc']], - }, - }; - - const migratedDoc = migration(doc); - - expect(migratedDoc).toEqual(doc); - }); - - test("doesn't modify search docs that have no sort array", () => { - const doc = { - id: '123', - type: 'search', - attributes: {}, - }; - - const migratedDoc = migration(doc); - - expect(migratedDoc).toEqual(doc); - }); - }); -}); diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 78f34e21b9e4..58e8fbae9f9e 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -19,10 +19,13 @@ import { CoreSetup, Plugin } from 'kibana/server'; import { registerRoutes } from './routes'; +import { indexPatternSavedObjectType } from '../saved_objects'; export class IndexPatternsService implements Plugin { - public setup({ http }: CoreSetup) { - registerRoutes(http); + public setup(core: CoreSetup) { + core.savedObjects.registerType(indexPatternSavedObjectType); + + registerRoutes(core.http); } public start() {} diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 616e65ad872a..efb8759e7bea 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -21,6 +21,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../.. import { IndexPatternsService } from './index_patterns'; import { ISearchSetup } from './search'; import { SearchService } from './search/search_service'; +import { QueryService } from './query/query_service'; import { ScriptsService } from './scripts'; import { KqlTelemetryService } from './kql_telemetry'; import { UsageCollectionSetup } from '../../usage_collection/server'; @@ -47,6 +48,7 @@ export class DataServerPlugin implements Plugin { + public setup(core: CoreSetup) { + core.savedObjects.registerType(querySavedObjectType); + } + + public start() {} +} diff --git a/src/plugins/data/server/saved_objects/index.ts b/src/plugins/data/server/saved_objects/index.ts new file mode 100644 index 000000000000..5d980974474d --- /dev/null +++ b/src/plugins/data/server/saved_objects/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. + */ + +export { searchSavedObjectType } from './search'; +export { querySavedObjectType } from './query'; +export { indexPatternSavedObjectType } from './index_patterns'; diff --git a/src/plugins/data/server/saved_objects/index_pattern_migrations.test.ts b/src/plugins/data/server/saved_objects/index_pattern_migrations.test.ts new file mode 100644 index 000000000000..b1410e249866 --- /dev/null +++ b/src/plugins/data/server/saved_objects/index_pattern_migrations.test.ts @@ -0,0 +1,97 @@ +/* + * 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 { SavedObjectMigrationContext } from 'kibana/server'; +import { indexPatternSavedObjectTypeMigrations } from './index_pattern_migrations'; + +const savedObjectMigrationContext = (null as unknown) as SavedObjectMigrationContext; + +describe('migration index-pattern', () => { + describe('6.5.0', () => { + const migrationFn = indexPatternSavedObjectTypeMigrations['6.5.0']; + + test('adds "type" and "typeMeta" properties to object when not declared', () => { + expect( + migrationFn( + { + type: 'index-pattern', + attributes: {}, + }, + savedObjectMigrationContext + ) + ).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "type": undefined, + "typeMeta": undefined, + }, + "type": "index-pattern", +} +`); + }); + + test('keeps "type" and "typeMeta" properties as is when declared', () => { + expect( + migrationFn( + { + type: 'index-pattern', + attributes: { + type: '123', + typeMeta: '123', + }, + }, + savedObjectMigrationContext + ) + ).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "type": "123", + "typeMeta": "123", + }, + "type": "index-pattern", +} +`); + }); + }); + + describe('7.6.0', () => { + const migrationFn = indexPatternSavedObjectTypeMigrations['7.6.0']; + + test('should remove the parent property and update the subType prop on every field that has them', () => { + const input = { + type: 'index-pattern', + attributes: { + title: 'test', + fields: + '[{"name":"customer_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":"multi","parent":"customer_name"}]', + }, + }; + const expected = { + type: 'index-pattern', + attributes: { + title: 'test', + fields: + '[{"name":"customer_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_name"}}}]', + }, + }; + + expect(migrationFn(input, savedObjectMigrationContext)).toEqual(expected); + }); + }); +}); diff --git a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts new file mode 100644 index 000000000000..7a16386ea484 --- /dev/null +++ b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts @@ -0,0 +1,60 @@ +/* + * 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 { flow, omit } from 'lodash'; +import { SavedObjectMigrationFn } from 'kibana/server'; + +const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => ({ + ...doc, + attributes: { + ...doc.attributes, + type: doc.attributes.type || undefined, + typeMeta: doc.attributes.typeMeta || undefined, + }, +}); + +const migrateSubTypeAndParentFieldProperties: SavedObjectMigrationFn = doc => { + if (!doc.attributes.fields) return doc; + + const fieldsString = doc.attributes.fields; + const fields = JSON.parse(fieldsString) as any[]; + const migratedFields = fields.map(field => { + if (field.subType === 'multi') { + return { + ...omit(field, 'parent'), + subType: { multi: { parent: field.parent } }, + }; + } + + return field; + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + fields: JSON.stringify(migratedFields), + }, + }; +}; + +export const indexPatternSavedObjectTypeMigrations = { + '6.5.0': flow(migrateAttributeTypeAndAttributeTypeMeta), + '7.6.0': flow(migrateSubTypeAndParentFieldProperties), +}; diff --git a/src/plugins/data/server/saved_objects/index_patterns.ts b/src/plugins/data/server/saved_objects/index_patterns.ts new file mode 100644 index 000000000000..9838071eee5a --- /dev/null +++ b/src/plugins/data/server/saved_objects/index_patterns.ts @@ -0,0 +1,58 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; +import { indexPatternSavedObjectTypeMigrations } from './index_pattern_migrations'; + +export const indexPatternSavedObjectType: SavedObjectsType = { + name: 'index-pattern', + hidden: false, + namespaceAgnostic: false, + management: { + icon: 'indexPatternApp', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getEditUrl(obj) { + return `/management/kibana/index_patterns/${encodeURIComponent(obj.id)}`; + }, + getInAppUrl(obj) { + return { + path: `/app/kibana#/management/kibana/index_patterns/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'management.kibana.index_patterns', + }; + }, + }, + mappings: { + properties: { + fieldFormatMap: { type: 'text' }, + fields: { type: 'text' }, + intervalName: { type: 'keyword' }, + notExpandable: { type: 'boolean' }, + sourceFilters: { type: 'text' }, + timeFieldName: { type: 'keyword' }, + title: { type: 'text' }, + type: { type: 'keyword' }, + typeMeta: { type: 'keyword' }, + }, + }, + migrations: indexPatternSavedObjectTypeMigrations, +}; diff --git a/src/plugins/data/server/saved_objects/query.ts b/src/plugins/data/server/saved_objects/query.ts new file mode 100644 index 000000000000..ff0a6cfde811 --- /dev/null +++ b/src/plugins/data/server/saved_objects/query.ts @@ -0,0 +1,52 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; + +export const querySavedObjectType: SavedObjectsType = { + name: 'query', + hidden: false, + namespaceAgnostic: false, + management: { + icon: 'search', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + return { + path: `/app/kibana#/discover?_a=(savedQuery:'${encodeURIComponent(obj.id)}')`, + uiCapabilitiesPath: 'discover.show', + }; + }, + }, + mappings: { + properties: { + title: { type: 'text' }, + description: { type: 'text' }, + query: { + properties: { language: { type: 'keyword' }, query: { type: 'keyword', index: false } }, + }, + filters: { type: 'object', enabled: false }, + timefilter: { type: 'object', enabled: false }, + }, + }, + migrations: {}, +}; diff --git a/src/plugins/data/server/saved_objects/search.ts b/src/plugins/data/server/saved_objects/search.ts new file mode 100644 index 000000000000..8b30ff7d0820 --- /dev/null +++ b/src/plugins/data/server/saved_objects/search.ts @@ -0,0 +1,60 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; +import { searchSavedObjectTypeMigrations } from './search_migrations'; + +export const searchSavedObjectType: SavedObjectsType = { + name: 'search', + hidden: false, + namespaceAgnostic: false, + management: { + icon: 'discoverApp', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getEditUrl(obj) { + return `/management/kibana/objects/savedSearches/${encodeURIComponent(obj.id)}`; + }, + getInAppUrl(obj) { + return { + path: `/app/kibana#/discover/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'discover.show', + }; + }, + }, + mappings: { + properties: { + columns: { type: 'keyword' }, + description: { type: 'text' }, + hits: { type: 'integer' }, + kibanaSavedObjectMeta: { + properties: { + searchSourceJSON: { type: 'text' }, + }, + }, + sort: { type: 'keyword' }, + title: { type: 'text' }, + version: { type: 'integer' }, + }, + }, + migrations: searchSavedObjectTypeMigrations, +}; diff --git a/src/plugins/data/server/saved_objects/search_migrations.test.ts b/src/plugins/data/server/saved_objects/search_migrations.test.ts new file mode 100644 index 000000000000..7fdf2e14aefe --- /dev/null +++ b/src/plugins/data/server/saved_objects/search_migrations.test.ts @@ -0,0 +1,299 @@ +/* + * 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 { SavedObjectMigrationContext } from 'kibana/server'; +import { searchSavedObjectTypeMigrations } from './search_migrations'; + +const savedObjectMigrationContext = (null as unknown) as SavedObjectMigrationContext; + +describe('migration search', () => { + describe('7.0.0', () => { + const migrationFn = searchSavedObjectTypeMigrations['7.0.0']; + + test('skips errors when searchSourceJSON is null', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: null, + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": null, + }, + }, + "id": "123", + "references": Array [], + "type": "search", +} +`); + }); + + test('skips errors when searchSourceJSON is undefined', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: undefined, + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": undefined, + }, + }, + "id": "123", + "references": Array [], + "type": "search", +} +`); + }); + + test('skips error when searchSourceJSON is not a string', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: 123, + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": 123, + }, + }, + "id": "123", + "references": Array [], + "type": "search", +} +`); + }); + + test('skips error when searchSourceJSON is invalid json', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: '{abc123}', + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{abc123}", + }, + }, + "id": "123", + "references": Array [], + "type": "search", +} +`); + }); + + test('skips error when "index" and "filter" is missing from searchSourceJSON', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ bar: true }), + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true}", + }, + }, + "id": "123", + "references": Array [], + "type": "search", +} +`); + }); + + test('extracts "index" attribute from doc', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }), + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", + }, + }, + "id": "123", + "references": Array [ + Object { + "id": "pattern*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern", + }, + ], + "type": "search", +} +`); + }); + + test('extracts index patterns from filter', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + bar: true, + filter: [ + { + meta: { + foo: true, + index: 'my-index', + }, + }, + ], + }), + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", + }, + }, + "id": "123", + "references": Array [ + Object { + "id": "my-index", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern", + }, + ], + "type": "search", +} +`); + }); + }); + + describe('7.4.0', function() { + const migrationFn = searchSavedObjectTypeMigrations['7.4.0']; + + test('transforms one dimensional sort arrays into two dimensional arrays', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + sort: ['bytes', 'desc'], + }, + }; + + const expected = { + id: '123', + type: 'search', + attributes: { + sort: [['bytes', 'desc']], + }, + }; + + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toEqual(expected); + }); + + test("doesn't modify search docs that already have two dimensional sort arrays", () => { + const doc = { + id: '123', + type: 'search', + attributes: { + sort: [['bytes', 'desc']], + }, + }; + + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toEqual(doc); + }); + + test("doesn't modify search docs that have no sort array", () => { + const doc = { + id: '123', + type: 'search', + attributes: {}, + }; + + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toEqual(doc); + }); + }); +}); diff --git a/src/plugins/data/server/saved_objects/search_migrations.ts b/src/plugins/data/server/saved_objects/search_migrations.ts new file mode 100644 index 000000000000..db545e52ce17 --- /dev/null +++ b/src/plugins/data/server/saved_objects/search_migrations.ts @@ -0,0 +1,92 @@ +/* + * 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 { flow, get } from 'lodash'; +import { SavedObjectMigrationFn } from 'kibana/server'; + +const migrateIndexPattern: SavedObjectMigrationFn = doc => { + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + if (typeof searchSourceJSON !== 'string') { + return doc; + } + let searchSource; + try { + searchSource = JSON.parse(searchSourceJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc; + } + + if (searchSource.index && Array.isArray(doc.references)) { + searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + doc.references.push({ + name: searchSource.indexRefName, + type: 'index-pattern', + id: searchSource.index, + }); + delete searchSource.index; + } + if (searchSource.filter) { + searchSource.filter.forEach((filterRow: any, i: number) => { + if (!filterRow.meta || !filterRow.meta.index || !Array.isArray(doc.references)) { + return; + } + filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + doc.references.push({ + name: filterRow.meta.indexRefName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + delete filterRow.meta.index; + }); + } + + doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); + + return doc; +}; + +const setNewReferences: SavedObjectMigrationFn = (doc, context) => { + doc.references = doc.references || []; + // Migrate index pattern + return migrateIndexPattern(doc, context); +}; + +const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => { + const sort = get(doc, 'attributes.sort'); + if (!sort) return doc; + + // Don't do anything if we already have a two dimensional array + if (Array.isArray(sort) && sort.length > 0 && Array.isArray(sort[0])) { + return doc; + } + + return { + ...doc, + attributes: { + ...doc.attributes, + sort: [doc.attributes.sort], + }, + }; +}; + +export const searchSavedObjectTypeMigrations = { + '7.0.0': flow(setNewReferences), + '7.4.0': flow(migrateSearchSortToNestedArray), +}; diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 46f90e3c6fc6..5ee19cd3df19 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -34,6 +34,8 @@ import { import { IRouteHandlerSearchContext } from './i_route_handler_search_context'; import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './es_search'; +import { searchSavedObjectType } from '../saved_objects'; + declare module 'kibana/server' { interface RequestHandlerContext { search?: IRouteHandlerSearchContext; @@ -53,6 +55,8 @@ export class SearchService implements Plugin { this.contextContainer = core.context.createContextContainer(); + core.savedObjects.registerType(searchSavedObjectType); + core.http.registerRouteHandlerContext<'search'>('search', context => { return createApi({ caller: context.core.elasticsearch.dataClient.callAsCurrentUser, diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 666df2900c2c..2a2d9bb414c1 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -733,7 +733,7 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/server/index.ts:184:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/plugin.ts:62:14 - (ae-forgotten-export) The symbol "ISearchSetup" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/plugin.ts:64:14 - (ae-forgotten-export) The symbol "ISearchSetup" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index cf79ce17293d..8e63ea783332 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -1,7 +1,7 @@ { "id": "visualizations", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": [ "expressions" diff --git a/src/plugins/visualizations/server/index.ts b/src/plugins/visualizations/server/index.ts new file mode 100644 index 000000000000..80c10c3945d4 --- /dev/null +++ b/src/plugins/visualizations/server/index.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. + */ + +import { PluginInitializerContext } from '../../../core/server'; +import { VisualizationsPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new VisualizationsPlugin(initializerContext); +} + +export { VisualizationsPluginSetup, VisualizationsPluginStart } from './types'; diff --git a/src/plugins/visualizations/server/plugin.ts b/src/plugins/visualizations/server/plugin.ts new file mode 100644 index 000000000000..79cce6b5867a --- /dev/null +++ b/src/plugins/visualizations/server/plugin.ts @@ -0,0 +1,54 @@ +/* + * 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 { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../core/server'; + +import { visualizationSavedObjectType } from './saved_objects'; + +import { VisualizationsPluginSetup, VisualizationsPluginStart } from './types'; + +export class VisualizationsPlugin + implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('visualizations: Setup'); + + core.savedObjects.registerType(visualizationSavedObjectType); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('visualizations: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/visualizations/server/saved_objects/index.ts b/src/plugins/visualizations/server/saved_objects/index.ts new file mode 100644 index 000000000000..be75f6358245 --- /dev/null +++ b/src/plugins/visualizations/server/saved_objects/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { visualizationSavedObjectType } from './visualization'; diff --git a/src/plugins/visualizations/server/saved_objects/visualization.ts b/src/plugins/visualizations/server/saved_objects/visualization.ts new file mode 100644 index 000000000000..9f4782f3ec73 --- /dev/null +++ b/src/plugins/visualizations/server/saved_objects/visualization.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. + */ + +import { SavedObjectsType } from 'kibana/server'; +import { visualizationSavedObjectTypeMigrations } from './visualization_migrations'; + +export const visualizationSavedObjectType: SavedObjectsType = { + name: 'visualization', + hidden: false, + namespaceAgnostic: false, + management: { + icon: 'visualizeApp', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getEditUrl(obj) { + return `/management/kibana/objects/savedVisualizations/${encodeURIComponent(obj.id)}`; + }, + getInAppUrl(obj) { + return { + path: `/app/kibana#/visualize/edit/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'visualize.show', + }; + }, + }, + mappings: { + properties: { + description: { type: 'text' }, + kibanaSavedObjectMeta: { properties: { searchSourceJSON: { type: 'text' } } }, + savedSearchRefName: { type: 'keyword' }, + title: { type: 'text' }, + uiStateJSON: { type: 'text' }, + version: { type: 'integer' }, + visState: { type: 'text' }, + }, + }, + migrations: visualizationSavedObjectTypeMigrations, +}; diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts new file mode 100644 index 000000000000..02c114bad4e7 --- /dev/null +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts @@ -0,0 +1,1356 @@ +/* + * 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 { visualizationSavedObjectTypeMigrations } from './visualization_migrations'; +import { SavedObjectMigrationContext, SavedObjectMigrationFn } from 'kibana/server'; + +const savedObjectMigrationContext = (null as unknown) as SavedObjectMigrationContext; + +describe('migration visualization', () => { + describe('6.7.2', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['6.7.2']( + doc as Parameters[0], + savedObjectMigrationContext + ); + let doc: any; + + describe('date histogram time zone removal', () => { + beforeEach(() => { + doc = { + attributes: { + visState: JSON.stringify({ + aggs: [ + { + enabled: true, + id: '1', + params: { + // Doesn't make much sense but we want to test it's not removing it from anything else + time_zone: 'Europe/Berlin', + }, + schema: 'metric', + type: 'count', + }, + { + enabled: true, + id: '2', + params: { + customInterval: '2h', + drop_partials: false, + extended_bounds: {}, + field: 'timestamp', + time_zone: 'Europe/Berlin', + interval: 'auto', + min_doc_count: 1, + useNormalizedEsInterval: true, + }, + schema: 'segment', + type: 'date_histogram', + }, + { + enabled: true, + id: '4', + params: { + customInterval: '2h', + drop_partials: false, + extended_bounds: {}, + field: 'timestamp', + interval: 'auto', + min_doc_count: 1, + useNormalizedEsInterval: true, + }, + schema: 'segment', + type: 'date_histogram', + }, + { + enabled: true, + id: '3', + params: { + customBucket: { + enabled: true, + id: '1-bucket', + params: { + customInterval: '2h', + drop_partials: false, + extended_bounds: {}, + field: 'timestamp', + interval: 'auto', + min_doc_count: 1, + time_zone: 'Europe/Berlin', + useNormalizedEsInterval: true, + }, + type: 'date_histogram', + }, + customMetric: { + enabled: true, + id: '1-metric', + params: {}, + type: 'count', + }, + }, + schema: 'metric', + type: 'max_bucket', + }, + ], + }), + }, + } as Parameters[0]; + }); + + it('should remove time_zone from date_histogram aggregations', () => { + const migratedDoc = migrate(doc); + const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; + + expect(aggs[1]).not.toHaveProperty('params.time_zone'); + }); + + it('should not remove time_zone from non date_histogram aggregations', () => { + const migratedDoc = migrate(doc); + const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; + + expect(aggs[0]).toHaveProperty('params.time_zone'); + }); + + it('should remove time_zone from nested aggregations', () => { + const migratedDoc = migrate(doc); + const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; + + expect(aggs[3]).not.toHaveProperty('params.customBucket.params.time_zone'); + }); + + it('should not fail on date histograms without a time_zone', () => { + const migratedDoc = migrate(doc); + const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; + + expect(aggs[2]).not.toHaveProperty('params.time_zone'); + }); + + it('should be able to apply the migration twice, since we need it for 6.7.2 and 7.0.1', () => { + const migratedDoc = migrate(doc); + const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; + + expect(aggs[1]).not.toHaveProperty('params.time_zone'); + expect(aggs[0]).toHaveProperty('params.time_zone'); + expect(aggs[3]).not.toHaveProperty('params.customBucket.params.time_zone'); + expect(aggs[2]).not.toHaveProperty('params.time_zone'); + }); + }); + }); + + describe('7.0.0', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.0.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const generateDoc = (type: any, aggs: any) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: JSON.stringify({ type, aggs }), + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + references: [], + }); + + it('does not throw error on empty object', () => { + const migratedDoc = migrate({ + attributes: { + visState: '{}', + }, + }); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{}", + }, + "references": Array [], +} +`); + }); + + it('skips errors when searchSourceJSON is null', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: null, + }, + savedSearchId: '123', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": null, + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('skips errors when searchSourceJSON is undefined', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: undefined, + }, + savedSearchId: '123', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": undefined, + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('skips error when searchSourceJSON is not a string', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: 123, + }, + savedSearchId: '123', + }, + }; + + expect(migrate(doc)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": 123, + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('skips error when searchSourceJSON is invalid json', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: '{abc123}', + }, + savedSearchId: '123', + }, + }; + + expect(migrate(doc)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{abc123}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('skips error when "index" and "filter" is missing from searchSourceJSON', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ bar: true }), + }, + savedSearchId: '123', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('extracts "index" attribute from doc', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }), + }, + savedSearchId: '123', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern", + }, + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('extracts index patterns from the filter', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + bar: true, + filter: [ + { + meta: { index: 'my-index', foo: true }, + }, + ], + }), + }, + savedSearchId: '123', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "my-index", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern", + }, + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('extracts index patterns from controls', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + foo: true, + visState: JSON.stringify({ + bar: false, + params: { + controls: [ + { + bar: true, + indexPattern: 'pattern*', + }, + { + foo: true, + }, + ], + }, + }), + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "visState": "{\\"bar\\":false,\\"params\\":{\\"controls\\":[{\\"bar\\":true,\\"indexPatternRefName\\":\\"control_0_index_pattern\\"},{\\"foo\\":true}]}}", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "control_0_index_pattern", + "type": "index-pattern", + }, + ], + "type": "visualization", +} +`); + }); + + it('skips extracting savedSearchId when missing', () => { + const doc = { + id: '1', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{}", + }, + "visState": "{}", + }, + "id": "1", + "references": Array [], +} +`); + }); + + it('extract savedSearchId from doc', () => { + const doc = { + id: '1', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + savedSearchId: '123', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], +} +`); + }); + + it('delete savedSearchId when empty string in doc', () => { + const doc = { + id: '1', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + savedSearchId: '', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{}", + }, + "visState": "{}", + }, + "id": "1", + "references": Array [], +} +`); + }); + + it('should return a new object if vis is table and has multiple split aggs', () => { + const aggs = [ + { + id: '1', + schema: 'metric', + params: {}, + }, + { + id: '2', + schema: 'split', + params: { foo: 'bar', row: true }, + }, + { + id: '3', + schema: 'split', + params: { hey: 'ya', row: false }, + }, + ]; + const tableDoc = generateDoc('table', aggs); + const expected = tableDoc; + const actual = migrate(tableDoc); + + expect(actual).not.toEqual(expected); + }); + + it('should not touch any vis that is not table', () => { + const pieDoc = generateDoc('pie', []); + const expected = pieDoc; + const actual = migrate(pieDoc); + + expect(actual).toEqual(expected); + }); + + it('should not change values in any vis that is not table', () => { + const aggs = [ + { + id: '1', + schema: 'metric', + params: {}, + }, + { + id: '2', + schema: 'split', + params: { foo: 'bar', row: true }, + }, + { + id: '3', + schema: 'segment', + params: { hey: 'ya' }, + }, + ]; + const pieDoc = generateDoc('pie', aggs); + const expected = pieDoc; + const actual = migrate(pieDoc); + + expect(actual).toEqual(expected); + }); + + it('should not touch table vis if there are not multiple split aggs', () => { + const aggs = [ + { + id: '1', + schema: 'metric', + params: {}, + }, + { + id: '2', + schema: 'split', + params: { foo: 'bar', row: true }, + }, + ]; + const tableDoc = generateDoc('table', aggs); + const expected = tableDoc; + const actual = migrate(tableDoc); + + expect(actual).toEqual(expected); + }); + + it('should change all split aggs to `bucket` except the first', () => { + const aggs = [ + { + id: '1', + schema: 'metric', + params: {}, + }, + { + id: '2', + schema: 'split', + params: { foo: 'bar', row: true }, + }, + { + id: '3', + schema: 'split', + params: { hey: 'ya', row: false }, + }, + { + id: '4', + schema: 'bucket', + params: { heyyy: 'yaaa' }, + }, + ]; + const expected = ['metric', 'split', 'bucket', 'bucket']; + const migrated = migrate(generateDoc('table', aggs)); + const actual = JSON.parse(migrated.attributes.visState); + + expect(actual.aggs.map((agg: any) => agg.schema)).toEqual(expected); + }); + + it('should remove `rows` param from any aggs that are not `split`', () => { + const aggs = [ + { + id: '1', + schema: 'metric', + params: {}, + }, + { + id: '2', + schema: 'split', + params: { foo: 'bar', row: true }, + }, + { + id: '3', + schema: 'split', + params: { hey: 'ya', row: false }, + }, + ]; + const expected = [{}, { foo: 'bar', row: true }, { hey: 'ya' }]; + const migrated = migrate(generateDoc('table', aggs)); + const actual = JSON.parse(migrated.attributes.visState); + + expect(actual.aggs.map((agg: any) => agg.params)).toEqual(expected); + }); + + it('should throw with a reference to the doc name if something goes wrong', () => { + const doc = { + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: '!/// Intentionally malformed JSON ///!', + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + }; + expect(() => migrate(doc)).toThrowError(/My Vis/); + }); + }); + + describe('7.2.0', () => { + describe('date histogram custom interval removal', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.2.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + let doc: any; + + beforeEach(() => { + doc = { + attributes: { + visState: JSON.stringify({ + aggs: [ + { + enabled: true, + id: '1', + params: { + customInterval: '1h', + }, + schema: 'metric', + type: 'count', + }, + { + enabled: true, + id: '2', + params: { + customInterval: '2h', + drop_partials: false, + extended_bounds: {}, + field: 'timestamp', + interval: 'auto', + min_doc_count: 1, + useNormalizedEsInterval: true, + }, + schema: 'segment', + type: 'date_histogram', + }, + { + enabled: true, + id: '4', + params: { + customInterval: '2h', + drop_partials: false, + extended_bounds: {}, + field: 'timestamp', + interval: 'custom', + min_doc_count: 1, + useNormalizedEsInterval: true, + }, + schema: 'segment', + type: 'date_histogram', + }, + { + enabled: true, + id: '3', + params: { + customBucket: { + enabled: true, + id: '1-bucket', + params: { + customInterval: '2h', + drop_partials: false, + extended_bounds: {}, + field: 'timestamp', + interval: 'custom', + min_doc_count: 1, + useNormalizedEsInterval: true, + }, + type: 'date_histogram', + }, + customMetric: { + enabled: true, + id: '1-metric', + params: {}, + type: 'count', + }, + }, + schema: 'metric', + type: 'max_bucket', + }, + ], + }), + }, + }; + }); + + it('should remove customInterval from date_histogram aggregations', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[1]).not.toHaveProperty('params.customInterval'); + }); + + it('should not change interval from date_histogram aggregations', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[1].params.interval).toBe( + JSON.parse(doc.attributes.visState).aggs[1].params.interval + ); + }); + + it('should not remove customInterval from non date_histogram aggregations', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[0]).toHaveProperty('params.customInterval'); + }); + + it('should set interval with customInterval value and remove customInterval when interval equals "custom"', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[2].params.interval).toBe( + JSON.parse(doc.attributes.visState).aggs[2].params.customInterval + ); + expect(aggs[2]).not.toHaveProperty('params.customInterval'); + }); + + it('should remove customInterval from nested aggregations', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[3]).not.toHaveProperty('params.customBucket.params.customInterval'); + }); + + it('should remove customInterval from nested aggregations and set interval with customInterval value', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[3].params.customBucket.params.interval).toBe( + JSON.parse(doc.attributes.visState).aggs[3].params.customBucket.params.customInterval + ); + expect(aggs[3]).not.toHaveProperty('params.customBucket.params.customInterval'); + }); + + it('should not fail on date histograms without a customInterval', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[3]).not.toHaveProperty('params.customInterval'); + }); + }); + }); + + describe('7.3.0', () => { + const logMsgArr: string[] = []; + const logger = ({ + log: { + warn: (msg: string) => logMsgArr.push(msg), + }, + } as unknown) as SavedObjectMigrationContext; + + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.3.0']( + doc as Parameters[0], + logger + ); + + it('migrates type = gauge verticalSplit: false to alignment: vertical', () => { + const migratedDoc = migrate({ + attributes: { + visState: JSON.stringify({ type: 'gauge', params: { gauge: { verticalSplit: false } } }), + }, + }); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"horizontal\\"}}}", + }, +} +`); + }); + + it('migrates type = gauge verticalSplit: false to alignment: horizontal', () => { + const migratedDoc = migrate({ + attributes: { + visState: JSON.stringify({ type: 'gauge', params: { gauge: { verticalSplit: true } } }), + }, + }); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"vertical\\"}}}", + }, +} +`); + }); + + it('doesnt migrate type = gauge containing invalid visState object, adds message to log', () => { + const migratedDoc = migrate({ + attributes: { + visState: JSON.stringify({ type: 'gauge' }), + }, + }); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\"}", + }, +} +`); + expect(logMsgArr).toMatchInlineSnapshot(` +Array [ + "Exception @ migrateGaugeVerticalSplitToAlignment! TypeError: Cannot read property 'gauge' of undefined", + "Exception @ migrateGaugeVerticalSplitToAlignment! Payload: {\\"type\\":\\"gauge\\"}", +] +`); + }); + + describe('filters agg query migration', () => { + const doc = { + attributes: { + visState: JSON.stringify({ + aggs: [ + { + type: 'filters', + params: { + filters: [ + { + input: { + query: 'response:200', + }, + label: '', + }, + { + input: { + query: 'response:404', + }, + label: 'bad response', + }, + { + input: { + query: { + exists: { + field: 'phpmemory', + }, + }, + }, + label: '', + }, + ], + }, + }, + ], + }), + }, + }; + + it('should add language property to filters without one, assuming lucene', () => { + const migrationResult = migrate(doc); + + expect(migrationResult).toEqual({ + attributes: { + visState: JSON.stringify({ + aggs: [ + { + type: 'filters', + params: { + filters: [ + { + input: { + query: 'response:200', + language: 'lucene', + }, + label: '', + }, + { + input: { + query: 'response:404', + language: 'lucene', + }, + label: 'bad response', + }, + { + input: { + query: { + exists: { + field: 'phpmemory', + }, + }, + language: 'lucene', + }, + label: '', + }, + ], + }, + }, + ], + }), + }, + }); + }); + }); + + describe('replaceMovAvgToMovFn()', () => { + let doc: any; + + beforeEach(() => { + doc = { + attributes: { + title: 'VIS', + visState: `{"title":"VIS","type":"metrics","params":{"id":"61ca57f0-469d-11e7-af02-69e470af7417", + "type":"timeseries","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(0,156,224,1)", + "split_mode":"terms","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"count", + "numerator":"FlightDelay:true"},{"settings":"","minimize":0,"window":5,"model": + "holt_winters","id":"23054fe0-8915-11e9-9b86-d3f94982620f","type":"moving_average","field": + "61ca57f2-469d-11e7-af02-69e470af7417","predict":1}],"separate_axis":0,"axis_position":"right", + "formatter":"number","chart_type":"line","line_width":"2","point_size":"0","fill":0.5,"stacked":"none", + "label":"Percent Delays","terms_size":"2","terms_field":"OriginCityName"}],"time_field":"timestamp", + "index_pattern":"kibana_sample_data_flights","interval":">=12h","axis_position":"left","axis_formatter": + "number","show_legend":1,"show_grid":1,"annotations":[{"fields":"FlightDelay,Cancelled,Carrier", + "template":"{{Carrier}}: Flight Delayed and Cancelled!","index_pattern":"kibana_sample_data_flights", + "query_string":"FlightDelay:true AND Cancelled:true","id":"53b7dff0-4c89-11e8-a66a-6989ad5a0a39", + "color":"rgba(0,98,177,1)","time_field":"timestamp","icon":"fa-exclamation-triangle", + "ignore_global_filters":1,"ignore_panel_filters":1,"hidden":true}],"legend_position":"bottom", + "axis_scale":"normal","default_index_pattern":"kibana_sample_data_flights","default_timefield":"timestamp"}, + "aggs":[]}`, + }, + migrationVersion: { + visualization: '7.2.0', + }, + type: 'visualization', + }; + }); + + test('should add some necessary moving_fn fields', () => { + const migratedDoc = migrate(doc); + const visState = JSON.parse(migratedDoc.attributes.visState); + const metric = visState.params.series[0].metrics[1]; + + expect(metric).toHaveProperty('model_type'); + expect(metric).toHaveProperty('alpha'); + expect(metric).toHaveProperty('beta'); + expect(metric).toHaveProperty('gamma'); + expect(metric).toHaveProperty('period'); + expect(metric).toHaveProperty('multiplicative'); + }); + }); + }); + + describe('7.3.0 tsvb', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.3.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const generateDoc = (params: any) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: JSON.stringify({ params }), + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + }); + it('should change series item filters from a string into an object', () => { + const params = { type: 'metric', series: [{ filter: 'Filter Bytes Test:>1000' }] }; + const testDoc1 = generateDoc(params); + const migratedTestDoc1 = migrate(testDoc1); + const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; + + expect(series[0].filter).toHaveProperty('query'); + expect(series[0].filter).toHaveProperty('language'); + }); + it('should not change a series item filter string in the object after migration', () => { + const markdownParams = { + type: 'markdown', + series: [ + { + filter: 'Filter Bytes Test:>1000', + split_filters: [{ filter: 'bytes:>1000' }], + }, + ], + }; + const markdownDoc = generateDoc(markdownParams); + const migratedMarkdownDoc = migrate(markdownDoc); + const markdownSeries = JSON.parse(migratedMarkdownDoc.attributes.visState).params.series; + + expect(markdownSeries[0].filter.query).toBe( + JSON.parse(markdownDoc.attributes.visState).params.series[0].filter + ); + expect(markdownSeries[0].split_filters[0].filter.query).toBe( + JSON.parse(markdownDoc.attributes.visState).params.series[0].split_filters[0].filter + ); + }); + + it('should change series item filters from a string into an object for all filters', () => { + const params = { + type: 'timeseries', + filter: 'bytes:>1000', + series: [ + { + filter: 'Filter Bytes Test:>1000', + split_filters: [{ filter: 'bytes:>1000' }], + }, + ], + annotations: [{ query_string: 'bytes:>1000' }], + }; + const timeSeriesDoc = generateDoc(params); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + + expect(Object.keys(timeSeriesParams.series[0].filter)).toEqual( + expect.arrayContaining(['query', 'language']) + ); + expect(Object.keys(timeSeriesParams.series[0].split_filters[0].filter)).toEqual( + expect.arrayContaining(['query', 'language']) + ); + expect(Object.keys(timeSeriesParams.annotations[0].query_string)).toEqual( + expect.arrayContaining(['query', 'language']) + ); + }); + + it('should not fail on a metric visualization without a filter in a series item', () => { + const params = { type: 'metric', series: [{}, {}, {}] }; + const testDoc1 = generateDoc(params); + const migratedTestDoc1 = migrate(testDoc1); + const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; + + expect(series[2]).not.toHaveProperty('filter.query'); + }); + + it('should not migrate a visualization of unknown type', () => { + const params = { type: 'unknown', series: [{ filter: 'foo:bar' }] }; + const doc = generateDoc(params); + const migratedDoc = migrate(doc); + const series = JSON.parse(migratedDoc.attributes.visState).params.series; + + expect(series[0].filter).toEqual(params.series[0].filter); + }); + }); + + describe('7.3.1', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.3.1']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + it('should migrate filters agg query string queries', () => { + const state = { + aggs: [ + { type: 'count', params: {} }, + { + type: 'filters', + params: { + filters: [ + { + input: { + query: { + query_string: { query: 'machine.os.keyword:"win 8"' }, + }, + }, + }, + ], + }, + }, + ], + }; + const expected = { + aggs: [ + { type: 'count', params: {} }, + { + type: 'filters', + params: { + filters: [{ input: { query: 'machine.os.keyword:"win 8"' } }], + }, + }, + ], + }; + const migratedDoc = migrate({ attributes: { visState: JSON.stringify(state) } }); + + expect(migratedDoc).toEqual({ attributes: { visState: JSON.stringify(expected) } }); + }); + }); + + describe('7.4.2 tsvb split_filters migration', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.4.2']( + doc as Parameters[0], + savedObjectMigrationContext + ); + const generateDoc = (params: any) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: JSON.stringify({ params }), + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + }); + + it('should change series item filters from a string into an object for all filters', () => { + const params = { + type: 'timeseries', + filter: { + query: 'bytes:>1000', + language: 'lucene', + }, + series: [ + { + split_filters: [{ filter: 'bytes:>1000' }], + }, + ], + }; + const timeSeriesDoc = generateDoc(params); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + + expect(Object.keys(timeSeriesParams.filter)).toEqual( + expect.arrayContaining(['query', 'language']) + ); + expect(timeSeriesParams.series[0].split_filters[0].filter).toEqual({ + query: 'bytes:>1000', + language: 'lucene', + }); + }); + + it('should change series item split filters when there is no filter item', () => { + const params = { + type: 'timeseries', + filter: { + query: 'bytes:>1000', + language: 'lucene', + }, + series: [ + { + split_filters: [{ filter: 'bytes:>1000' }], + }, + ], + annotations: [ + { + query_string: { + query: 'bytes:>1000', + language: 'lucene', + }, + }, + ], + }; + const timeSeriesDoc = generateDoc(params); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + + expect(timeSeriesParams.series[0].split_filters[0].filter).toEqual({ + query: 'bytes:>1000', + language: 'lucene', + }); + }); + + it('should not convert split_filters to objects if there are no split filter filters', () => { + const params = { + type: 'timeseries', + filter: { + query: 'bytes:>1000', + language: 'lucene', + }, + series: [ + { + split_filters: [], + }, + ], + }; + const timeSeriesDoc = generateDoc(params); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + + expect(timeSeriesParams.series[0].split_filters).not.toHaveProperty('query'); + }); + + it('should do nothing if a split_filter is already a query:language object', () => { + const params = { + type: 'timeseries', + filter: { + query: 'bytes:>1000', + language: 'lucene', + }, + series: [ + { + split_filters: [ + { + filter: { + query: 'bytes:>1000', + language: 'lucene', + }, + }, + ], + }, + ], + annotations: [ + { + query_string: { + query: 'bytes:>1000', + language: 'lucene', + }, + }, + ], + }; + const timeSeriesDoc = generateDoc(params); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + + expect(timeSeriesParams.series[0].split_filters[0].filter.query).toEqual('bytes:>1000'); + expect(timeSeriesParams.series[0].split_filters[0].filter.language).toEqual('lucene'); + }); + }); +}); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts new file mode 100644 index 000000000000..9ee355cbb23c --- /dev/null +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -0,0 +1,574 @@ +/* + * 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 { SavedObjectMigrationFn } from 'kibana/server'; +import { cloneDeep, get, omit, has, flow } from 'lodash'; + +const migrateIndexPattern: SavedObjectMigrationFn = doc => { + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + if (typeof searchSourceJSON !== 'string') { + return doc; + } + let searchSource; + try { + searchSource = JSON.parse(searchSourceJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc; + } + + if (searchSource.index && Array.isArray(doc.references)) { + searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + doc.references.push({ + name: searchSource.indexRefName, + type: 'index-pattern', + id: searchSource.index, + }); + delete searchSource.index; + } + if (searchSource.filter) { + searchSource.filter.forEach((filterRow: any, i: number) => { + if (!filterRow.meta || !filterRow.meta.index || !Array.isArray(doc.references)) { + return; + } + filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + doc.references.push({ + name: filterRow.meta.indexRefName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + delete filterRow.meta.index; + }); + } + + doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); + + return doc; +}; + +// [TSVB] Migrate percentile-rank aggregation (value -> values) +const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + if (visState && visState.type === 'metrics') { + const series: any[] = get(visState, 'params.series') || []; + + series.forEach(part => { + (part.metrics || []).forEach((metric: any) => { + if (metric.type === 'percentile_rank' && has(metric, 'value')) { + metric.values = [metric.value]; + + delete metric.value; + } + }); + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } + return doc; +}; + +// Migrate date histogram aggregation (remove customInterval) +const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + + if (visState && visState.aggs) { + visState.aggs.forEach((agg: any) => { + if (agg.type === 'date_histogram' && agg.params) { + if (agg.params.interval === 'custom') { + agg.params.interval = agg.params.customInterval; + } + delete agg.params.customInterval; + } + + if ( + get(agg, 'params.customBucket.type', null) === 'date_histogram' && + agg.params.customBucket.params + ) { + if (agg.params.customBucket.params.interval === 'custom') { + agg.params.customBucket.params.interval = agg.params.customBucket.params.customInterval; + } + delete agg.params.customBucket.params.customInterval; + } + }); + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } + return doc; +}; + +const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + if (visStateJSON) { + let visState; + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + if (visState && visState.aggs) { + visState.aggs.forEach((agg: any) => { + // We're checking always for the existance of agg.params here. This should always exist, but better + // be safe then sorry during migrations. + if (agg.type === 'date_histogram' && agg.params) { + delete agg.params.time_zone; + } + + if ( + get(agg, 'params.customBucket.type', null) === 'date_histogram' && + agg.params.customBucket.params + ) { + delete agg.params.customBucket.params.time_zone; + } + }); + doc.attributes.visState = JSON.stringify(visState); + } + } + return doc; +}; + +// migrate gauge verticalSplit to alignment +// https://github.com/elastic/kibana/issues/34636 +const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logger) => { + const visStateJSON = get(doc, 'attributes.visState'); + + if (visStateJSON) { + try { + const visState = JSON.parse(visStateJSON); + if (visState && visState.type === 'gauge' && !visState.params.gauge.alignment) { + visState.params.gauge.alignment = visState.params.gauge.verticalSplit + ? 'vertical' + : 'horizontal'; + delete visState.params.gauge.verticalSplit; + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + logger.log.warn(`Exception @ migrateGaugeVerticalSplitToAlignment! ${e}`); + logger.log.warn(`Exception @ migrateGaugeVerticalSplitToAlignment! Payload: ${visStateJSON}`); + } + } + return doc; +}; +// Migrate filters (string -> { query: string, language: lucene }) +/* + Enabling KQL in TSVB causes problems with savedObject visualizations when these are saved with filters. + In a visualisation type of saved object, if the visState param is of type metric, the filter is saved as a string that is not interpretted correctly as a lucene query in the visualization itself. + We need to transform the filter string into an object containing the original string as a query and specify the query language as lucene. + For Metrics visualizations (param.type === "metric"), filters can be applied to each series object in the series array within the SavedObject.visState.params object. + Path to the series array is thus: + attributes.visState. +*/ +const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) => { + // Migrate filters + // If any filters exist and they are a string, we assume it to be lucene and transform the filter into an object accordingly + const newDoc = cloneDeep(doc); + const visStateJSON = get(doc, 'attributes.visState'); + if (visStateJSON) { + let visState; + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // let it go, the data is invalid and we'll leave it as is + } + if (visState) { + const visType = get(visState, 'params.type'); + const tsvbTypes = ['metric', 'markdown', 'top_n', 'gauge', 'table', 'timeseries']; + if (tsvbTypes.indexOf(visType) === -1) { + // skip + return doc; + } + // migrate the params fitler + const params: any = get(visState, 'params'); + if (params.filter && typeof params.filter === 'string') { + const paramsFilterObject = { + query: params.filter, + language: 'lucene', + }; + params.filter = paramsFilterObject; + } + + // migrate the annotations query string: + const annotations: any[] = get(visState, 'params.annotations') || []; + annotations.forEach(item => { + if (!item.query_string) { + // we don't need to transform anything if there isn't a filter at all + return; + } + if (typeof item.query_string === 'string') { + const itemQueryStringObject = { + query: item.query_string, + language: 'lucene', + }; + item.query_string = itemQueryStringObject; + } + }); + // migrate the series filters + const series: any[] = get(visState, 'params.series') || []; + + series.forEach(item => { + if (!item.filter) { + // we don't need to transform anything if there isn't a filter at all + return; + } + // series item filter + if (typeof item.filter === 'string') { + const itemfilterObject = { + query: item.filter, + language: 'lucene', + }; + item.filter = itemfilterObject; + } + // series item split filters filter + if (item.split_filters) { + const splitFilters: any[] = get(item, 'split_filters') || []; + splitFilters.forEach(filter => { + if (!filter.filter) { + // we don't need to transform anything if there isn't a filter at all + return; + } + if (typeof filter.filter === 'string') { + const filterfilterObject = { + query: filter.filter, + language: 'lucene', + }; + filter.filter = filterfilterObject; + } + }); + } + }); + newDoc.attributes.visState = JSON.stringify(visState); + } + } + return newDoc; +}; + +const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => { + // Migrate split_filters in TSVB objects that weren't migrated in 7.3 + // If any filters exist and they are a string, we assume them to be lucene syntax and transform the filter into an object accordingly + const newDoc = cloneDeep(doc); + const visStateJSON = get(doc, 'attributes.visState'); + if (visStateJSON) { + let visState; + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // let it go, the data is invalid and we'll leave it as is + } + if (visState) { + const visType = get(visState, 'params.type'); + const tsvbTypes = ['metric', 'markdown', 'top_n', 'gauge', 'table', 'timeseries']; + if (tsvbTypes.indexOf(visType) === -1) { + // skip + return doc; + } + // migrate the series split_filter filters + const series: any[] = get(visState, 'params.series') || []; + series.forEach(item => { + // series item split filters filter + if (item.split_filters) { + const splitFilters: any[] = get(item, 'split_filters') || []; + if (splitFilters.length > 0) { + // only transform split_filter filters if we have filters + splitFilters.forEach(filter => { + if (typeof filter.filter === 'string') { + const filterfilterObject = { + query: filter.filter, + language: 'lucene', + }; + filter.filter = filterfilterObject; + } + }); + } + } + }); + newDoc.attributes.visState = JSON.stringify(visState); + } + } + return newDoc; +}; + +const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + + if (visStateJSON) { + try { + const visState = JSON.parse(visStateJSON); + + if (visState && visState.aggs) { + visState.aggs.forEach((agg: any) => { + if (agg.type !== 'filters') return; + + agg.params.filters.forEach((filter: any) => { + if (filter.input.language) return filter; + filter.input.language = 'lucene'; + }); + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + } + return doc; +}; + +const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + + if (visState && visState.type === 'metrics') { + const series: any[] = get(visState, 'params.series', []); + + series.forEach(part => { + if (part.metrics && Array.isArray(part.metrics)) { + part.metrics.forEach((metric: any) => { + if (metric.type === 'moving_average') { + metric.model_type = metric.model; + metric.alpha = get(metric, 'settings.alpha', 0.3); + metric.beta = get(metric, 'settings.beta', 0.1); + metric.gamma = get(metric, 'settings.gamma', 0.3); + metric.period = get(metric, 'settings.period', 1); + metric.multiplicative = get(metric, 'settings.type') === 'mult'; + + delete metric.minimize; + delete metric.model; + delete metric.settings; + delete metric.predict; + } + }); + } + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + logger.log.warn(`Exception @ replaceMovAvgToMovFn! ${e}`); + logger.log.warn(`Exception @ replaceMovAvgToMovFn! Payload: ${visStateJSON}`); + } + } + + return doc; +}; + +const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger) => { + const visStateJSON = get(doc, 'attributes.visState'); + + if (visStateJSON) { + try { + const visState = JSON.parse(visStateJSON); + + if (visState && visState.aggs) { + visState.aggs.forEach((agg: any) => { + if (agg.type !== 'filters') return doc; + + agg.params.filters.forEach((filter: any) => { + if (filter.input.query.query_string) { + filter.input.query = filter.input.query.query_string.query; + } + }); + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + } + return doc; +}; + +const addDocReferences: SavedObjectMigrationFn = doc => ({ + ...doc, + references: doc.references || [], +}); + +const migrateSavedSearch: SavedObjectMigrationFn = doc => { + const savedSearchId = get(doc, 'attributes.savedSearchId'); + + if (savedSearchId && doc.references) { + doc.references.push({ + type: 'search', + name: 'search_0', + id: savedSearchId, + }); + doc.attributes.savedSearchRefName = 'search_0'; + } + + delete doc.attributes.savedSearchId; + + return doc; +}; + +const migrateControls: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + + if (visStateJSON) { + let visState; + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + if (visState) { + const controls: any[] = get(visState, 'params.controls') || []; + controls.forEach((control, i) => { + if (!control.indexPattern || !doc.references) { + return; + } + control.indexPatternRefName = `control_${i}_index_pattern`; + doc.references.push({ + name: control.indexPatternRefName, + type: 'index-pattern', + id: control.indexPattern, + }); + delete control.indexPattern; + }); + doc.attributes.visState = JSON.stringify(visState); + } + } + + return doc; +}; + +const migrateTableSplits: SavedObjectMigrationFn = doc => { + try { + const visState = JSON.parse(doc.attributes.visState); + if (get(visState, 'type') !== 'table') { + return doc; // do nothing; we only want to touch tables + } + + let splitCount = 0; + visState.aggs = visState.aggs.map((agg: any) => { + if (agg.schema !== 'split') { + return agg; + } + + splitCount++; + if (splitCount === 1) { + return agg; // leave the first split agg unchanged + } + agg.schema = 'bucket'; + // the `row` param is exclusively used by split aggs, so we remove it + agg.params = omit(agg.params, ['row']); + return agg; + }); + + if (splitCount <= 1) { + return doc; // do nothing; we only want to touch tables with multiple split aggs + } + + const newDoc = cloneDeep(doc); + newDoc.attributes.visState = JSON.stringify(visState); + + return newDoc; + } catch (e) { + throw new Error(`Failure attempting to migrate saved object '${doc.attributes.title}' - ${e}`); + } +}; + +export const visualizationSavedObjectTypeMigrations = { + /** + * We need to have this migration twice, once with a version prior to 7.0.0 once with a version + * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already + * released. Thus a user who already had 7.0.0 installed already got the 7.0.0 migrations below running, + * so we need a version higher than that. But this fix was backported to the 6.7 release, meaning if we + * would only have the 7.0.1 migration in here a user on the 6.7 release will migrate their saved objects + * to the 7.0.1 state, and thus when updating their Kibana to 7.0, will never run the 7.0.0 migrations introduced + * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 + * only contained the 6.7.2 migration and not the 7.0.1 migration. + */ + '6.7.2': flow(removeDateHistogramTimeZones), + '7.0.0': flow( + addDocReferences, + migrateIndexPattern, + migrateSavedSearch, + migrateControls, + migrateTableSplits + ), + '7.0.1': flow(removeDateHistogramTimeZones), + '7.2.0': flow( + migratePercentileRankAggregation, + migrateDateHistogramAggregation + ), + '7.3.0': flow( + migrateGaugeVerticalSplitToAlignment, + transformFilterStringToQueryObject, + migrateFiltersAggQuery, + replaceMovAvgToMovFn + ), + '7.3.1': flow(migrateFiltersAggQueryStringQueries), + '7.4.2': flow(transformSplitFiltersStringToQueryObject), +}; diff --git a/src/plugins/visualizations/server/types.ts b/src/plugins/visualizations/server/types.ts new file mode 100644 index 000000000000..6924edb29627 --- /dev/null +++ b/src/plugins/visualizations/server/types.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. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface VisualizationsPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface VisualizationsPluginStart {} From 271c9597bed0487a899821538d183d91342e7461 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 16 Mar 2020 14:33:03 +0200 Subject: [PATCH 005/115] [SIEM][CASE] Change configuration button (#60229) * Change button * Make URLs constants --- .../pages/case/components/all_cases/index.tsx | 18 +++++++++--------- .../siem/public/pages/case/translations.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) 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 1349246494ec..7b655999ace0 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 @@ -8,7 +8,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiBasicTable, EuiButton, - EuiButtonIcon, EuiContextMenuPanel, EuiEmptyPrompt, EuiFlexGroup, @@ -45,6 +44,9 @@ import { OpenClosedStats } from '../open_closed_stats'; import { getActions } from './actions'; import { CasesTableFilters } from './table_filters'; +const CONFIGURE_CASES_URL = getConfigureCasesUrl(); +const CREATE_CASE_URL = getCreateCaseUrl(); + const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; `; @@ -259,16 +261,14 @@ export const AllCases = React.memo(() => { /> - - {i18n.CREATE_TITLE} + + {i18n.CONFIGURE_CASES_BUTTON} - + + {i18n.CREATE_TITLE} + @@ -325,7 +325,7 @@ export const AllCases = React.memo(() => { titleSize="xs" body={i18n.NO_CASES_BODY} actions={ - + {i18n.ADD_NEW_CASE} } diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 9c0287a56ccb..6ef412d408ae 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -120,7 +120,7 @@ export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate( ); export const CONFIGURE_CASES_BUTTON = i18n.translate('xpack.siem.case.configureCasesButton', { - defaultMessage: 'Configure cases', + defaultMessage: 'Edit third-party connection', }); export const ADD_COMMENT = i18n.translate('xpack.siem.case.caseView.comment.addComment', { From dd7531deb4c83a2711b0d4bc4871acea425eb8cf Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 16 Mar 2020 14:30:20 +0100 Subject: [PATCH 006/115] Add UiSettings validation & Kibana default route redirection (#59694) * add schema to ui settings params * add validation for defaults and overrides * validate in ui settings client * ui settings routes validation * clean up tests * use schema for defaultRoutes * move URL redirection to NP * fix spaces test * update docs * update kbn pm * fix karma test * fix tests * address comments * get rid of getDEfaultRoute * regen docs * fix tests * fix enter-spaces test * validate on relative url format * update i18n * fix enter-spoace test * move relative url validation to utils * add CoreApp containing application logic * extract public uiSettings params in a separate type * make schema required * update docs --- ...in-core-public.iuisettingsclient.getall.md | 2 +- ...na-plugin-core-public.iuisettingsclient.md | 2 +- .../core/public/kibana-plugin-core-public.md | 1 + ...ugin-core-public.publicuisettingsparams.md | 13 ++ ...ana-plugin-core-public.uisettingsparams.md | 5 +- ...gin-core-public.uisettingsparams.schema.md | 11 ++ ...ugin-core-public.uisettingsparams.value.md | 2 +- ...-server.iuisettingsclient.getregistered.md | 2 +- ...na-plugin-core-server.iuisettingsclient.md | 2 +- .../core/server/kibana-plugin-core-server.md | 1 + ...ugin-core-server.publicuisettingsparams.md | 13 ++ ...ana-plugin-core-server.uisettingsparams.md | 5 +- ...gin-core-server.uisettingsparams.schema.md | 11 ++ ...ugin-core-server.uisettingsparams.value.md | 2 +- .../src/errors/validation_error.ts | 3 + .../src/kbn_client/kbn_client_ui_settings.ts | 4 +- packages/kbn-pm/dist/index.js | 4 +- src/core/public/index.ts | 2 +- src/core/public/public.api.md | 16 +- src/core/public/types.ts | 1 + .../ui_settings_api.test.ts.snap | 8 +- src/core/public/ui_settings/types.ts | 6 +- .../ui_settings/ui_settings_api.test.ts | 2 +- .../public/ui_settings/ui_settings_api.ts | 10 +- .../public/ui_settings/ui_settings_client.ts | 8 +- src/core/server/core_app/core_app.ts | 52 ++++++ src/core/server/core_app/index.ts | 20 +++ .../default_route_provider_config.test.ts | 90 ++++++++++ src/core/server/index.ts | 1 + src/core/server/server.api.md | 11 +- src/core/server/server.ts | 15 +- src/core/server/ui_settings/index.ts | 1 + .../integration_tests/routes.test.ts | 66 ++++++++ src/core/server/ui_settings/routes/set.ts | 4 +- .../server/ui_settings/routes/set_many.ts | 4 +- src/core/server/ui_settings/types.ts | 5 +- .../ui_settings/ui_settings_client.test.ts | 159 ++++++++++++++++-- .../server/ui_settings/ui_settings_client.ts | 73 +++++--- .../ui_settings/ui_settings_service.test.ts | 42 +++++ .../server/ui_settings/ui_settings_service.ts | 20 +++ src/core/types/ui_settings.ts | 19 ++- src/core/utils/url.test.ts | 16 +- src/core/utils/url.ts | 16 ++ .../kibana/ui_setting_defaults.js | 24 ++- .../tests_bundle/tests_entry_template.js | 11 +- src/legacy/server/http/index.js | 11 -- .../default_route_provider.test.ts | 87 ---------- .../default_route_provider_config.test.ts | 54 ------ .../http/setup_default_route_provider.ts | 74 -------- src/legacy/server/kbn_server.d.ts | 1 - .../management_app/advanced_settings.test.tsx | 10 +- .../lib/to_editable_config.test.ts | 6 +- .../management_app/lib/to_editable_config.ts | 4 +- .../public/management_app/types.ts | 11 +- src/plugins/data/public/public.api.md | 2 +- src/test_utils/kbn_server.ts | 1 + test/api_integration/apis/index.js | 1 - .../ui_settings_plugin/server/plugin.ts | 3 +- .../spaces/server/routes/views/enter_space.ts | 8 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../functional/apps/spaces/enter_space.ts | 32 ++-- .../es_archives/spaces/enter_space/data.json | 4 +- 63 files changed, 724 insertions(+), 372 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.publicuisettingsparams.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.uisettingsparams.schema.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.publicuisettingsparams.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.uisettingsparams.schema.md create mode 100644 src/core/server/core_app/core_app.ts create mode 100644 src/core/server/core_app/index.ts create mode 100644 src/core/server/core_app/integration_tests/default_route_provider_config.test.ts create mode 100644 src/core/server/ui_settings/integration_tests/routes.test.ts delete mode 100644 src/legacy/server/http/integration_tests/default_route_provider.test.ts delete mode 100644 src/legacy/server/http/integration_tests/default_route_provider_config.test.ts delete mode 100644 src/legacy/server/http/setup_default_route_provider.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.getall.md b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.getall.md index 805ac57b2fb9..004979977376 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.getall.md +++ b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.getall.md @@ -9,5 +9,5 @@ Gets the metadata about all uiSettings, including the type, default value, and u Signature: ```typescript -getAll: () => Readonly>; +getAll: () => Readonly>; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md index da566ed25cff..87ef5784a6c6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md +++ b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md @@ -18,7 +18,7 @@ export interface IUiSettingsClient | --- | --- | --- | | [get](./kibana-plugin-core-public.iuisettingsclient.get.md) | <T = any>(key: string, defaultOverride?: T) => T | Gets the value for a specific uiSetting. If this setting has no user-defined value then the defaultOverride parameter is returned (and parsed if setting is of type "json" or "number). If the parameter is not defined and the key is not registered by any plugin then an error is thrown, otherwise reads the default value defined by a plugin. | | [get$](./kibana-plugin-core-public.iuisettingsclient.get_.md) | <T = any>(key: string, defaultOverride?: T) => Observable<T> | Gets an observable of the current value for a config key, and all updates to that config key in the future. Providing a defaultOverride argument behaves the same as it does in \#get() | -| [getAll](./kibana-plugin-core-public.iuisettingsclient.getall.md) | () => Readonly<Record<string, UiSettingsParams & UserProvidedValues>> | Gets the metadata about all uiSettings, including the type, default value, and user value for each key. | +| [getAll](./kibana-plugin-core-public.iuisettingsclient.getall.md) | () => Readonly<Record<string, PublicUiSettingsParams & UserProvidedValues>> | Gets the metadata about all uiSettings, including the type, default value, and user value for each key. | | [getSaved$](./kibana-plugin-core-public.iuisettingsclient.getsaved_.md) | <T = any>() => Observable<{
key: string;
newValue: T;
oldValue: T;
}> | Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. | | [getUpdate$](./kibana-plugin-core-public.iuisettingsclient.getupdate_.md) | <T = any>() => Observable<{
key: string;
newValue: T;
oldValue: T;
}> | Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. | | [getUpdateErrors$](./kibana-plugin-core-public.iuisettingsclient.getupdateerrors_.md) | () => Observable<Error> | Returns an Observable that notifies subscribers of each error while trying to update the settings, containing the actual Error class. | diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index bafc2eb3a4bc..a9fbaa25ea15 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -147,6 +147,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [MountPoint](./kibana-plugin-core-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. | | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | +| [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [RecursiveReadonly](./kibana-plugin-core-public.recursivereadonly.md) | | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.publicuisettingsparams.md b/docs/development/core/public/kibana-plugin-core-public.publicuisettingsparams.md new file mode 100644 index 000000000000..678a69289ff2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.publicuisettingsparams.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) + +## PublicUiSettingsParams type + +A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. + +Signature: + +```typescript +export declare type PublicUiSettingsParams = Omit; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md index 00f1c0f0deca..e7facb4a109c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md @@ -9,7 +9,7 @@ UiSettings parameters defined by the plugins. Signature: ```typescript -export interface UiSettingsParams +export interface UiSettingsParams ``` ## Properties @@ -24,7 +24,8 @@ export interface UiSettingsParams | [options](./kibana-plugin-core-public.uisettingsparams.options.md) | string[] | array of permitted values for this setting | | [readonly](./kibana-plugin-core-public.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed | | [requiresPageReload](./kibana-plugin-core-public.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading | +| [schema](./kibana-plugin-core-public.uisettingsparams.schema.md) | Type<T> | | | [type](./kibana-plugin-core-public.uisettingsparams.type.md) | UiSettingsType | defines a type of UI element [UiSettingsType](./kibana-plugin-core-public.uisettingstype.md) | | [validation](./kibana-plugin-core-public.uisettingsparams.validation.md) | ImageValidation | StringValidation | | -| [value](./kibana-plugin-core-public.uisettingsparams.value.md) | SavedObjectAttribute | default value to fall back to if a user doesn't provide any | +| [value](./kibana-plugin-core-public.uisettingsparams.value.md) | T | default value to fall back to if a user doesn't provide any | diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.schema.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.schema.md new file mode 100644 index 000000000000..f90d5161f96a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.schema.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) > [schema](./kibana-plugin-core-public.uisettingsparams.schema.md) + +## UiSettingsParams.schema property + +Signature: + +```typescript +schema: Type; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.value.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.value.md index 8775588290d7..2740f169eeec 100644 --- a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.value.md +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.value.md @@ -9,5 +9,5 @@ default value to fall back to if a user doesn't provide any Signature: ```typescript -value?: SavedObjectAttribute; +value?: T; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.getregistered.md b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.getregistered.md index 2ca6b4cbe158..71a2bbf88472 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.getregistered.md +++ b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.getregistered.md @@ -9,5 +9,5 @@ Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-core-ser Signature: ```typescript -getRegistered: () => Readonly>; +getRegistered: () => Readonly>; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md index 42fcc81419cb..af99b5e5bb21 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md @@ -18,7 +18,7 @@ export interface IUiSettingsClient | --- | --- | --- | | [get](./kibana-plugin-core-server.iuisettingsclient.get.md) | <T = any>(key: string) => Promise<T> | Retrieves uiSettings values set by the user with fallbacks to default values if not specified. | | [getAll](./kibana-plugin-core-server.iuisettingsclient.getall.md) | <T = any>() => Promise<Record<string, T>> | Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified. | -| [getRegistered](./kibana-plugin-core-server.iuisettingsclient.getregistered.md) | () => Readonly<Record<string, UiSettingsParams>> | Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) | +| [getRegistered](./kibana-plugin-core-server.iuisettingsclient.getregistered.md) | () => Readonly<Record<string, PublicUiSettingsParams>> | Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) | | [getUserProvided](./kibana-plugin-core-server.iuisettingsclient.getuserprovided.md) | <T = any>() => Promise<Record<string, UserProvidedValues<T>>> | Retrieves a set of all uiSettings values set by the user. | | [isOverridden](./kibana-plugin-core-server.iuisettingsclient.isoverridden.md) | (key: string) => boolean | Shows whether the uiSettings value set by the user. | | [remove](./kibana-plugin-core-server.iuisettingsclient.remove.md) | (key: string) => Promise<void> | Removes uiSettings value by key. | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 8a88329031e1..54cf496b2d6a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -232,6 +232,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializer](./kibana-plugin-core-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-core-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | | [PluginOpaqueId](./kibana-plugin-core-server.pluginopaqueid.md) | | +| [PublicUiSettingsParams](./kibana-plugin-core-server.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) exposed to the client-side. | | [RecursiveReadonly](./kibana-plugin-core-server.recursivereadonly.md) | | | [RedirectResponseOptions](./kibana-plugin-core-server.redirectresponseoptions.md) | HTTP response parameters for redirection response | | [RequestHandler](./kibana-plugin-core-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. | diff --git a/docs/development/core/server/kibana-plugin-core-server.publicuisettingsparams.md b/docs/development/core/server/kibana-plugin-core-server.publicuisettingsparams.md new file mode 100644 index 000000000000..4ccc91fbe1f7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.publicuisettingsparams.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PublicUiSettingsParams](./kibana-plugin-core-server.publicuisettingsparams.md) + +## PublicUiSettingsParams type + +A sub-set of [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) exposed to the client-side. + +Signature: + +```typescript +export declare type PublicUiSettingsParams = Omit; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md index fe6e5d956f3e..f134decb5102 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md @@ -9,7 +9,7 @@ UiSettings parameters defined by the plugins. Signature: ```typescript -export interface UiSettingsParams +export interface UiSettingsParams ``` ## Properties @@ -24,7 +24,8 @@ export interface UiSettingsParams | [options](./kibana-plugin-core-server.uisettingsparams.options.md) | string[] | array of permitted values for this setting | | [readonly](./kibana-plugin-core-server.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed | | [requiresPageReload](./kibana-plugin-core-server.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading | +| [schema](./kibana-plugin-core-server.uisettingsparams.schema.md) | Type<T> | | | [type](./kibana-plugin-core-server.uisettingsparams.type.md) | UiSettingsType | defines a type of UI element [UiSettingsType](./kibana-plugin-core-server.uisettingstype.md) | | [validation](./kibana-plugin-core-server.uisettingsparams.validation.md) | ImageValidation | StringValidation | | -| [value](./kibana-plugin-core-server.uisettingsparams.value.md) | SavedObjectAttribute | default value to fall back to if a user doesn't provide any | +| [value](./kibana-plugin-core-server.uisettingsparams.value.md) | T | default value to fall back to if a user doesn't provide any | diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.schema.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.schema.md new file mode 100644 index 000000000000..f181fbd309b7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.schema.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) > [schema](./kibana-plugin-core-server.uisettingsparams.schema.md) + +## UiSettingsParams.schema property + +Signature: + +```typescript +schema: Type; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.value.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.value.md index ca00cd0cd639..78c8f0c8fcf8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.value.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.value.md @@ -9,5 +9,5 @@ default value to fall back to if a user doesn't provide any Signature: ```typescript -value?: SavedObjectAttribute; +value?: T; ``` diff --git a/packages/kbn-config-schema/src/errors/validation_error.ts b/packages/kbn-config-schema/src/errors/validation_error.ts index d688d022da85..2a4f887bc434 100644 --- a/packages/kbn-config-schema/src/errors/validation_error.ts +++ b/packages/kbn-config-schema/src/errors/validation_error.ts @@ -44,5 +44,8 @@ export class ValidationError extends SchemaError { constructor(error: SchemaTypeError, namespace?: string) { super(ValidationError.extractMessage(error, namespace), error); + + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, ValidationError.prototype); } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts index ad01dea624c3..dbfa87e70032 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts @@ -67,7 +67,7 @@ export class KbnClientUiSettings { * Replace all uiSettings with the `doc` values, `doc` is merged * with some defaults */ - async replace(doc: UiSettingValues) { + async replace(doc: UiSettingValues, { retries = 5 }: { retries?: number } = {}) { this.log.debug('replacing kibana config doc: %j', doc); const changes: Record = { @@ -85,7 +85,7 @@ export class KbnClientUiSettings { method: 'POST', path: '/api/kibana/settings', body: { changes }, - retries: 5, + retries, }); } diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index fe0491870e4b..338d48930052 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -43920,7 +43920,7 @@ class KbnClientUiSettings { * Replace all uiSettings with the `doc` values, `doc` is merged * with some defaults */ - async replace(doc) { + async replace(doc, { retries = 5 } = {}) { this.log.debug('replacing kibana config doc: %j', doc); const changes = { ...this.defaults, @@ -43935,7 +43935,7 @@ class KbnClientUiSettings { method: 'POST', path: '/api/kibana/settings', body: { changes }, - retries: 5, + retries, }); } /** diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 483d4dbfdf7c..0ff044878afa 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -171,7 +171,7 @@ export { ErrorToastOptions, } from './notifications'; -export { MountPoint, UnmountCallback } from './types'; +export { MountPoint, UnmountCallback, PublicUiSettingsParams } from './types'; /** * Core services exposed to the `Plugin` setup lifecycle diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 69668176a397..46667230edc3 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -16,10 +16,11 @@ import { Location } from 'history'; import { LocationDescriptorObject } from 'history'; import { MaybePromise } from '@kbn/utility-types'; import { Observable } from 'rxjs'; +import { PublicUiSettingsParams as PublicUiSettingsParams_2 } from 'src/core/server/types'; import React from 'react'; import * as Rx from 'rxjs'; import { ShallowPromise } from '@kbn/utility-types'; -import { UiSettingsParams as UiSettingsParams_2 } from 'src/core/server/types'; +import { Type } from '@kbn/config-schema'; import { UnregisterCallback } from 'history'; import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types'; @@ -784,7 +785,7 @@ export type IToasts = Pick(key: string, defaultOverride?: T) => Observable; get: (key: string, defaultOverride?: T) => T; - getAll: () => Readonly>; + getAll: () => Readonly>; getSaved$: () => Observable<{ key: string; newValue: T; @@ -933,6 +934,9 @@ export interface PluginInitializerContext // @public (undocumented) export type PluginOpaqueId = symbol; +// @public +export type PublicUiSettingsParams = Omit; + // Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -1291,7 +1295,7 @@ export type ToastsSetup = IToasts; export type ToastsStart = IToasts; // @public -export interface UiSettingsParams { +export interface UiSettingsParams { category?: string[]; // Warning: (ae-forgotten-export) The symbol "DeprecationSettings" needs to be exported by the entry point index.d.ts deprecation?: DeprecationSettings; @@ -1301,16 +1305,18 @@ export interface UiSettingsParams { options?: string[]; readonly?: boolean; requiresPageReload?: boolean; + // (undocumented) + schema: Type; type?: UiSettingsType; // (undocumented) validation?: ImageValidation | StringValidation; - value?: SavedObjectAttribute; + value?: T; } // @public (undocumented) export interface UiSettingsState { // (undocumented) - [key: string]: UiSettingsParams_2 & UserProvidedValues_2; + [key: string]: PublicUiSettingsParams_2 & UserProvidedValues_2; } // @public diff --git a/src/core/public/types.ts b/src/core/public/types.ts index 267a9e9f7e01..26f1e4683637 100644 --- a/src/core/public/types.ts +++ b/src/core/public/types.ts @@ -19,6 +19,7 @@ export { UiSettingsParams, + PublicUiSettingsParams, UserProvidedValues, UiSettingsType, ImageValidation, diff --git a/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap index cd55c77526d5..b737c04a5f26 100644 --- a/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap +++ b/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap @@ -84,21 +84,21 @@ Array [ exports[`#batchSet rejects all promises for batched requests that fail: promise rejections 1`] = ` Array [ Object { - "error": [Error: Request failed with status code: 400], + "error": [Error: invalid], "isRejected": true, }, Object { - "error": [Error: Request failed with status code: 400], + "error": [Error: invalid], "isRejected": true, }, Object { - "error": [Error: Request failed with status code: 400], + "error": [Error: invalid], "isRejected": true, }, ] `; -exports[`#batchSet rejects on 301 1`] = `"Request failed with status code: 301"`; +exports[`#batchSet rejects on 301 1`] = `"Moved Permanently"`; exports[`#batchSet rejects on 404 response 1`] = `"Request failed with status code: 404"`; diff --git a/src/core/public/ui_settings/types.ts b/src/core/public/ui_settings/types.ts index 19fd91924f24..d92c033ae8c8 100644 --- a/src/core/public/ui_settings/types.ts +++ b/src/core/public/ui_settings/types.ts @@ -18,11 +18,11 @@ */ import { Observable } from 'rxjs'; -import { UiSettingsParams, UserProvidedValues } from 'src/core/server/types'; +import { PublicUiSettingsParams, UserProvidedValues } from 'src/core/server/types'; /** @public */ export interface UiSettingsState { - [key: string]: UiSettingsParams & UserProvidedValues; + [key: string]: PublicUiSettingsParams & UserProvidedValues; } /** @@ -53,7 +53,7 @@ export interface IUiSettingsClient { * Gets the metadata about all uiSettings, including the type, default value, and user value * for each key. */ - getAll: () => Readonly>; + getAll: () => Readonly>; /** * Sets the value for a uiSetting. If the setting is not registered by any plugin diff --git a/src/core/public/ui_settings/ui_settings_api.test.ts b/src/core/public/ui_settings/ui_settings_api.test.ts index 1170c42cea70..9a462e054134 100644 --- a/src/core/public/ui_settings/ui_settings_api.test.ts +++ b/src/core/public/ui_settings/ui_settings_api.test.ts @@ -148,7 +148,7 @@ describe('#batchSet', () => { '*', { status: 400, - body: 'invalid', + body: { message: 'invalid' }, }, { overwriteRoutes: false, diff --git a/src/core/public/ui_settings/ui_settings_api.ts b/src/core/public/ui_settings/ui_settings_api.ts index 33b43107acf1..c5efced0a41e 100644 --- a/src/core/public/ui_settings/ui_settings_api.ts +++ b/src/core/public/ui_settings/ui_settings_api.ts @@ -152,10 +152,14 @@ export class UiSettingsApi { }, }); } catch (err) { - if (err.response && err.response.status >= 300) { - throw new Error(`Request failed with status code: ${err.response.status}`); + if (err.response) { + if (err.response.status === 400) { + throw new Error(err.body.message); + } + if (err.response.status > 400) { + throw new Error(`Request failed with status code: ${err.response.status}`); + } } - throw err; } finally { this.loadingCount$.next(this.loadingCount$.getValue() - 1); diff --git a/src/core/public/ui_settings/ui_settings_client.ts b/src/core/public/ui_settings/ui_settings_client.ts index f0071ed08435..f5596b1bc34f 100644 --- a/src/core/public/ui_settings/ui_settings_client.ts +++ b/src/core/public/ui_settings/ui_settings_client.ts @@ -21,14 +21,14 @@ import { cloneDeep, defaultsDeep } from 'lodash'; import { Observable, Subject, concat, defer, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; -import { UiSettingsParams, UserProvidedValues } from 'src/core/server/types'; +import { UserProvidedValues, PublicUiSettingsParams } from 'src/core/server/types'; import { IUiSettingsClient, UiSettingsState } from './types'; import { UiSettingsApi } from './ui_settings_api'; interface UiSettingsClientParams { api: UiSettingsApi; - defaults: Record; + defaults: Record; initialSettings?: UiSettingsState; done$: Observable; } @@ -39,8 +39,8 @@ export class UiSettingsClient implements IUiSettingsClient { private readonly updateErrors$ = new Subject(); private readonly api: UiSettingsApi; - private readonly defaults: Record; - private cache: Record; + private readonly defaults: Record; + private cache: Record; constructor(params: UiSettingsClientParams) { this.api = params.api; diff --git a/src/core/server/core_app/core_app.ts b/src/core/server/core_app/core_app.ts new file mode 100644 index 000000000000..2f8c85f47a76 --- /dev/null +++ b/src/core/server/core_app/core_app.ts @@ -0,0 +1,52 @@ +/* + * 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 { InternalCoreSetup } from '../internal_types'; +import { CoreContext } from '../core_context'; +import { Logger } from '../logging'; + +/** @internal */ +export class CoreApp { + private readonly logger: Logger; + constructor(core: CoreContext) { + this.logger = core.logger.get('core-app'); + } + setup(coreSetup: InternalCoreSetup) { + this.logger.debug('Setting up core app.'); + this.registerDefaultRoutes(coreSetup); + } + + private registerDefaultRoutes(coreSetup: InternalCoreSetup) { + const httpSetup = coreSetup.http; + const router = httpSetup.createRouter('/'); + router.get({ path: '/', validate: false }, async (context, req, res) => { + const defaultRoute = await context.core.uiSettings.client.get('defaultRoute'); + const basePath = httpSetup.basePath.get(req); + const url = `${basePath}${defaultRoute}`; + + return res.redirected({ + headers: { + location: url, + }, + }); + }); + router.get({ path: '/core', validate: false }, async (context, req, res) => + res.ok({ body: { version: '0.0.1' } }) + ); + } +} diff --git a/src/core/server/core_app/index.ts b/src/core/server/core_app/index.ts new file mode 100644 index 000000000000..342ed43f1ff8 --- /dev/null +++ b/src/core/server/core_app/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { CoreApp } from './core_app'; diff --git a/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts new file mode 100644 index 000000000000..221e6fa42471 --- /dev/null +++ b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts @@ -0,0 +1,90 @@ +/* + * 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 * as kbnTestServer from '../../../../test_utils/kbn_server'; +import { Root } from '../../root'; + +const { startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), +}); +let esServer: kbnTestServer.TestElasticsearchUtils; + +describe('default route provider', () => { + let root: Root; + + beforeAll(async () => { + esServer = await startES(); + root = kbnTestServer.createRootWithCorePlugins({ + server: { + basePath: '/hello', + }, + }); + + await root.setup(); + await root.start(); + }); + + afterAll(async () => { + await esServer.stop(); + await root.shutdown(); + }); + + it('redirects to the configured default route respecting basePath', async function() { + const { status, header } = await kbnTestServer.request.get(root, '/'); + + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/hello/app/kibana', + }); + }); + + it('ignores invalid values', async function() { + const invalidRoutes = [ + 'http://not-your-kibana.com', + '///example.com', + '//example.com', + ' //example.com', + ]; + + for (const url of invalidRoutes) { + await kbnTestServer.request + .post(root, '/api/kibana/settings/defaultRoute') + .send({ value: url }) + .expect(400); + } + + const { status, header } = await kbnTestServer.request.get(root, '/'); + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/hello/app/kibana', + }); + }); + + it('consumes valid values', async function() { + await kbnTestServer.request + .post(root, '/api/kibana/settings/defaultRoute') + .send({ value: '/valid' }) + .expect(200); + + const { status, header } = await kbnTestServer.request.get(root, '/'); + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/hello/valid', + }); + }); +}); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 4a1ac8988e4e..89fee92a7ef0 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -250,6 +250,7 @@ export { export { IUiSettingsClient, UiSettingsParams, + PublicUiSettingsParams, UiSettingsType, UiSettingsServiceSetup, UiSettingsServiceStart, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 9cd0c26ea249..84fe081adaae 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -998,7 +998,7 @@ export interface IScopedRenderingClient { export interface IUiSettingsClient { get: (key: string) => Promise; getAll: () => Promise>; - getRegistered: () => Readonly>; + getRegistered: () => Readonly>; getUserProvided: () => Promise>>; isOverridden: (key: string) => boolean; remove: (key: string) => Promise; @@ -1443,6 +1443,9 @@ export interface PluginsServiceStart { contracts: Map; } +// @public +export type PublicUiSettingsParams = Omit; + // Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -2284,7 +2287,7 @@ export interface StringValidationRegexString { } // @public -export interface UiSettingsParams { +export interface UiSettingsParams { category?: string[]; deprecation?: DeprecationSettings; description?: string; @@ -2293,10 +2296,12 @@ export interface UiSettingsParams { options?: string[]; readonly?: boolean; requiresPageReload?: boolean; + // (undocumented) + schema: Type; type?: UiSettingsType; // (undocumented) validation?: ImageValidation | StringValidation; - value?: SavedObjectAttribute; + value?: T; } // @public (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 2402504f717c..09a1328f346d 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -26,8 +26,9 @@ import { RawConfigurationProvider, coreDeprecationProvider, } from './config'; +import { CoreApp } from './core_app'; import { ElasticsearchService } from './elasticsearch'; -import { HttpService, InternalHttpServiceSetup } from './http'; +import { HttpService } from './http'; import { RenderingService, RenderingServiceSetup } from './rendering'; import { LegacyService, ensureValidConfiguration } from './legacy'; import { Logger, LoggerFactory } from './logging'; @@ -69,6 +70,7 @@ export class Server { private readonly uiSettings: UiSettingsService; private readonly uuid: UuidService; private readonly metrics: MetricsService; + private readonly coreApp: CoreApp; private coreStart?: InternalCoreStart; @@ -92,6 +94,7 @@ export class Server { this.capabilities = new CapabilitiesService(core); this.uuid = new UuidService(core); this.metrics = new MetricsService(core); + this.coreApp = new CoreApp(core); } public async setup() { @@ -122,8 +125,6 @@ export class Server { context: contextServiceSetup, }); - this.registerDefaultRoute(httpSetup); - const capabilitiesSetup = this.capabilities.setup({ http: httpSetup }); const elasticsearchServiceSetup = await this.elasticsearch.setup({ @@ -168,6 +169,7 @@ export class Server { }); this.registerCoreContext(coreSetup, renderingSetup); + this.coreApp.setup(coreSetup); return coreSetup; } @@ -216,13 +218,6 @@ export class Server { await this.metrics.stop(); } - private registerDefaultRoute(httpSetup: InternalHttpServiceSetup) { - const router = httpSetup.createRouter('/core'); - router.get({ path: '/', validate: false }, async (context, req, res) => - res.ok({ body: { version: '0.0.1' } }) - ); - } - private registerCoreContext(coreSetup: InternalCoreSetup, rendering: RenderingServiceSetup) { coreSetup.http.registerRouteHandlerContext( coreId, diff --git a/src/core/server/ui_settings/index.ts b/src/core/server/ui_settings/index.ts index ddb66df3ffcb..b11f398d59fa 100644 --- a/src/core/server/ui_settings/index.ts +++ b/src/core/server/ui_settings/index.ts @@ -27,6 +27,7 @@ export { UiSettingsServiceStart, IUiSettingsClient, UiSettingsParams, + PublicUiSettingsParams, InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart, UiSettingsType, diff --git a/src/core/server/ui_settings/integration_tests/routes.test.ts b/src/core/server/ui_settings/integration_tests/routes.test.ts new file mode 100644 index 000000000000..c1261bc7c135 --- /dev/null +++ b/src/core/server/ui_settings/integration_tests/routes.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import * as kbnTestServer from '../../../../test_utils/kbn_server'; + +describe('ui settings service', () => { + describe('routes', () => { + let root: ReturnType; + beforeAll(async () => { + root = kbnTestServer.createRoot(); + + const { uiSettings } = await root.setup(); + uiSettings.register({ + custom: { + value: '42', + schema: schema.string(), + }, + }); + + await root.start(); + }, 30000); + afterAll(async () => await root.shutdown()); + + describe('set', () => { + it('validates value', async () => { + const response = await kbnTestServer.request + .post(root, '/api/kibana/settings/custom') + .send({ value: 100 }) + .expect(400); + + expect(response.body.message).toBe( + '[validation [custom]]: expected value of type [string] but got [number]' + ); + }); + }); + describe('set many', () => { + it('validates value', async () => { + const response = await kbnTestServer.request + .post(root, '/api/kibana/settings') + .send({ changes: { custom: 100, foo: 'bar' } }) + .expect(400); + + expect(response.body.message).toBe( + '[validation [custom]]: expected value of type [string] but got [number]' + ); + }); + }); + }); +}); diff --git a/src/core/server/ui_settings/routes/set.ts b/src/core/server/ui_settings/routes/set.ts index 51ad256b5133..e5158e274245 100644 --- a/src/core/server/ui_settings/routes/set.ts +++ b/src/core/server/ui_settings/routes/set.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { schema } from '@kbn/config-schema'; +import { schema, ValidationError } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; @@ -56,7 +56,7 @@ export function registerSetRoute(router: IRouter) { }); } - if (error instanceof CannotOverrideError) { + if (error instanceof CannotOverrideError || error instanceof ValidationError) { return response.badRequest({ body: error }); } diff --git a/src/core/server/ui_settings/routes/set_many.ts b/src/core/server/ui_settings/routes/set_many.ts index 3794eba004be..5623c3fe11b8 100644 --- a/src/core/server/ui_settings/routes/set_many.ts +++ b/src/core/server/ui_settings/routes/set_many.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { schema } from '@kbn/config-schema'; +import { schema, ValidationError } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; @@ -50,7 +50,7 @@ export function registerSetManyRoute(router: IRouter) { }); } - if (error instanceof CannotOverrideError) { + if (error instanceof CannotOverrideError || error instanceof ValidationError) { return response.badRequest({ body: error }); } diff --git a/src/core/server/ui_settings/types.ts b/src/core/server/ui_settings/types.ts index f3eb1f5a6859..076e1de4458d 100644 --- a/src/core/server/ui_settings/types.ts +++ b/src/core/server/ui_settings/types.ts @@ -17,9 +17,10 @@ * under the License. */ import { SavedObjectsClientContract } from '../saved_objects/types'; -import { UiSettingsParams, UserProvidedValues } from '../../types'; +import { UiSettingsParams, UserProvidedValues, PublicUiSettingsParams } from '../../types'; export { UiSettingsParams, + PublicUiSettingsParams, StringValidationRegexString, StringValidationRegex, StringValidation, @@ -41,7 +42,7 @@ export interface IUiSettingsClient { /** * Returns registered uiSettings values {@link UiSettingsParams} */ - getRegistered: () => Readonly>; + getRegistered: () => Readonly>; /** * Retrieves uiSettings values set by the user with fallbacks to default values if not specified. */ diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index b8aa57291dcc..4ce33eed267a 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -18,6 +18,7 @@ */ import Chance from 'chance'; +import { schema } from '@kbn/config-schema'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { createOrUpgradeSavedConfigMock } from './create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock'; @@ -145,6 +146,22 @@ describe('ui settings', () => { expect(error.message).toBe('Unable to update "foo" because it is overridden'); } }); + + it('validates value if a schema presents', async () => { + const defaults = { foo: { schema: schema.string() } }; + const { uiSettings, savedObjectsClient } = setup({ defaults }); + + await expect( + uiSettings.setMany({ + bar: 2, + foo: 1, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: [validation [foo]]: expected value of type [string] but got [number]]` + ); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(0); + }); }); describe('#set()', () => { @@ -163,6 +180,17 @@ describe('ui settings', () => { }); }); + it('validates value if a schema presents', async () => { + const defaults = { foo: { schema: schema.string() } }; + const { uiSettings, savedObjectsClient } = setup({ defaults }); + + await expect(uiSettings.set('foo', 1)).rejects.toMatchInlineSnapshot( + `[Error: [validation [foo]]: expected value of type [string] but got [number]]` + ); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(0); + }); + it('throws CannotOverrideError if the key is overridden', async () => { const { uiSettings } = setup({ overrides: { @@ -193,6 +221,20 @@ describe('ui settings', () => { expect(savedObjectsClient.update).toHaveBeenCalledWith(TYPE, ID, { one: null }); }); + it('does not fail validation', async () => { + const defaults = { + foo: { + schema: schema.string(), + value: '1', + }, + }; + const { uiSettings, savedObjectsClient } = setup({ defaults }); + + await uiSettings.remove('foo'); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + }); + it('throws CannotOverrideError if the key is overridden', async () => { const { uiSettings } = setup({ overrides: { @@ -235,6 +277,20 @@ describe('ui settings', () => { }); }); + it('does not fail validation', async () => { + const defaults = { + foo: { + schema: schema.string(), + value: '1', + }, + }; + const { uiSettings, savedObjectsClient } = setup({ defaults }); + + await uiSettings.removeMany(['foo', 'bar']); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + }); + it('throws CannotOverrideError if any key is overridden', async () => { const { uiSettings } = setup({ overrides: { @@ -256,7 +312,13 @@ describe('ui settings', () => { const value = chance.word(); const defaults = { key: { value } }; const { uiSettings } = setup({ defaults }); - expect(uiSettings.getRegistered()).toBe(defaults); + expect(uiSettings.getRegistered()).toEqual(defaults); + }); + it('does not leak validation schema outside', () => { + const value = chance.word(); + const defaults = { key: { value, schema: schema.string() } }; + const { uiSettings } = setup({ defaults }); + expect(uiSettings.getRegistered()).toStrictEqual({ key: { value } }); }); }); @@ -274,7 +336,7 @@ describe('ui settings', () => { const { uiSettings } = setup({ esDocSource }); const result = await uiSettings.getUserProvided(); - expect(result).toEqual({ + expect(result).toStrictEqual({ user: { userValue: 'customized', }, @@ -286,7 +348,7 @@ describe('ui settings', () => { const { uiSettings } = setup({ esDocSource }); const result = await uiSettings.getUserProvided(); - expect(result).toEqual({ + expect(result).toStrictEqual({ user: { userValue: 'customized', }, @@ -296,6 +358,32 @@ describe('ui settings', () => { }); }); + it('ignores user-configured value if it fails validation', async () => { + const esDocSource = { user: 'foo', id: 'bar' }; + const defaults = { + id: { + value: 42, + schema: schema.number(), + }, + }; + const { uiSettings } = setup({ esDocSource, defaults }); + const result = await uiSettings.getUserProvided(); + + expect(result).toStrictEqual({ + user: { + userValue: 'foo', + }, + }); + + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Ignore invalid UiSettings value. Error: [validation [id]]: expected value of type [number] but got [string].", + ], + ] + `); + }); + it('automatically creates the savedConfig if it is missing and returns empty object', async () => { const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); savedObjectsClient.get = jest @@ -303,7 +391,7 @@ describe('ui settings', () => { .mockRejectedValueOnce(SavedObjectsClient.errors.createGenericNotFoundError()) .mockResolvedValueOnce({ attributes: {} }); - expect(await uiSettings.getUserProvided()).toEqual({}); + expect(await uiSettings.getUserProvided()).toStrictEqual({}); expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); @@ -320,7 +408,7 @@ describe('ui settings', () => { SavedObjectsClient.errors.createGenericNotFoundError() ); - expect(await uiSettings.getUserProvided()).toEqual({ foo: { userValue: 'bar ' } }); + expect(await uiSettings.getUserProvided()).toStrictEqual({ foo: { userValue: 'bar ' } }); }); it('returns an empty object on Forbidden responses', async () => { @@ -329,7 +417,7 @@ describe('ui settings', () => { const error = SavedObjectsClient.errors.decorateForbiddenError(new Error()); savedObjectsClient.get.mockRejectedValue(error); - expect(await uiSettings.getUserProvided()).toEqual({}); + expect(await uiSettings.getUserProvided()).toStrictEqual({}); expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); }); @@ -339,7 +427,7 @@ describe('ui settings', () => { const error = SavedObjectsClient.errors.decorateEsUnavailableError(new Error()); savedObjectsClient.get.mockRejectedValue(error); - expect(await uiSettings.getUserProvided()).toEqual({}); + expect(await uiSettings.getUserProvided()).toStrictEqual({}); expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); }); @@ -382,7 +470,7 @@ describe('ui settings', () => { }; const { uiSettings } = setup({ esDocSource, overrides }); - expect(await uiSettings.getUserProvided()).toEqual({ + expect(await uiSettings.getUserProvided()).toStrictEqual({ user: { userValue: 'customized', }, @@ -404,15 +492,40 @@ describe('ui settings', () => { expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, ID); }); - it(`returns defaults when es doc is empty`, async () => { + it('returns defaults when es doc is empty', async () => { const esDocSource = {}; const defaults = { foo: { value: 'bar' } }; const { uiSettings } = setup({ esDocSource, defaults }); - expect(await uiSettings.getAll()).toEqual({ + expect(await uiSettings.getAll()).toStrictEqual({ foo: 'bar', }); }); + it('ignores user-configured value if it fails validation', async () => { + const esDocSource = { user: 'foo', id: 'bar' }; + const defaults = { + id: { + value: 42, + schema: schema.number(), + }, + }; + const { uiSettings } = setup({ esDocSource, defaults }); + const result = await uiSettings.getAll(); + + expect(result).toStrictEqual({ + id: 42, + user: 'foo', + }); + + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Ignore invalid UiSettings value. Error: [validation [id]]: expected value of type [number] but got [string].", + ], + ] + `); + }); + it(`merges user values, including ones without defaults, into key value pairs`, async () => { const esDocSource = { foo: 'user-override', @@ -427,7 +540,7 @@ describe('ui settings', () => { const { uiSettings } = setup({ esDocSource, defaults }); - expect(await uiSettings.getAll()).toEqual({ + expect(await uiSettings.getAll()).toStrictEqual({ foo: 'user-override', bar: 'user-provided', }); @@ -451,7 +564,7 @@ describe('ui settings', () => { const { uiSettings } = setup({ esDocSource, defaults, overrides }); - expect(await uiSettings.getAll()).toEqual({ + expect(await uiSettings.getAll()).toStrictEqual({ foo: 'bax', bar: 'user-provided', }); @@ -518,6 +631,28 @@ describe('ui settings', () => { expect(await uiSettings.get('dateFormat')).toBe('foo'); }); + + it('returns the default value if user-configured value fails validation', async () => { + const esDocSource = { id: 'bar' }; + const defaults = { + id: { + value: 42, + schema: schema.number(), + }, + }; + + const { uiSettings } = setup({ esDocSource, defaults }); + + expect(await uiSettings.get('id')).toBe(42); + + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Ignore invalid UiSettings value. Error: [validation [id]]: expected value of type [number] but got [string].", + ], + ] + `); + }); }); describe('#isOverridden()', () => { diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index a7e55d2b2da6..76c8284175f1 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { defaultsDeep } from 'lodash'; +import { defaultsDeep, omit } from 'lodash'; import { SavedObjectsErrorHelpers } from '../saved_objects'; import { SavedObjectsClientContract } from '../saved_objects/types'; import { Logger } from '../logging'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; -import { IUiSettingsClient, UiSettingsParams } from './types'; +import { IUiSettingsClient, UiSettingsParams, PublicUiSettingsParams } from './types'; import { CannotOverrideError } from './ui_settings_errors'; export interface UiSettingsServiceOptions { @@ -40,14 +40,14 @@ interface ReadOptions { autoCreateOrUpgradeIfMissing?: boolean; } -interface UserProvidedValue { +interface UserProvidedValue { userValue?: T; isOverridden?: boolean; } type UiSettingsRawValue = UiSettingsParams & UserProvidedValue; -type UserProvided = Record>; +type UserProvided = Record>; type UiSettingsRaw = Record; export class UiSettingsClient implements IUiSettingsClient { @@ -72,7 +72,11 @@ export class UiSettingsClient implements IUiSettingsClient { } getRegistered() { - return this.defaults; + const copiedDefaults: Record = {}; + for (const [key, value] of Object.entries(this.defaults)) { + copiedDefaults[key] = omit(value, 'schema'); + } + return copiedDefaults; } async get(key: string): Promise { @@ -90,29 +94,21 @@ export class UiSettingsClient implements IUiSettingsClient { }, {} as Record); } - async getUserProvided(): Promise> { - const userProvided: UserProvided = {}; - - // write the userValue for each key stored in the saved object that is not overridden - for (const [key, userValue] of Object.entries(await this.read())) { - if (userValue !== null && !this.isOverridden(key)) { - userProvided[key] = { - userValue, - }; - } - } + async getUserProvided(): Promise> { + const userProvided: UserProvided = this.onReadHook(await this.read()); // write all overridden keys, dropping the userValue is override is null and // adding keys for overrides that are not in saved object - for (const [key, userValue] of Object.entries(this.overrides)) { + for (const [key, value] of Object.entries(this.overrides)) { userProvided[key] = - userValue === null ? { isOverridden: true } : { isOverridden: true, userValue }; + value === null ? { isOverridden: true } : { isOverridden: true, userValue: value }; } return userProvided; } async setMany(changes: Record) { + this.onWriteHook(changes); await this.write({ changes }); } @@ -147,6 +143,43 @@ export class UiSettingsClient implements IUiSettingsClient { return defaultsDeep(userProvided, this.defaults); } + private validateKey(key: string, value: unknown) { + const definition = this.defaults[key]; + if (value === null || definition === undefined) return; + if (definition.schema) { + definition.schema.validate(value, {}, `validation [${key}]`); + } + } + + private onWriteHook(changes: Record) { + for (const key of Object.keys(changes)) { + this.assertUpdateAllowed(key); + } + + for (const [key, value] of Object.entries(changes)) { + this.validateKey(key, value); + } + } + + private onReadHook(values: Record) { + // write the userValue for each key stored in the saved object that is not overridden + // validate value read from saved objects as it can be changed via SO API + const filteredValues: UserProvided = {}; + for (const [key, userValue] of Object.entries(values)) { + if (userValue === null || this.isOverridden(key)) continue; + try { + this.validateKey(key, userValue); + filteredValues[key] = { + userValue: userValue as T, + }; + } catch (error) { + this.log.warn(`Ignore invalid UiSettings value. ${error}.`); + } + } + + return filteredValues; + } + private async write({ changes, autoCreateOrUpgradeIfMissing = true, @@ -154,10 +187,6 @@ export class UiSettingsClient implements IUiSettingsClient { changes: Record; autoCreateOrUpgradeIfMissing?: boolean; }) { - for (const key of Object.keys(changes)) { - this.assertUpdateAllowed(key); - } - try { await this.savedObjectsClient.update(this.type, this.id, changes); } catch (error) { diff --git a/src/core/server/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_service.test.ts index 11766713b3be..08400f56ad28 100644 --- a/src/core/server/ui_settings/ui_settings_service.test.ts +++ b/src/core/server/ui_settings/ui_settings_service.test.ts @@ -17,6 +17,8 @@ * under the License. */ import { BehaviorSubject } from 'rxjs'; +import { schema } from '@kbn/config-schema'; + import { MockUiSettingsClientConstructor } from './ui_settings_service.test.mock'; import { UiSettingsService, SetupDeps } from './ui_settings_service'; import { httpServiceMock } from '../http/http_service.mock'; @@ -35,6 +37,7 @@ const defaults = { value: 'bar', category: [], description: '', + schema: schema.string(), }, }; @@ -104,6 +107,45 @@ describe('uiSettings', () => { }); describe('#start', () => { + describe('validation', () => { + it('validates registered definitions', async () => { + const { register } = await service.setup(setupDeps); + register({ + custom: { + value: 42, + schema: schema.string(), + }, + }); + + await expect(service.start()).rejects.toMatchInlineSnapshot( + `[Error: [ui settings defaults [custom]]: expected value of type [string] but got [number]]` + ); + }); + + it('validates overrides', async () => { + const coreContext = mockCoreContext.create(); + coreContext.configService.atPath.mockReturnValueOnce( + new BehaviorSubject({ + overrides: { + custom: 42, + }, + }) + ); + const customizedService = new UiSettingsService(coreContext); + const { register } = await customizedService.setup(setupDeps); + register({ + custom: { + value: '42', + schema: schema.string(), + }, + }); + + await expect(customizedService.start()).rejects.toMatchInlineSnapshot( + `[Error: [ui settings overrides [custom]]: expected value of type [string] but got [number]]` + ); + }); + }); + describe('#asScopedToClient', () => { it('passes saved object type "config" to UiSettingsClient', async () => { await service.setup(setupDeps); diff --git a/src/core/server/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_service.ts index de2cc9d510e0..83e66cf6dd06 100644 --- a/src/core/server/ui_settings/ui_settings_service.ts +++ b/src/core/server/ui_settings/ui_settings_service.ts @@ -70,6 +70,9 @@ export class UiSettingsService } public async start(): Promise { + this.validatesDefinitions(); + this.validatesOverrides(); + return { asScopedToClient: this.getScopedClientFactory(), }; @@ -101,4 +104,21 @@ export class UiSettingsService this.uiSettingsDefaults.set(key, value); }); } + + private validatesDefinitions() { + for (const [key, definition] of this.uiSettingsDefaults) { + if (definition.schema) { + definition.schema.validate(definition.value, {}, `ui settings defaults [${key}]`); + } + } + } + + private validatesOverrides() { + for (const [key, value] of Object.entries(this.overrides)) { + const definition = this.uiSettingsDefaults.get(key); + if (definition?.schema) { + definition.schema.validate(value, {}, `ui settings overrides [${key}]`); + } + } + } } diff --git a/src/core/types/ui_settings.ts b/src/core/types/ui_settings.ts index eccd3f9616af..ed1076b57196 100644 --- a/src/core/types/ui_settings.ts +++ b/src/core/types/ui_settings.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -import { SavedObjectAttribute } from './saved_objects'; +import { Type } from '@kbn/config-schema'; /** * UI element type to represent the settings. @@ -49,11 +48,11 @@ export interface DeprecationSettings { * UiSettings parameters defined by the plugins. * @public * */ -export interface UiSettingsParams { +export interface UiSettingsParams { /** title in the UI */ name?: string; /** default value to fall back to if a user doesn't provide any */ - value?: SavedObjectAttribute; + value?: T; /** description provided to a user in UI */ description?: string; /** used to group the configured setting in the UI */ @@ -73,10 +72,22 @@ export interface UiSettingsParams { /* * Allows defining a custom validation applicable to value change on the client. * @deprecated + * Use schema instead. */ validation?: ImageValidation | StringValidation; + /* + * Value validation schema + * Used to validate value on write and read. + */ + schema: Type; } +/** + * A sub-set of {@link UiSettingsParams} exposed to the client-side. + * @public + * */ +export type PublicUiSettingsParams = Omit; + /** * Allows regex objects or a regex string * @public diff --git a/src/core/utils/url.test.ts b/src/core/utils/url.test.ts index 3c35ba44455b..419c0cda2b8c 100644 --- a/src/core/utils/url.test.ts +++ b/src/core/utils/url.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { modifyUrl } from './url'; +import { modifyUrl, isRelativeUrl } from './url'; describe('modifyUrl()', () => { test('throws an error with invalid input', () => { @@ -69,3 +69,17 @@ describe('modifyUrl()', () => { ).toEqual('mail:localhost'); }); }); + +describe('isRelativeUrl()', () => { + test('returns "true" for a relative URL', () => { + expect(isRelativeUrl('good')).toBe(true); + expect(isRelativeUrl('/good')).toBe(true); + expect(isRelativeUrl('/good/even/better')).toBe(true); + }); + test('returns "false" for a non-relative URL', () => { + expect(isRelativeUrl('http://evil.com')).toBe(false); + expect(isRelativeUrl('//evil.com')).toBe(false); + expect(isRelativeUrl('///evil.com')).toBe(false); + expect(isRelativeUrl(' //evil.com')).toBe(false); + }); +}); diff --git a/src/core/utils/url.ts b/src/core/utils/url.ts index 31de7e181403..c2bf80ce3f86 100644 --- a/src/core/utils/url.ts +++ b/src/core/utils/url.ts @@ -99,3 +99,19 @@ export function modifyUrl( slashes: modifiedParts.slashes, } as UrlObject); } + +export function isRelativeUrl(candidatePath: string) { + // validate that `candidatePath` is not attempting a redirect to somewhere + // outside of this Kibana install + const all = parseUrl(candidatePath, false /* parseQueryString */, true /* slashesDenoteHost */); + const { protocol, hostname, port } = all; + // We should explicitly compare `protocol`, `port` and `hostname` to null to make sure these are not + // detected in the URL at all. For example `hostname` can be empty string for Node URL parser, but + // browser (because of various bwc reasons) processes URL differently (e.g. `///abc.com` - for browser + // hostname is `abc.com`, but for Node hostname is an empty string i.e. everything between schema (`//`) + // and the first slash that belongs to path. + if (protocol !== null || hostname !== null || port !== null) { + return false; + } + return true; +} diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index c0628b72c2ce..85b1956f4533 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -16,11 +16,13 @@ * specific language governing permissions and limitations * under the License. */ - import moment from 'moment-timezone'; import numeralLanguages from '@elastic/numeral/languages'; import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + import { DEFAULT_QUERY_LANGUAGE } from '../../../plugins/data/common'; +import { isRelativeUrl } from '../../../core/utils'; export function getUiSettingDefaults() { const weekdays = moment.weekdays().slice(); @@ -67,17 +69,23 @@ export function getUiSettingDefaults() { defaultMessage: 'Default route', }), value: '/app/kibana', - validation: { - regexString: '^/', - message: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteValidationMessage', { - defaultMessage: 'The route must start with a slash ("/")', - }), - }, + schema: schema.string({ + validate(value) { + if (!value.startsWith('/') || !isRelativeUrl(value)) { + return i18n.translate( + 'kbn.advancedSettings.defaultRoute.defaultRouteIsRelativeValidationMessage', + { + defaultMessage: 'Must be a relative URL.', + } + ); + } + }, + }), description: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteText', { defaultMessage: 'This setting specifies the default route when opening Kibana. ' + 'You can use this setting to modify the landing page when opening Kibana. ' + - 'The route must start with a slash ("/").', + 'The route must be a relative URL.', }), }, 'query:queryString:options': { diff --git a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js index 57adf730f3dd..3e3dc284671d 100644 --- a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { Type } from '@kbn/config-schema'; import pkg from '../../../../package.json'; export const createTestEntryTemplate = defaultUiSettings => bundle => ` @@ -87,7 +87,14 @@ const coreSystem = new CoreSystem({ buildNum: 1234, devMode: true, uiSettings: { - defaults: ${JSON.stringify(defaultUiSettings, null, 2) + defaults: ${JSON.stringify( + defaultUiSettings, + (key, value) => { + if (value instanceof Type) return null; + return value; + }, + 2 + ) .split('\n') .join('\n ')}, user: {} diff --git a/src/legacy/server/http/index.js b/src/legacy/server/http/index.js index 265d71e95b30..d616afb533d0 100644 --- a/src/legacy/server/http/index.js +++ b/src/legacy/server/http/index.js @@ -24,15 +24,12 @@ import Boom from 'boom'; import { registerHapiPlugins } from './register_hapi_plugins'; import { setupBasePathProvider } from './setup_base_path_provider'; -import { setupDefaultRouteProvider } from './setup_default_route_provider'; export default async function(kbnServer, server, config) { server = kbnServer.server; setupBasePathProvider(kbnServer); - setupDefaultRouteProvider(server); - await registerHapiPlugins(server); // provide a simple way to expose static directories @@ -60,14 +57,6 @@ export default async function(kbnServer, server, config) { }); }); - server.route({ - path: '/', - method: 'GET', - async handler(req, h) { - return h.redirect(await req.getDefaultRoute()); - }, - }); - server.route({ method: 'GET', path: '/{p*}', diff --git a/src/legacy/server/http/integration_tests/default_route_provider.test.ts b/src/legacy/server/http/integration_tests/default_route_provider.test.ts deleted file mode 100644 index d91438d90455..000000000000 --- a/src/legacy/server/http/integration_tests/default_route_provider.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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. - */ -jest.mock('../../../ui/ui_settings/ui_settings_mixin', () => { - return jest.fn(); -}); - -import * as kbnTestServer from '../../../../test_utils/kbn_server'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Root } from '../../../../core/server/root'; - -let mockDefaultRouteSetting: any = ''; - -describe('default route provider', () => { - let root: Root; - beforeAll(async () => { - root = kbnTestServer.createRoot({ migrations: { skip: true } }); - - await root.setup(); - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - - kbnServer.server.decorate('request', 'getUiSettingsService', function() { - return { - get: (key: string) => { - if (key === 'defaultRoute') { - return Promise.resolve(mockDefaultRouteSetting); - } - throw Error(`unsupported ui setting: ${key}`); - }, - getRegistered: () => { - return { - defaultRoute: { - value: '/app/kibana', - }, - }; - }, - }; - }); - }, 30000); - - afterAll(async () => await root.shutdown()); - - it('redirects to the configured default route', async function() { - mockDefaultRouteSetting = '/app/some/default/route'; - - const { status, header } = await kbnTestServer.request.get(root, '/'); - expect(status).toEqual(302); - expect(header).toMatchObject({ - location: '/app/some/default/route', - }); - }); - - const invalidRoutes = [ - 'http://not-your-kibana.com', - '///example.com', - '//example.com', - ' //example.com', - ]; - for (const route of invalidRoutes) { - it(`falls back to /app/kibana when the configured route (${route}) is not a valid relative path`, async function() { - mockDefaultRouteSetting = route; - - const { status, header } = await kbnTestServer.request.get(root, '/'); - expect(status).toEqual(302); - expect(header).toMatchObject({ - location: '/app/kibana', - }); - }); - } -}); diff --git a/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts b/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts deleted file mode 100644 index 8365941cbeb1..000000000000 --- a/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 * as kbnTestServer from '../../../../test_utils/kbn_server'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Root } from '../../../../core/server/root'; - -describe('default route provider', () => { - let root: Root; - - afterEach(async () => await root.shutdown()); - - it('redirects to the configured default route', async function() { - root = kbnTestServer.createRoot({ - server: { - defaultRoute: '/app/some/default/route', - }, - migrations: { skip: true }, - }); - - await root.setup(); - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - - kbnServer.server.decorate('request', 'getSavedObjectsClient', function() { - return { - get: (type: string, id: string) => ({ attributes: {} }), - }; - }); - - const { status, header } = await kbnTestServer.request.get(root, '/'); - - expect(status).toEqual(302); - expect(header).toMatchObject({ - location: '/app/some/default/route', - }); - }); -}); diff --git a/src/legacy/server/http/setup_default_route_provider.ts b/src/legacy/server/http/setup_default_route_provider.ts deleted file mode 100644 index 9a580dd1c59b..000000000000 --- a/src/legacy/server/http/setup_default_route_provider.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 { Legacy } from 'kibana'; -import { parse } from 'url'; - -export function setupDefaultRouteProvider(server: Legacy.Server) { - server.decorate('request', 'getDefaultRoute', async function() { - // @ts-ignore - const request: Legacy.Request = this; - - const serverBasePath: string = server.config().get('server.basePath'); - - const uiSettings = request.getUiSettingsService(); - - const defaultRoute = await uiSettings.get('defaultRoute'); - const qualifiedDefaultRoute = `${request.getBasePath()}${defaultRoute}`; - - if (isRelativePath(qualifiedDefaultRoute, serverBasePath)) { - return qualifiedDefaultRoute; - } else { - server.log( - ['http', 'warn'], - `Ignoring configured default route of '${defaultRoute}', as it is malformed.` - ); - - const fallbackRoute = uiSettings.getRegistered().defaultRoute.value; - - const qualifiedFallbackRoute = `${request.getBasePath()}${fallbackRoute}`; - return qualifiedFallbackRoute; - } - }); - - function isRelativePath(candidatePath: string, basePath = '') { - // validate that `candidatePath` is not attempting a redirect to somewhere - // outside of this Kibana install - const { protocol, hostname, port, pathname } = parse( - candidatePath, - false /* parseQueryString */, - true /* slashesDenoteHost */ - ); - - // We should explicitly compare `protocol`, `port` and `hostname` to null to make sure these are not - // detected in the URL at all. For example `hostname` can be empty string for Node URL parser, but - // browser (because of various bwc reasons) processes URL differently (e.g. `///abc.com` - for browser - // hostname is `abc.com`, but for Node hostname is an empty string i.e. everything between schema (`//`) - // and the first slash that belongs to path. - if (protocol !== null || hostname !== null || port !== null) { - return false; - } - - if (!String(pathname).startsWith(basePath)) { - return false; - } - - return true; - } -} diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 68b5a6387137..9952b345fa06 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -92,7 +92,6 @@ declare module 'hapi' { interface Request { getSavedObjectsClient(options?: SavedObjectsClientProviderOptions): SavedObjectsClientContract; getBasePath(): string; - getDefaultRoute(): Promise; getUiSettingsService(): IUiSettingsClient; } diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx index 7a2ab648ec25..6103041cf0a4 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx @@ -22,7 +22,11 @@ import { Observable } from 'rxjs'; import { ReactWrapper } from 'enzyme'; import { mountWithI18nProvider } from 'test_utils/enzyme_helpers'; import dedent from 'dedent'; -import { UiSettingsParams, UserProvidedValues, UiSettingsType } from '../../../../core/public'; +import { + PublicUiSettingsParams, + UserProvidedValues, + UiSettingsType, +} from '../../../../core/public'; import { FieldSetting } from './types'; import { AdvancedSettingsComponent } from './advanced_settings'; import { notificationServiceMock, docLinksServiceMock } from '../../../../core/public/mocks'; @@ -68,7 +72,7 @@ function mockConfig() { remove: (key: string) => Promise.resolve(true), isCustom: (key: string) => false, isOverridden: (key: string) => Boolean(config.getAll()[key].isOverridden), - getRegistered: () => ({} as Readonly>), + getRegistered: () => ({} as Readonly>), overrideLocalDefault: (key: string, value: any) => {}, getUpdate$: () => new Observable<{ @@ -89,7 +93,7 @@ function mockConfig() { getUpdateErrors$: () => new Observable(), get: (key: string, defaultOverride?: any): any => config.getAll()[key] || defaultOverride, get$: (key: string) => new Observable(config.get(key)), - getAll: (): Readonly> => { + getAll: (): Readonly> => { return { 'test:array:setting': { ...defaultConfig, diff --git a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.test.ts b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.test.ts index 881a2eb003cc..7ac9b281eb99 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.test.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { UiSettingsParams, StringValidationRegex } from 'src/core/public'; +import { PublicUiSettingsParams, StringValidationRegex } from 'src/core/public'; import expect from '@kbn/expect'; import { toEditableConfig } from './to_editable_config'; @@ -30,7 +30,7 @@ function invoke({ name = 'woah', value = 'forreal', }: { - def?: UiSettingsParams & { isOverridden?: boolean }; + def?: PublicUiSettingsParams & { isOverridden?: boolean }; name?: string; value?: any; }) { @@ -55,7 +55,7 @@ describe('Settings', function() { }); describe('when given a setting definition object', function() { - let def: UiSettingsParams & { isOverridden?: boolean }; + let def: PublicUiSettingsParams & { isOverridden?: boolean }; beforeEach(function() { def = { value: 'the original', diff --git a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts index 2c27d72f7f64..406bc35f826e 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts @@ -18,7 +18,7 @@ */ import { - UiSettingsParams, + PublicUiSettingsParams, UserProvidedValues, StringValidationRegexString, SavedObjectAttribute, @@ -40,7 +40,7 @@ export function toEditableConfig({ isCustom, isOverridden, }: { - def: UiSettingsParams & UserProvidedValues; + def: PublicUiSettingsParams & UserProvidedValues; name: string; value: SavedObjectAttribute; isCustom: boolean; diff --git a/src/plugins/advanced_settings/public/management_app/types.ts b/src/plugins/advanced_settings/public/management_app/types.ts index d44a05ce36f5..ee9b9b0535b7 100644 --- a/src/plugins/advanced_settings/public/management_app/types.ts +++ b/src/plugins/advanced_settings/public/management_app/types.ts @@ -17,17 +17,12 @@ * under the License. */ -import { - UiSettingsType, - StringValidation, - ImageValidation, - SavedObjectAttribute, -} from '../../../../core/public'; +import { UiSettingsType, StringValidation, ImageValidation } from '../../../../core/public'; export interface FieldSetting { displayName: string; name: string; - value: SavedObjectAttribute; + value: unknown; description?: string; options?: string[]; optionLabels?: Record; @@ -36,7 +31,7 @@ export interface FieldSetting { category: string[]; ariaName: string; isOverridden: boolean; - defVal: SavedObjectAttribute; + defVal: unknown; isCustom: boolean; validation?: StringValidation | ImageValidation; readOnly?: boolean; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 333b13eedc17..783411bbf27e 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -38,6 +38,7 @@ import { Observable } from 'rxjs'; import { Plugin as Plugin_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import { PopoverAnchorPosition } from '@elastic/eui'; +import { PublicUiSettingsParams } from 'src/core/server/types'; import React from 'react'; import * as React_2 from 'react'; import { Required } from '@kbn/utility-types'; @@ -49,7 +50,6 @@ import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; import { SimpleSavedObject } from 'src/core/public'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; -import { UiSettingsParams } from 'src/core/server/types'; import { Unit } from '@elastic/datemath'; import { UnregisterCallback } from 'history'; import { UserProvidedValues } from 'src/core/server/types'; diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts index f4c3ecd8243c..12f7eb5a0a04 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/test_utils/kbn_server.ts @@ -50,6 +50,7 @@ const DEFAULTS_SETTINGS = { logging: { silent: true }, plugins: {}, optimize: { enabled: false }, + migrations: { skip: true }, }; const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index 8cdbbf8e74a3..57e9120773f3 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -33,6 +33,5 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./status')); loadTestFile(require.resolve('./stats')); loadTestFile(require.resolve('./ui_metric')); - loadTestFile(require.resolve('./core')); }); } diff --git a/test/plugin_functional/plugins/ui_settings_plugin/server/plugin.ts b/test/plugin_functional/plugins/ui_settings_plugin/server/plugin.ts index c32e8a75d95d..3801d3bbce05 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/server/plugin.ts +++ b/test/plugin_functional/plugins/ui_settings_plugin/server/plugin.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { schema } from '@kbn/config-schema'; import { Plugin, CoreSetup } from 'kibana/server'; export class UiSettingsPlugin implements Plugin { @@ -27,6 +27,7 @@ export class UiSettingsPlugin implements Plugin { description: 'just for testing', value: '2', category: ['any'], + schema: schema.string(), }, }); diff --git a/x-pack/legacy/plugins/spaces/server/routes/views/enter_space.ts b/x-pack/legacy/plugins/spaces/server/routes/views/enter_space.ts index 337faa2a18fb..60ae3a1fa77b 100644 --- a/x-pack/legacy/plugins/spaces/server/routes/views/enter_space.ts +++ b/x-pack/legacy/plugins/spaces/server/routes/views/enter_space.ts @@ -14,7 +14,13 @@ export function initEnterSpaceView(server: Legacy.Server) { path: ENTER_SPACE_PATH, async handler(request, h) { try { - return h.redirect(await request.getDefaultRoute()); + const uiSettings = request.getUiSettingsService(); + const defaultRoute = await uiSettings.get('defaultRoute'); + + const basePath = server.newPlatform.setup.core.http.basePath.get(request); + const url = `${basePath}${defaultRoute}`; + + return h.redirect(url); } catch (e) { server.log(['spaces', 'error'], `Error navigating to space: ${e}`); return wrapError(e); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fb0eb6e4bf80..3763020a0b69 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -833,7 +833,6 @@ "kbn.advancedSettings.defaultIndexTitle": "デフォルトのインデックス", "kbn.advancedSettings.defaultRoute.defaultRouteText": "この設定は、Kibana 起動時のデフォルトのルートを設定します。この設定で、Kibana 起動時のランディングページを変更できます。ルートはスラッシュ (\"/\") で始まる必要があります。", "kbn.advancedSettings.defaultRoute.defaultRouteTitle": "デフォルトのルート", - "kbn.advancedSettings.defaultRoute.defaultRouteValidationMessage": "ルートはスラッシュ (\"/\") で始まる必要があります。", "kbn.advancedSettings.disableAnimationsText": "Kibana UI の不要なアニメーションをオフにします。変更を適用するにはページを更新してください。", "kbn.advancedSettings.disableAnimationsTitle": "アニメーションを無効にする", "kbn.advancedSettings.discover.aggsTermsSizeText": "「可視化」ボタンをクリックした際に、フィールドドロップダウンやディスカバリサイドバーに可視化される用語の数を設定します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0a9c82afaec1..0477470a4b8a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -833,7 +833,6 @@ "kbn.advancedSettings.defaultIndexTitle": "默认索引", "kbn.advancedSettings.defaultRoute.defaultRouteText": "此设置指定打开 Kibana 时的默认路由。您可以使用此设置修改打开 Kibana 时的登陆页面。路由必须以正斜杠(“/”)开头。", "kbn.advancedSettings.defaultRoute.defaultRouteTitle": "默认路由", - "kbn.advancedSettings.defaultRoute.defaultRouteValidationMessage": "路由必须以正斜杠(“/”)开头", "kbn.advancedSettings.disableAnimationsText": "在 Kibana UI 中关闭所有没有必要的动画。刷新页面以应用更改。", "kbn.advancedSettings.disableAnimationsTitle": "禁用动画", "kbn.advancedSettings.discover.aggsTermsSizeText": "确定在单击“可视化”按钮时将在发现侧边栏的字段下拉列表中可视化多少个词。", diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index e0b1ec544d46..38220c15cb26 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -10,7 +10,6 @@ export default function enterSpaceFunctonalTests({ getPageObjects, }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['security', 'spaceSelector']); describe('Enter Space', function() { @@ -25,8 +24,8 @@ export default function enterSpaceFunctonalTests({ await PageObjects.security.forceLogout(); }); - it('allows user to navigate to different spaces, respecting the configured default route', async () => { - const spaceId = 'another-space'; + it('falls back to the default home page when the configured default route is malformed', async () => { + const spaceId = 'default'; await PageObjects.security.login(null, null, { expectSpaceSelector: true, @@ -34,22 +33,11 @@ export default function enterSpaceFunctonalTests({ await PageObjects.spaceSelector.clickSpaceCard(spaceId); - await PageObjects.spaceSelector.expectRoute(spaceId, '/app/kibana/#/dashboard'); - - await PageObjects.spaceSelector.openSpacesNav(); - - // change spaces - - await PageObjects.spaceSelector.clickSpaceAvatar('default'); - - await PageObjects.spaceSelector.expectRoute('default', '/app/canvas'); + await PageObjects.spaceSelector.expectHomePage(spaceId); }); - it('falls back to the default home page when the configured default route is malformed', async () => { - await kibanaServer.uiSettings.replace({ defaultRoute: 'http://example.com/evil' }); - - // This test only works with the default space, as other spaces have an enforced relative url of `${serverBasePath}/s/space-id/${defaultRoute}` - const spaceId = 'default'; + it('allows user to navigate to different spaces, respecting the configured default route', async () => { + const spaceId = 'another-space'; await PageObjects.security.login(null, null, { expectSpaceSelector: true, @@ -57,7 +45,15 @@ export default function enterSpaceFunctonalTests({ await PageObjects.spaceSelector.clickSpaceCard(spaceId); - await PageObjects.spaceSelector.expectHomePage(spaceId); + await PageObjects.spaceSelector.expectRoute(spaceId, '/app/canvas'); + + await PageObjects.spaceSelector.openSpacesNav(); + + // change spaces + const newSpaceId = 'default'; + await PageObjects.spaceSelector.clickSpaceAvatar(newSpaceId); + + await PageObjects.spaceSelector.expectHomePage(newSpaceId); }); }); } diff --git a/x-pack/test/functional/es_archives/spaces/enter_space/data.json b/x-pack/test/functional/es_archives/spaces/enter_space/data.json index 462a2a1ee38f..475fc14e96e6 100644 --- a/x-pack/test/functional/es_archives/spaces/enter_space/data.json +++ b/x-pack/test/functional/es_archives/spaces/enter_space/data.json @@ -7,7 +7,7 @@ "config": { "buildNum": 8467, "dateFormat:tz": "UTC", - "defaultRoute": "/app/canvas" + "defaultRoute": "http://example.com/evil" }, "type": "config" } @@ -24,7 +24,7 @@ "config": { "buildNum": 8467, "dateFormat:tz": "UTC", - "defaultRoute": "/app/kibana/#dashboard" + "defaultRoute": "/app/canvas" }, "type": "config" } From 1f8e938b9c9b996e438df8cae11da62c59ec743b Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 16 Mar 2020 14:38:31 +0100 Subject: [PATCH 007/115] [Searchprofiler] Spacing between rendered shards (#60238) * Added unique key and some spacing to rendered shards * Give key to React.Fragment --- .../components/profile_tree/profile_tree.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/profile_tree.tsx b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/profile_tree.tsx index 87a73cdefba3..1dec8f0161c5 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/profile_tree.tsx +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/profile_tree.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo } from 'react'; +import React, { memo, Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { IndexDetails } from './index_details'; @@ -53,13 +53,11 @@ export const ProfileTree = memo(({ data, target, onHighlight }: Props) => { - {index.shards.map(shard => ( - + {index.shards.map((shard, idx) => ( + + + {idx < index.shards.length - 1 ? : undefined} + ))} From 6598298d016f6e7ca4feab4cd74f734fbe9c404f Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 16 Mar 2020 15:13:03 +0100 Subject: [PATCH 008/115] skips 'config_open.ts' files from linter check (#60248) --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index 7af162f349b5..3d6a5c262c45 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -322,6 +322,7 @@ module.exports = { 'x-pack/test/functional/apps/**/*.js', 'x-pack/legacy/plugins/apm/**/*.js', 'test/*/config.ts', + 'test/*/config_open.ts', 'test/*/{tests,test_suites,apis,apps}/**/*', 'test/visual_regression/tests/**/*', 'x-pack/test/*/{tests,test_suites,apis,apps}/**/*', From 56006534af1ccb7437e7ae0d60204760dd4b2110 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 16 Mar 2020 09:58:16 -0500 Subject: [PATCH 009/115] [DOCS] Removed below references (#60159) --- docs/apm/advanced-queries.asciidoc | 2 +- docs/apm/spans.asciidoc | 6 +- docs/apm/transactions.asciidoc | 8 +- .../canvas/canvas-tinymath-functions.asciidoc | 207 +++++++++--------- .../searchprofiler/more-complicated.asciidoc | 4 +- .../development-functional-tests.asciidoc | 4 +- ...pment-plugin-feature-registration.asciidoc | 4 +- .../development-plugin-localization.asciidoc | 2 +- docs/discover/kuery.asciidoc | 26 +-- docs/discover/search.asciidoc | 15 +- docs/epm/index.asciidoc | 10 +- docs/management/numeral.asciidoc | 2 +- .../create_and_manage_rollups.asciidoc | 10 +- docs/maps/geojson-upload.asciidoc | 2 +- .../indexing-geojson-data-tutorial.asciidoc | 4 +- docs/maps/vector-style.asciidoc | 8 +- docs/migration/migrate_8_0.asciidoc | 14 +- docs/settings/apm-settings.asciidoc | 2 +- docs/settings/ssl-settings.asciidoc | 6 +- docs/uptime-guide/install.asciidoc | 2 +- docs/uptime-guide/security.asciidoc | 5 +- docs/user/graph/getting-started.asciidoc | 68 +++--- docs/user/monitoring/beats-details.asciidoc | 2 +- docs/user/security/rbac_tutorial.asciidoc | 47 ++-- docs/visualize/vega.asciidoc | 2 +- 25 files changed, 227 insertions(+), 235 deletions(-) diff --git a/docs/apm/advanced-queries.asciidoc b/docs/apm/advanced-queries.asciidoc index ed77ebb4c493..add6f601489e 100644 --- a/docs/apm/advanced-queries.asciidoc +++ b/docs/apm/advanced-queries.asciidoc @@ -5,7 +5,7 @@ When querying in the APM app, you're simply searching and selecting data from fi Queries entered into the query bar are also added as parameters to the URL, so it's easy to share a specific query or view with others. -In the screenshot below, you can begin to see some of the transaction fields available for filtering on: +You can begin to see some of the transaction fields available for filtering: [role="screenshot"] image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app in Kibana] diff --git a/docs/apm/spans.asciidoc b/docs/apm/spans.asciidoc index b1d54ce49c7c..b09de576f2d4 100644 --- a/docs/apm/spans.asciidoc +++ b/docs/apm/spans.asciidoc @@ -12,12 +12,12 @@ This makes it useful for visualizing where the selected transaction spent most o image::apm/images/apm-transaction-sample.png[Example of distributed trace colors in the APM app in Kibana] View a span in detail by clicking on it in the timeline waterfall. -For example, in the below screenshot we've clicked on an SQL Select database query. -The information displayed includes the actual SQL that was executed, how long it took, +When you click on an SQL Select database query, +the information displayed includes the actual SQL that was executed, how long it took, and the percentage of the trace's total time. You also get a stack trace, which shows the SQL query in your code. Finally, APM knows which files are your code and which are just modules or libraries that you've installed. -These library frames will be minimized by default in order to show you the most relevant stack trace. +These library frames will be minimized by default in order to show you the most relevant stack trace. [role="screenshot"] image::apm/images/apm-span-detail.png[Example view of a span detail in the APM app in Kibana] diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 9c21a569f152..536ab2ec29c8 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -50,7 +50,7 @@ If there's a particular endpoint you're worried about, you can click on it to vi [IMPORTANT] ==== If you only see one route in the Transactions table, or if you have transactions named "unknown route", -it could be a symptom that the agent either wasn't installed correctly or doesn't support your framework. +it could be a symptom that the agent either wasn't installed correctly or doesn't support your framework. For further details, including troubleshooting and custom implementation instructions, refer to the documentation for each {apm-agents-ref}[APM Agent] you've implemented. @@ -103,9 +103,7 @@ The number of requests per bucket is displayed when hovering over the graph, and [role="screenshot"] image::apm/images/apm-transaction-duration-dist.png[Example view of transactions duration distribution graph] -Let's look at an example. -In the screenshot below, -you'll notice most of the requests fall into buckets on the left side of the graph, +Most of the requests fall into buckets on the left side of the graph, with a long tail of smaller buckets to the right. This is a typical distribution, and indicates most of our requests were served quickly - awesome! It's the requests on the right, the ones taking longer than average, that we probably want to focus on. @@ -133,4 +131,4 @@ For a particular transaction sample, we can get even more information in the *me * Custom - You can configure your agent to add custom contextual information on transactions. TIP: All of this data is stored in documents in Elasticsearch. -This means you can select "Actions - View sample document" to see the actual Elasticsearch document under the discover tab. \ No newline at end of file +This means you can select "Actions - View sample document" to see the actual Elasticsearch document under the discover tab. diff --git a/docs/canvas/canvas-tinymath-functions.asciidoc b/docs/canvas/canvas-tinymath-functions.asciidoc index 8c9f445b052a..73808fc6625d 100644 --- a/docs/canvas/canvas-tinymath-functions.asciidoc +++ b/docs/canvas/canvas-tinymath-functions.asciidoc @@ -3,21 +3,21 @@ === TinyMath functions TinyMath provides a set of functions that can be used with the Canvas expression -language to perform complex math calculations. Read on for detailed information about -the functions available in TinyMath, including what parameters each function accepts, +language to perform complex math calculations. Read on for detailed information about +the functions available in TinyMath, including what parameters each function accepts, the return value of that function, and examples of how each function behaves. -Most of the functions below accept arrays and apply JavaScript Math methods to -each element of that array. For the functions that accept multiple arrays as -parameters, the function generally does the calculation index by index. +Most of the functions accept arrays and apply JavaScript Math methods to +each element of that array. For the functions that accept multiple arrays as +parameters, the function generally does the calculation index by index. -Any function below can be wrapped by another function as long as the return type +Any function can be wrapped by another function as long as the return type of the inner function matches the acceptable parameter type of the outer function. [float] === abs( a ) -Calculates the absolute value of a number. For arrays, the function will be +Calculates the absolute value of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -29,7 +29,7 @@ applied index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The absolute value of `a`. Returns +*Returns*: `number` | `Array.`. The absolute value of `a`. Returns an array with the absolute values of each element if `a` is an array. *Example* @@ -43,7 +43,7 @@ abs([-1 , -2, 3, -4]) // returns [1, 2, 3, 4] [float] === add( ...args ) -Calculates the sum of one or more numbers/arrays passed into the function. If at +Calculates the sum of one or more numbers/arrays passed into the function. If at least one array of numbers is passed into the function, the function will calculate the sum by index. [cols="3*^<"] @@ -55,9 +55,9 @@ least one array of numbers is passed into the function, the function will calcul |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The sum of all numbers in `args` if `args` -contains only numbers. Returns an array of sums of the elements at each index, -including all scalar numbers in `args` in the calculation at each index if `args` +*Returns*: `number` | `Array.`. The sum of all numbers in `args` if `args` +contains only numbers. Returns an array of sums of the elements at each index, +including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Throws*: `'Array length mismatch'` if `args` contains arrays of different lengths @@ -73,7 +73,7 @@ add([1, 2], 3, [4, 5], 6) // returns [(1 + 3 + 4 + 6), (2 + 3 + 5 + 6)] = [14, 1 [float] === cbrt( a ) -Calculates the cube root of a number. For arrays, the function will be applied +Calculates the cube root of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -85,7 +85,7 @@ index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The cube root of `a`. Returns an array with +*Returns*: `number` | `Array.`. The cube root of `a`. Returns an array with the cube roots of each element if `a` is an array. *Example* @@ -99,7 +99,7 @@ cbrt([27, 64, 125]) // returns [3, 4, 5] [float] === ceil( a ) -Calculates the ceiling of a number, i.e., rounds a number towards positive infinity. +Calculates the ceiling of a number, i.e., rounds a number towards positive infinity. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -111,7 +111,7 @@ For arrays, the function will be applied index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The ceiling of `a`. Returns an array with +*Returns*: `number` | `Array.`. The ceiling of `a`. Returns an array with the ceilings of each element if `a` is an array. *Example* @@ -125,7 +125,7 @@ ceil([1.1, 2.2, 3.3]) // returns [2, 3, 4] [float] === clamp( ...a, min, max ) -Restricts value to a given range and returns closed available value. If only `min` +Restricts value to a given range and returns closed available value. If only `min` is provided, values are restricted to only a lower bound. [cols="3*^<"] @@ -145,11 +145,11 @@ is provided, values are restricted to only a lower bound. |(optional) The maximum value this function will return. |=== -*Returns*: `number` | `Array.`. The closest value between `min` (inclusive) -and `max` (inclusive). Returns an array with values greater than or equal to `min` +*Returns*: `number` | `Array.`. The closest value between `min` (inclusive) +and `max` (inclusive). Returns an array with values greater than or equal to `min` and less than or equal to `max` (if provided) at each index. -*Throws*: +*Throws*: - `'Array length mismatch'` if a `min` and/or `max` are arrays of different lengths @@ -194,7 +194,7 @@ count(100) // returns 1 [float] === cube( a ) -Calculates the cube of a number. For arrays, the function will be applied +Calculates the cube of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -206,7 +206,7 @@ index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The cube of `a`. Returns an array +*Returns*: `number` | `Array.`. The cube of `a`. Returns an array with the cubes of each element if `a` is an array. *Example* @@ -219,7 +219,7 @@ cube([3, 4, 5]) // returns [27, 64, 125] [float] === divide( a, b ) -Divides two numbers. If at least one array of numbers is passed into the function, +Divides two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. [cols="3*^<"] @@ -235,8 +235,8 @@ the function will be applied index-wise to each element. |divisor, a number or an array of numbers, b != 0 |=== -*Returns*: `number` | `Array.`. Returns the quotient of `a` and `b` -if both are numbers. Returns an array with the quotients applied index-wise to +*Returns*: `number` | `Array.`. Returns the quotient of `a` and `b` +if both are numbers. Returns an array with the quotients applied index-wise to each element if `a` or `b` is an array. *Throws*: @@ -257,7 +257,7 @@ divide([14, 42, 65, 108], [2, 7, 5, 12]) // returns [7, 6, 13, 9] [float] === exp( a ) -Calculates _e^x_ where _e_ is Euler's number. For arrays, the function will be applied +Calculates _e^x_ where _e_ is Euler's number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -269,7 +269,7 @@ index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. Returns an array with the values of +*Returns*: `number` | `Array.`. Returns an array with the values of `e^x` evaluated where `x` is each element of `a` if `a` is an array. *Example* @@ -282,7 +282,7 @@ exp([1, 2, 3]) // returns [e^1, e^2, e^3] = [2.718281828459045, 7.38905609893064 [float] === first( a ) -Returns the first element of an array. If anything other than an array is passed +Returns the first element of an array. If anything other than an array is passed in, the input is returned. [cols="3*^<"] @@ -306,7 +306,7 @@ first([1, 2, 3]) // returns 1 [float] === fix( a ) -Calculates the fix of a number, i.e., rounds a number towards 0. For arrays, the +Calculates the fix of a number, i.e., rounds a number towards 0. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -318,7 +318,7 @@ function will be applied index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The fix of `a`. Returns an array with +*Returns*: `number` | `Array.`. The fix of `a`. Returns an array with the fixes for each element if `a` is an array. *Example* @@ -332,7 +332,7 @@ fix([1.8, 2.9, -3.7, -4.6]) // returns [1, 2, -3, -4] [float] === floor( a ) -Calculates the floor of a number, i.e., rounds a number towards negative infinity. +Calculates the floor of a number, i.e., rounds a number towards negative infinity. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -344,7 +344,7 @@ For arrays, the function will be applied index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The floor of `a`. Returns an array +*Returns*: `number` | `Array.`. The floor of `a`. Returns an array with the floor of each element if `a` is an array. *Example* @@ -358,7 +358,7 @@ floor([1.7, 2.8, 3.9]) // returns [1, 2, 3] [float] === last( a ) -Returns the last element of an array. If anything other than an array is passed +Returns the last element of an array. If anything other than an array is passed in, the input is returned. [cols="3*^<"] @@ -382,7 +382,7 @@ last([1, 2, 3]) // returns 3 [float] === log( a, b ) -Calculates the logarithm of a number. For arrays, the function will be applied +Calculates the logarithm of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -398,7 +398,7 @@ index-wise to each element. |(optional) base for the logarithm. If not provided a value, the default base is e, and the natural log is calculated. |=== -*Returns*: `number` | `Array.`. The logarithm of `a`. Returns an array +*Returns*: `number` | `Array.`. The logarithm of `a`. Returns an array with the the logarithms of each element if `a` is an array. *Throws*: @@ -419,7 +419,7 @@ log([2, 4, 8, 16, 32], 2) // returns [1, 2, 3, 4, 5] [float] === log10( a ) -Calculates the logarithm base 10 of a number. For arrays, the function will be +Calculates the logarithm base 10 of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -431,7 +431,7 @@ applied index-wise to each element. |a number or an array of numbers, `a` must be greater than 0 |=== -*Returns*: `number` | `Array.`. The logarithm of `a`. Returns an array +*Returns*: `number` | `Array.`. The logarithm of `a`. Returns an array with the the logarithms base 10 of each element if `a` is an array. *Throws*: `'Must be greater than 0'` if `a` < 0 @@ -448,8 +448,8 @@ log([10, 100, 1000, 10000, 100000]) // returns [1, 2, 3, 4, 5] [float] === max( ...args ) -Finds the maximum value of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will +Finds the maximum value of one of more numbers/arrays of numbers passed into the function. +If at least one array of numbers is passed into the function, the function will find the maximum by index. [cols="3*^<"] @@ -461,9 +461,9 @@ find the maximum by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The maximum value of all numbers if -`args` contains only numbers. Returns an array with the the maximum values at each -index, including all scalar numbers in `args` in the calculation at each index if +*Returns*: `number` | `Array.`. The maximum value of all numbers if +`args` contains only numbers. Returns an array with the the maximum values at each +index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Throws*: `'Array length mismatch'` if `args` contains arrays of different lengths @@ -479,8 +479,8 @@ max([1, 9], 4, [3, 5]) // returns [max([1, 4, 3]), max([9, 4, 5])] = [4, 9] [float] === mean( ...args ) -Finds the mean value of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will +Finds the mean value of one of more numbers/arrays of numbers passed into the function. +If at least one array of numbers is passed into the function, the function will find the mean by index. [cols="3*^<"] @@ -492,9 +492,9 @@ find the mean by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The maximum value of all numbers if -`args` contains only numbers. Returns an array with the the maximum values at each -index, including all scalar numbers in `args` in the calculation at each index if +*Returns*: `number` | `Array.`. The maximum value of all numbers if +`args` contains only numbers. Returns an array with the the maximum values at each +index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Throws*: `'Array length mismatch'` if `args` contains arrays of different lengths @@ -510,8 +510,8 @@ max([1, 9], 4, [3, 5]) // returns [max([1, 4, 3]), max([9, 4, 5])] = [4, 9] [float] === mean( ...args ) -Finds the mean value of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will +Finds the mean value of one of more numbers/arrays of numbers passed into the function. +If at least one array of numbers is passed into the function, the function will find the mean by index. [cols="3*^<"] @@ -523,9 +523,9 @@ find the mean by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The mean value of all numbers if `args` -contains only numbers. Returns an array with the the mean values of each index, -including all scalar numbers in `args` in the calculation at each index if `args` +*Returns*: `number` | `Array.`. The mean value of all numbers if `args` +contains only numbers. Returns an array with the the mean values of each index, +including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Example* @@ -539,8 +539,8 @@ mean([1, 9], 5, [3, 4]) // returns [mean([1, 5, 3]), mean([9, 5, 4])] = [3, 6] [float] === median( ...args ) -Finds the median value(s) of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will +Finds the median value(s) of one of more numbers/arrays of numbers passed into the function. +If at least one array of numbers is passed into the function, the function will find the median by index. [cols="3*^<"] @@ -552,9 +552,9 @@ find the median by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The median value of all numbers if `args` -contains only numbers. Returns an array with the the median values of each index, -including all scalar numbers in `args` in the calculation at each index if `args` +*Returns*: `number` | `Array.`. The median value of all numbers if `args` +contains only numbers. Returns an array with the the median values of each index, +including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Example* @@ -569,8 +569,8 @@ median([1, 9], 2, 4, [3, 5]) // returns [median([1, 2, 4, 3]), median([9, 2, 4, [float] === min( ...args ) -Finds the minimum value of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will +Finds the minimum value of one of more numbers/arrays of numbers passed into the function. +If at least one array of numbers is passed into the function, the function will find the minimum by index. [cols="3*^<"] @@ -582,9 +582,9 @@ find the minimum by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The minimum value of all numbers if -`args` contains only numbers. Returns an array with the the minimum values of each -index, including all scalar numbers in `args` in the calculation at each index if `a` +*Returns*: `number` | `Array.`. The minimum value of all numbers if +`args` contains only numbers. Returns an array with the the minimum values of each +index, including all scalar numbers in `args` in the calculation at each index if `a` is an array. *Throws*: `'Array length mismatch'` if `args` contains arrays of different lengths. @@ -600,7 +600,7 @@ min([1, 9], 4, [3, 5]) // returns [min([1, 4, 3]), min([9, 4, 5])] = [1, 4] [float] === mod( a, b ) -Remainder after dividing two numbers. If at least one array of numbers is passed +Remainder after dividing two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. [cols="3*^<"] @@ -616,8 +616,8 @@ into the function, the function will be applied index-wise to each element. |divisor, a number or an array of numbers, b != 0 |=== -*Returns*: `number` | `Array.`. The remainder of `a` divided by `b` if -both are numbers. Returns an array with the the remainders applied index-wise to +*Returns*: `number` | `Array.`. The remainder of `a` divided by `b` if +both are numbers. Returns an array with the the remainders applied index-wise to each element if `a` or `b` is an array. *Throws*: @@ -638,8 +638,8 @@ mod([14, 42, 65, 108], [5, 4, 14, 2]) // returns [5, 2, 9, 0] [float] === mode( ...args ) -Finds the mode value(s) of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will +Finds the mode value(s) of one of more numbers/arrays of numbers passed into the function. +If at least one array of numbers is passed into the function, the function will find the mode by index. [cols="3*^<"] @@ -651,9 +651,9 @@ find the mode by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.>`. An array of mode value(s) of all -numbers if `args` contains only numbers. Returns an array of arrays with mode value(s) -of each index, including all scalar numbers in `args` in the calculation at each index +*Returns*: `number` | `Array.>`. An array of mode value(s) of all +numbers if `args` contains only numbers. Returns an array of arrays with mode value(s) +of each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Example* @@ -668,7 +668,7 @@ mode([1, 9], 1, 4, [3, 5]) // returns [mode([1, 1, 4, 3]), mode([9, 1, 4, 5])] = [float] === multiply( a, b ) -Multiplies two numbers. If at least one array of numbers is passed into the function, +Multiplies two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. [cols="3*^<"] @@ -684,11 +684,11 @@ the function will be applied index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The product of `a` and `b` if both are -numbers. Returns an array with the the products applied index-wise to each element +*Returns*: `number` | `Array.`. The product of `a` and `b` if both are +numbers. Returns an array with the the products applied index-wise to each element if `a` or `b` is an array. -*Throws*: `'Array length mismatch'` if `a` and `b` are arrays with different lengths +*Throws*: `'Array length mismatch'` if `a` and `b` are arrays with different lengths *Example* [source, js] @@ -702,7 +702,7 @@ multiply([1, 2, 3, 4], [2, 7, 5, 12]) // returns [2, 14, 15, 48] [float] === pow( a, b ) -Calculates the cube root of a number. For arrays, the function will be applied +Calculates the cube root of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -718,7 +718,7 @@ index-wise to each element. |the power that `a` is raised to |=== -*Returns*: `number` | `Array.`. `a` raised to the power of `b`. Returns +*Returns*: `number` | `Array.`. `a` raised to the power of `b`. Returns an array with the each element raised to the power of `b` if `a` is an array. *Throws*: `'Missing exponent'` if `b` is not provided @@ -733,8 +733,8 @@ pow([1, 2, 3], 4) // returns [1, 16, 81] [float] === random( a, b ) -Generates a random number within the given range where the lower bound is inclusive -and the upper bound is exclusive. If no numbers are passed in, it will return a +Generates a random number within the given range where the lower bound is inclusive +and the upper bound is exclusive. If no numbers are passed in, it will return a number between 0 and 1. If only one number is passed in, it will return a number between 0 and the number passed in. @@ -751,11 +751,11 @@ between 0 and the number passed in. |(optional) must be greater than `a` |=== -*Returns*: `number`. A random number between 0 and 1 if no numbers are passed in. -Returns a random number between 0 and `a` if only one number is passed in. Returns +*Returns*: `number`. A random number between 0 and 1 if no numbers are passed in. +Returns a random number between 0 and `a` if only one number is passed in. Returns a random number between `a` and `b` if two numbers are passed in. -*Throws*: `'Min must be greater than max'` if `a` < 0 when only `a` is passed in +*Throws*: `'Min must be greater than max'` if `a` < 0 when only `a` is passed in or if `a` > `b` when both `a` and `b` are passed in *Example* @@ -769,8 +769,8 @@ random(-10,10) // returns a random number between -10 (inclusive) and 10 (exclus [float] === range( ...args ) -Finds the range of one of more numbers/arrays of numbers passed into the function. If at -least one array of numbers is passed into the function, the function will find +Finds the range of one of more numbers/arrays of numbers passed into the function. If at +least one array of numbers is passed into the function, the function will find the range by index. [cols="3*^<"] @@ -782,9 +782,9 @@ the range by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The range value of all numbers if `args` -contains only numbers. Returns an array with the range values at each index, -including all scalar numbers in `args` in the calculation at each index if `args` +*Returns*: `number` | `Array.`. The range value of all numbers if `args` +contains only numbers. Returns an array with the range values at each index, +including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Example* @@ -798,8 +798,8 @@ range([1, 9], 4, [3, 5]) // returns [range([1, 4, 3]), range([9, 4, 5])] = [3, 5 [float] === range( ...args ) -Finds the range of one of more numbers/arrays of numbers into the function. If at -least one array of numbers is passed into the function, the function will find +Finds the range of one of more numbers/arrays of numbers into the function. If at +least one array of numbers is passed into the function, the function will find the range by index. [cols="3*^<"] @@ -811,9 +811,9 @@ the range by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The range value of all numbers if `args` -contains only numbers. Returns an array with the the range values at each index, -including all scalar numbers in `args` in the calculation at each index if `args` +*Returns*: `number` | `Array.`. The range value of all numbers if `args` +contains only numbers. Returns an array with the the range values at each index, +including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Example* @@ -827,7 +827,7 @@ range([1, 9], 4, [3, 5]) // returns [range([1, 4, 3]), range([9, 4, 5])] = [3, 5 [float] === round( a, b ) -Rounds a number towards the nearest integer by default, or decimal place (if passed in as `b`). +Rounds a number towards the nearest integer by default, or decimal place (if passed in as `b`). For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -843,7 +843,7 @@ For arrays, the function will be applied index-wise to each element. |(optional) number of decimal places, default value: 0 |=== -*Returns*: `number` | `Array.`. The rounded value of `a`. Returns an +*Returns*: `number` | `Array.`. The rounded value of `a`. Returns an array with the the rounded values of each element if `a` is an array. *Example* @@ -885,7 +885,7 @@ size(100) // returns 1 [float] === sqrt( a ) -Calculates the square root of a number. For arrays, the function will be applied +Calculates the square root of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -897,7 +897,7 @@ index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The square root of `a`. Returns an array +*Returns*: `number` | `Array.`. The square root of `a`. Returns an array with the the square roots of each element if `a` is an array. *Throws*: `'Unable find the square root of a negative number'` if `a` < 0 @@ -913,7 +913,7 @@ sqrt([9, 16, 25]) // returns [3, 4, 5] [float] === square( a ) -Calculates the square of a number. For arrays, the function will be applied +Calculates the square of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -925,7 +925,7 @@ index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The square of `a`. Returns an array +*Returns*: `number` | `Array.`. The square of `a`. Returns an array with the the squares of each element if `a` is an array. *Example* @@ -938,7 +938,7 @@ square([3, 4, 5]) // returns [9, 16, 25] [float] === subtract( a, b ) -Subtracts two numbers. If at least one array of numbers is passed into the function, +Subtracts two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. [cols="3*^<"] @@ -954,7 +954,7 @@ the function will be applied index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The difference of `a` and `b` if both are +*Returns*: `number` | `Array.`. The difference of `a` and `b` if both are numbers, or an array of differences applied index-wise to each element. *Throws*: `'Array length mismatch'` if `a` and `b` are arrays with different lengths @@ -971,11 +971,11 @@ subtract([14, 42, 65, 108], [2, 7, 5, 12]) // returns [12, 35, 52, 96] [float] === sum( ...args ) -Calculates the sum of one or more numbers/arrays passed into the function. If at -least one array is passed, the function will sum up one or more numbers/arrays of +Calculates the sum of one or more numbers/arrays passed into the function. If at +least one array is passed, the function will sum up one or more numbers/arrays of numbers and distinct values of an array. Sum accepts arrays of different lengths. -*Returns*: `number`. The sum of one or more numbers/arrays of numbers including +*Returns*: `number`. The sum of one or more numbers/arrays of numbers including distinct values in arrays *Example* @@ -992,7 +992,7 @@ sum([10, 20, 30, 40], 10, [1, 2, 3], 22) // returns sum(10, 20, 30, 40, 10, 1, 2 Counts the number of unique values in an array. -*Returns*: `number`. The number of unique values in the array. Returns 1 if `a` +*Returns*: `number`. The number of unique values in the array. Returns 1 if `a` is not an array. *Example* @@ -1003,4 +1003,3 @@ unique([]) // returns 0 unique([1, 2, 3, 4]) // returns 4 unique([1, 2, 3, 4, 2, 2, 2, 3, 4, 2, 4, 5, 2, 1, 4, 2]) // returns 5 ------------ - diff --git a/docs/dev-tools/searchprofiler/more-complicated.asciidoc b/docs/dev-tools/searchprofiler/more-complicated.asciidoc index a0771f4a0f24..338341d65924 100644 --- a/docs/dev-tools/searchprofiler/more-complicated.asciidoc +++ b/docs/dev-tools/searchprofiler/more-complicated.asciidoc @@ -25,11 +25,11 @@ POST test/_bulk // CONSOLE -- -. From the {searchprofiler}, enter "test" in the *Index* field to restrict profiled +. From the {searchprofiler}, enter "test" in the *Index* field to restrict profiled queries to the `test` index. . Replace the default `match_all` query in the query editor with a query that has two sub-query -components and includes a simple aggregation, like the example below. +components and includes a simple aggregation: + -- [source,js] diff --git a/docs/developer/core/development-functional-tests.asciidoc b/docs/developer/core/development-functional-tests.asciidoc index 77a2bfe77b4a..dcb3d65b8b83 100644 --- a/docs/developer/core/development-functional-tests.asciidoc +++ b/docs/developer/core/development-functional-tests.asciidoc @@ -69,7 +69,7 @@ node scripts/functional_tests_server.js node ../scripts/functional_test_runner.js ---------- -** Selenium tests are run in headless mode on CI. Locally the same tests will be executed in a real browser. You can activate headless mode by setting the environment variable below: +** Selenium tests are run in headless mode on CI. Locally the same tests will be executed in a real browser. You can activate headless mode by setting the environment variable: + ["source", "shell"] ---------- @@ -181,7 +181,7 @@ node scripts/functional_test_runner --config test/functional/config.firefox.js [float] ===== Anatomy of a test file -The annotated example file below shows the basic structure every test suite uses. It starts by importing https://github.com/elastic/kibana/tree/master/packages/kbn-expect[`@kbn/expect`] and defining its default export: an anonymous Test Provider. The test provider then destructures the Provider API for the `getService()` and `getPageObjects()` functions. It uses these functions to collect the dependencies of this suite. The rest of the test file will look pretty normal to mocha.js users. `describe()`, `it()`, `before()` and the lot are used to define suites that happen to automate a browser via services and objects of type `PageObject`. +This annotated example file shows the basic structure every test suite uses. It starts by importing https://github.com/elastic/kibana/tree/master/packages/kbn-expect[`@kbn/expect`] and defining its default export: an anonymous Test Provider. The test provider then destructures the Provider API for the `getService()` and `getPageObjects()` functions. It uses these functions to collect the dependencies of this suite. The rest of the test file will look pretty normal to mocha.js users. `describe()`, `it()`, `before()` and the lot are used to define suites that happen to automate a browser via services and objects of type `PageObject`. ["source","js"] ---- diff --git a/docs/developer/plugin/development-plugin-feature-registration.asciidoc b/docs/developer/plugin/development-plugin-feature-registration.asciidoc index 2c686964d369..ca61e5309ce8 100644 --- a/docs/developer/plugin/development-plugin-feature-registration.asciidoc +++ b/docs/developer/plugin/development-plugin-feature-registration.asciidoc @@ -46,7 +46,7 @@ Registering a feature consists of the following fields. For more information, co |`privileges` (required) |{repo}blob/{branch}/x-pack/plugins/features/server/feature.ts[`FeatureWithAllOrReadPrivileges`]. -|see examples below +|See <> and <> |The set of privileges this feature requires to function. |`icon` @@ -80,6 +80,7 @@ if (canUserSave) { } ----------- +[[example-1-canvas]] ==== Example 1: Canvas Application ["source","javascript"] ----------- @@ -134,6 +135,7 @@ if (canUserSave) { Because the `read` privilege does not define the `save` capability, users with read-only access will have their `uiCapabilities.canvas.save` flag set to `false`. +[[example-2-dev-tools]] ==== Example 2: Dev Tools ["source","javascript"] diff --git a/docs/developer/plugin/development-plugin-localization.asciidoc b/docs/developer/plugin/development-plugin-localization.asciidoc index 78ee933f681f..1fb8b6aa0cbd 100644 --- a/docs/developer/plugin/development-plugin-localization.asciidoc +++ b/docs/developer/plugin/development-plugin-localization.asciidoc @@ -161,7 +161,7 @@ Full details are {repo}tree/master/packages/kbn-i18n#angularjs[here]. To learn more about i18n tooling, see {blob}src/dev/i18n/README.md[i18n dev tooling]. -To learn more about implementing i18n in the UI, follow the links below: +To learn more about implementing i18n in the UI, use the following links: * {blob}packages/kbn-i18n/README.md[i18n plugin] * {blob}packages/kbn-i18n/GUIDELINE.md[i18n guidelines] diff --git a/docs/discover/kuery.asciidoc b/docs/discover/kuery.asciidoc index c835c1502807..48a7c65bdbf1 100644 --- a/docs/discover/kuery.asciidoc +++ b/docs/discover/kuery.asciidoc @@ -2,13 +2,13 @@ === Kibana Query Language In Kibana 6.3, we introduced a number of exciting experimental query language enhancements. These -features are now available by default in 7.0. Out of the box, Kibana's query language now includes scripted field support and a -simplified, easier to use syntax. If you have a Basic license or above, autocomplete functionality will also be enabled. +features are now available by default in 7.0. Out of the box, Kibana's query language now includes scripted field support and a +simplified, easier to use syntax. If you have a Basic license or above, autocomplete functionality will also be enabled. ==== Language Syntax -If you're familiar with Kibana's old lucene query syntax, you should feel right at home with the new syntax. The basics -stay the same, we've simply refined things to make the query language easier to use. Read about the changes below. +If you're familiar with Kibana's old Lucene query syntax, you should feel right at home with the new syntax. The basics +stay the same, we've simply refined things to make the query language easier to use. `response:200` will match documents where the response field matches the value 200. @@ -19,8 +19,8 @@ they appear. This means documents with "quick brown fox" will match, but so will to search for a phrase. The query parser will no longer split on whitespace. Multiple search terms must be separated by explicit -boolean operators. Lucene will combine search terms with an `or` by default, so `response:200 extension:php` would -become `response:200 or extension:php` in KQL. This will match documents where response matches 200, extension matches php, or both. +boolean operators. Lucene will combine search terms with an `or` by default, so `response:200 extension:php` would +become `response:200 or extension:php` in KQL. This will match documents where response matches 200, extension matches php, or both. Note that boolean operators are not case sensitive. We can make terms required by using `and`. @@ -48,9 +48,9 @@ Entire groups can also be inverted. `response:200 and not (extension:php or extension:css)` -Ranges are similar to lucene with a small syntactical difference. +Ranges are similar to lucene with a small syntactical difference. -Instead of `bytes:>1000`, we omit the colon: `bytes > 1000`. +Instead of `bytes:>1000`, we omit the colon: `bytes > 1000`. `>, >=, <, <=` are all valid range operators. @@ -76,15 +76,15 @@ in the response field, but a query for just `200` will search for 200 across all KQL supports querying on {ref}/nested.html[nested fields] through a special syntax. You can query nested fields in subtly different ways, depending on the results you want, so crafting nested queries requires extra thought. - + One main consideration is how to match parts of the nested query to the individual nested documents. There are two main approaches to take: * *Parts of the query may only match a single nested document.* This is what most users want when querying on a nested field. -* *Parts of the query can match different nested documents.* This is how a regular object field works. +* *Parts of the query can match different nested documents.* This is how a regular object field works. Although generally less useful, there might be occasions where you want to query a nested field in this way. -Let's take a look at the first approach. In the following document, `items` is a nested field. Each document in the nested +Let's take a look at the first approach. In the following document, `items` is a nested field. Each document in the nested field contains a name, stock, and category. [source,json] @@ -122,7 +122,7 @@ To find stores that have more than 10 bananas in stock, you would write a query `items:{ name:banana and stock > 10 }` -`items` is the "nested path". Everything inside the curly braces (the "nested group") must match a single nested document. +`items` is the "nested path". Everything inside the curly braces (the "nested group") must match a single nested document. The following example returns no matches because no single nested document has bananas with a stock of 9. @@ -138,7 +138,7 @@ The subqueries in this example are in separate nested groups and can match diffe ==== Combine approaches -You can combine these two approaches to create complex queries. What if you wanted to find a store with more than 10 +You can combine these two approaches to create complex queries. What if you wanted to find a store with more than 10 bananas that *also* stocks vegetables? You could do this: `items:{ name:banana and stock > 10 } and items:{ category:vegetable }` diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index 9c4e406455c2..21ae4560fba9 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -56,7 +56,7 @@ query language you can also submit queries using the {ref}/query-dsl.html[Elasti [[save-open-search]] === Saving searches -A saved search persists your current view of Discover for later retrieval and reuse. You can reload a saved search into Discover, add it to a dashboard, and use it as the basis for a <>. +A saved search persists your current view of Discover for later retrieval and reuse. You can reload a saved search into Discover, add it to a dashboard, and use it as the basis for a <>. A saved search includes the query text, filters, and optionally, the time filter. A saved search also includes the selected columns in the document table, the sort order, and the current index pattern. @@ -164,12 +164,9 @@ You can import, export, and delete saved queries from <>. +index pattern are searched. +To change the indices you are searching, click the index pattern and select a +different <>. [[autorefresh]] === Refresh the search results @@ -180,7 +177,7 @@ retrieve the latest results. . Click image:images/time-filter-calendar.png[]. -. In the *Refresh every* field, enter the refresh rate, then select the interval +. In the *Refresh every* field, enter the refresh rate, then select the interval from the dropdown. . Click *Start*. @@ -189,5 +186,5 @@ image::images/autorefresh-intervals.png[] To disable auto refresh, click *Stop*. -If auto refresh is not enabled, click *Refresh* to manually refresh the search +If auto refresh is not enabled, click *Refresh* to manually refresh the search results. diff --git a/docs/epm/index.asciidoc b/docs/epm/index.asciidoc index 46d45b85690e..d2ebe003afd6 100644 --- a/docs/epm/index.asciidoc +++ b/docs/epm/index.asciidoc @@ -47,12 +47,12 @@ A user-specified string that will be used to part of the index name in Elasticse ==== Package -A package contains all the assets for the Elastic Stack. A more detailed definition of a package can be found under https://github.com/elastic/package-registry . +A package contains all the assets for the Elastic Stack. A more detailed definition of a package can be found under https://github.com/elastic/package-registry. == Indexing Strategy -Ingest Management enforces an indexing strategy to allow the system to automically detect indices and run queries on it. In short the indexing strategy looks as following: +Ingest Management enforces an indexing strategy to allow the system to automatically detect indices and run queries on it. In short the indexing strategy looks as following: ``` {type}-{dataset}-{namespace} @@ -85,7 +85,7 @@ The version is included in each pipeline to allow upgrades. The pipeline itself === Templates & ILM Policies -To make the above strategy possible, alias templates are required. For each type there is a basic alias template with a default ILM policy. These default templates apply to all indices which follow the indexing strategy and do not have a more specific dataset alias template. +To make the above strategy possible, alias templates are required. For each type there is a basic alias template with a default ILM policy. These default templates apply to all indices which follow the indexing strategy and do not have a more specific dataset alias template. The `metrics` and `logs` alias template contain all the basic fields from ECS. @@ -109,7 +109,7 @@ Filtering for data in queries for example in visualizations or dashboards should === Security permissions -Security permissions can be set on different levels. To set special permissions for the access on the prod namespace an index pattern as below can be used: +Security permissions can be set on different levels. To set special permissions for the access on the prod namespace, use the following index pattern: ``` /(logs|metrics)-[^-]+-prod-$/ @@ -142,5 +142,3 @@ The new ingest pipeline is expected to still work with the data coming from olde In case of a breaking change in the data structure, the new ingest pipeline is also expected to deal with this change. In case there are breaking changes which cannot be dealt with in an ingest pipeline, a new package has to be created. Each package lists its minimal required agent version. In case there are agents enrolled with an older version, the user is notified to upgrade these agents as otherwise the new configs cannot be rolled out. - - diff --git a/docs/management/numeral.asciidoc b/docs/management/numeral.asciidoc index 65dfdab3abd3..5d4d48ca785e 100644 --- a/docs/management/numeral.asciidoc +++ b/docs/management/numeral.asciidoc @@ -19,7 +19,7 @@ The numeral pattern syntax expresses: Number of decimal places:: The `.` character turns on the option to show decimal places using a locale-specific decimal separator, most often `.` or `,`. To add trailing zeroes such as `5.00`, use a pattern like `0.00`. -To have optional zeroes, use the `[]` characters. Examples below. +To have optional zeroes, use the `[]` characters. Thousands separator:: The thousands separator `,` turns on the option to group thousands using a locale-specific separator. The separator is most often `,` or `.`, and sometimes ` `. diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 565c179b741f..6a56970687fd 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -70,11 +70,7 @@ This allows for more granular queries, such as 2h and 12h. [float] ==== Create the rollup job -As you walk through the *Create rollup job* UI, enter the data shown in -the table below. The terms, histogram, and metrics fields reflect -the key information to retain in the rolled up data: where visitors are from (geo.src), -what operating system they are using (machine.os.keyword), -and how much data is being sent (bytes). +As you walk through the *Create rollup job* UI, enter the data: |=== |*Field* |*Value* @@ -118,6 +114,10 @@ and how much data is being sent (bytes). |bytes (average) |=== +The terms, histogram, and metrics fields reflect +the key information to retain in the rolled up data: where visitors are from (geo.src), +what operating system they are using (machine.os.keyword), +and how much data is being sent (bytes). You can now use the rolled up data for analysis at a fraction of the storage cost of the original index. The original data can live side by side with the new diff --git a/docs/maps/geojson-upload.asciidoc b/docs/maps/geojson-upload.asciidoc index 8c3cb371b6ad..ad20264f5613 100644 --- a/docs/maps/geojson-upload.asciidoc +++ b/docs/maps/geojson-upload.asciidoc @@ -14,7 +14,7 @@ GeoJSON is the most commonly used and flexible option. [float] === Upload a GeoJSON file -Follow the instructions below to upload a GeoJSON data file, or try the +Follow these instructions to upload a GeoJSON data file, or try the <>. . Open *Elastic Maps*, and then click *Add layer*. diff --git a/docs/maps/indexing-geojson-data-tutorial.asciidoc b/docs/maps/indexing-geojson-data-tutorial.asciidoc index 22b736032cb7..a94e5757d5df 100644 --- a/docs/maps/indexing-geojson-data-tutorial.asciidoc +++ b/docs/maps/indexing-geojson-data-tutorial.asciidoc @@ -46,7 +46,7 @@ image::maps/images/fu_gs_new_england_map.png[] === Upload and index GeoJSON files For each GeoJSON file you downloaded, complete the following steps: -. Below the map legend, click *Add layer*. +. Click *Add layer*. . From the list of layer types, click *Uploaded GeoJSON*. . Using the File Picker, upload the GeoJSON file. + @@ -86,7 +86,7 @@ hot spots are. An advantage of having indexed {ref}/geo-point.html[geo_point] data for the lightning strikes is that you can perform aggregations on the data. -. Below the map legend, click *Add layer*. +. Click *Add layer*. . From the list of layer types, click *Grid aggregation*. + Because you indexed `lightning_detected.geojson` using the index name and diff --git a/docs/maps/vector-style.asciidoc b/docs/maps/vector-style.asciidoc index 509b1fae4066..80e4c4ed5f84 100644 --- a/docs/maps/vector-style.asciidoc +++ b/docs/maps/vector-style.asciidoc @@ -12,7 +12,7 @@ For each property, you can specify whether to use a constant or data driven valu Use static styling to specificy a constant value for a style property. -The image below shows an example of static styling using the <> data set. +This image shows an example of static styling using the <> data set. The *kibana_sample_data_logs* layer uses static styling for all properties. [role="screenshot"] @@ -26,7 +26,7 @@ image::maps/images/vector_style_static.png[] Use data driven styling to symbolize features by property values. To enable data driven styling for a style property, change the selected value from *Fixed* or *Solid* to *By value*. -The image below shows an example of data driven styling using the <> data set. +This image shows an example of data driven styling using the <> data set. The *kibana_sample_data_logs* layer uses data driven styling for fill color and symbol size style properties. * The `hour_of_day` property determines the fill color for each feature based on where the value fits on a linear scale. @@ -87,7 +87,7 @@ Qualitative data driven styling is available for the following styling propertie Qualitative data driven styling uses a {ref}/search-aggregations-bucket-terms-aggregation.html[terms aggregation] to retrieve the top nine categories for the property. Feature values within the top categories are assigned a unique color. Feature values outside of the top categories are grouped into the *Other* category. A feature is assigned the *Other* category when the property value is undefined. -The image below shows an example of quantitative data driven styling using the <> data set. +This image shows an example of quantitative data driven styling using the <> data set. The `machine.os.keyword` property determines the color of each symbol based on category. [role="screenshot"] @@ -101,7 +101,7 @@ image::maps/images/quantitative_data_driven_styling.png[] Class styling symbolizes features by class and requires multiple layers. Use <> to define the class for each layer, and <> to symbolize each class. -The image below shows an example of class styling using the <> data set. +This image shows an example of class styling using the <> data set. * The *Mac OS requests* layer applies the filter `machine.os : osx` so the layer only contains Mac OS requests. The fill color is a static value of green. diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index a34f956ace26..ce4c97391f1b 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -19,16 +19,16 @@ See also <> and <>. [float] [[breaking_80_index_pattern_changes]] -=== Index pattern changes +=== Index pattern changes [float] ==== Removed support for time-based internal index patterns -*Details:* Time-based interval index patterns were deprecated in 5.x. In 6.x, -you could no longer create time-based interval index patterns, but they continued +*Details:* Time-based interval index patterns were deprecated in 5.x. In 6.x, +you could no longer create time-based interval index patterns, but they continued to function as expected. Support for these index patterns has been removed in 8.0. -*Impact:* You must migrate your time_based index patterns to a wildcard pattern, -for example, `logstash-*`. +*Impact:* You must migrate your time_based index patterns to a wildcard pattern, +for example, `logstash-*`. [float] @@ -76,7 +76,7 @@ specified explicitly. [float] ==== `/api/security/v1/saml` endpoint is no longer supported -*Details:* The deprecated `/api/security/v1/saml` endpoint is no longer supported. +*Details:* The deprecated `/api/security/v1/saml` endpoint is no longer supported. *Impact:* Rely on `/api/security/saml/callback` endpoint when using SAML instead. This change should be reflected in Kibana `server.xsrf.whitelist` config as well as in Elasticsearch and Identity Provider SAML settings. @@ -108,7 +108,7 @@ access level. [float] ==== Legacy job parameters are no longer supported -*Details:* POST URL snippets that were copied in Kibana 6.2 or below are no longer supported. These logs have +*Details:* POST URL snippets that were copied in Kibana 6.2 or earlier are no longer supported. These logs have been deprecated with warnings that have been logged throughout 7.x. Please use Kibana UI to re-generate the POST URL snippets if you depend on these for automated PDF reports. diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index a6eeffec51cb..91bbef5690fd 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -32,7 +32,7 @@ image::settings/images/apm-settings.png[APM app settings in Kibana] // tag::general-apm-settings[] If you'd like to change any of the default values, -copy and paste the relevant settings below into your `kibana.yml` configuration file. +copy and paste the relevant settings into your `kibana.yml` configuration file. xpack.apm.enabled:: Set to `false` to disabled the APM plugin {kib}. Defaults to `true`. diff --git a/docs/settings/ssl-settings.asciidoc b/docs/settings/ssl-settings.asciidoc index 5341d3543e7c..3a0a474d9d59 100644 --- a/docs/settings/ssl-settings.asciidoc +++ b/docs/settings/ssl-settings.asciidoc @@ -44,7 +44,7 @@ Java Cryptography Architecture documentation]. Defaults to the value of The following settings are used to specify a private key, certificate, and the trusted certificates that should be used when communicating over an SSL/TLS connection. -If none of the settings below are specified, the default values are used. +If none of the settings are specified, the default values are used. See {ref}/security-settings.html[Default TLS/SSL settings]. ifdef::server[] @@ -54,8 +54,8 @@ ifndef::server[] A private key and certificate are optional and would be used if the server requires client authentication for PKI authentication. endif::server[] -If none of the settings below are specified, the defaults values are used. -See {ref}/security-settings.html[Default TLS/SSL settings]. +If none of the settings bare specified, the defaults values are used. +See {ref}/security-settings.html[Default TLS/SSL settings]. [float] ===== PEM encoded files diff --git a/docs/uptime-guide/install.asciidoc b/docs/uptime-guide/install.asciidoc index 5d32a26529f8..e7c50bb7604c 100644 --- a/docs/uptime-guide/install.asciidoc +++ b/docs/uptime-guide/install.asciidoc @@ -20,7 +20,7 @@ then jump straight to <>. === Install the stack yourself If you'd rather install the stack yourself, -first see the https://www.elastic.co/support/matrix[Elastic Support Matrix] for information about supported operating systems and product compatibility. Then, follow the steps below. +first see the https://www.elastic.co/support/matrix[Elastic Support Matrix] for information about supported operating systems and product compatibility. * <> * <> diff --git a/docs/uptime-guide/security.asciidoc b/docs/uptime-guide/security.asciidoc index 6651b33ea0e0..0c6fa4c6c4f5 100644 --- a/docs/uptime-guide/security.asciidoc +++ b/docs/uptime-guide/security.asciidoc @@ -1,9 +1,8 @@ [[uptime-security]] == Elasticsearch Security -If you use Elasticsearch security, you'll need to enable certain privileges for users -that would like to access the Uptime app. Below is an example of creating -a user and support role to implement those privileges. +If you use Elasticsearch security, you'll need to enable certain privileges for users +that would like to access the Uptime app. For example, create user and support roles to implement the privileges: [float] === Create a role diff --git a/docs/user/graph/getting-started.asciidoc b/docs/user/graph/getting-started.asciidoc index 7b3bd1014796..1749678ace9e 100644 --- a/docs/user/graph/getting-started.asciidoc +++ b/docs/user/graph/getting-started.asciidoc @@ -2,7 +2,7 @@ [[graph-getting-started]] == Using Graph -You must index data into {es} before you can create a graph. +You must index data into {es} before you can create a graph. <> or get started with a <>. [float] @@ -11,24 +11,24 @@ You must index data into {es} before you can create a graph. . From the side navigation, open *Graph*. + -If this is your first graph, follow the prompts to create it. +If this is your first graph, follow the prompts to create it. For subsequent graphs, click *New*. . Select a data source to explore. . Add one or more multi-value fields that contain the terms you want to -graph. +graph. + The vertices in the graph are selected from these terms. . Enter a search query to discover relationships between terms in the selected -fields. +fields. + -For example, if you are using the {kib} sample web logs data set, and you want +For example, if you are using the {kib} sample web logs data set, and you want to generate a graph of the successful requests to particular pages from different locations, you could search for the 200 response code. The weight of the connection between two vertices indicates how strongly they -are related. +are related. + [role="screenshot"] image::user/graph/images/graph-url-connections.png["URL connections"] @@ -45,11 +45,11 @@ additional connections: image:user/graph/images/graph-expand-button.png[Expand Selection]. * To display additional connections between the displayed vertices, click the link icon -image:user/graph/images/graph-link-button.png[Add links to existing terms]. +image:user/graph/images/graph-link-button.png[Add links to existing terms]. * To explore a particular area of the graph, select the vertices you are interested in, and then click expand or link. * To step back through your changes to the graph, click undo -image:user/graph/images/graph-undo-button.png[Undo] and redo +image:user/graph/images/graph-undo-button.png[Undo] and redo image:user/graph/images/graph-redo-button.png[Redo]. . To see more relationships in your data, submit additional queries. @@ -63,61 +63,61 @@ image::user/graph/images/graph-add-query.png["Adding networks"] [[style-vertex-properties]] === Style vertex properties -Each vertex has a color, icon, and label. To change -the color or icon of all vertices -of a certain field, click the field badge below the search bar, and then +Each vertex has a color, icon, and label. To change +the color or icon of all vertices +of a certain field, click it's badge, and then select *Edit settings*. -To change the color and label of selected vertices, +To change the color and label of selected vertices, click the style icon image:user/graph/images/graph-style-button.png[Style] -in the control bar on the right. +in the control bar on the right. [float] [[edit-graph-settings]] === Edit graph settings -By default, *Graph* is configured to tune out noise in your data. +By default, *Graph* is configured to tune out noise in your data. If this isn't a good fit for your data, use *Settings > Advanced settings* -to adjust the way *Graph* queries your data. You can tune the graph to show -only the results relevant to you and to improve performance. -For more information, see <>. +to adjust the way *Graph* queries your data. You can tune the graph to show +only the results relevant to you and to improve performance. +For more information, see <>. -You can configure the number of vertices that a search or +You can configure the number of vertices that a search or expand operation adds to the graph. -By default, only the five most relevant terms for any given field are added -at a time. This keeps the graph from overflowing. To increase this number, click -a field below the search bar, select *Edit Settings*, and change *Terms per hop*. +By default, only the five most relevant terms for any given field are added +at a time. This keeps the graph from overflowing. To increase this number, click +a field, select *Edit Settings*, and change *Terms per hop*. [float] [[graph-block-terms]] === Block terms from the graph -Documents that match a blocked term are not allowed in the graph. -To block a term, select its vertex and click +Documents that match a blocked term are not allowed in the graph. +To block a term, select its vertex and click the block icon image:user/graph/images/graph-block-button.png[Block selection] -in the control panel. +in the control panel. For a list of blocked terms, go to *Settings > Blocked terms*. [float] [[graph-drill-down]] === Drill down into raw documents -With drilldowns, you can display additional information about a -selected vertex in a new browser window. For example, you might -configure a drilldown URL to perform a web search for the selected vertex term. +With drilldowns, you can display additional information about a +selected vertex in a new browser window. For example, you might +configure a drilldown URL to perform a web search for the selected vertex term. -Use the drilldown icon image:user/graph/images/graph-info-icon.png[Drilldown selection] +Use the drilldown icon image:user/graph/images/graph-info-icon.png[Drilldown selection] in the control panel to show the drilldown buttons for the selected vertices. -To configure drilldowns, go to *Settings > Drilldowns*. See also +To configure drilldowns, go to *Settings > Drilldowns*. See also <>. [float] [[graph-run-layout]] === Run and pause layout -Graph uses a "force layout", where vertices behave like magnets, -pushing off of one another. By default, when you add a new vertex to -the graph, all vertices begin moving. In some cases, the movement might -go on for some time. To freeze the current vertex position, +Graph uses a "force layout", where vertices behave like magnets, +pushing off of one another. By default, when you add a new vertex to +the graph, all vertices begin moving. In some cases, the movement might +go on for some time. To freeze the current vertex position, click the pause icon image:user/graph/images/graph-pause-button.png[Block selection] -in the control panel. +in the control panel. diff --git a/docs/user/monitoring/beats-details.asciidoc b/docs/user/monitoring/beats-details.asciidoc index 672ed6226e42..0b2be4dd9e3d 100644 --- a/docs/user/monitoring/beats-details.asciidoc +++ b/docs/user/monitoring/beats-details.asciidoc @@ -13,7 +13,7 @@ image::user/monitoring/images/monitoring-beats.jpg["Monitoring Beats",link="imag To view an overview of the Beats data in the cluster, click *Overview*. The overview page has a section for activity in the last day, which is a real-time -sample of data. Below that, a summary bar and charts follow the typical paradigm +sample of data. The summary bar and charts follow the typical paradigm of data in the Monitoring UI, which is bound to the span of the time filter in the top right corner of the page. This overview page can therefore show up-to-date or historical information. diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc index e4dbdc2483f7..d45aae86a9cc 100644 --- a/docs/user/security/rbac_tutorial.asciidoc +++ b/docs/user/security/rbac_tutorial.asciidoc @@ -10,10 +10,10 @@ Kibana spaces. ==== Scenario Our user is a web developer working on a bank's -online mortgage service. The web developer has these +online mortgage service. The web developer has these three requirements: -* Have access to the data for that service +* Have access to the data for that service * Build visualizations and dashboards * Monitor the performance of the system @@ -24,28 +24,28 @@ You'll provide the web developer with the access and privileges to get the job d To complete this tutorial, you'll need the following: -* **Administrative privileges**: You must have a role that grants privileges to create a space, role, and user. This is any role which grants the `manage_security` cluster privilege. By default, the `superuser` role provides this access. See the {ref}/built-in-roles.html[built-in] roles. -* **A space**: In this tutorial, use `Dev Mortgage` as the space +* **Administrative privileges**: You must have a role that grants privileges to create a space, role, and user. This is any role which grants the `manage_security` cluster privilege. By default, the `superuser` role provides this access. See the {ref}/built-in-roles.html[built-in] roles. +* **A space**: In this tutorial, use `Dev Mortgage` as the space name. See <> for details on creating a space. -* **Data**: You can use <> or -live data. In the steps below, Filebeat and Metricbeat data are used. +* **Data**: You can use <> or +live data. In the following steps, Filebeat and Metricbeat data are used. [float] ==== Steps -With the requirements in mind, here are the steps that you will work +With the requirements in mind, here are the steps that you will work through in this tutorial: * Create a role named `mortgage-developer` * Give the role permission to access the data in the relevant indices -* Give the role permission to create visualizations and dashboards +* Give the role permission to create visualizations and dashboards * Create the web developer's user account with the proper roles [float] ==== Create a role -Go to **Management > Roles** +Go to **Management > Roles** for an overview of your roles. This view provides actions for you to create, edit, and delete roles. @@ -53,21 +53,21 @@ for you to create, edit, and delete roles. image::security/images/role-management.png["Role management"] -You can create as many roles as you like. Click *Create role* and -provide a name. Use `dev-mortgage` because this role is for a developer +You can create as many roles as you like. Click *Create role* and +provide a name. Use `dev-mortgage` because this role is for a developer working on the bank's mortgage application. [float] ==== Give the role permission to access the data -Access to data in indices is an index-level privilege, so in -*Index privileges*, add lines for the indices that contain the -data for this role. Two privileges are required: `read` and -`view_index_metadata`. All privileges are detailed in the +Access to data in indices is an index-level privilege, so in +*Index privileges*, add lines for the indices that contain the +data for this role. Two privileges are required: `read` and +`view_index_metadata`. All privileges are detailed in the https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html[security privileges] documentation. -In the screenshots, Filebeat and Metricbeat data is used, but you +In the screenshots, Filebeat and Metricbeat data is used, but you should use the index patterns for your indices. [role="screenshot"] @@ -76,12 +76,12 @@ image::security/images/role-index-privilege.png["Index privilege"] [float] ==== Give the role permission to create visualizations and dashboards -By default, roles do not give Kibana privileges. Click **Add space +By default, roles do not give Kibana privileges. Click **Add space privilege** and associate this role with the `Dev Mortgage` space. -To enable users with the `dev-mortgage` role to create visualizations -and dashboards, click *All* for *Visualize* and *Dashboard*. Also -assign *All* for *Discover* because it is common for developers +To enable users with the `dev-mortgage` role to create visualizations +and dashboards, click *All* for *Visualize* and *Dashboard*. Also +assign *All* for *Discover* because it is common for developers to create saved searches while designing visualizations. [role="screenshot"] @@ -90,15 +90,14 @@ image::security/images/role-space-visualization.png["Associate space"] [float] ==== Create the developer's user account with the proper roles -Go to **Management > Users** and click on **Create user** to create a -user. Give the user the `dev-mortgage` role +Go to **Management > Users** and click on **Create user** to create a +user. Give the user the `dev-mortgage` role and the `monitoring-user` role, which is required for users of **Stack Monitoring**. [role="screenshot"] image::security/images/role-new-user.png["Developer user"] -Finally, have the developer log in and access the Dev Mortgage space +Finally, have the developer log in and access the Dev Mortgage space and create a new visualization. NOTE: If the user is assigned to only one space, they will automatically enter that space on login. - diff --git a/docs/visualize/vega.asciidoc b/docs/visualize/vega.asciidoc index c9cf1e7aeb82..b8c0d1dbe3dd 100644 --- a/docs/visualize/vega.asciidoc +++ b/docs/visualize/vega.asciidoc @@ -324,7 +324,7 @@ replace `"url": "data/world-110m.json"` with `"url": "https://vega.github.io/editor/data/world-110m.json"`. Also, regular Vega examples use `"autosize": "pad"` layout model, whereas Kibana uses `fit`. Remove all `autosize`, `width`, and `height` -values. See link:#sizing-and-positioning[sizing and positioning] below. +values. See link:#sizing-and-positioning[sizing and positioning]. [[vega-additional-configuration-options]] ==== Additional configuration options From 7fa5c2face8211328417d7915dc05134e6fed805 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 16 Mar 2020 09:23:58 -0600 Subject: [PATCH 010/115] [skip-ci] Service Status RFC (#59621) --- rfcs/text/0010_service_status.md | 373 +++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 rfcs/text/0010_service_status.md diff --git a/rfcs/text/0010_service_status.md b/rfcs/text/0010_service_status.md new file mode 100644 index 000000000000..ded594930a36 --- /dev/null +++ b/rfcs/text/0010_service_status.md @@ -0,0 +1,373 @@ +- Start Date: 2020-03-07 +- RFC PR: https://github.com/elastic/kibana/pull/59621 +- Kibana Issue: https://github.com/elastic/kibana/issues/41983 + +# Summary + +A set API for describing the current status of a system (Core service or plugin) +in Kibana. + +# Basic example + +```ts +// Override default behavior and only elevate severity when elasticsearch is not available +core.status.set( + core.status.core$.pipe(core => core.elasticsearch); +) +``` + +# Motivation + +Kibana should do as much possible to help users keep their installation in a working state. This includes providing as much detail about components that are not working as well as ensuring that failures in one part of the application do not block using other portions of the application. + +In order to provide the user with as much detail as possible about any systems that are not working correctly, the status mechanism should provide excellent defaults in terms of expressing relationships between services and presenting detailed information to the user. + +# Detailed design + +## Failure Guidelines + +While this RFC primarily describes how status information is signaled from individual services and plugins to Core, it's first important to define how Core expects these services and plugins to behave in the face of failure more broadly. + +Core is designed to be resilient and adaptive to change. When at all possible, Kibana should automatically recover from failure, rather than requiring any kind of intervention by the user or administrator. + +Given this goal, Core expects the following from plugins: +- During initialization, `setup`, and `start` plugins should only throw an exception if a truly unrecoverable issue is encountered. Examples: HTTP port is unavailable, server does not have the appropriate file permissions. +- Temporary error conditions should always be retried automatically. A user should not have to restart Kibana in order to resolve a problem when avoidable. This means all initialization code should include error handling and automated retries. Examples: creating an Elasticsearch index, connecting to an external service. + - It's important to note that some issues do require manual intervention in _other services_ (eg. Elasticsearch). Kibana should still recover without restarting once that external issue is resolved. +- Unhandled promise rejections are not permitted. In the future, Node.js will crash on unhandled promise rejections. It is impossible for Core to be able to properly handle and retry these situations, so all services and plugins should handle all rejected promises and retry when necessary. +- Plugins should only crash the Kibana server when absolutely necessary. Some features are considered "mission-critical" to customers and may need to halt Kibana if they are not functioning correctly. Example: audit logging. + +## API Design + +### Types + +```ts +/** + * The current status of a service at a point in time. + * + * @typeParam Meta - JSON-serializable object. Plugins should export this type to allow other plugins to read the `meta` + * field in a type-safe way. + */ +type ServiceStatus = unknown> = { + /** + * The current availability level of the service. + */ + level: ServiceStatusLevel.available; + /** + * A high-level summary of the service status. + */ + summary?: string; + /** + * A more detailed description of the service status. + */ + detail?: string; + /** + * A URL to open in a new tab about how to resolve or troubleshoot the problem. + */ + documentationUrl?: string; + /** + * Any JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained, + * machine-readable information about the service status. May include status information for underlying features. + */ + meta?: Meta; +} | { + level: ServiceStatusLevel; + summary: string; // required when level !== available + detail?: string; + documentationUrl?: string; + meta?: Meta; +} + +/** + * The current "level" of availability of a service. + */ +enum ServiceStatusLevel { + /** + * Everything is working! + */ + available, + /** + * Some features may not be working. + */ + degraded, + /** + * The service is unavailable, but other functions that do not depend on this service should work. + */ + unavailable, + /** + * Block all user functions and display the status page, reserved for Core services only. + * Note: In the real implementation, this will be split out to a different type. Kept as a single type here to make + * the RFC easier to follow. + */ + critical +} + +/** + * Status of core services. Only contains entries for backend services that could have a non-available `status`. + * For example, `context` cannot possibly be broken, so it is not included. + */ +interface CoreStatus { + elasticsearch: ServiceStatus; + http: ServiceStatus; + savedObjects: ServiceStatus; + uiSettings: ServiceStatus; + metrics: ServiceStatus; +} +``` + +### Plugin API + +```ts +/** + * The API exposed to plugins on CoreSetup.status + */ +interface StatusSetup { + /** + * Allows a plugin to specify a custom status dependent on its own criteria. + * Completely overrides the default inherited status. + */ + set(status$: Observable): void; + + /** + * Current status for all Core services. + */ + core$: Observable; + + /** + * Current status for all dependencies of the current plugin. + * Each key of the `Record` is a plugin id. + */ + plugins$: Observable>; + + /** + * The status of this plugin as derived from its dependencies. + * + * @remarks + * By default, plugins inherit this derived status from their dependencies. + * Calling {@link StatusSetup.set} overrides this default status. + */ + derivedStatus$: Observable; +} +``` + +### HTTP API + +The HTTP endpoint should return basic information about the Kibana node as well as the overall system status and the status of each individual system. + +This API does not need to include UI-specific details like the existing API such as `uiColor` and `icon`. + +```ts +/** + * Response type for the endpoint: GET /api/status + */ +interface StatusResponse { + /** server.name */ + name: string; + /** server.uuid */ + uuid: string; + /** Currently exposed by existing status API */ + version: { + number: string; + build_hash: string; + build_number: number; + build_snapshot: boolean; + }; + /** Similar format to existing API, but slightly different shape */ + status: { + /** See "Overall status calculation" section below */ + overall: ServiceStatus; + core: CoreStatus; + plugins: Record; + } +} +``` + +## Behaviors + +### Levels + +Each member of the `ServiceStatusLevel` enum has specific behaviors associated with it: +- **`available`**: + - All endpoints and apps associated with the service are accessible +- **`degraded`**: + - All endpoints and apps are available by default + - Some APIs may return `503 Unavailable` responses. This is not automatic, must be implemented directly by the service. + - Some plugin contract APIs may throw errors. This is not automatic, must be implemented directly by the service. +- **`unavailable`**: + - All endpoints (with some exceptions in Core) in Kibana return a `503 Unavailable` responses by default. This is automatic. + - When trying to access any app associated with the unavailable service, the user is presented with an error UI with detail about the outage. + - Some plugin contract APIs may throw errors. This is not automatic, must be implemented directly by the service. +- **`critical`**: + - All endpoints (with some exceptions in Core) in Kibana return a `503 Unavailable` response by default. This is automatic. + - All applications redirect to the system-wide status page with detail about which services are down and any relevant detail. This is automatic. + - Some plugin contract APIs may throw errors. This is not automatic, must be implemented directly by the service. + - This level is reserved for Core services only. + +### Overall status calculation + +The status level of the overall system is calculated to be the highest severity status of all core services and plugins. + +The `summary` property is calculated as follows: +- If the overall status level is `available`, the `summary` is `"Kibana is operating normally"` +- If a single core service or plugin is not `available`, the `summary` is `Kibana is ${level} due to ${serviceName}. See ${statusPageUrl} for more information.` +- If multiple core services or plugins are not `available`, the `summary` is `Kibana is ${level} due to multiple components. See ${statusPageUrl} for more information.` + +### Status inheritance + +By default, plugins inherit their status from all Core services and their dependencies on other plugins. + +This can be summarized by the following matrix: + +| core | required | optional | inherited | +|----------------|----------------|----------------|-------------| +| critical | _any_ | _any_ | critical | +| unavailable | <= unavailable | <= unavailable | unavailable | +| degraded | <= degraded | <= degraded | degraded | +| <= unavailable | unavailable | <= unavailable | unavailable | +| <= degraded | degraded | <= degraded | degraded | +| <= degraded | <= degraded | unavailable | degraded | +| <= degraded | <= degraded | degraded | degraded | +| available | available | available | available | + +If a plugin calls the `StatusSetup#set` API, the inherited status is completely overridden. They status the plugin specifies is the source of truth. If a plugin wishes to "merge" its custom status with the inherited status calculated by Core, it may do so by using the `StatusSetup#inherited$` property in its calculated status. + +If a plugin never calls the `StatusSetup#set` API, the plugin's status defaults to the inherited status. + +_Disabled_ plugins, that is plugins that are explicitly disabled in Kibana's configuration, do not have any status. They are not present in any status APIs and are **not** considered `unavailable`. Disabled plugins are excluded from the status inheritance calculation, even if a plugin has a optional dependency on a disabled plugin. In summary, if a plugin has an optional dependency on a disabled plugin, the plugin will not be considered `degraded` just because that optional dependency is disabled. + +### HTTP responses + +As specified in the [_Levels section_](#levels), a service's HTTP endpoints will respond with `503 Unavailable` responses in some status levels. + +In both the `critical` and `unavailable` levels, all of a service's endpoints will return 503s. However, in the `degraded` level, it is up to service authors to decide which endpoints should return a 503. This may be implemented directly in the route handler logic or by using any of the [utilities provided](#status-utilities). + +When a 503 is returned either via the default behavior or behavior implemented using the [provided utilities](#status-utilities), the HTTP response will include the following: +- `Retry-After` header, set to `60` seconds +- A body with mime type `application/json` containing the status of the service the HTTP route belongs to: + ```json5 + { + "error": "Unavailable", + // `ServiceStatus#summary` + "message": "Newsfeed API cannot be reached", + "attributes": { + "status": { + // Human readable form of `ServiceStatus#level` + "level": "critical", + // `ServiceStatus#summary` + "summary": "Newsfeed API cannot be reached", + // `ServiceStatus#detail` or null + "detail": null, + // `ServiceStatus#documentationUrl` or null + "documentationUrl": null, + // JSON-serialized from `ServiceStatus#meta` or null + "meta": {} + } + }, + "statusCode": 503 + } + ``` + +## Status Utilities + +Though many plugins should be able to rely on the default status inheritance and associated behaviors, there are common patterns and overrides that some plugins will need. The status service should provide some utilities for these common patterns out-of-the-box. + +```ts +/** + * Extension of the main Status API + */ +interface StatusSetup { + /** + * Helpers for expressing status in HTTP routes. + */ + http: { + /** + * High-order route handler function for wrapping routes with 503 logic based + * on a predicate. + * + * @remarks + * When a 503 is returned, it also includes detailed information from the service's + * current `ServiceStatus` including `meta` information. + * + * @example + * ```ts + * router.get( + * { path: '/my-api' } + * unavailableWhen( + * ServiceStatusLevel.degraded, + * async (context, req, res) => { + * return res.ok({ body: 'done' }); + * } + * ) + * ) + * ``` + * + * @param predicate When a level is specified, if the plugin's current status + * level is >= to the severity of the specified level, route + * returns a 503. When a function is specified, if that + * function returns `true`, a 503 is returned. + * @param handler The route handler to execute when a 503 is not returned. + * @param options.retryAfter Number of seconds to set the `Retry-After` + * header to when the endpoint is unavailable. + * Defaults to `60`. + */ + unavailableWhen( + predicate: ServiceStatusLevel | + (self: ServiceStatus, core: CoreStatus, plugins: Record) => boolean, + handler: RouteHandler, + options?: { retryAfter?: number } + ): RouteHandler; + } +} +``` + +## Additional Examples + +### Combine inherited status with check against external dependency +```ts +const getExternalDepHealth = async () => { + const resp = await window.fetch('https://myexternaldep.com/_healthz'); + return resp.json(); +} + +// Create an observable that checks the status of an external service every every 10s +const myExternalDependency$: Observable = interval(10000).pipe( + mergeMap(() => of(getExternalDepHealth())), + map(health => health.ok ? ServiceStatusLevel.available : ServiceStatusLevel.unavailable), + catchError(() => of(ServiceStatusLevel.unavailable)) +); + +// Merge the inherited status with the external check +core.status.set( + combineLatest( + core.status.inherited$, + myExternalDependency$ + ).pipe( + map(([inherited, external]) => ({ + level: Math.max(inherited.level, external) + })) + ) +); +``` + +# Drawbacks + +1. **The default behaviors and inheritance of statuses may appear to be "magic" to developers who do not read the documentation about how this works.** Compared to the legacy status mechanism, these defaults are much more opinionated and the resulting status is less explicit in plugin code compared to the legacy `mirrorPluginStatus` mechanism. +2. **The default behaviors and inheritance may not fit real-world status very well.** If many plugins must customize their status in order to opt-out of the defaults, this would be a step backwards from the legacy mechanism. + +# Alternatives + +We could somewhat reduce the complexity of the status inheritance by leveraging the dependencies between plugins to enable and disable plugins based on whether or not their upstream dependencies are available. This may simplify plugin code but would greatly complicate how Kibana fundamentally operates, requiring that plugins may get stopped and started multiple times within a single Kibana server process. We would be trading simplicity in one area for complexity in another. + +# Adoption strategy + +By default, most plugins would not need to do much at all. Today, very few plugins leverage the legacy status system. The majority of ones that do, simply call the `mirrorPluginStatus` utility to follow the status of the legacy elasticsearch plugin. + +Plugins that wish to expose more detail about their availability will easily be able to do so, including providing detailed information such as links to documentation to resolve the problem. + +# How we teach this + +This largely follows the same patterns we have used for other Core APIs: Observables, composable utilties, etc. + +This should be taught using the same channels we've leveraged for other Kibana Platform APIs: API documentation, additions to the [Migration Guide](../../src/core/MIGRATION.md) and [Migration Examples](../../src/core/MIGRATION_EXMAPLES.md). + +# Unresolved questions From 6a648658ce5d331bc781da707a5e66e8899e98ee Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 16 Mar 2020 16:58:17 +0100 Subject: [PATCH 011/115] Bump acorn sub-dependency to version ^7.1.1 (#60239) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5b13c8bd37ae..ae55508cee88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5921,9 +5921,9 @@ acorn@^6.2.1: integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== acorn@^7.0.0, acorn@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" - integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== + version "7.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" + integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== address@1.1.0: version "1.1.0" From 3a4683ce70cbcae7a0c382c55c33b7d43a4db426 Mon Sep 17 00:00:00 2001 From: Chris Queen Date: Mon, 16 Mar 2020 12:37:51 -0400 Subject: [PATCH 012/115] Passing in a null value as a param to compareFilters no longer throws an exception (#59609) * The 'filter' parameter passed into the mapFilter function is now checked for nullish-ness via optional chaining. * Added tests in compare_filters.test.ts for when compareFilters accepts a null value for it's 'filter' parameter * Removed recently added optional chaining to the filters param in the mapFilter function. Instead, the compareFilters function now performs a null check on both filter params, and returns false if either are null * Updated null check in compareFilters function to use negation null check instead of checking strictly for a null value * fix tests types Co-authored-by: Elastic Machine Co-authored-by: Liza K --- .../filter_manager/lib/compare_filters.test.ts | 16 ++++++++++++++++ .../query/filter_manager/lib/compare_filters.ts | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts b/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts index 5d6c25b0d96c..da8f5b356494 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts @@ -48,6 +48,22 @@ describe('filter manager utilities', () => { expect(compareFilters(f1, f2)).toBeTruthy(); }); + test('should compare filters, where one filter is null', () => { + const f1 = buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ); + const f2 = null; + expect(compareFilters(f1, f2 as any)).toBeFalsy(); + }); + + test('should compare a null filter with an empty filter', () => { + const f1 = null; + const f2 = buildEmptyFilter(true); + expect(compareFilters(f1 as any, f2)).toBeFalsy(); + }); + test('should compare duplicates, ignoring meta attributes', () => { const f1 = buildQueryFilter( { _type: { match: { query: 'apache', type: 'phrase' } } }, diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts b/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts index b4402885bc0b..a2105fdc1d3e 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts @@ -74,6 +74,8 @@ export const compareFilters = ( second: Filter | Filter[], comparatorOptions: FilterCompareOptions = {} ) => { + if (!first || !second) return false; + let comparators: FilterCompareOptions = {}; const excludedAttributes: string[] = ['$$hashKey', 'meta']; From 8a578960c05f04879963927d4f80075736e73e8a Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 16 Mar 2020 17:28:07 +0000 Subject: [PATCH 013/115] [ML] Use real datafeed ID for datafeed preview (#60275) --- .../jobs_list/components/job_details/datafeed_preview_tab.js | 5 +++-- x-pack/plugins/ml/public/application/services/job_service.js | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js index 7a98ec5e5ce4..216c416f30a6 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js @@ -57,7 +57,8 @@ export class DatafeedPreviewPane extends Component { } componentDidMount() { - const canPreviewDatafeed = checkPermission('canPreviewDatafeed'); + const canPreviewDatafeed = + checkPermission('canPreviewDatafeed') && this.props.job.datafeed_config !== undefined; this.setState({ canPreviewDatafeed }); updateDatafeedPreview(this.props.job, canPreviewDatafeed) @@ -87,7 +88,7 @@ function updateDatafeedPreview(job, canPreviewDatafeed) { return new Promise((resolve, reject) => { if (canPreviewDatafeed) { mlJobService - .getDatafeedPreview(job.job_id) + .getDatafeedPreview(job.datafeed_config.datafeed_id) .then(resp => { if (Array.isArray(resp)) { resolve(JSON.stringify(resp.slice(0, ML_DATA_PREVIEW_COUNT), null, 2)); diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index fe3663d6a3dd..f092e85bef5c 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -747,8 +747,7 @@ class JobService { return datafeedId; } - getDatafeedPreview(jobId) { - const datafeedId = this.getDatafeedId(jobId); + getDatafeedPreview(datafeedId) { return ml.datafeedPreview({ datafeedId }); } From dfff4fd6fa8a8063c2d30a8ebb16228da702f12d Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 16 Mar 2020 12:18:27 -0600 Subject: [PATCH 014/115] [SIEM][Detection Engine] Refactors signal rule alert type into smaller code by creating functions Refactors signal rule alert type into a smaller executor ## Summary * Breaks out the schema into its own file and function * Breaks out the action group into its own file and function * Moves misc types being added to this into the `./types` file * Breaks out all the writing of errors and success into their own functions * Uses destructuring to pull data out of some of the data types * Tweaks the gap detection to accept a date instead of moment to ease "ergonomics" * Updates unit tests for the gap detection ### 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 --- .../get_current_status_saved_object.ts | 56 ++++ .../signals/get_rule_status_saved_objects.ts | 31 ++ .../signals/siem_rule_action_groups.ts | 16 + .../signals/signal_params_schema.ts | 40 +++ .../signals/signal_rule_alert_type.ts | 300 +++++------------- .../lib/detection_engine/signals/types.ts | 12 + .../detection_engine/signals/utils.test.ts | 48 ++- .../lib/detection_engine/signals/utils.ts | 2 +- .../signals/write_current_status_succeeded.ts | 30 ++ .../write_gap_error_to_saved_object.ts | 61 ++++ ...e_signal_rule_exception_to_saved_object.ts | 58 ++++ 11 files changed, 416 insertions(+), 238 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_current_status_saved_object.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/siem_rule_action_groups.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_current_status_succeeded.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_gap_error_to_saved_object.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_signal_rule_exception_to_saved_object.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_current_status_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_current_status_saved_object.ts new file mode 100644 index 000000000000..e5057b6b6899 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_current_status_saved_object.ts @@ -0,0 +1,56 @@ +/* + * 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 { SavedObjectsFindResponse, SavedObject } from 'src/core/server'; + +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; +import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; + +interface CurrentStatusSavedObjectParams { + alertId: string; + services: AlertServices; + ruleStatusSavedObjects: SavedObjectsFindResponse; +} + +export const getCurrentStatusSavedObject = async ({ + alertId, + services, + ruleStatusSavedObjects, +}: CurrentStatusSavedObjectParams): Promise> => { + if (ruleStatusSavedObjects.saved_objects.length === 0) { + // create + const date = new Date().toISOString(); + const currentStatusSavedObject = await services.savedObjectsClient.create< + IRuleSavedAttributesSavedObjectAttributes + >(ruleStatusSavedObjectType, { + alertId, // do a search for this id. + statusDate: date, + status: 'going to run', + lastFailureAt: null, + lastSuccessAt: null, + lastFailureMessage: null, + lastSuccessMessage: null, + }); + return currentStatusSavedObject; + } else { + // update 0th to executing. + const currentStatusSavedObject = ruleStatusSavedObjects.saved_objects[0]; + const sDate = new Date().toISOString(); + currentStatusSavedObject.attributes.status = 'going to run'; + currentStatusSavedObject.attributes.statusDate = sDate; + await services.savedObjectsClient.update( + ruleStatusSavedObjectType, + currentStatusSavedObject.id, + { + ...currentStatusSavedObject.attributes, + } + ); + return currentStatusSavedObject; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts new file mode 100644 index 000000000000..5a59d0413cfb --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.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 { SavedObjectsFindResponse } from 'kibana/server'; +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; +import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; + +interface GetRuleStatusSavedObject { + alertId: string; + services: AlertServices; +} + +export const getRuleStatusSavedObjects = async ({ + alertId, + services, +}: GetRuleStatusSavedObject): Promise> => { + return services.savedObjectsClient.find({ + type: ruleStatusSavedObjectType, + perPage: 6, // 0th element is current status, 1-5 is last 5 failures. + sortField: 'statusDate', + sortOrder: 'desc', + search: `${alertId}`, + searchFields: ['alertId'], + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/siem_rule_action_groups.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/siem_rule_action_groups.ts new file mode 100644 index 000000000000..50c63df14996 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/siem_rule_action_groups.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const siemRuleActionGroups = [ + { + id: 'default', + name: i18n.translate('xpack.siem.detectionEngine.signalRuleAlert.actionGroups.default', { + defaultMessage: 'Default', + }), + }, +]; 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 new file mode 100644 index 000000000000..d1726f93108c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; + +/** + * This is the schema for the Alert Rule that represents the SIEM alert for signals + * that index into the .siem-signals-${space-id} + */ +export const signalParamsSchema = () => + schema.object({ + description: schema.string(), + note: schema.nullable(schema.string()), + falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), + from: schema.string(), + ruleId: schema.string(), + immutable: schema.boolean({ defaultValue: false }), + index: schema.nullable(schema.arrayOf(schema.string())), + language: schema.nullable(schema.string()), + outputIndex: schema.nullable(schema.string()), + savedId: schema.nullable(schema.string()), + timelineId: schema.nullable(schema.string()), + timelineTitle: schema.nullable(schema.string()), + meta: schema.nullable(schema.object({}, { allowUnknowns: true })), + query: schema.nullable(schema.string()), + filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), + maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), + riskScore: schema.number(), + severity: schema.string(), + threat: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), + to: schema.string(), + type: schema.string(), + references: schema.arrayOf(schema.string(), { defaultValue: [] }), + version: schema.number({ defaultValue: 1 }), + }); 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 b467dfdaff30..e3ea121a9ebb 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 @@ -4,35 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; import { Logger } from 'src/core/server'; -import moment from 'moment'; -import { i18n } from '@kbn/i18n'; -import { - SIGNALS_ID, - DEFAULT_MAX_SIGNALS, - DEFAULT_SEARCH_AFTER_PAGE_SIZE, -} from '../../../../common/constants'; +import { SIGNALS_ID, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants'; import { buildEventsSearchQuery } from './build_events_query'; import { getInputIndex } from './get_input_output_index'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { getFilter } from './get_filter'; -import { SignalRuleAlertTypeDefinition } from './types'; +import { SignalRuleAlertTypeDefinition, AlertAttributes } from './types'; import { getGapBetweenRuns } from './utils'; -import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; -import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; -interface AlertAttributes { - enabled: boolean; - name: string; - tags: string[]; - createdBy: string; - createdAt: string; - updatedBy: string; - schedule: { - interval: string; - }; -} +import { writeSignalRuleExceptionToSavedObject } from './write_signal_rule_exception_to_saved_object'; +import { signalParamsSchema } from './signal_params_schema'; +import { siemRuleActionGroups } from './siem_rule_action_groups'; +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'; + export const signalRulesAlertType = ({ logger, version, @@ -43,43 +31,11 @@ export const signalRulesAlertType = ({ return { id: SIGNALS_ID, name: 'SIEM Signals', - actionGroups: [ - { - id: 'default', - name: i18n.translate('xpack.siem.detectionEngine.signalRuleAlert.actionGroups.default', { - defaultMessage: 'Default', - }), - }, - ], + actionGroups: siemRuleActionGroups, defaultActionGroupId: 'default', validate: { - params: schema.object({ - description: schema.string(), - note: schema.nullable(schema.string()), - falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), - from: schema.string(), - ruleId: schema.string(), - immutable: schema.boolean({ defaultValue: false }), - index: schema.nullable(schema.arrayOf(schema.string())), - language: schema.nullable(schema.string()), - outputIndex: schema.nullable(schema.string()), - savedId: schema.nullable(schema.string()), - timelineId: schema.nullable(schema.string()), - timelineTitle: schema.nullable(schema.string()), - meta: schema.nullable(schema.object({}, { allowUnknowns: true })), - query: schema.nullable(schema.string()), - filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), - maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), - riskScore: schema.number(), - severity: schema.string(), - threat: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), - to: schema.string(), - type: schema.string(), - references: schema.arrayOf(schema.string(), { defaultValue: [] }), - version: schema.number({ defaultValue: 1 }), - }), + params: signalParamsSchema(), }, - // fun fact: previousStartedAt is not actually a Date but a String of a date async executor({ previousStartedAt, alertId, services, params }) { const { from, @@ -93,89 +49,43 @@ export const signalRulesAlertType = ({ to, type, } = params; - // TODO: Remove this hard extraction of name once this is fixed: https://github.com/elastic/kibana/issues/50522 const savedObject = await services.savedObjectsClient.get('alert', alertId); - const ruleStatusSavedObjects = await services.savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, - perPage: 6, // 0th element is current status, 1-5 is last 5 failures. - sortField: 'statusDate', - sortOrder: 'desc', - search: `${alertId}`, - searchFields: ['alertId'], + + const ruleStatusSavedObjects = await getRuleStatusSavedObjects({ + alertId, + services, }); - let currentStatusSavedObject; - if (ruleStatusSavedObjects.saved_objects.length === 0) { - // create - const date = new Date().toISOString(); - currentStatusSavedObject = await services.savedObjectsClient.create< - IRuleSavedAttributesSavedObjectAttributes - >(ruleStatusSavedObjectType, { - alertId, // do a search for this id. - statusDate: date, - status: 'going to run', - lastFailureAt: null, - lastSuccessAt: null, - lastFailureMessage: null, - lastSuccessMessage: null, - }); - } else { - // update 0th to executing. - currentStatusSavedObject = ruleStatusSavedObjects.saved_objects[0]; - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'going to run'; - currentStatusSavedObject.attributes.statusDate = sDate; - await services.savedObjectsClient.update( - ruleStatusSavedObjectType, - currentStatusSavedObject.id, - { - ...currentStatusSavedObject.attributes, - } - ); - } - const name = savedObject.attributes.name; - const tags = savedObject.attributes.tags; + const currentStatusSavedObject = await getCurrentStatusSavedObject({ + alertId, + services, + ruleStatusSavedObjects, + }); + + const { + name, + tags, + createdAt, + createdBy, + updatedBy, + enabled, + schedule: { interval }, + } = savedObject.attributes; - const createdBy = savedObject.attributes.createdBy; - const createdAt = savedObject.attributes.createdAt; - const updatedBy = savedObject.attributes.updatedBy; const updatedAt = savedObject.updated_at ?? ''; - const interval = savedObject.attributes.schedule.interval; - const enabled = savedObject.attributes.enabled; - const gap = getGapBetweenRuns({ - previousStartedAt: previousStartedAt != null ? moment(previousStartedAt) : null, // TODO: Remove this once previousStartedAt is no longer a string - interval, - from, - to, - }); - if (gap != null && gap.asMilliseconds() > 0) { - logger.warn( - `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.` - ); - // write a failure status whenever we have a time gap - // this is a temporary solution until general activity - // monitoring is developed as a feature - const gapDate = new Date().toISOString(); - await services.savedObjectsClient.create(ruleStatusSavedObjectType, { - alertId, - statusDate: gapDate, - status: 'failed', - lastFailureAt: gapDate, - lastSuccessAt: currentStatusSavedObject.attributes.lastSuccessAt, - lastFailureMessage: `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.`, - lastSuccessMessage: currentStatusSavedObject.attributes.lastSuccessMessage, - }); - if (ruleStatusSavedObjects.saved_objects.length >= 6) { - // delete fifth status and prepare to insert a newer one. - const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); - await toDelete.forEach(async item => - services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) - ); - } - } + const gap = getGapBetweenRuns({ previousStartedAt, interval, from, to }); + + await writeGapErrorToSavedObject({ + alertId, + logger, + ruleId: ruleId ?? '(unknown rule id)', + currentStatusSavedObject, + services, + gap, + 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 @@ -243,107 +153,45 @@ export const signalRulesAlertType = ({ logger.debug( `Finished signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"` ); - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'succeeded'; - currentStatusSavedObject.attributes.statusDate = sDate; - currentStatusSavedObject.attributes.lastSuccessAt = sDate; - currentStatusSavedObject.attributes.lastSuccessMessage = 'succeeded'; - await services.savedObjectsClient.update( - ruleStatusSavedObjectType, - currentStatusSavedObject.id, - { - ...currentStatusSavedObject.attributes, - } - ); + await writeCurrentStatusSucceeded({ + services, + currentStatusSavedObject, + }); } else { - logger.error( - `Error processing signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"` - ); - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'failed'; - currentStatusSavedObject.attributes.statusDate = sDate; - currentStatusSavedObject.attributes.lastFailureAt = sDate; - currentStatusSavedObject.attributes.lastFailureMessage = `Bulk Indexing signals failed. Check logs for further details \nRule name: "${name}"\nid: "${alertId}"\nrule_id: "${ruleId}"\n`; - // current status is failing - await services.savedObjectsClient.update( - ruleStatusSavedObjectType, - currentStatusSavedObject.id, - { - ...currentStatusSavedObject.attributes, - } - ); - // create new status for historical purposes - await services.savedObjectsClient.create(ruleStatusSavedObjectType, { - ...currentStatusSavedObject.attributes, + 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)', }); - - if (ruleStatusSavedObjects.saved_objects.length >= 6) { - // delete fifth status and prepare to insert a newer one. - const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); - await toDelete.forEach(async item => - services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) - ); - } } } catch (err) { - logger.error( - `Error from signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}", ${err.message}` - ); - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'failed'; - currentStatusSavedObject.attributes.statusDate = sDate; - currentStatusSavedObject.attributes.lastFailureAt = sDate; - currentStatusSavedObject.attributes.lastFailureMessage = err.message; - // current status is failing - await services.savedObjectsClient.update( - ruleStatusSavedObjectType, - currentStatusSavedObject.id, - { - ...currentStatusSavedObject.attributes, - } - ); - // create new status for historical purposes - await services.savedObjectsClient.create(ruleStatusSavedObjectType, { - ...currentStatusSavedObject.attributes, + await writeSignalRuleExceptionToSavedObject({ + name, + alertId, + currentStatusSavedObject, + logger, + message: err?.message ?? '(no error message given)', + services, + ruleStatusSavedObjects, + ruleId: ruleId ?? '(unknown rule id)', }); - - if (ruleStatusSavedObjects.saved_objects.length >= 6) { - // delete fifth status and prepare to insert a newer one. - const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); - await toDelete.forEach(async item => - services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) - ); - } } } catch (exception) { - logger.error( - `Error from signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" message: ${exception.message}` - ); - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'failed'; - currentStatusSavedObject.attributes.statusDate = sDate; - currentStatusSavedObject.attributes.lastFailureAt = sDate; - currentStatusSavedObject.attributes.lastFailureMessage = exception.message; - // current status is failing - await services.savedObjectsClient.update( - ruleStatusSavedObjectType, - currentStatusSavedObject.id, - { - ...currentStatusSavedObject.attributes, - } - ); - // create new status for historical purposes - await services.savedObjectsClient.create(ruleStatusSavedObjectType, { - ...currentStatusSavedObject.attributes, + await writeSignalRuleExceptionToSavedObject({ + name, + alertId, + currentStatusSavedObject, + logger, + message: exception?.message ?? '(no error message given)', + services, + ruleStatusSavedObjects, + ruleId: ruleId ?? '(unknown rule id)', }); - - if (ruleStatusSavedObjects.saved_objects.length >= 6) { - // delete fifth status and prepare to insert a newer one. - const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); - await toDelete.forEach(async item => - services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) - ); - } } }, }; 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 744254511731..eaed3f2ead3a 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 @@ -145,3 +145,15 @@ export interface SignalHit { event: object; signal: Partial; } + +export interface AlertAttributes { + enabled: boolean; + name: string; + tags: string[]; + createdBy: string; + createdAt: string; + updatedBy: string; + schedule: { + interval: string; + }; +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts index bf25ab8bfd7e..873e06fcbb44 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts @@ -179,7 +179,10 @@ describe('utils', () => { describe('getGapBetweenRuns', () => { test('it returns a gap of 0 when "from" and interval match each other and the previous started was from the previous interval time', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(5, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(5, 'minutes') + .toDate(), interval: '5m', from: 'now-5m', to: 'now', @@ -191,7 +194,10 @@ describe('utils', () => { test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(5, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(5, 'minutes') + .toDate(), interval: '5m', from: 'now-6m', to: 'now', @@ -203,7 +209,10 @@ describe('utils', () => { test('it returns a negative gap of 5 minutes when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(5, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(5, 'minutes') + .toDate(), interval: '5m', from: 'now-10m', to: 'now', @@ -215,7 +224,10 @@ describe('utils', () => { test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(10, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(10, 'minutes') + .toDate(), interval: '10m', from: 'now-11m', to: 'now', @@ -230,7 +242,8 @@ describe('utils', () => { previousStartedAt: nowDate .clone() .subtract(5, 'minutes') - .subtract(30, 'seconds'), + .subtract(30, 'seconds') + .toDate(), interval: '5m', from: 'now-6m', to: 'now', @@ -242,7 +255,10 @@ describe('utils', () => { test('it returns an exact 0 gap when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is one minute late', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(6, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(6, 'minutes') + .toDate(), interval: '5m', from: 'now-6m', to: 'now', @@ -257,7 +273,8 @@ describe('utils', () => { previousStartedAt: nowDate .clone() .subtract(6, 'minutes') - .subtract(30, 'seconds'), + .subtract(30, 'seconds') + .toDate(), interval: '5m', from: 'now-6m', to: 'now', @@ -269,7 +286,10 @@ describe('utils', () => { test('it returns a gap of 1 minute when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is two minutes late', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(7, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(7, 'minutes') + .toDate(), interval: '5m', from: 'now-6m', to: 'now', @@ -292,7 +312,7 @@ describe('utils', () => { test('it returns null if the interval is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone(), + previousStartedAt: nowDate.clone().toDate(), interval: 'invalid', // if not set to "x" where x is an interval such as 6m from: 'now-5m', to: 'now', @@ -303,7 +323,10 @@ describe('utils', () => { test('it returns the expected result when "from" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(7, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(7, 'minutes') + .toDate(), interval: '5m', from: 'invalid', to: 'now', @@ -315,7 +338,10 @@ describe('utils', () => { test('it returns the expected result when "to" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(7, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(7, 'minutes') + .toDate(), interval: '5m', from: 'now-6m', to: 'invalid', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts index 016aed9fabcd..8e7fb9c38d65 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts @@ -68,7 +68,7 @@ export const getGapBetweenRuns = ({ to, now = moment(), }: { - previousStartedAt: moment.Moment | undefined | null; + previousStartedAt: Date | undefined | null; interval: string; from: string; to: string; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_current_status_succeeded.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_current_status_succeeded.ts new file mode 100644 index 000000000000..6b06235b2906 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_current_status_succeeded.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObject } from 'src/core/server'; +import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; + +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; + +interface GetRuleStatusSavedObject { + services: AlertServices; + currentStatusSavedObject: SavedObject; +} + +export const writeCurrentStatusSucceeded = async ({ + services, + currentStatusSavedObject, +}: GetRuleStatusSavedObject): Promise => { + const sDate = new Date().toISOString(); + currentStatusSavedObject.attributes.status = 'succeeded'; + currentStatusSavedObject.attributes.statusDate = sDate; + currentStatusSavedObject.attributes.lastSuccessAt = sDate; + currentStatusSavedObject.attributes.lastSuccessMessage = 'succeeded'; + await services.savedObjectsClient.update(ruleStatusSavedObjectType, currentStatusSavedObject.id, { + ...currentStatusSavedObject.attributes, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_gap_error_to_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_gap_error_to_saved_object.ts new file mode 100644 index 000000000000..3650548c80ad --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_gap_error_to_saved_object.ts @@ -0,0 +1,61 @@ +/* + * 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 moment from 'moment'; +import { Logger, SavedObject, SavedObjectsFindResponse } from 'src/core/server'; + +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; +import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; + +interface WriteGapErrorToSavedObjectParams { + logger: Logger; + alertId: string; + ruleId: string; + currentStatusSavedObject: SavedObject; + ruleStatusSavedObjects: SavedObjectsFindResponse; + services: AlertServices; + gap: moment.Duration | null | undefined; + name: string; +} + +export const writeGapErrorToSavedObject = async ({ + alertId, + currentStatusSavedObject, + logger, + services, + ruleStatusSavedObjects, + ruleId, + gap, + name, +}: WriteGapErrorToSavedObjectParams): Promise => { + if (gap != null && gap.asMilliseconds() > 0) { + logger.warn( + `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.` + ); + // write a failure status whenever we have a time gap + // this is a temporary solution until general activity + // monitoring is developed as a feature + const gapDate = new Date().toISOString(); + await services.savedObjectsClient.create(ruleStatusSavedObjectType, { + alertId, + statusDate: gapDate, + status: 'failed', + lastFailureAt: gapDate, + lastSuccessAt: currentStatusSavedObject.attributes.lastSuccessAt, + lastFailureMessage: `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.`, + lastSuccessMessage: currentStatusSavedObject.attributes.lastSuccessMessage, + }); + + if (ruleStatusSavedObjects.saved_objects.length >= 6) { + // delete fifth status and prepare to insert a newer one. + const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); + await toDelete.forEach(async item => + services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) + ); + } + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_signal_rule_exception_to_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_signal_rule_exception_to_saved_object.ts new file mode 100644 index 000000000000..5ca0808902a5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_signal_rule_exception_to_saved_object.ts @@ -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 { Logger, SavedObject, SavedObjectsFindResponse } from 'src/core/server'; + +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; +import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; + +interface SignalRuleExceptionParams { + logger: Logger; + alertId: string; + ruleId: string; + currentStatusSavedObject: SavedObject; + ruleStatusSavedObjects: SavedObjectsFindResponse; + message: string; + services: AlertServices; + name: string; +} + +export const writeSignalRuleExceptionToSavedObject = async ({ + alertId, + currentStatusSavedObject, + logger, + message, + services, + ruleStatusSavedObjects, + ruleId, + name, +}: SignalRuleExceptionParams): Promise => { + logger.error( + `Error from signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" message: ${message}` + ); + const sDate = new Date().toISOString(); + currentStatusSavedObject.attributes.status = 'failed'; + currentStatusSavedObject.attributes.statusDate = sDate; + currentStatusSavedObject.attributes.lastFailureAt = sDate; + currentStatusSavedObject.attributes.lastFailureMessage = message; + // current status is failing + await services.savedObjectsClient.update(ruleStatusSavedObjectType, currentStatusSavedObject.id, { + ...currentStatusSavedObject.attributes, + }); + // create new status for historical purposes + await services.savedObjectsClient.create(ruleStatusSavedObjectType, { + ...currentStatusSavedObject.attributes, + }); + + if (ruleStatusSavedObjects.saved_objects.length >= 6) { + // delete fifth status and prepare to insert a newer one. + const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); + await toDelete.forEach(async item => + services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) + ); + } +}; From 6cd888f75fefc6d3ec4405f7b937069e5ea9bf75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Mon, 16 Mar 2020 19:47:56 +0100 Subject: [PATCH 015/115] [Logs UI] Fix log rate table row expansion (#60096) This fixes the log rate table row expansion button, which broke in #54586 during a refactoring. --- .../sections/anomalies/table.tsx | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index 6eaa5de90008..5cb5f3a993d4 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo, useState } from 'react'; @@ -20,8 +20,8 @@ import { LogEntryRateResults } from '../../use_log_entry_rate_results'; import { AnomaliesTableExpandedRow } from './expanded_row'; interface TableItem { - id: string; - partition: string; + partitionName: string; + partitionId: string; topAnomalyScore: number; } @@ -55,11 +55,10 @@ export const AnomaliesTable: React.FunctionComponent<{ const tableItems: TableItem[] = useMemo(() => { return Object.entries(results.partitionBuckets).map(([key, value]) => { return { - // Note: EUI's table expanded rows won't work with a key of '' in itemIdToExpandedRowMap, so we have to use the friendly name here - id: getFriendlyNameForPartitionId(key), // The real ID partitionId: key, - partition: getFriendlyNameForPartitionId(key), + // Note: EUI's table expanded rows won't work with a key of '' in itemIdToExpandedRowMap, so we have to use the friendly name here + partitionName: getFriendlyNameForPartitionId(key), topAnomalyScore: formatAnomalyScore(value.topAnomalyScore), }; }); @@ -91,8 +90,8 @@ export const AnomaliesTable: React.FunctionComponent<{ const sortedTableItems = useMemo(() => { let sortedItems: TableItem[] = []; - if (sorting.sort.field === 'partition') { - sortedItems = tableItems.sort((a, b) => (a.partition > b.partition ? 1 : -1)); + if (sorting.sort.field === 'partitionName') { + sortedItems = tableItems.sort((a, b) => (a.partitionId > b.partitionId ? 1 : -1)); } else if (sorting.sort.field === 'topAnomalyScore') { sortedItems = tableItems.sort((a, b) => a.topAnomalyScore - b.topAnomalyScore); } @@ -100,10 +99,10 @@ export const AnomaliesTable: React.FunctionComponent<{ }, [tableItems, sorting]); const expandItem = useCallback( - item => { + (item: TableItem) => { const newItemIdToExpandedRowMap = { ...itemIdToExpandedRowMap, - [item.id]: ( + [item.partitionName]: ( { - if (itemIdToExpandedRowMap[item.id]) { - const { [item.id]: toggledItem, ...remainingExpandedRowMap } = itemIdToExpandedRowMap; + (item: TableItem) => { + if (itemIdToExpandedRowMap[item.partitionName]) { + const { + [item.partitionName]: toggledItem, + ...remainingExpandedRowMap + } = itemIdToExpandedRowMap; setItemIdToExpandedRowMap(remainingExpandedRowMap); } }, [itemIdToExpandedRowMap] ); - const columns = [ + const columns: Array> = [ { - field: 'partition', + field: 'partitionName', name: partitionColumnName, sortable: true, truncateText: true, @@ -149,8 +151,8 @@ export const AnomaliesTable: React.FunctionComponent<{ isExpander: true, render: (item: TableItem) => ( @@ -161,7 +163,7 @@ export const AnomaliesTable: React.FunctionComponent<{ return ( Date: Mon, 16 Mar 2020 13:33:40 -0600 Subject: [PATCH 016/115] [Maps] add draw control to create distance filter (#58163) * [Maps] add distance filter to draw controls * create distance filter * update jest snapshot * remove duplicated code * reset circle draw when user hits escape * i18n cleanup * ts MultiIndexGeoFieldSelect * ts DistanceFilterForm * remove unused prop * make interface a type * move geo_field_with_index to components folder * convert draw_circle to TS Co-authored-by: Elastic Machine --- .../geometry_filter_form.test.js.snap | 248 ++++++------------ .../components/distance_filter_form.tsx | 99 +++++++ .../public/components/geo_field_with_index.ts | 17 ++ .../public/components/geometry_filter_form.js | 95 ++----- .../multi_index_geo_field_select.tsx | 78 ++++++ .../map/mb/draw_control/draw_circle.ts | 137 ++++++++++ .../map/mb/draw_control/draw_control.js | 51 ++-- .../map/mb/draw_control/draw_tooltip.js | 26 +- .../__snapshots__/tools_control.test.js.snap | 46 ++++ .../tools_control/tools_control.js | 68 ++++- .../maps/public/elasticsearch_geo_utils.js | 33 +++ x-pack/package.json | 1 + x-pack/plugins/maps/common/constants.ts | 1 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - yarn.lock | 16 ++ 16 files changed, 636 insertions(+), 282 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/public/components/distance_filter_form.tsx create mode 100644 x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts create mode 100644 x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx create mode 100644 x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap index c62b07a89e7a..85a073c8d9ac 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap @@ -17,49 +17,27 @@ exports[`should not render relation select when geo field is geo_point 1`] = ` value="My shape" /> - - - - - My index - - -
- my geo field - , - "value": "My index/my geo field", - }, - ] + -
+ } + /> @@ -95,49 +73,27 @@ exports[`should not show "within" relation when filter geometry is not closed 1` value="My shape" /> - - - - - My index - - -
- my geo field - , - "value": "My index/my geo field", - }, - ] + -
+ } + /> - - - - - My index - - -
- my geo field - , - "value": "My index/my geo field", - }, - ] + -
+ } + /> @@ -281,49 +215,27 @@ exports[`should render relation select when geo field is geo_shape 1`] = ` value="My shape" /> - - - - - My index - - -
- my geo field - , - "value": "My index/my geo field", - }, - ] + -
+ } + /> void; +} + +interface State { + selectedField: GeoFieldWithIndex | undefined; + filterLabel: string; +} + +export class DistanceFilterForm extends Component { + state = { + selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, + filterLabel: '', + }; + + _onGeoFieldChange = (selectedField: GeoFieldWithIndex | undefined) => { + this.setState({ selectedField }); + }; + + _onFilterLabelChange = (e: ChangeEvent) => { + this.setState({ + filterLabel: e.target.value, + }); + }; + + _onSubmit = () => { + if (!this.state.selectedField) { + return; + } + this.props.onSubmit({ + filterLabel: this.state.filterLabel, + indexPatternId: this.state.selectedField.indexPatternId, + geoFieldName: this.state.selectedField.geoFieldName, + }); + }; + + render() { + return ( + + + + + + + + + + + + {this.props.buttonLabel} + + + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts b/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts new file mode 100644 index 000000000000..863e0adda8fb --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts @@ -0,0 +1,17 @@ +/* + * 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 */ + +// Maps can contain geo fields from multiple index patterns. GeoFieldWithIndex is used to: +// 1) Combine the geo field along with associated index pattern state. +// 2) Package asynchronously looked up state via indexPatternService to avoid +// PITA of looking up async state in downstream react consumers. +export type GeoFieldWithIndex = { + geoFieldName: string; + geoFieldType: string; + indexPatternTitle: string; + indexPatternId: string; +}; diff --git a/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js b/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js index 3308155caa3e..ac6461345e8b 100644 --- a/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js +++ b/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js @@ -9,9 +9,6 @@ import PropTypes from 'prop-types'; import { EuiForm, EuiFormRow, - EuiSuperSelect, - EuiTextColor, - EuiText, EuiFieldText, EuiButton, EuiSelect, @@ -22,20 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../common/constants'; import { getEsSpatialRelationLabel } from '../../common/i18n_getters'; - -const GEO_FIELD_VALUE_DELIMITER = '/'; // `/` is not allowed in index pattern name so should not have collisions - -function createIndexGeoFieldName({ indexPatternTitle, geoFieldName }) { - return `${indexPatternTitle}${GEO_FIELD_VALUE_DELIMITER}${geoFieldName}`; -} - -function splitIndexGeoFieldName(value) { - const split = value.split(GEO_FIELD_VALUE_DELIMITER); - return { - indexPatternTitle: split[0], - geoFieldName: split[1], - }; -} +import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select'; export class GeometryFilterForm extends Component { static propTypes = { @@ -52,27 +36,13 @@ export class GeometryFilterForm extends Component { }; state = { - geoFieldTag: this.props.geoFields.length - ? createIndexGeoFieldName(this.props.geoFields[0]) - : '', + selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, geometryLabel: this.props.intitialGeometryLabel, relation: ES_SPATIAL_RELATIONS.INTERSECTS, }; - _getSelectedGeoField = () => { - if (!this.state.geoFieldTag) { - return null; - } - - const { indexPatternTitle, geoFieldName } = splitIndexGeoFieldName(this.state.geoFieldTag); - - return this.props.geoFields.find(option => { - return option.indexPatternTitle === indexPatternTitle && option.geoFieldName === geoFieldName; - }); - }; - - _onGeoFieldChange = selectedValue => { - this.setState({ geoFieldTag: selectedValue }); + _onGeoFieldChange = selectedField => { + this.setState({ selectedField }); }; _onGeometryLabelChange = e => { @@ -88,25 +58,21 @@ export class GeometryFilterForm extends Component { }; _onSubmit = () => { - const geoField = this._getSelectedGeoField(); this.props.onSubmit({ geometryLabel: this.state.geometryLabel, - indexPatternId: geoField.indexPatternId, - geoFieldName: geoField.geoFieldName, - geoFieldType: geoField.geoFieldType, + indexPatternId: this.state.selectedField.indexPatternId, + geoFieldName: this.state.selectedField.geoFieldName, + geoFieldType: this.state.selectedField.geoFieldType, relation: this.state.relation, }); }; _renderRelationInput() { - if (!this.state.geoFieldTag) { - return null; - } - - const { geoFieldType } = this._getSelectedGeoField(); - // relationship only used when filtering geo_shape fields - if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { + if ( + !this.state.selectedField || + this.state.selectedField.geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT + ) { return null; } @@ -141,20 +107,6 @@ export class GeometryFilterForm extends Component { } render() { - const options = this.props.geoFields.map(({ indexPatternTitle, geoFieldName }) => { - return { - inputDisplay: ( - - - {indexPatternTitle} - -
- {geoFieldName} -
- ), - value: createIndexGeoFieldName({ indexPatternTitle, geoFieldName }), - }; - }); let error; if (this.props.errorMsg) { error = {this.props.errorMsg}; @@ -174,24 +126,11 @@ export class GeometryFilterForm extends Component { />
- - - + {this._renderRelationInput()} @@ -204,7 +143,7 @@ export class GeometryFilterForm extends Component { size="s" fill onClick={this._onSubmit} - isDisabled={!this.state.geometryLabel || !this.state.geoFieldTag} + isDisabled={!this.state.geometryLabel || !this.state.selectedField} isLoading={this.props.isLoading} > {this.props.buttonLabel} diff --git a/x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx b/x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx new file mode 100644 index 000000000000..0e5b94f0c642 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx @@ -0,0 +1,78 @@ +/* + * 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 { EuiFormRow, EuiSuperSelect, EuiTextColor, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { GeoFieldWithIndex } from './geo_field_with_index'; + +const OPTION_ID_DELIMITER = '/'; + +function createOptionId(geoField: GeoFieldWithIndex): string { + // Namespace field with indexPatterId to avoid collisions between field names + return `${geoField.indexPatternId}${OPTION_ID_DELIMITER}${geoField.geoFieldName}`; +} + +function splitOptionId(optionId: string) { + const split = optionId.split(OPTION_ID_DELIMITER); + return { + indexPatternId: split[0], + geoFieldName: split[1], + }; +} + +interface Props { + fields: GeoFieldWithIndex[]; + onChange: (newSelectedField: GeoFieldWithIndex | undefined) => void; + selectedField: GeoFieldWithIndex | undefined; +} + +export function MultiIndexGeoFieldSelect({ fields, onChange, selectedField }: Props) { + function onFieldSelect(selectedOptionId: string) { + const { indexPatternId, geoFieldName } = splitOptionId(selectedOptionId); + + const newSelectedField = fields.find(field => { + return field.indexPatternId === indexPatternId && field.geoFieldName === geoFieldName; + }); + onChange(newSelectedField); + } + + const options = fields.map((geoField: GeoFieldWithIndex) => { + return { + inputDisplay: ( + + + {geoField.indexPatternTitle} + +
+ {geoField.geoFieldName} +
+ ), + value: createOptionId(geoField), + }; + }); + + return ( + + + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts new file mode 100644 index 000000000000..f2ceb8685d43 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts @@ -0,0 +1,137 @@ +/* + * 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 */ + +// @ts-ignore +import turf from 'turf'; +// @ts-ignore +import turfCircle from '@turf/circle'; + +type DrawCircleState = { + circle: { + properties: { + center: {} | null; + radiusKm: number; + }; + id: string | number; + incomingCoords: (coords: unknown[]) => void; + toGeoJSON: () => unknown; + }; +}; + +type MouseEvent = { + lngLat: { + lng: number; + lat: number; + }; +}; + +export const DrawCircle = { + onSetup() { + // @ts-ignore + const circle: unknown = this.newFeature({ + type: 'Feature', + properties: { + center: null, + radiusKm: 0, + }, + geometry: { + type: 'Polygon', + coordinates: [[]], + }, + }); + + // @ts-ignore + this.addFeature(circle); + // @ts-ignore + this.clearSelectedFeatures(); + // @ts-ignore + this.updateUIClasses({ mouse: 'add' }); + // @ts-ignore + this.setActionableState({ + trash: true, + }); + return { + circle, + }; + }, + onKeyUp(state: DrawCircleState, e: { keyCode: number }) { + if (e.keyCode === 27) { + // clear point when user hits escape + state.circle.properties.center = null; + state.circle.properties.radiusKm = 0; + state.circle.incomingCoords([[]]); + } + }, + onClick(state: DrawCircleState, e: MouseEvent) { + if (!state.circle.properties.center) { + // first click, start circle + state.circle.properties.center = [e.lngLat.lng, e.lngLat.lat]; + } else { + // second click, finish draw + // @ts-ignore + this.updateUIClasses({ mouse: 'pointer' }); + state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, [ + e.lngLat.lng, + e.lngLat.lat, + ]); + // @ts-ignore + this.changeMode('simple_select', { featuresId: state.circle.id }); + } + }, + onMouseMove(state: DrawCircleState, e: MouseEvent) { + if (!state.circle.properties.center) { + // circle not started, nothing to update + return; + } + + const mouseLocation = [e.lngLat.lng, e.lngLat.lat]; + state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, mouseLocation); + const newCircleFeature = turfCircle( + state.circle.properties.center, + state.circle.properties.radiusKm + ); + state.circle.incomingCoords(newCircleFeature.geometry.coordinates); + }, + onStop(state: DrawCircleState) { + // @ts-ignore + this.updateUIClasses({ mouse: 'none' }); + // @ts-ignore + this.activateUIButton(); + + // @ts-ignore + if (this.getFeature(state.circle.id) === undefined) return; + + if (state.circle.properties.center && state.circle.properties.radiusKm > 0) { + // @ts-ignore + this.map.fire('draw.create', { + features: [state.circle.toGeoJSON()], + }); + } else { + // @ts-ignore + this.deleteFeature([state.circle.id], { silent: true }); + // @ts-ignore + this.changeMode('simple_select', {}, { silent: true }); + } + }, + toDisplayFeatures( + state: DrawCircleState, + geojson: { properties: { active: string } }, + display: (geojson: unknown) => unknown + ) { + if (state.circle.properties.center) { + geojson.properties.active = 'true'; + return display(geojson); + } + }, + onTrash(state: DrawCircleState) { + // @ts-ignore + this.deleteFeature([state.circle.id], { silent: true }); + // @ts-ignore + this.changeMode('simple_select'); + }, +}; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js index f1b4fe2aad1f..99abe5d108b5 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js @@ -9,7 +9,9 @@ import React from 'react'; import { DRAW_TYPE } from '../../../../../common/constants'; import MapboxDraw from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw-unminified'; import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; +import { DrawCircle } from './draw_circle'; import { + createDistanceFilterWithMeta, createSpatialFilterWithBoundingBox, createSpatialFilterWithGeometry, getBoundingBoxGeometry, @@ -19,6 +21,7 @@ import { DrawTooltip } from './draw_tooltip'; const mbDrawModes = MapboxDraw.modes; mbDrawModes.draw_rectangle = DrawRectangle; +mbDrawModes.draw_circle = DrawCircle; export class DrawControl extends React.Component { constructor() { @@ -60,7 +63,21 @@ export class DrawControl extends React.Component { return; } - const isBoundingBox = this.props.drawState.drawType === DRAW_TYPE.BOUNDS; + if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + const circle = e.features[0]; + roundCoordinates(circle.properties.center); + const filter = createDistanceFilterWithMeta({ + alias: this.props.drawState.filterLabel, + distanceKm: _.round(circle.properties.radiusKm, circle.properties.radiusKm > 10 ? 0 : 2), + geoFieldName: this.props.drawState.geoFieldName, + indexPatternId: this.props.drawState.indexPatternId, + point: circle.properties.center, + }); + this.props.addFilters([filter]); + this.props.disableDrawState(); + return; + } + const geometry = e.features[0].geometry; // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number roundCoordinates(geometry.coordinates); @@ -73,15 +90,16 @@ export class DrawControl extends React.Component { geometryLabel: this.props.drawState.geometryLabel, relation: this.props.drawState.relation, }; - const filter = isBoundingBox - ? createSpatialFilterWithBoundingBox({ - ...options, - geometry: getBoundingBoxGeometry(geometry), - }) - : createSpatialFilterWithGeometry({ - ...options, - geometry, - }); + const filter = + this.props.drawState.drawType === DRAW_TYPE.BOUNDS + ? createSpatialFilterWithBoundingBox({ + ...options, + geometry: getBoundingBoxGeometry(geometry), + }) + : createSpatialFilterWithGeometry({ + ...options, + geometry, + }); this.props.addFilters([filter]); } catch (error) { // TODO notify user why filter was not created @@ -109,11 +127,14 @@ export class DrawControl extends React.Component { this.props.mbMap.getCanvas().style.cursor = 'crosshair'; this.props.mbMap.on('draw.create', this._onDraw); } - const mbDrawMode = - this.props.drawState.drawType === DRAW_TYPE.POLYGON - ? this._mbDrawControl.modes.DRAW_POLYGON - : 'draw_rectangle'; - this._mbDrawControl.changeMode(mbDrawMode); + + if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) { + this._mbDrawControl.changeMode('draw_rectangle'); + } else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + this._mbDrawControl.changeMode('draw_circle'); + } else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) { + this._mbDrawControl.changeMode(this._mbDrawControl.modes.DRAW_POLYGON); + } } render() { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js index 463fe5298141..c8bde29b94fb 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js @@ -42,14 +42,24 @@ export class DrawTooltip extends Component { } render() { - const instructions = - this.props.drawState.drawType === DRAW_TYPE.BOUNDS - ? i18n.translate('xpack.maps.drawTooltip.boundsInstructions', { - defaultMessage: 'Click to start rectangle. Click again to finish.', - }) - : i18n.translate('xpack.maps.drawTooltip.polygonInstructions', { - defaultMessage: 'Click to add vertex. Double click to finish.', - }); + let instructions; + if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) { + instructions = i18n.translate('xpack.maps.drawTooltip.boundsInstructions', { + defaultMessage: + 'Click to start rectangle. Move mouse to adjust rectangle size. Click again to finish.', + }); + } else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + instructions = i18n.translate('xpack.maps.drawTooltip.distanceInstructions', { + defaultMessage: 'Click to set point. Move mouse to adjust distance. Click to finish.', + }); + } else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) { + instructions = i18n.translate('xpack.maps.drawTooltip.polygonInstructions', { + defaultMessage: 'Click to start shape. Click to add vertex. Double click to finish.', + }); + } else { + // unknown draw type, tooltip not needed + return null; + } const tooltipAnchor = (
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap index 681c3f0fbfd6..d7fa099fe9db 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap @@ -41,6 +41,10 @@ exports[`Should render cancel button when drawing 1`] = ` "name": "Draw bounds to filter data", "panel": 2, }, + Object { + "name": "Draw distance to filter data", + "panel": 3, + }, ], "title": "Tools", }, @@ -86,6 +90,25 @@ exports[`Should render cancel button when drawing 1`] = ` "id": 2, "title": "Draw bounds", }, + Object { + "content": , + "id": 3, + "title": "Draw distance", + }, ] } /> @@ -144,6 +167,10 @@ exports[`renders 1`] = ` "name": "Draw bounds to filter data", "panel": 2, }, + Object { + "name": "Draw distance to filter data", + "panel": 3, + }, ], "title": "Tools", }, @@ -189,6 +216,25 @@ exports[`renders 1`] = ` "id": 2, "title": "Draw bounds", }, + Object { + "content": , + "id": 3, + "title": "Draw distance", + }, ] } /> diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js index ea6ffe3ba143..e7c125abe70c 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js @@ -14,9 +14,10 @@ import { EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DRAW_TYPE } from '../../../../common/constants'; +import { DRAW_TYPE, ES_GEO_FIELD_TYPE } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; import { GeometryFilterForm } from '../../../components/geometry_filter_form'; +import { DistanceFilterForm } from '../../../components/distance_filter_form'; const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', { defaultMessage: 'Draw shape to filter data', @@ -26,6 +27,10 @@ const DRAW_BOUNDS_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLa defaultMessage: 'Draw bounds to filter data', }); +const DRAW_DISTANCE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawDistanceLabel', { + defaultMessage: 'Draw distance to filter data', +}); + const DRAW_SHAPE_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabelShort', { defaultMessage: 'Draw shape', }); @@ -34,6 +39,13 @@ const DRAW_BOUNDS_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawBo defaultMessage: 'Draw bounds', }); +const DRAW_DISTANCE_LABEL_SHORT = i18n.translate( + 'xpack.maps.toolbarOverlay.drawDistanceLabelShort', + { + defaultMessage: 'Draw distance', + } +); + export class ToolsControl extends Component { state = { isPopoverOpen: false, @@ -65,23 +77,43 @@ export class ToolsControl extends Component { this._closePopover(); }; + _initiateDistanceDraw = options => { + this.props.initiateDraw({ + drawType: DRAW_TYPE.DISTANCE, + ...options, + }); + this._closePopover(); + }; + _getDrawPanels() { + const tools = [ + { + name: DRAW_SHAPE_LABEL, + panel: 1, + }, + { + name: DRAW_BOUNDS_LABEL, + panel: 2, + }, + ]; + + const hasGeoPoints = this.props.geoFields.some(({ geoFieldType }) => { + return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; + }); + if (hasGeoPoints) { + tools.push({ + name: DRAW_DISTANCE_LABEL, + panel: 3, + }); + } + return [ { id: 0, title: i18n.translate('xpack.maps.toolbarOverlay.tools.toolbarTitle', { defaultMessage: 'Tools', }), - items: [ - { - name: DRAW_SHAPE_LABEL, - panel: 1, - }, - { - name: DRAW_BOUNDS_LABEL, - panel: 2, - }, - ], + items: tools, }, { id: 1, @@ -119,6 +151,20 @@ export class ToolsControl extends Component { /> ), }, + { + id: 3, + title: DRAW_DISTANCE_LABEL_SHORT, + content: ( + { + return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; + })} + onSubmit={this._initiateDistanceDraw} + /> + ), + }, ]; } diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js index 9b33d3036785..79467e26ec3f 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js @@ -344,6 +344,39 @@ function createGeometryFilterWithMeta({ return createGeoPolygonFilter(geometry.coordinates, geoFieldName, { meta }); } +export function createDistanceFilterWithMeta({ + alias, + distanceKm, + geoFieldName, + indexPatternId, + point, +}) { + const meta = { + type: SPATIAL_FILTER_TYPE, + negate: false, + index: indexPatternId, + key: geoFieldName, + alias: alias + ? alias + : i18n.translate('xpack.maps.es_geo_utils.distanceFilterAlias', { + defaultMessage: '{geoFieldName} within {distanceKm}km of {pointLabel}', + values: { + distanceKm, + geoFieldName, + pointLabel: point.join(','), + }, + }), + }; + + return { + geo_distance: { + distance: `${distanceKm}km`, + [geoFieldName]: point, + }, + meta, + }; +} + export function roundCoordinates(coordinates) { for (let i = 0; i < coordinates.length; i++) { const value = coordinates[i]; diff --git a/x-pack/package.json b/x-pack/package.json index b9c4f7c554e9..3c8aa435c3e4 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -196,6 +196,7 @@ "@scant/router": "^0.1.0", "@slack/webhook": "^5.0.0", "@turf/boolean-contains": "6.0.1", + "@turf/circle": "6.0.1", "angular": "^1.7.9", "angular-resource": "1.7.9", "angular-sanitize": "1.7.9", diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index ae3e164ffb2b..b1483cefa43b 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -120,6 +120,7 @@ export const EMPTY_FEATURE_COLLECTION = { export const DRAW_TYPE = { BOUNDS: 'BOUNDS', + DISTANCE: 'DISTANCE', POLYGON: 'POLYGON', }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3763020a0b69..ad49f0242e8e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7073,7 +7073,6 @@ "xpack.maps.feature.appDescription": "Elasticsearch と Elastic Maps Service の地理空間データを閲覧します", "xpack.maps.featureRegistry.mapsFeatureName": "マップ", "xpack.maps.geoGrid.resolutionLabel": "グリッド解像度", - "xpack.maps.geometryFilterForm.geoFieldLabel": "フィルタリングされたフィールド", "xpack.maps.geometryFilterForm.geometryLabelLabel": "ジオメトリラベル", "xpack.maps.geometryFilterForm.relationLabel": "空間関係", "xpack.maps.heatmap.colorRampLabel": "色の範囲", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0477470a4b8a..76ecf333eb10 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7073,7 +7073,6 @@ "xpack.maps.feature.appDescription": "从 Elasticsearch 和 Elastic 地图服务浏览地理空间数据", "xpack.maps.featureRegistry.mapsFeatureName": "Maps", "xpack.maps.geoGrid.resolutionLabel": "网格分辨率", - "xpack.maps.geometryFilterForm.geoFieldLabel": "已筛选字段", "xpack.maps.geometryFilterForm.geometryLabelLabel": "几何标签", "xpack.maps.geometryFilterForm.relationLabel": "空间关系", "xpack.maps.heatmap.colorRampLabel": "颜色范围", diff --git a/yarn.lock b/yarn.lock index ae55508cee88..dcb360587e4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4086,6 +4086,22 @@ "@turf/helpers" "6.x" "@turf/invariant" "6.x" +"@turf/circle@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/circle/-/circle-6.0.1.tgz#0ab72083373ae3c76b700c17a504ab1b5c0910b9" + integrity sha512-pF9XsYtCvY9ZyNqJ3hFYem9VaiGdVNQb0SFq/zzDMwH3iWZPPJQHnnDB/3e8RD1VDtBBov9p5uO2k7otsfezjw== + dependencies: + "@turf/destination" "6.x" + "@turf/helpers" "6.x" + +"@turf/destination@6.x": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/destination/-/destination-6.0.1.tgz#5275887fa96ec463f44864a2c17f0b712361794a" + integrity sha512-MroK4nRdp7as174miCAugp8Uvorhe6rZ7MJiC9Hb4+hZR7gNFJyVKmkdDDXIoCYs6MJQsx0buI+gsCpKwgww0Q== + dependencies: + "@turf/helpers" "6.x" + "@turf/invariant" "6.x" + "@turf/helpers@6.x": version "6.1.4" resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.1.4.tgz#d6fd7ebe6782dd9c87dca5559bda5c48ae4c3836" From dccfa593dc3c0e2d95dc77709647e6313ac9c1b2 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Mon, 16 Mar 2020 15:37:42 -0400 Subject: [PATCH 017/115] Embeddable API cleanup (#60207) * wip * Remove test in legacy functional plugin --- .../hello_world_embeddable_factory.ts | 2 +- .../public/list_container/list_container.tsx | 7 +- .../list_container/list_container_factory.ts | 13 ++-- .../multi_task_todo_embeddable_factory.ts | 2 +- examples/embeddable_examples/public/plugin.ts | 47 +++++++------ .../searchable_list_container.tsx | 7 +- .../searchable_list_container_factory.ts | 13 ++-- .../public/todo/todo_embeddable_factory.tsx | 11 ++- examples/embeddable_explorer/public/app.tsx | 4 +- .../public/embeddable_panel_example.tsx | 7 +- .../public/hello_world_embeddable_example.tsx | 4 +- .../public/list_container_example.tsx | 7 +- .../embeddable_explorer/public/plugin.tsx | 4 +- .../public/todo_embeddable_example.tsx | 4 +- .../public/dashboard/np_ready/application.ts | 4 +- .../kibana/public/dashboard/plugin.ts | 6 +- .../embeddable/search_embeddable_factory.ts | 19 +++-- .../kibana/public/discover/plugin.ts | 31 ++++---- .../public/visualize/kibana_services.ts | 4 +- .../public/visualize/np_ready/types.d.ts | 4 +- .../kibana/public/visualize/plugin.ts | 6 +- .../visualize_embeddable_factory.tsx | 5 +- .../public/np_ready/public/mocks.ts | 2 +- .../public/np_ready/public/plugin.ts | 4 +- .../ui/public/new_platform/new_platform.ts | 6 +- .../actions/expand_panel_action.test.tsx | 3 +- .../actions/open_replace_panel_flyout.tsx | 4 +- .../actions/replace_panel_action.test.tsx | 3 +- .../public/actions/replace_panel_action.tsx | 4 +- .../public/actions/replace_panel_flyout.tsx | 4 +- .../public/embeddable/dashboard_container.tsx | 4 +- .../dashboard_container_factory.tsx | 48 ++++++------- .../embeddable/grid/dashboard_grid.test.tsx | 4 +- src/plugins/dashboard/public/plugin.tsx | 70 +++++++++++-------- .../public/api/get_embeddable_factories.ts | 26 ------- .../public/api/get_embeddable_factory.ts | 34 --------- src/plugins/embeddable/public/api/index.ts | 47 ------------- .../public/api/register_embeddable_factory.ts | 32 --------- .../embeddable/public/api/tests/helpers.ts | 27 ------- src/plugins/embeddable/public/api/types.ts | 43 ------------ src/plugins/embeddable/public/index.ts | 4 +- .../lib/actions/edit_panel_action.test.tsx | 12 ++-- .../public/lib/actions/edit_panel_action.ts | 5 +- .../public/lib/containers/container.ts | 4 +- .../embeddable_child_panel.test.tsx | 5 +- .../lib/containers/embeddable_child_panel.tsx | 6 +- .../lib/embeddables/embeddable_factory.ts | 6 +- .../embeddable_factory_renderer.test.tsx | 8 +-- .../embeddable_factory_renderer.tsx | 4 +- .../lib/panel/embeddable_panel.test.tsx | 4 +- .../public/lib/panel/embeddable_panel.tsx | 7 +- .../add_panel/add_panel_action.test.tsx | 6 +- .../add_panel/add_panel_action.ts | 7 +- .../add_panel/add_panel_flyout.test.tsx | 9 +-- .../add_panel/add_panel_flyout.tsx | 6 +- .../add_panel/open_add_panel_flyout.tsx | 6 +- .../customize_panel_action.test.ts | 3 +- .../inspect_panel_action.test.tsx | 6 +- .../remove_panel_action.test.tsx | 7 +- .../contact_card_embeddable_factory.tsx | 2 +- .../slow_contact_card_embeddable_factory.ts | 2 +- .../embeddables/filterable_container.tsx | 4 +- .../filterable_container_factory.ts | 6 +- .../filterable_embeddable_factory.ts | 2 +- .../embeddables/hello_world_container.tsx | 6 +- .../hello_world_container_component.tsx | 6 +- src/plugins/embeddable/public/lib/types.ts | 4 -- src/plugins/embeddable/public/mocks.ts | 7 +- .../tests/registry.test.ts => plugin.test.ts} | 16 +++-- src/plugins/embeddable/public/plugin.ts | 67 +++++++++++++----- .../public/tests/apply_filter_action.test.ts | 14 ++-- .../embeddable/public/tests/container.test.ts | 12 ++-- .../tests/customize_panel_modal.test.tsx | 8 +-- .../embeddable/public/tests/test_plugin.ts | 6 +- .../public/np_ready/public/app/app.tsx | 9 +-- .../app/dashboard_container_example.tsx | 16 +++-- .../hello_world_embeddable_factory.ts | 28 -------- .../public/np_ready/public/plugin.tsx | 30 ++------ .../dashboard_container.js | 14 ---- .../embeddable/embeddable_factory.ts | 42 ++++++----- .../editor_frame_service/service.test.tsx | 11 ++- .../public/editor_frame_service/service.tsx | 40 ++++++----- x-pack/legacy/plugins/lens/public/plugin.tsx | 6 +- .../embeddables/embedded_map_helpers.tsx | 11 +-- .../public/components/embeddables/types.ts | 6 -- x-pack/legacy/plugins/siem/public/plugin.tsx | 4 +- .../advanced_ui_actions/public/plugin.ts | 8 +-- .../test_helpers/time_range_container.ts | 4 +- .../time_range_embeddable_factory.ts | 2 +- .../public/embeddables/resolver/factory.ts | 2 +- x-pack/plugins/endpoint/public/plugin.ts | 4 +- .../plugins/resolver_test/public/plugin.ts | 8 ++- 92 files changed, 451 insertions(+), 647 deletions(-) delete mode 100644 src/plugins/embeddable/public/api/get_embeddable_factories.ts delete mode 100644 src/plugins/embeddable/public/api/get_embeddable_factory.ts delete mode 100644 src/plugins/embeddable/public/api/index.ts delete mode 100644 src/plugins/embeddable/public/api/register_embeddable_factory.ts delete mode 100644 src/plugins/embeddable/public/api/tests/helpers.ts delete mode 100644 src/plugins/embeddable/public/api/types.ts rename src/plugins/embeddable/public/{api/tests/registry.test.ts => plugin.test.ts} (70%) delete mode 100644 test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddables/hello_world_embeddable_factory.ts diff --git a/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts b/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts index de5a3d9380de..2995c99ac9e5 100644 --- a/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts +++ b/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts @@ -33,7 +33,7 @@ export class HelloWorldEmbeddableFactory extends EmbeddableFactory { * embeddables should check the UI Capabilities service to be sure of * the right permissions. */ - public isEditable() { + public async isEditable() { return true; } diff --git a/examples/embeddable_examples/public/list_container/list_container.tsx b/examples/embeddable_examples/public/list_container/list_container.tsx index 35a674a03573..bbbd0d6e3230 100644 --- a/examples/embeddable_examples/public/list_container/list_container.tsx +++ b/examples/embeddable_examples/public/list_container/list_container.tsx @@ -21,7 +21,7 @@ import ReactDOM from 'react-dom'; import { Container, ContainerInput, - GetEmbeddableFactory, + EmbeddableStart, } from '../../../../src/plugins/embeddable/public'; import { ListContainerComponent } from './list_container_component'; @@ -31,7 +31,10 @@ export class ListContainer extends Container<{}, ContainerInput> { public readonly type = LIST_CONTAINER; private node?: HTMLElement; - constructor(input: ContainerInput, getEmbeddableFactory: GetEmbeddableFactory) { + constructor( + input: ContainerInput, + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'] + ) { super(input, { embeddableLoaded: {} }, getEmbeddableFactory); } diff --git a/examples/embeddable_examples/public/list_container/list_container_factory.ts b/examples/embeddable_examples/public/list_container/list_container_factory.ts index de6b7d5f5e50..247cf48b41bd 100644 --- a/examples/embeddable_examples/public/list_container/list_container_factory.ts +++ b/examples/embeddable_examples/public/list_container/list_container_factory.ts @@ -20,25 +20,30 @@ import { i18n } from '@kbn/i18n'; import { EmbeddableFactory, - GetEmbeddableFactory, ContainerInput, + EmbeddableStart, } from '../../../../src/plugins/embeddable/public'; import { LIST_CONTAINER, ListContainer } from './list_container'; +interface StartServices { + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; +} + export class ListContainerFactory extends EmbeddableFactory { public readonly type = LIST_CONTAINER; public readonly isContainerType = true; - constructor(private getEmbeddableFactory: GetEmbeddableFactory) { + constructor(private getStartServices: () => Promise) { super(); } - public isEditable() { + public async isEditable() { return true; } public async create(initialInput: ContainerInput) { - return new ListContainer(initialInput, this.getEmbeddableFactory); + const { getEmbeddableFactory } = await this.getStartServices(); + return new ListContainer(initialInput, getEmbeddableFactory); } public getDisplayName() { diff --git a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts index a54201b157a6..9afdeabaee76 100644 --- a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts +++ b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts @@ -32,7 +32,7 @@ export class MultiTaskTodoEmbeddableFactory extends EmbeddableFactory< > { public readonly type = MULTI_TASK_TODO_EMBEDDABLE; - public isEditable() { + public async isEditable() { return true; } diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index b7a4f5c078d5..3663af68ae2c 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -17,20 +17,11 @@ * under the License. */ -import { - IEmbeddableSetup, - IEmbeddableStart, - EmbeddableFactory, -} from '../../../src/plugins/embeddable/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public'; import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE } from './hello_world'; import { TODO_EMBEDDABLE, TodoEmbeddableFactory, TodoInput, TodoOutput } from './todo'; -import { - MULTI_TASK_TODO_EMBEDDABLE, - MultiTaskTodoEmbeddableFactory, - MultiTaskTodoOutput, - MultiTaskTodoInput, -} from './multi_task_todo'; +import { MULTI_TASK_TODO_EMBEDDABLE, MultiTaskTodoEmbeddableFactory } from './multi_task_todo'; import { SEARCHABLE_LIST_CONTAINER, SearchableListContainerFactory, @@ -38,46 +29,56 @@ import { import { LIST_CONTAINER, ListContainerFactory } from './list_container'; interface EmbeddableExamplesSetupDependencies { - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; } interface EmbeddableExamplesStartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; } export class EmbeddableExamplesPlugin implements Plugin { - public setup(core: CoreSetup, deps: EmbeddableExamplesSetupDependencies) { + public setup( + core: CoreSetup, + deps: EmbeddableExamplesSetupDependencies + ) { deps.embeddable.registerEmbeddableFactory( HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory() ); - deps.embeddable.registerEmbeddableFactory< - EmbeddableFactory - >(MULTI_TASK_TODO_EMBEDDABLE, new MultiTaskTodoEmbeddableFactory()); - } + deps.embeddable.registerEmbeddableFactory( + MULTI_TASK_TODO_EMBEDDABLE, + new MultiTaskTodoEmbeddableFactory() + ); - public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) { // These are registered in the start method because `getEmbeddableFactory ` // is only available in start. We could reconsider this I think and make it // available in both. deps.embeddable.registerEmbeddableFactory( SEARCHABLE_LIST_CONTAINER, - new SearchableListContainerFactory(deps.embeddable.getEmbeddableFactory) + new SearchableListContainerFactory(async () => ({ + getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + })) ); deps.embeddable.registerEmbeddableFactory( LIST_CONTAINER, - new ListContainerFactory(deps.embeddable.getEmbeddableFactory) + new ListContainerFactory(async () => ({ + getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + })) ); - deps.embeddable.registerEmbeddableFactory>( + deps.embeddable.registerEmbeddableFactory( TODO_EMBEDDABLE, - new TodoEmbeddableFactory(core.overlays.openModal) + new TodoEmbeddableFactory(async () => ({ + openModal: (await core.getStartServices())[0].overlays.openModal, + })) ); } + public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) {} + public stop() {} } diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx index 3079abb867c3..06462937c768 100644 --- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx +++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx @@ -21,7 +21,7 @@ import ReactDOM from 'react-dom'; import { Container, ContainerInput, - GetEmbeddableFactory, + EmbeddableStart, EmbeddableInput, } from '../../../../src/plugins/embeddable/public'; import { SearchableListContainerComponent } from './searchable_list_container_component'; @@ -40,7 +40,10 @@ export class SearchableListContainer extends Container Promise) { super(); } - public isEditable() { + public async isEditable() { return true; } public async create(initialInput: SearchableContainerInput) { - return new SearchableListContainer(initialInput, this.getEmbeddableFactory); + const { getEmbeddableFactory } = await this.getStartServices(); + return new SearchableListContainer(initialInput, getEmbeddableFactory); } public getDisplayName() { diff --git a/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx b/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx index dd2168bb39ee..d7be43690538 100644 --- a/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx +++ b/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx @@ -43,6 +43,10 @@ function TaskInput({ onSave }: { onSave: (task: string) => void }) { ); } +interface StartServices { + openModal: OverlayStart['openModal']; +} + export class TodoEmbeddableFactory extends EmbeddableFactory< TodoInput, TodoOutput, @@ -50,11 +54,11 @@ export class TodoEmbeddableFactory extends EmbeddableFactory< > { public readonly type = TODO_EMBEDDABLE; - constructor(private openModal: OverlayStart['openModal']) { + constructor(private getStartServices: () => Promise) { super(); } - public isEditable() { + public async isEditable() { return true; } @@ -69,9 +73,10 @@ export class TodoEmbeddableFactory extends EmbeddableFactory< * in this case, the task string. */ public async getExplicitInput() { + const { openModal } = await this.getStartServices(); return new Promise<{ task: string }>(resolve => { const onSave = (task: string) => resolve({ task }); - const overlay = this.openModal( + const overlay = openModal( toMountPoint( { diff --git a/examples/embeddable_explorer/public/app.tsx b/examples/embeddable_explorer/public/app.tsx index da7e8cc188e3..9c8568454855 100644 --- a/examples/embeddable_explorer/public/app.tsx +++ b/examples/embeddable_explorer/public/app.tsx @@ -23,7 +23,7 @@ import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from import { EuiPage, EuiPageSideBar, EuiSideNav } from '@elastic/eui'; -import { IEmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from '../../../src/plugins/inspector/public'; import { @@ -74,7 +74,7 @@ const Nav = withRouter(({ history, navigateToApp, pages }: NavProps) => { interface Props { basename: string; navigateToApp: CoreStart['application']['navigateToApp']; - embeddableApi: IEmbeddableStart; + embeddableApi: EmbeddableStart; uiActionsApi: UiActionsStart; overlays: OverlayStart; notifications: CoreStart['notifications']; diff --git a/examples/embeddable_explorer/public/embeddable_panel_example.tsx b/examples/embeddable_explorer/public/embeddable_panel_example.tsx index e6687d8563f5..b26111bed7ff 100644 --- a/examples/embeddable_explorer/public/embeddable_panel_example.tsx +++ b/examples/embeddable_explorer/public/embeddable_panel_example.tsx @@ -31,9 +31,8 @@ import { import { EuiSpacer } from '@elastic/eui'; import { OverlayStart, CoreStart, SavedObjectsStart, IUiSettingsClient } from 'kibana/public'; import { - GetEmbeddableFactory, EmbeddablePanel, - IEmbeddableStart, + EmbeddableStart, IEmbeddable, } from '../../../src/plugins/embeddable/public'; import { @@ -47,8 +46,8 @@ import { Start as InspectorStartContract } from '../../../src/plugins/inspector/ import { getSavedObjectFinder } from '../../../src/plugins/saved_objects/public'; interface Props { - getAllEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories']; - getEmbeddableFactory: GetEmbeddableFactory; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; uiActionsApi: UiActionsStart; overlays: OverlayStart; notifications: CoreStart['notifications']; diff --git a/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx b/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx index 74a6766a1b5e..ea1c3d781ebf 100644 --- a/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx +++ b/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx @@ -29,14 +29,14 @@ import { EuiText, } from '@elastic/eui'; import { - GetEmbeddableFactory, + EmbeddableStart, EmbeddableFactoryRenderer, EmbeddableRoot, } from '../../../src/plugins/embeddable/public'; import { HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE } from '../../embeddable_examples/public'; interface Props { - getEmbeddableFactory: GetEmbeddableFactory; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; } export function HelloWorldEmbeddableExample({ getEmbeddableFactory }: Props) { diff --git a/examples/embeddable_explorer/public/list_container_example.tsx b/examples/embeddable_explorer/public/list_container_example.tsx index 2c7b12a27d96..969fdb0ca46d 100644 --- a/examples/embeddable_explorer/public/list_container_example.tsx +++ b/examples/embeddable_explorer/public/list_container_example.tsx @@ -29,10 +29,7 @@ import { EuiText, } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; -import { - GetEmbeddableFactory, - EmbeddableFactoryRenderer, -} from '../../../src/plugins/embeddable/public'; +import { EmbeddableFactoryRenderer, EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { HELLO_WORLD_EMBEDDABLE, TODO_EMBEDDABLE, @@ -42,7 +39,7 @@ import { } from '../../embeddable_examples/public'; interface Props { - getEmbeddableFactory: GetEmbeddableFactory; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; } export function ListContainerExample({ getEmbeddableFactory }: Props) { diff --git a/examples/embeddable_explorer/public/plugin.tsx b/examples/embeddable_explorer/public/plugin.tsx index 1294e0c89c9e..7c75b108d991 100644 --- a/examples/embeddable_explorer/public/plugin.tsx +++ b/examples/embeddable_explorer/public/plugin.tsx @@ -19,12 +19,12 @@ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; import { UiActionsService } from '../../../src/plugins/ui_actions/public'; -import { IEmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; interface StartDeps { uiActions: UiActionsService; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; inspector: InspectorStart; } diff --git a/examples/embeddable_explorer/public/todo_embeddable_example.tsx b/examples/embeddable_explorer/public/todo_embeddable_example.tsx index b1c93087faf8..ce92301236c2 100644 --- a/examples/embeddable_explorer/public/todo_embeddable_example.tsx +++ b/examples/embeddable_explorer/public/todo_embeddable_example.tsx @@ -39,10 +39,10 @@ import { TODO_EMBEDDABLE, TodoEmbeddableFactory, } from '../../../examples/embeddable_examples/public/todo'; -import { GetEmbeddableFactory, EmbeddableRoot } from '../../../src/plugins/embeddable/public'; +import { EmbeddableStart, EmbeddableRoot } from '../../../src/plugins/embeddable/public'; interface Props { - getEmbeddableFactory: GetEmbeddableFactory; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; } interface State { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index 9ca84735cac1..fe0e7a1d3e6d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -39,7 +39,7 @@ import { } from '../legacy_imports'; // @ts-ignore import { initDashboardApp } from './legacy_app'; -import { IEmbeddableStart } from '../../../../../../plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../plugins/navigation/public'; import { DataPublicPluginStart } from '../../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../../plugins/share/public'; @@ -67,7 +67,7 @@ export interface RenderDeps { chrome: ChromeStart; addBasePath: (path: string) => string; savedQueryService: DataPublicPluginStart['query']['savedQueries']; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; localStorage: Storage; share: SharePluginStart; config: KibanaLegacyStart['config']; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index d94612225782..a9ee77921ed4 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -35,7 +35,7 @@ import { DataPublicPluginSetup, esFilters, } from '../../../../../plugins/data/public'; -import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { DashboardConstants } from './np_ready/dashboard_constants'; @@ -54,7 +54,7 @@ import { createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public' export interface DashboardPluginStartDependencies { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; share: SharePluginStart; kibanaLegacy: KibanaLegacyStart; @@ -70,7 +70,7 @@ export class DashboardPlugin implements Plugin { private startDependencies: { data: DataPublicPluginStart; savedObjectsClient: SavedObjectsClientContract; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; share: SharePluginStart; dashboardConfig: KibanaLegacyStart['dashboardConfig']; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts index 90f1549c9f36..6f3adc1f4fcc 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts @@ -32,6 +32,11 @@ import { SearchEmbeddable } from './search_embeddable'; import { SearchInput, SearchOutput } from './types'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; +interface StartServices { + executeTriggerActions: UiActionsStart['executeTriggerActions']; + isEditable: () => boolean; +} + export class SearchEmbeddableFactory extends EmbeddableFactory< SearchInput, SearchOutput, @@ -40,12 +45,10 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< public readonly type = SEARCH_EMBEDDABLE_TYPE; private $injector: auto.IInjectorService | null; private getInjector: () => Promise | null; - public isEditable: () => boolean; constructor( - private readonly executeTriggerActions: UiActionsStart['executeTriggerActions'], - getInjector: () => Promise, - isEditable: () => boolean + private getStartServices: () => Promise, + getInjector: () => Promise ) { super({ savedObjectMetaData: { @@ -58,13 +61,16 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< }); this.$injector = null; this.getInjector = getInjector; - this.isEditable = isEditable; } public canCreateNew() { return false; } + public async isEditable() { + return (await this.getStartServices()).isEditable(); + } + public getDisplayName() { return i18n.translate('kbn.embeddable.search.displayName', { defaultMessage: 'search', @@ -90,6 +96,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< try { const savedObject = await getServices().getSavedSearchById(savedObjectId); const indexPattern = savedObject.searchSource.getField('index'); + const { executeTriggerActions } = await this.getStartServices(); return new SearchEmbeddable( { savedSearch: savedObject, @@ -101,7 +108,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< indexPatterns: indexPattern ? [indexPattern] : [], }, input, - this.executeTriggerActions, + executeTriggerActions, parent ); } catch (e) { diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 3ba0418d35f7..ba671a64592a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -30,7 +30,7 @@ import { } from '../../../../../plugins/data/public'; import { registerFeature } from './np_ready/register_feature'; import './kibana_services'; -import { IEmbeddableStart, IEmbeddableSetup } from '../../../../../plugins/embeddable/public'; +import { EmbeddableStart, EmbeddableSetup } from '../../../../../plugins/embeddable/public'; import { getInnerAngularModule, getInnerAngularModuleEmbeddable } from './get_inner_angular'; import { setAngularModule, setServices } from './kibana_services'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; @@ -63,7 +63,7 @@ export interface DiscoverSetup { export type DiscoverStart = void; export interface DiscoverSetupPlugins { uiActions: UiActionsSetup; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; kibanaLegacy: KibanaLegacySetup; home: HomePublicPluginSetup; visualizations: VisualizationsSetup; @@ -71,7 +71,7 @@ export interface DiscoverSetupPlugins { } export interface DiscoverStartPlugins { uiActions: UiActionsStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; charts: ChartsPluginStart; data: DataPublicPluginStart; @@ -103,7 +103,7 @@ export class DiscoverPlugin implements Plugin { public initializeInnerAngular?: () => void; public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; - setup(core: CoreSetup, plugins: DiscoverSetupPlugins): DiscoverSetup { + setup(core: CoreSetup, plugins: DiscoverSetupPlugins): DiscoverSetup { const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), defaultSubUrl: '#/discover', @@ -173,6 +173,7 @@ export class DiscoverPlugin implements Plugin { }); registerFeature(plugins.home); + this.registerEmbeddable(core, plugins); return { addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), }; @@ -203,8 +204,6 @@ export class DiscoverPlugin implements Plugin { return { core, plugins }; }; - - this.registerEmbeddable(core, plugins); } stop() { @@ -216,19 +215,25 @@ export class DiscoverPlugin implements Plugin { /** * register embeddable with a slimmer embeddable version of inner angular */ - private async registerEmbeddable(core: CoreStart, plugins: DiscoverStartPlugins) { + private async registerEmbeddable( + core: CoreSetup, + plugins: DiscoverSetupPlugins + ) { const { SearchEmbeddableFactory } = await import('./np_ready/embeddable'); - const isEditable = () => core.application.capabilities.discover.save as boolean; if (!this.getEmbeddableInjector) { throw Error('Discover plugin method getEmbeddableInjector is undefined'); } - const factory = new SearchEmbeddableFactory( - plugins.uiActions.executeTriggerActions, - this.getEmbeddableInjector, - isEditable - ); + const getStartServices = async () => { + const [coreStart, deps] = await core.getStartServices(); + return { + executeTriggerActions: deps.uiActions.executeTriggerActions, + isEditable: () => coreStart.application.capabilities.discover.save as boolean, + }; + }; + + const factory = new SearchEmbeddableFactory(getStartServices, this.getEmbeddableInjector); plugins.embeddable.registerEmbeddableFactory(factory.type, factory); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index cfd12b328345..7e96d7bde6e1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -29,7 +29,7 @@ import { import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; -import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; import { SharePluginStart } from '../../../../../plugins/share/public'; import { DataPublicPluginStart, IndexPatternsContract } from '../../../../../plugins/data/public'; import { VisualizationsStart } from '../../../visualizations/public'; @@ -44,7 +44,7 @@ export interface VisualizeKibanaServices { chrome: ChromeStart; core: CoreStart; data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; getBasePath: () => string; indexPatterns: IndexPatternsContract; localStorage: Storage; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index ccb3b3ddbb1d..01ce872aeb67 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -24,7 +24,7 @@ import { DataPublicPluginStart, SavedQuery, } from 'src/plugins/data/public'; -import { IEmbeddableStart } from 'src/plugins/embeddable/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { PersistedState } from 'src/plugins/visualizations/public'; import { LegacyCoreStart } from 'kibana/public'; import { Vis } from 'src/legacy/core_plugins/visualizations/public'; @@ -61,7 +61,7 @@ export interface EditorRenderProps { appState: { save(): void }; core: LegacyCoreStart; data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; filters: Filter[]; uiState: PersistedState; timeRange: TimeRange; diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index b9e4487ae84f..9d88152c59aa 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -36,7 +36,7 @@ import { DataPublicPluginSetup, esFilters, } from '../../../../../plugins/data/public'; -import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { SharePluginStart } from '../../../../../plugins/share/public'; import { @@ -55,7 +55,7 @@ import { DefaultEditorController } from '../../../vis_default_editor/public'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; share: SharePluginStart; visualizations: VisualizationsStart; @@ -71,7 +71,7 @@ export interface VisualizePluginSetupDependencies { export class VisualizePlugin implements Plugin { private startDependencies: { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; savedObjectsClient: SavedObjectsClientContract; share: SharePluginStart; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable_factory.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable_factory.tsx index 237eeff42d9b..1cd97115ee10 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable_factory.tsx @@ -83,7 +83,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< }); } - public isEditable() { + public async isEditable() { return getCapabilities().visualize.save as boolean; } @@ -114,13 +114,14 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< const indexPattern = await getIndexPattern(savedObject); const indexPatterns = indexPattern ? [indexPattern] : []; + const editable = await this.isEditable(); return new VisualizeEmbeddable( getTimeFilter(), { savedVisualization: savedObject, indexPatterns, editUrl, - editable: this.isEditable(), + editable, appState: input.appState, uiState: input.uiState, }, diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts index 2785247296ff..4ee727e46f4d 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts @@ -49,7 +49,7 @@ const createInstance = async () => { const setup = plugin.setup(coreMock.createSetup(), { data: dataPluginMock.createSetupContract(), expressions: expressionsPluginMock.createSetupContract(), - embeddable: embeddablePluginMock.createStartContract(), + embeddable: embeddablePluginMock.createSetupContract(), usageCollection: usageCollectionPluginMock.createSetupContract(), }); const doStart = () => diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts index 5a8a55d47054..953caecefb97 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts @@ -42,7 +42,7 @@ import { } from './services'; import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeEmbeddableFactory } from './embeddable'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../../plugins/expressions/public'; -import { IEmbeddableSetup } from '../../../../../../plugins/embeddable/public'; +import { EmbeddableSetup } from '../../../../../../plugins/embeddable/public'; import { visualization as visualizationFunction } from './expressions/visualization_function'; import { visualization as visualizationRenderer } from './expressions/visualization_renderer'; import { @@ -73,7 +73,7 @@ export interface VisualizationsStart extends TypesStart { export interface VisualizationsSetupDeps { expressions: ExpressionsSetup; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; usageCollection: UsageCollectionSetup; data: DataPublicPluginSetup; } diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index ce4e1b055188..07e17ad56229 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -20,7 +20,7 @@ import { IScope } from 'angular'; import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; -import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public'; +import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public'; import { createBrowserHistory } from 'history'; import { LegacyCoreSetup, @@ -68,7 +68,7 @@ export interface PluginsSetup { bfetch: BfetchPublicSetup; charts: ChartsPluginSetup; data: ReturnType; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; expressions: ReturnType; home: HomePublicPluginSetup; inspector: InspectorSetup; @@ -88,7 +88,7 @@ export interface PluginsStart { bfetch: BfetchPublicStart; charts: ChartsPluginStart; data: ReturnType; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; expressions: ReturnType; inspector: InspectorStart; uiActions: UiActionsStart; diff --git a/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx b/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx index f8c05170e8f6..22cf854a4662 100644 --- a/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx +++ b/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx @@ -28,7 +28,6 @@ import { ContactCardEmbeddableInput, ContactCardEmbeddableOutput, } from '../embeddable_plugin_test_samples'; -import { DashboardOptions } from '../embeddable/dashboard_container_factory'; const embeddableFactories = new Map(); embeddableFactories.set( @@ -40,7 +39,7 @@ let container: DashboardContainer; let embeddable: ContactCardEmbeddable; beforeEach(async () => { - const options: DashboardOptions = { + const options = { ExitFullScreenButton: () => null, SavedObjectFinder: () => null, application: {} as any, diff --git a/src/plugins/dashboard/public/actions/open_replace_panel_flyout.tsx b/src/plugins/dashboard/public/actions/open_replace_panel_flyout.tsx index f15d538703e2..3472d208f814 100644 --- a/src/plugins/dashboard/public/actions/open_replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/actions/open_replace_panel_flyout.tsx @@ -24,7 +24,7 @@ import { IEmbeddable, EmbeddableInput, EmbeddableOutput, - IEmbeddableStart, + EmbeddableStart, IContainer, } from '../embeddable_plugin'; @@ -34,7 +34,7 @@ export async function openReplacePanelFlyout(options: { savedObjectFinder: React.ComponentType; notifications: CoreStart['notifications']; panelToRemove: IEmbeddable; - getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; }) { const { embeddable, diff --git a/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx b/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx index 4438a6c99712..69346dc8c118 100644 --- a/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx +++ b/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx @@ -27,7 +27,6 @@ import { ContactCardEmbeddableInput, ContactCardEmbeddableOutput, } from '../embeddable_plugin_test_samples'; -import { DashboardOptions } from '../embeddable/dashboard_container_factory'; import { coreMock } from '../../../../core/public/mocks'; import { CoreStart } from 'kibana/public'; @@ -43,7 +42,7 @@ let embeddable: ContactCardEmbeddable; let coreStart: CoreStart; beforeEach(async () => { coreStart = coreMock.createStart(); - const options: DashboardOptions = { + const options = { ExitFullScreenButton: () => null, SavedObjectFinder: () => null, application: {} as any, diff --git a/src/plugins/dashboard/public/actions/replace_panel_action.tsx b/src/plugins/dashboard/public/actions/replace_panel_action.tsx index 26d9c5c8ad4d..21ec961917d1 100644 --- a/src/plugins/dashboard/public/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard/public/actions/replace_panel_action.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from '../../../../core/public'; -import { IEmbeddable, ViewMode, IEmbeddableStart } from '../embeddable_plugin'; +import { IEmbeddable, ViewMode, EmbeddableStart } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; import { ActionByType, IncompatibleActionError } from '../ui_actions_plugin'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; @@ -43,7 +43,7 @@ export class ReplacePanelAction implements ActionByType, private notifications: CoreStart['notifications'], - private getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories'] + private getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'] ) {} public getDisplayName({ embeddable }: ReplacePanelActionContext) { diff --git a/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx index 670105650f95..a1cd865f771d 100644 --- a/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; -import { GetEmbeddableFactories } from 'src/plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { DashboardPanelState } from '../embeddable'; import { NotificationsStart, Toast } from '../../../../core/public'; import { IContainer, IEmbeddable, EmbeddableInput, EmbeddableOutput } from '../embeddable_plugin'; @@ -31,7 +31,7 @@ interface Props { onClose: () => void; notifications: NotificationsStart; panelToRemove: IEmbeddable; - getEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; } export class ReplacePanelFlyout extends React.Component { diff --git a/src/plugins/dashboard/public/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/embeddable/dashboard_container.tsx index f9443ab97416..86a6e374d3e2 100644 --- a/src/plugins/dashboard/public/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/embeddable/dashboard_container.tsx @@ -30,7 +30,7 @@ import { ViewMode, EmbeddableFactory, IEmbeddable, - IEmbeddableStart, + EmbeddableStart, } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; import { createPanelState } from './panel'; @@ -77,7 +77,7 @@ export interface DashboardContainerOptions { application: CoreStart['application']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; inspector: InspectorStartContract; SavedObjectFinder: React.ComponentType; ExitFullScreenButton: React.ComponentType; diff --git a/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx b/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx index a358e41f7b50..0fa62fc87560 100644 --- a/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx +++ b/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx @@ -18,24 +18,29 @@ */ import { i18n } from '@kbn/i18n'; -import { SavedObjectMetaData } from '../../../saved_objects/public'; -import { SavedObjectAttributes } from '../../../../core/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { CoreStart } from '../../../../core/public'; import { ContainerOutput, EmbeddableFactory, ErrorEmbeddable, Container, } from '../embeddable_plugin'; -import { - DashboardContainer, - DashboardContainerInput, - DashboardContainerOptions, -} from './dashboard_container'; -import { DashboardCapabilities } from '../types'; +import { DashboardContainer, DashboardContainerInput } from './dashboard_container'; import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; +import { Start as InspectorStartContract } from '../../../inspector/public'; -export interface DashboardOptions extends DashboardContainerOptions { - savedObjectMetaData?: SavedObjectMetaData; +interface StartServices { + capabilities: CoreStart['application']['capabilities']; + application: CoreStart['application']; + overlays: CoreStart['overlays']; + notifications: CoreStart['notifications']; + embeddable: EmbeddableStart; + inspector: InspectorStartContract; + SavedObjectFinder: React.ComponentType; + ExitFullScreenButton: React.ComponentType; + uiActions: UiActionsStart; } export class DashboardContainerFactory extends EmbeddableFactory< @@ -45,23 +50,13 @@ export class DashboardContainerFactory extends EmbeddableFactory< public readonly isContainerType = true; public readonly type = DASHBOARD_CONTAINER_TYPE; - private readonly allowEditing: boolean; - - constructor(private readonly options: DashboardOptions) { - super({ savedObjectMetaData: options.savedObjectMetaData }); - - const capabilities = (options.application.capabilities - .dashboard as unknown) as DashboardCapabilities; - - if (!capabilities || typeof capabilities !== 'object') { - throw new TypeError('Dashboard capabilities not found.'); - } - - this.allowEditing = !!capabilities.createNew && !!capabilities.showWriteControls; + constructor(private readonly getStartServices: () => Promise) { + super(); } - public isEditable() { - return this.allowEditing; + public async isEditable() { + const { capabilities } = await this.getStartServices(); + return !!capabilities.createNew && !!capabilities.showWriteControls; } public getDisplayName() { @@ -82,6 +77,7 @@ export class DashboardContainerFactory extends EmbeddableFactory< initialInput: DashboardContainerInput, parent?: Container ): Promise { - return new DashboardContainer(initialInput, this.options, parent); + const services = await this.getStartServices(); + return new DashboardContainer(initialInput, services, parent); } } diff --git a/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx index c1a3d88979f4..0f1b9c6dc930 100644 --- a/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx @@ -23,7 +23,7 @@ import sizeMe from 'react-sizeme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { skip } from 'rxjs/operators'; -import { EmbeddableFactory, GetEmbeddableFactory } from '../../embeddable_plugin'; +import { EmbeddableFactory } from '../../embeddable_plugin'; import { DashboardGrid, DashboardGridProps } from './dashboard_grid'; import { DashboardContainer, DashboardContainerOptions } from '../dashboard_container'; import { getSampleDashboardInput } from '../../test_helpers'; @@ -41,7 +41,7 @@ function prepare(props?: Partial) { CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory({} as any, (() => {}) as any, {} as any) ); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); const initialInput = getSampleDashboardInput({ panels: { '1': { diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 6f78829af19f..8a6e747aac17 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -23,7 +23,7 @@ import * as React from 'react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { SharePluginSetup } from 'src/plugins/share/public'; import { UiActionsSetup, UiActionsStart } from '../../../plugins/ui_actions/public'; -import { CONTEXT_MENU_TRIGGER, IEmbeddableSetup, IEmbeddableStart } from './embeddable_plugin'; +import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart } from './embeddable_plugin'; import { ExpandPanelAction, ReplacePanelAction } from '.'; import { DashboardContainerFactory } from './embeddable/dashboard_container_factory'; import { Start as InspectorStartContract } from '../../../plugins/inspector/public'; @@ -47,13 +47,13 @@ declare module '../../share/public' { } interface SetupDependencies { - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; uiActions: UiActionsSetup; share?: SharePluginSetup; } interface StartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; inspector: InspectorStartContract; uiActions: UiActionsStart; } @@ -72,7 +72,10 @@ export class DashboardEmbeddableContainerPublicPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { share, uiActions }: SetupDependencies): Setup { + public setup( + core: CoreSetup, + { share, uiActions, embeddable }: SetupDependencies + ): Setup { const expandPanelAction = new ExpandPanelAction(); uiActions.registerAction(expandPanelAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction); @@ -86,26 +89,44 @@ export class DashboardEmbeddableContainerPublicPlugin })) ); } + + const getStartServices = async () => { + const [coreStart, deps] = await core.getStartServices(); + + const useHideChrome = () => { + React.useEffect(() => { + coreStart.chrome.setIsVisible(false); + return () => coreStart.chrome.setIsVisible(true); + }, []); + }; + + const ExitFullScreenButton: React.FC = props => { + useHideChrome(); + return ; + }; + return { + capabilities: coreStart.application.capabilities, + application: coreStart.application, + notifications: coreStart.notifications, + overlays: coreStart.overlays, + embeddable: deps.embeddable, + inspector: deps.inspector, + SavedObjectFinder: getSavedObjectFinder(coreStart.savedObjects, coreStart.uiSettings), + ExitFullScreenButton, + uiActions: deps.uiActions, + }; + }; + + const factory = new DashboardContainerFactory(getStartServices); + embeddable.registerEmbeddableFactory(factory.type, factory); } public start(core: CoreStart, plugins: StartDependencies): Start { - const { application, notifications, overlays } = core; - const { embeddable, inspector, uiActions } = plugins; + const { notifications } = core; + const { uiActions } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); - const useHideChrome = () => { - React.useEffect(() => { - core.chrome.setIsVisible(false); - return () => core.chrome.setIsVisible(true); - }, []); - }; - - const ExitFullScreenButton: React.FC = props => { - useHideChrome(); - return ; - }; - const changeViewAction = new ReplacePanelAction( core, SavedObjectFinder, @@ -114,19 +135,6 @@ export class DashboardEmbeddableContainerPublicPlugin ); uiActions.registerAction(changeViewAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction); - - const factory = new DashboardContainerFactory({ - application, - notifications, - overlays, - embeddable, - inspector, - SavedObjectFinder, - ExitFullScreenButton, - uiActions, - }); - - embeddable.registerEmbeddableFactory(factory.type, factory); } public stop() {} diff --git a/src/plugins/embeddable/public/api/get_embeddable_factories.ts b/src/plugins/embeddable/public/api/get_embeddable_factories.ts deleted file mode 100644 index c12d1283905f..000000000000 --- a/src/plugins/embeddable/public/api/get_embeddable_factories.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { EmbeddableApiPure } from './types'; - -export const getEmbeddableFactories: EmbeddableApiPure['getEmbeddableFactories'] = ({ - embeddableFactories, -}) => () => { - return embeddableFactories.values(); -}; diff --git a/src/plugins/embeddable/public/api/get_embeddable_factory.ts b/src/plugins/embeddable/public/api/get_embeddable_factory.ts deleted file mode 100644 index 8e98da287c5e..000000000000 --- a/src/plugins/embeddable/public/api/get_embeddable_factory.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EmbeddableApiPure } from './types'; - -export const getEmbeddableFactory: EmbeddableApiPure['getEmbeddableFactory'] = ({ - embeddableFactories, -}) => embeddableFactoryId => { - const factory = embeddableFactories.get(embeddableFactoryId); - - if (!factory) { - throw new Error( - `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] does not exist.` - ); - } - - return factory; -}; diff --git a/src/plugins/embeddable/public/api/index.ts b/src/plugins/embeddable/public/api/index.ts deleted file mode 100644 index aec539330de9..000000000000 --- a/src/plugins/embeddable/public/api/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 { - EmbeddableApiPure, - EmbeddableDependencies, - EmbeddableApi, - EmbeddableDependenciesInternal, -} from './types'; -import { getEmbeddableFactories } from './get_embeddable_factories'; -import { getEmbeddableFactory } from './get_embeddable_factory'; -import { registerEmbeddableFactory } from './register_embeddable_factory'; - -export * from './types'; - -export const pureApi: EmbeddableApiPure = { - getEmbeddableFactories, - getEmbeddableFactory, - registerEmbeddableFactory, -}; - -export const createApi = (deps: EmbeddableDependencies) => { - const partialApi: Partial = {}; - const depsInternal: EmbeddableDependenciesInternal = { ...deps, api: partialApi }; - for (const [key, fn] of Object.entries(pureApi)) { - (partialApi as any)[key] = fn(depsInternal); - } - Object.freeze(partialApi); - const api = partialApi as EmbeddableApi; - return { api, depsInternal }; -}; diff --git a/src/plugins/embeddable/public/api/register_embeddable_factory.ts b/src/plugins/embeddable/public/api/register_embeddable_factory.ts deleted file mode 100644 index 8b7bcdee5911..000000000000 --- a/src/plugins/embeddable/public/api/register_embeddable_factory.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 { EmbeddableApiPure } from './types'; - -export const registerEmbeddableFactory: EmbeddableApiPure['registerEmbeddableFactory'] = ({ - embeddableFactories, -}) => (embeddableFactoryId, factory) => { - if (embeddableFactories.has(embeddableFactoryId)) { - throw new Error( - `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] already registered in Embeddables API.` - ); - } - - embeddableFactories.set(embeddableFactoryId, factory); -}; diff --git a/src/plugins/embeddable/public/api/tests/helpers.ts b/src/plugins/embeddable/public/api/tests/helpers.ts deleted file mode 100644 index be8e9a0dec3c..000000000000 --- a/src/plugins/embeddable/public/api/tests/helpers.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 { EmbeddableDependencies } from '../types'; - -export const createDeps = (): EmbeddableDependencies => { - const deps: EmbeddableDependencies = { - embeddableFactories: new Map(), - }; - return deps; -}; diff --git a/src/plugins/embeddable/public/api/types.ts b/src/plugins/embeddable/public/api/types.ts deleted file mode 100644 index 179d96a4aff8..000000000000 --- a/src/plugins/embeddable/public/api/types.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 { EmbeddableFactoryRegistry } from '../types'; -import { EmbeddableFactory, GetEmbeddableFactories } from '../lib'; - -export interface EmbeddableApi { - getEmbeddableFactory: (embeddableFactoryId: string) => EmbeddableFactory; - getEmbeddableFactories: GetEmbeddableFactories; - // TODO: Make `registerEmbeddableFactory` receive only `factory` argument. - registerEmbeddableFactory: ( - id: string, - factory: TEmbeddableFactory - ) => void; -} - -export interface EmbeddableDependencies { - embeddableFactories: EmbeddableFactoryRegistry; -} - -export interface EmbeddableDependenciesInternal extends EmbeddableDependencies { - api: Readonly>; -} - -export type EmbeddableApiPure = { - [K in keyof EmbeddableApi]: (deps: EmbeddableDependenciesInternal) => EmbeddableApi[K]; -}; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 1474f9ed6305..eca74af4ec25 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -48,8 +48,6 @@ export { EmbeddableRoot, EmbeddableVisTriggerContext, ErrorEmbeddable, - GetEmbeddableFactories, - GetEmbeddableFactory, IContainer, IEmbeddable, isErrorEmbeddable, @@ -68,4 +66,4 @@ export function plugin(initializerContext: PluginInitializerContext) { return new EmbeddablePublicPlugin(initializerContext); } -export { IEmbeddableSetup, IEmbeddableStart } from './plugin'; +export { EmbeddableSetup, EmbeddableStart } from './plugin'; diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index 142a237a6a31..9aeaf34f3311 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -19,11 +19,13 @@ import { EditPanelAction } from './edit_panel_action'; import { EmbeddableFactory, Embeddable, EmbeddableInput } from '../embeddables'; -import { GetEmbeddableFactory, ViewMode } from '../types'; +import { ViewMode } from '../types'; import { ContactCardEmbeddable } from '../test_samples'; +import { EmbeddableStart } from '../../plugin'; const embeddableFactories = new Map(); -const getFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); +const getFactory = ((id: string) => + embeddableFactories.get(id)) as EmbeddableStart['getEmbeddableFactory']; class EditableEmbeddable extends Embeddable { public readonly type = 'EDITABLE_EMBEDDABLE'; @@ -82,7 +84,8 @@ test('is not compatible when edit url is not available', async () => { test('is not visible when edit url is available but in view mode', async () => { embeddableFactories.clear(); - const action = new EditPanelAction(type => embeddableFactories.get(type)); + const action = new EditPanelAction((type => + embeddableFactories.get(type)) as EmbeddableStart['getEmbeddableFactory']); expect( await action.isCompatible({ embeddable: new EditableEmbeddable( @@ -98,7 +101,8 @@ test('is not visible when edit url is available but in view mode', async () => { test('is not compatible when edit url is available, in edit mode, but not editable', async () => { embeddableFactories.clear(); - const action = new EditPanelAction(type => embeddableFactories.get(type)); + const action = new EditPanelAction((type => + embeddableFactories.get(type)) as EmbeddableStart['getEmbeddableFactory']); expect( await action.isCompatible({ embeddable: new EditableEmbeddable( diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 82f8e33b7ae2..9125dc0813f9 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -19,9 +19,10 @@ import { i18n } from '@kbn/i18n'; import { Action } from 'src/plugins/ui_actions/public'; -import { GetEmbeddableFactory, ViewMode } from '../types'; +import { ViewMode } from '../types'; import { EmbeddableFactoryNotFoundError } from '../errors'; import { IEmbeddable } from '../embeddables'; +import { EmbeddableStart } from '../../plugin'; export const ACTION_EDIT_PANEL = 'editPanel'; @@ -34,7 +35,7 @@ export class EditPanelAction implements Action { public readonly id = ACTION_EDIT_PANEL; public order = 15; - constructor(private readonly getEmbeddableFactory: GetEmbeddableFactory) {} + constructor(private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']) {} public getDisplayName({ embeddable }: ActionContext) { const factory = this.getEmbeddableFactory(embeddable.type); diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 71e7cca3552b..5ce79537ccaf 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -29,7 +29,7 @@ import { } from '../embeddables'; import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors'; -import { GetEmbeddableFactory } from '../types'; +import { EmbeddableStart } from '../../plugin'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -49,7 +49,7 @@ export abstract class Container< constructor( input: TContainerInput, output: TContainerOutput, - protected readonly getFactory: GetEmbeddableFactory, + protected readonly getFactory: EmbeddableStart['getEmbeddableFactory'], parent?: Container ) { super(input, output, parent); diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx index 3c9e6e31220b..07915ce59e6c 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { nextTick } from 'test_utils/enzyme_helpers'; import { EmbeddableChildPanel } from './embeddable_child_panel'; -import { GetEmbeddableFactory } from '../types'; import { EmbeddableFactory } from '../embeddables'; import { CONTACT_CARD_EMBEDDABLE } from '../test_samples/embeddables/contact_card/contact_card_embeddable_factory'; import { SlowContactCardEmbeddableFactory } from '../test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory'; @@ -42,7 +41,7 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async CONTACT_CARD_EMBEDDABLE, new SlowContactCardEmbeddableFactory({ execAction: (() => null) as any }) ); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); const container = new HelloWorldContainer({ id: 'hello', panels: {} }, { getEmbeddableFactory, @@ -88,7 +87,7 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async test(`EmbeddableChildPanel renders an error message if the factory doesn't exist`, async () => { const inspector = inspectorPluginMock.createStartContract(); - const getEmbeddableFactory: GetEmbeddableFactory = () => undefined; + const getEmbeddableFactory = () => undefined; const container = new HelloWorldContainer( { id: 'hello', diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx index e15f1faaa397..4c08a80a356b 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx @@ -29,15 +29,15 @@ import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { ErrorEmbeddable, IEmbeddable } from '../embeddables'; import { EmbeddablePanel } from '../panel'; import { IContainer } from './i_container'; -import { GetEmbeddableFactory, GetEmbeddableFactories } from '../types'; +import { EmbeddableStart } from '../../plugin'; export interface EmbeddableChildPanelProps { embeddableId: string; className?: string; container: IContainer; getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index 162da75c228a..81f7f35c900c 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -74,13 +74,11 @@ export abstract class EmbeddableFactory< this.savedObjectMetaData = savedObjectMetaData; } - // TODO: Can this be a property? If this "...should be based of capabilities service...", - // TODO: maybe then it should be *async*? /** * Returns whether the current user should be allowed to edit this type of - * embeddable. Most of the time this should be based off the capabilities service. + * embeddable. Most of the time this should be based off the capabilities service, hence it's async. */ - public abstract isEditable(): boolean; + public abstract async isEditable(): Promise; /** * Returns a display name for this type of embeddable. Used in "Create new... " options diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx index 7c3a1c6ca45c..51b83ea0ecaa 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx @@ -22,21 +22,21 @@ import { HelloWorldEmbeddableFactory, } from '../../../../../../examples/embeddable_examples/public'; import { EmbeddableFactory } from './embeddable_factory'; -import { GetEmbeddableFactory } from '../types'; import { EmbeddableFactoryRenderer } from './embeddable_factory_renderer'; import { mount } from 'enzyme'; import { nextTick } from 'test_utils/enzyme_helpers'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; +import { EmbeddableStart } from '../../plugin'; test('EmbeddableFactoryRenderer renders an embeddable', async () => { const embeddableFactories = new Map(); embeddableFactories.set(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory()); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); const component = mount( @@ -54,7 +54,7 @@ test('EmbeddableFactoryRenderer renders an embeddable', async () => { }); test('EmbeddableRoot renders an error if the type does not exist', async () => { - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => undefined; + const getEmbeddableFactory = (id: string) => undefined; const component = mount( >(); const triggerRegistry = new Map(); const embeddableFactories = new Map(); -const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); +const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); const editModeAction = createEditModeAction(); const trigger: Trigger = { diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 28474544f40b..b95060a73252 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -27,7 +27,7 @@ import { toMountPoint } from '../../../../kibana_react/public'; import { Start as InspectorStartContract } from '../inspector'; import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, EmbeddableContext } from '../triggers'; import { IEmbeddable } from '../embeddables/i_embeddable'; -import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../types'; +import { ViewMode } from '../types'; import { RemovePanelAction } from './panel_header/panel_actions'; import { AddPanelAction } from './panel_header/panel_actions/add_panel/add_panel_action'; @@ -36,12 +36,13 @@ import { PanelHeader } from './panel_header/panel_header'; import { InspectPanelAction } from './panel_header/panel_actions/inspect_panel_action'; import { EditPanelAction } from '../actions'; import { CustomizePanelModal } from './panel_header/panel_actions/customize_title/customize_panel_modal'; +import { EmbeddableStart } from '../../plugin'; interface Props { embeddable: IEmbeddable; getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx index 028d6a530236..8ee8c8dad9df 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx @@ -27,15 +27,15 @@ import { } from '../../../../test_samples/embeddables/filterable_embeddable'; import { FilterableEmbeddableFactory } from '../../../../test_samples/embeddables/filterable_embeddable_factory'; import { FilterableContainer } from '../../../../test_samples/embeddables/filterable_container'; -import { GetEmbeddableFactory } from '../../../../types'; // eslint-disable-next-line import { coreMock } from '../../../../../../../../core/public/mocks'; import { ContactCardEmbeddable } from '../../../../test_samples'; import { esFilters, Filter } from '../../../../../../../../plugins/data/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); -const getFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); +const getFactory = (id: string) => embeddableFactories.get(id); let container: FilterableContainer; let embeddable: FilterableEmbeddable; @@ -58,7 +58,7 @@ beforeEach(async () => { }; container = new FilterableContainer( { id: 'hello', panels: {}, filters: [derivedFilter] }, - getFactory + getFactory as EmbeddableStart['getEmbeddableFactory'] ); const filterableEmbeddable = await container.addNewEmbeddable< diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts index 36bb742040cc..f3a483bb4bda 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts @@ -19,7 +19,8 @@ import { i18n } from '@kbn/i18n'; import { Action } from 'src/plugins/ui_actions/public'; import { NotificationsStart, OverlayStart } from 'src/core/public'; -import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; +import { ViewMode } from '../../../../types'; import { openAddPanelFlyout } from './open_add_panel_flyout'; import { IContainer } from '../../../../containers'; @@ -34,8 +35,8 @@ export class AddPanelAction implements Action { public readonly id = ACTION_ADD_PANEL; constructor( - private readonly getFactory: GetEmbeddableFactory, - private readonly getAllFactories: GetEmbeddableFactories, + private readonly getFactory: EmbeddableStart['getEmbeddableFactory'], + private readonly getAllFactories: EmbeddableStart['getEmbeddableFactories'], private readonly overlays: OverlayStart, private readonly notifications: NotificationsStart, private readonly SavedObjectFinder: React.ComponentType diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx index 5f06e4ec4478..2fa21e40ca0f 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -19,7 +19,6 @@ import * as React from 'react'; import { AddPanelFlyout } from './add_panel_flyout'; -import { GetEmbeddableFactory } from '../../../../types'; import { ContactCardEmbeddableFactory, CONTACT_CARD_EMBEDDABLE, @@ -32,6 +31,7 @@ import { ReactWrapper } from 'enzyme'; import { coreMock } from '../../../../../../../../core/public/mocks'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; function DummySavedObjectFinder(props: { children: React.ReactNode }) { return ( @@ -55,7 +55,7 @@ test('createNewEmbeddable() add embeddable to container', async () => { firstName: 'foo', lastName: 'bar', } as any); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => contactCardEmbeddableFactory; + const getEmbeddableFactory = (id: string) => contactCardEmbeddableFactory; const input: ContainerInput<{ firstName: string; lastName: string }> = { id: '1', panels: {}, @@ -66,7 +66,7 @@ test('createNewEmbeddable() add embeddable to container', async () => { new Set([contactCardEmbeddableFactory]).values()} notifications={core.notifications} SavedObjectFinder={() => null} @@ -100,7 +100,8 @@ test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()' firstName: 'foo', lastName: 'bar', } as any); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => contactCardEmbeddableFactory; + const getEmbeddableFactory = ((id: string) => + contactCardEmbeddableFactory) as EmbeddableStart['getEmbeddableFactory']; const input: ContainerInput<{ firstName: string; lastName: string }> = { id: '1', panels: {}, diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index 815394ebd97e..95eeb63710c3 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -29,16 +29,16 @@ import { EuiTitle, } from '@elastic/eui'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; import { IContainer } from '../../../../containers'; import { EmbeddableFactoryNotFoundError } from '../../../../errors'; -import { GetEmbeddableFactories, GetEmbeddableFactory } from '../../../../types'; import { SavedObjectFinderCreateNew } from './saved_object_finder_create_new'; interface Props { onClose: () => void; container: IContainer; - getFactory: GetEmbeddableFactory; - getAllFactories: GetEmbeddableFactories; + getFactory: EmbeddableStart['getEmbeddableFactory']; + getAllFactories: EmbeddableStart['getEmbeddableFactories']; notifications: CoreSetup['notifications']; SavedObjectFinder: React.ComponentType; } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx index 481693501066..a452e07b5157 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx @@ -18,15 +18,15 @@ */ import React from 'react'; import { NotificationsStart, OverlayStart } from 'src/core/public'; +import { EmbeddableStart } from '../../../../../plugin'; import { toMountPoint } from '../../../../../../../kibana_react/public'; import { IContainer } from '../../../../containers'; import { AddPanelFlyout } from './add_panel_flyout'; -import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types'; export async function openAddPanelFlyout(options: { embeddable: IContainer; - getFactory: GetEmbeddableFactory; - getAllFactories: GetEmbeddableFactories; + getFactory: EmbeddableStart['getEmbeddableFactory']; + getAllFactories: EmbeddableStart['getEmbeddableFactories']; overlays: OverlayStart; notifications: NotificationsStart; SavedObjectFinder: React.ComponentType; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts index 4ba63bb025a8..3f7c917cd161 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts @@ -32,7 +32,6 @@ import { ContactCardEmbeddableFactory, } from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable_factory'; import { HelloWorldContainer } from '../../../../test_samples/embeddables/hello_world_container'; -import { GetEmbeddableFactory } from '../../../../types'; import { EmbeddableFactory } from '../../../../embeddables'; let container: Container; @@ -40,7 +39,7 @@ let embeddable: ContactCardEmbeddable; function createHelloWorldContainer(input = { id: '123', panels: {} }) { const embeddableFactories = new Map(); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); embeddableFactories.set( CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory({}, (() => {}) as any, {} as any) diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx index 8d9beec940ac..e19acda8419d 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx @@ -34,14 +34,14 @@ import { isErrorEmbeddable, ErrorEmbeddable, } from '../../../embeddables'; -import { GetEmbeddableFactory } from '../../../types'; import { of } from '../../../../tests/helpers'; import { esFilters } from '../../../../../../../plugins/data/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; const setup = async () => { const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); - const getFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getFactory = (id: string) => embeddableFactories.get(id); const container = new FilterableContainer( { id: 'hello', @@ -54,7 +54,7 @@ const setup = async () => { }, ], }, - getFactory + getFactory as EmbeddableStart['getEmbeddableFactory'] ); const embeddable: FilterableEmbeddable | ErrorEmbeddable = await container.addNewEmbeddable< diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx index be096a4cc60c..f4d5aa148373 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx @@ -20,6 +20,7 @@ import { EmbeddableOutput, isErrorEmbeddable } from '../../../'; import { RemovePanelAction } from './remove_panel_action'; import { EmbeddableFactory } from '../../../embeddables'; +import { EmbeddableStart } from '../../../../plugin'; import { FILTERABLE_EMBEDDABLE, FilterableEmbeddable, @@ -27,13 +28,13 @@ import { } from '../../../test_samples/embeddables/filterable_embeddable'; import { FilterableEmbeddableFactory } from '../../../test_samples/embeddables/filterable_embeddable_factory'; import { FilterableContainer } from '../../../test_samples/embeddables/filterable_container'; -import { GetEmbeddableFactory, ViewMode } from '../../../types'; +import { ViewMode } from '../../../types'; import { ContactCardEmbeddable } from '../../../test_samples/embeddables/contact_card/contact_card_embeddable'; import { esFilters, Filter } from '../../../../../../../plugins/data/public'; const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); -const getFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); +const getFactory = (id: string) => embeddableFactories.get(id); let container: FilterableContainer; let embeddable: FilterableEmbeddable; @@ -46,7 +47,7 @@ beforeEach(async () => { }; container = new FilterableContainer( { id: 'hello', panels: {}, filters: [derivedFilter], viewMode: ViewMode.EDIT }, - getFactory + getFactory as EmbeddableStart['getEmbeddableFactory'] ); const filterableEmbeddable = await container.addNewEmbeddable< diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx index 7a9ba4fbbf6d..20a5a8112f4d 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx @@ -42,7 +42,7 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory { public readonly type = FILTERABLE_CONTAINER; constructor( - private readonly getFactory: GetEmbeddableFactory, + private readonly getFactory: EmbeddableStart['getEmbeddableFactory'], options: EmbeddableFactoryOptions = {} ) { super(options); @@ -43,7 +43,7 @@ export class FilterableContainerFactory extends EmbeddableFactory { public readonly type = FILTERABLE_EMBEDDABLE; - public isEditable() { + public async isEditable() { return true; } diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx index c5ba054bebb7..a88c3ba08632 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx @@ -24,7 +24,7 @@ import { UiActionsService } from 'src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { Container, ViewMode, ContainerInput } from '../..'; import { HelloWorldContainerComponent } from './hello_world_container_component'; -import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../types'; +import { EmbeddableStart } from '../../../plugin'; export const HELLO_WORLD_CONTAINER = 'HELLO_WORLD_CONTAINER'; @@ -46,8 +46,8 @@ interface HelloWorldContainerInput extends ContainerInput { interface HelloWorldContainerOptions { getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx index e9acfd453976..e8c1464edab3 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx @@ -24,13 +24,13 @@ import { CoreStart } from 'src/core/public'; import { UiActionsService } from 'src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { IContainer, PanelState, EmbeddableChildPanel } from '../..'; -import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../types'; +import { EmbeddableStart } from '../../../plugin'; interface Props { container: IContainer; getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/src/plugins/embeddable/public/lib/types.ts b/src/plugins/embeddable/public/lib/types.ts index 68ea5bc17f7c..1cfff7baca18 100644 --- a/src/plugins/embeddable/public/lib/types.ts +++ b/src/plugins/embeddable/public/lib/types.ts @@ -18,7 +18,6 @@ */ import { Adapters } from './inspector'; -import { EmbeddableFactory } from './embeddables/embeddable_factory'; export interface Trigger { id: string; @@ -40,6 +39,3 @@ export enum ViewMode { } export { Adapters }; - -export type GetEmbeddableFactory = (id: string) => EmbeddableFactory | undefined; -export type GetEmbeddableFactories = () => IterableIterator; diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts index fd299bc626fb..ba2f78e42e10 100644 --- a/src/plugins/embeddable/public/mocks.ts +++ b/src/plugins/embeddable/public/mocks.ts @@ -17,15 +17,15 @@ * under the License. */ -import { IEmbeddableStart, IEmbeddableSetup } from '.'; +import { EmbeddableStart, EmbeddableSetup } from '.'; import { EmbeddablePublicPlugin } from './plugin'; import { coreMock } from '../../../core/public/mocks'; // eslint-disable-next-line import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; -export type Setup = jest.Mocked; -export type Start = jest.Mocked; +export type Setup = jest.Mocked; +export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { @@ -36,7 +36,6 @@ const createSetupContract = (): Setup => { const createStartContract = (): Start => { const startContract: Start = { - registerEmbeddableFactory: jest.fn(), getEmbeddableFactories: jest.fn(), getEmbeddableFactory: jest.fn(), }; diff --git a/src/plugins/embeddable/public/api/tests/registry.test.ts b/src/plugins/embeddable/public/plugin.test.ts similarity index 70% rename from src/plugins/embeddable/public/api/tests/registry.test.ts rename to src/plugins/embeddable/public/plugin.test.ts index 30a8a71d243f..c334411004e2 100644 --- a/src/plugins/embeddable/public/api/tests/registry.test.ts +++ b/src/plugins/embeddable/public/plugin.test.ts @@ -16,18 +16,20 @@ * specific language governing permissions and limitations * under the License. */ - -import { createApi } from '..'; -import { createDeps } from './helpers'; +import { coreMock } from '../../../core/public/mocks'; +import { testPlugin } from './tests/test_plugin'; test('cannot register embeddable factory with the same ID', async () => { - const deps = createDeps(); - const { api } = createApi(deps); + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + const { setup } = testPlugin(coreSetup, coreStart); const embeddableFactoryId = 'ID'; const embeddableFactory = {} as any; - api.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory); - expect(() => api.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory)).toThrowError( + setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory); + expect(() => + setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory) + ).toThrowError( 'Embeddable factory [embeddableFactoryId = ID] already registered in Embeddables API.' ); }); diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.ts index c84fb888412e..381665c359ff 100644 --- a/src/plugins/embeddable/public/plugin.ts +++ b/src/plugins/embeddable/public/plugin.ts @@ -16,45 +16,78 @@ * specific language governing permissions and limitations * under the License. */ - import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { EmbeddableFactoryRegistry } from './types'; -import { createApi, EmbeddableApi } from './api'; import { bootstrap } from './bootstrap'; +import { EmbeddableFactory, EmbeddableInput, EmbeddableOutput } from './lib'; -export interface IEmbeddableSetupDependencies { +export interface EmbeddableSetupDependencies { uiActions: UiActionsSetup; } -export interface IEmbeddableSetup { - registerEmbeddableFactory: EmbeddableApi['registerEmbeddableFactory']; +export interface EmbeddableSetup { + registerEmbeddableFactory: ( + id: string, + factory: EmbeddableFactory + ) => void; +} +export interface EmbeddableStart { + getEmbeddableFactory: < + I extends EmbeddableInput = EmbeddableInput, + O extends EmbeddableOutput = EmbeddableOutput + >( + embeddableFactoryId: string + ) => EmbeddableFactory | undefined; + getEmbeddableFactories: () => IterableIterator; } -export type IEmbeddableStart = EmbeddableApi; - -export class EmbeddablePublicPlugin implements Plugin { +export class EmbeddablePublicPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); - private api!: EmbeddableApi; constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { uiActions }: IEmbeddableSetupDependencies) { - ({ api: this.api } = createApi({ - embeddableFactories: this.embeddableFactories, - })); + public setup(core: CoreSetup, { uiActions }: EmbeddableSetupDependencies) { bootstrap(uiActions); - const { registerEmbeddableFactory } = this.api; - return { - registerEmbeddableFactory, + registerEmbeddableFactory: this.registerEmbeddableFactory, }; } public start(core: CoreStart) { - return this.api; + return { + getEmbeddableFactory: this.getEmbeddableFactory, + getEmbeddableFactories: () => this.embeddableFactories.values(), + }; } public stop() {} + + private registerEmbeddableFactory = (embeddableFactoryId: string, factory: EmbeddableFactory) => { + if (this.embeddableFactories.has(embeddableFactoryId)) { + throw new Error( + `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] already registered in Embeddables API.` + ); + } + + this.embeddableFactories.set(embeddableFactoryId, factory); + }; + + private getEmbeddableFactory = < + I extends EmbeddableInput = EmbeddableInput, + O extends EmbeddableOutput = EmbeddableOutput + >( + embeddableFactoryId: string + ) => { + const factory = this.embeddableFactories.get(embeddableFactoryId); + + if (!factory) { + throw new Error( + `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] does not exist.` + ); + } + + return factory as EmbeddableFactory; + }; } diff --git a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts index 0721acb1a1fb..6beef35bbe13 100644 --- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts @@ -35,14 +35,14 @@ import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks'; import { esFilters } from '../../../../plugins/data/public'; test('ApplyFilterAction applies the filter to the root of the container tree', async () => { - const { doStart } = testPlugin(); + const { doStart, setup } = testPlugin(); const api = doStart(); const factory1 = new FilterableContainerFactory(api.getEmbeddableFactory); const factory2 = new FilterableEmbeddableFactory(); - api.registerEmbeddableFactory(factory1.type, factory1); - api.registerEmbeddableFactory(factory2.type, factory2); + setup.registerEmbeddableFactory(factory1.type, factory1); + setup.registerEmbeddableFactory(factory2.type, factory2); const applyFilterAction = createFilterAction(); @@ -93,7 +93,7 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a }); test('ApplyFilterAction is incompatible if the root container does not accept a filter as input', async () => { - const { doStart, coreStart } = testPlugin(); + const { doStart, coreStart, setup } = testPlugin(); const api = doStart(); const inspector = inspectorPluginMock.createStartContract(); @@ -112,7 +112,7 @@ test('ApplyFilterAction is incompatible if the root container does not accept a ); const factory = new FilterableEmbeddableFactory(); - api.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const embeddable = await parent.addNewEmbeddable< FilterableContainerInput, @@ -129,12 +129,12 @@ test('ApplyFilterAction is incompatible if the root container does not accept a }); test('trying to execute on incompatible context throws an error ', async () => { - const { doStart, coreStart } = testPlugin(); + const { doStart, coreStart, setup } = testPlugin(); const api = doStart(); const inspector = inspectorPluginMock.createStartContract(); const factory = new FilterableEmbeddableFactory(); - api.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const applyFilterAction = createFilterAction(); const parent = new HelloWorldContainer( diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts index be19ac206999..1ee52f474913 100644 --- a/src/plugins/embeddable/public/tests/container.test.ts +++ b/src/plugins/embeddable/public/tests/container.test.ts @@ -562,7 +562,7 @@ test('Panel added to input state', async () => { test('Container changes made directly after adding a new embeddable are propagated', async done => { const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); - const { doStart, uiActions } = testPlugin(coreSetup, coreStart); + const { setup, doStart, uiActions } = testPlugin(coreSetup, coreStart); const start = doStart(); const container = new HelloWorldContainer( @@ -586,7 +586,7 @@ test('Container changes made directly after adding a new embeddable are propagat loadTickCount: 3, execAction: uiActions.executeTriggerActions, }); - start.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const subscription = Rx.merge(container.getOutput$(), container.getInput$()) .pipe(skip(2)) @@ -755,7 +755,7 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy }); test('untilEmbeddableLoaded resolves with undefined if child is subsequently removed', async done => { - const { doStart, coreStart, uiActions } = testPlugin( + const { doStart, setup, coreStart, uiActions } = testPlugin( coreMock.createSetup(), coreMock.createStart() ); @@ -764,7 +764,7 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem loadTickCount: 3, execAction: uiActions.executeTriggerActions, }); - start.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const container = new HelloWorldContainer( { id: 'hello', @@ -795,7 +795,7 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem }); test('adding a panel then subsequently removing it before its loaded removes the panel', async done => { - const { doStart, coreStart, uiActions } = testPlugin( + const { doStart, coreStart, uiActions, setup } = testPlugin( coreMock.createSetup(), coreMock.createStart() ); @@ -804,7 +804,7 @@ test('adding a panel then subsequently removing it before its loaded removes the loadTickCount: 1, execAction: uiActions.executeTriggerActions, }); - start.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const container = new HelloWorldContainer( { id: 'hello', diff --git a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx index 70d7c99d3fb9..99d5a7c747d1 100644 --- a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx +++ b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx @@ -34,16 +34,16 @@ import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world // eslint-disable-next-line import { coreMock } from '../../../../core/public/mocks'; import { testPlugin } from './test_plugin'; -import { EmbeddableApi } from '../api'; import { CustomizePanelModal } from '../lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal'; import { mount } from 'enzyme'; +import { EmbeddableStart } from '../plugin'; -let api: EmbeddableApi; +let api: EmbeddableStart; let container: Container; let embeddable: ContactCardEmbeddable; beforeEach(async () => { - const { doStart, coreStart, uiActions } = testPlugin( + const { doStart, coreStart, uiActions, setup } = testPlugin( coreMock.createSetup(), coreMock.createStart() ); @@ -54,7 +54,7 @@ beforeEach(async () => { uiActions.executeTriggerActions, {} as any ); - api.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory); + setup.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory); container = new HelloWorldContainer( { id: '123', panels: {} }, diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts index 1edc33278033..e199ef193aa1 100644 --- a/src/plugins/embeddable/public/tests/test_plugin.ts +++ b/src/plugins/embeddable/public/tests/test_plugin.ts @@ -22,14 +22,14 @@ import { CoreSetup, CoreStart } from 'src/core/public'; import { uiActionsPluginMock } from 'src/plugins/ui_actions/public/mocks'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; import { coreMock } from '../../../../core/public/mocks'; -import { EmbeddablePublicPlugin, IEmbeddableSetup, IEmbeddableStart } from '../plugin'; +import { EmbeddablePublicPlugin, EmbeddableSetup, EmbeddableStart } from '../plugin'; export interface TestPluginReturn { plugin: EmbeddablePublicPlugin; coreSetup: CoreSetup; coreStart: CoreStart; - setup: IEmbeddableSetup; - doStart: (anotherCoreStart?: CoreStart) => IEmbeddableStart; + setup: EmbeddableSetup; + doStart: (anotherCoreStart?: CoreStart) => EmbeddableStart; uiActions: UiActionsStart; } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx index 144954800c91..54d13efe4d79 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx @@ -19,18 +19,15 @@ import { EuiTab } from '@elastic/eui'; import React, { Component } from 'react'; import { CoreStart } from 'src/core/public'; -import { - GetEmbeddableFactory, - GetEmbeddableFactories, -} from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { UiActionsService } from '../../../../../../../../src/plugins/ui_actions/public'; import { DashboardContainerExample } from './dashboard_container_example'; import { Start as InspectorStartContract } from '../../../../../../../../src/plugins/inspector/public'; export interface AppProps { getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx index 7cc9c1df1c94..f8625e4490e5 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx @@ -18,18 +18,19 @@ */ import React from 'react'; import { EuiButton, EuiLoadingChart } from '@elastic/eui'; +import { ContainerOutput } from 'src/plugins/embeddable/public'; import { ErrorEmbeddable, ViewMode, isErrorEmbeddable, EmbeddablePanel, - GetEmbeddableFactory, - GetEmbeddableFactories, + EmbeddableStart, } from '../embeddable_api'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer, DashboardContainerFactory, + DashboardContainerInput, } from '../../../../../../../../src/plugins/dashboard/public'; import { CoreStart } from '../../../../../../../../src/core/public'; @@ -39,8 +40,8 @@ import { UiActionsService } from '../../../../../../../../src/plugins/ui_actions interface Props { getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; @@ -67,9 +68,10 @@ export class DashboardContainerExample extends React.Component { public async componentDidMount() { this.mounted = true; - const dashboardFactory = this.props.getEmbeddableFactory( - DASHBOARD_CONTAINER_TYPE - ) as DashboardContainerFactory; + const dashboardFactory = this.props.getEmbeddableFactory< + DashboardContainerInput, + ContainerOutput + >(DASHBOARD_CONTAINER_TYPE) as DashboardContainerFactory; if (dashboardFactory) { this.container = await dashboardFactory.create(dashboardInput); if (this.mounted) { diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddables/hello_world_embeddable_factory.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddables/hello_world_embeddable_factory.ts deleted file mode 100644 index 0c90cb3b8586..000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddables/hello_world_embeddable_factory.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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. - */ - -// eslint-disable-next-line -import { npSetup } from '../../../../../../../../src/legacy/ui/public/new_platform'; -// eslint-disable-next-line -import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE } from '../../../../../../../../examples/embeddable_examples/public'; - -npSetup.plugins.embeddable.registerEmbeddableFactory( - HELLO_WORLD_EMBEDDABLE, - new HelloWorldEmbeddableFactory() -); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx index 25666dc0359d..18ceec652392 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx @@ -31,21 +31,16 @@ import { CONTEXT_MENU_TRIGGER } from './embeddable_api'; const REACT_ROOT_ID = 'embeddableExplorerRoot'; -import { - SayHelloAction, - createSendMessageAction, - ContactCardEmbeddableFactory, -} from './embeddable_api'; +import { SayHelloAction, createSendMessageAction } from './embeddable_api'; import { App } from './app'; import { getSavedObjectFinder } from '../../../../../../../src/plugins/saved_objects/public'; -import { HelloWorldEmbeddableFactory } from '../../../../../../../examples/embeddable_examples/public'; import { - IEmbeddableStart, - IEmbeddableSetup, + EmbeddableStart, + EmbeddableSetup, } from '.../../../../../../../src/plugins/embeddable/public'; export interface SetupDependencies { - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; inspector: InspectorSetupContract; __LEGACY: { ExitFullScreenButton: React.ComponentType; @@ -53,7 +48,7 @@ export interface SetupDependencies { } interface StartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; uiActions: UiActionsStart; inspector: InspectorStartContract; __LEGACY: { @@ -74,12 +69,6 @@ export class EmbeddableExplorerPublicPlugin const helloWorldAction = createHelloWorldAction(core.overlays); const sayHelloAction = new SayHelloAction(alert); const sendMessageAction = createSendMessageAction(core.overlays); - const helloWorldEmbeddableFactory = new HelloWorldEmbeddableFactory(); - const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory( - {}, - plugins.uiActions.executeTriggerActions, - core.overlays - ); plugins.uiActions.registerAction(helloWorldAction); plugins.uiActions.registerAction(sayHelloAction); @@ -87,15 +76,6 @@ export class EmbeddableExplorerPublicPlugin plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, helloWorldAction); - plugins.embeddable.registerEmbeddableFactory( - helloWorldEmbeddableFactory.type, - helloWorldEmbeddableFactory - ); - plugins.embeddable.registerEmbeddableFactory( - contactCardEmbeddableFactory.type, - contactCardEmbeddableFactory - ); - plugins.__LEGACY.onRenderComplete(() => { const root = document.getElementById(REACT_ROOT_ID); ReactDOM.render( diff --git a/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js b/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js index 203378e547c8..4a1bcecc0d5a 100644 --- a/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js +++ b/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js @@ -17,11 +17,8 @@ * under the License. */ -import expect from '@kbn/expect'; - export default function({ getService }) { const testSubjects = getService('testSubjects'); - const retry = getService('retry'); const pieChart = getService('pieChart'); const dashboardExpect = getService('dashboardExpect'); @@ -30,17 +27,6 @@ export default function({ getService }) { await testSubjects.click('embedExplorerTab-dashboardContainer'); }); - it('hello world embeddable renders', async () => { - await retry.try(async () => { - const text = await testSubjects.getVisibleText('helloWorldEmbeddable'); - expect(text).to.be('HELLO WORLD!'); - }); - }); - - it('contact card embeddable renders', async () => { - await testSubjects.existOrFail('embeddablePanelHeading-HelloSue'); - }); - it('pie charts', async () => { await pieChart.expectPieSliceCount(5); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index d30ad62b385c..2bde698e2356 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -27,17 +27,19 @@ import { Embeddable } from './embeddable'; import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence'; import { getEditPath } from '../../../../../../plugins/lens/common'; +interface StartServices { + timefilter: TimefilterContract; + coreHttp: HttpSetup; + capabilities: RecursiveReadonly; + savedObjectsClient: SavedObjectsClientContract; + expressionRenderer: ReactExpressionRendererType; + indexPatternService: IndexPatternsContract; +} + export class EmbeddableFactory extends AbstractEmbeddableFactory { type = DOC_TYPE; - constructor( - private timefilter: TimefilterContract, - private coreHttp: HttpSetup, - private capabilities: RecursiveReadonly, - private savedObjectsClient: SavedObjectsClientContract, - private expressionRenderer: ReactExpressionRendererType, - private indexPatternService: IndexPatternsContract - ) { + constructor(private getStartServices: () => Promise) { super({ savedObjectMetaData: { name: i18n.translate('xpack.lens.lensSavedObjectLabel', { @@ -49,8 +51,9 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { }); } - public isEditable() { - return this.capabilities.visualize.save as boolean; + public async isEditable() { + const { capabilities } = await this.getStartServices(); + return capabilities.visualize.save as boolean; } canCreateNew() { @@ -68,13 +71,20 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { input: Partial & { id: string }, parent?: IContainer ) { - const store = new SavedObjectIndexStore(this.savedObjectsClient); + const { + savedObjectsClient, + coreHttp, + indexPatternService, + timefilter, + expressionRenderer, + } = await this.getStartServices(); + const store = new SavedObjectIndexStore(savedObjectsClient); const savedVis = await store.load(savedObjectId); const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map( async ({ id }) => { try { - return await this.indexPatternService.get(id); + return await indexPatternService.get(id); } catch (error) { // Unable to load index pattern, ignore error as the index patterns are only used to // configure the filter and query bar - there is still a good chance to get the visualization @@ -90,12 +100,12 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { ); return new Embeddable( - this.timefilter, - this.expressionRenderer, + timefilter, + expressionRenderer, { savedVis, - editUrl: this.coreHttp.basePath.prepend(getEditPath(savedObjectId)), - editable: this.isEditable(), + editUrl: coreHttp.basePath.prepend(getEditPath(savedObjectId)), + editable: await this.isEditable(), indexPatterns, }, input, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx index 2e1645c81614..6b9dc88e7ed1 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx @@ -12,6 +12,7 @@ import { createMockSetupDependencies, createMockStartDependencies, } from './mocks'; +import { CoreSetup } from 'kibana/public'; jest.mock('ui/new_platform'); @@ -41,7 +42,10 @@ describe('editor_frame service', () => { it('should create an editor frame instance which mounts and unmounts', async () => { await expect( (async () => { - pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); + pluginInstance.setup( + coreMock.createSetup() as CoreSetup, + pluginSetupDependencies + ); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); const instance = await publicAPI.createInstance({}); instance.mount(mountpoint, { @@ -57,7 +61,10 @@ describe('editor_frame service', () => { }); it('should not have child nodes after unmount', async () => { - pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); + pluginInstance.setup( + coreMock.createSetup() as CoreSetup, + pluginSetupDependencies + ); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); const instance = await publicAPI.createInstance({}); instance.mount(mountpoint, { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx index 5347be47e145..1375c60060ca 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx @@ -12,10 +12,7 @@ import { ExpressionsSetup, ExpressionsStart, } from '../../../../../../src/plugins/expressions/public'; -import { - IEmbeddableSetup, - IEmbeddableStart, -} from '../../../../../../src/plugins/embeddable/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, @@ -35,13 +32,13 @@ import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; export interface EditorFrameSetupPlugins { data: DataPublicPluginSetup; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; expressions: ExpressionsSetup; } export interface EditorFrameStartPlugins { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; expressions: ExpressionsStart; } @@ -63,10 +60,27 @@ export class EditorFrameService { private readonly datasources: Array> = []; private readonly visualizations: Array> = []; - public setup(core: CoreSetup, plugins: EditorFrameSetupPlugins): EditorFrameSetup { + public setup( + core: CoreSetup, + plugins: EditorFrameSetupPlugins + ): EditorFrameSetup { plugins.expressions.registerFunction(() => mergeTables); plugins.expressions.registerFunction(() => formatColumn); + const getStartServices = async () => { + const [coreStart, deps] = await core.getStartServices(); + return { + capabilities: coreStart.application.capabilities, + savedObjectsClient: coreStart.savedObjects.client, + coreHttp: coreStart.http, + timefilter: deps.data.query.timefilter.timefilter, + expressionRenderer: deps.expressions.ReactExpressionRenderer, + indexPatternService: deps.data.indexPatterns, + }; + }; + + plugins.embeddable.registerEmbeddableFactory('lens', new EmbeddableFactory(getStartServices)); + return { registerDatasource: datasource => { this.datasources.push(datasource as Datasource); @@ -78,18 +92,6 @@ export class EditorFrameService { } public start(core: CoreStart, plugins: EditorFrameStartPlugins): EditorFrameStart { - plugins.embeddable.registerEmbeddableFactory( - 'lens', - new EmbeddableFactory( - plugins.data.query.timefilter.timefilter, - core.http, - core.application.capabilities, - core.savedObjects.client, - plugins.expressions.ReactExpressionRenderer, - plugins.data.indexPatterns - ) - ); - const createInstance = async (): Promise => { let domElement: Element; const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx index 7afe6d7abedc..cc029fee49d1 100644 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -36,7 +36,7 @@ import { getLensUrlFromDashboardAbsoluteUrl, } from '../../../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper'; import { FormatFactory } from './legacy_imports'; -import { IEmbeddableSetup, IEmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { VisualizationsSetup } from './legacy_imports'; @@ -45,7 +45,7 @@ export interface LensPluginSetupDependencies { kibanaLegacy: KibanaLegacySetup; expressions: ExpressionsSetup; data: DataPublicPluginSetup; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; __LEGACY: { formatFactory: FormatFactory; visualizations: VisualizationsSetup; @@ -54,7 +54,7 @@ export interface LensPluginSetupDependencies { export interface LensPluginStartDependencies { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; expressions: ExpressionsStart; } diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx index 0c93cd51abd7..888df8447a72 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -9,18 +9,13 @@ import React from 'react'; import { OutPortal, PortalNode } from 'react-reverse-portal'; import minimatch from 'minimatch'; import { ViewMode } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; -import { - IndexPatternMapping, - MapEmbeddable, - RenderTooltipContentParams, - SetQuery, - EmbeddableApi, -} from './types'; +import { IndexPatternMapping, MapEmbeddable, RenderTooltipContentParams, SetQuery } from './types'; import { getLayerList } from './map_config'; // @ts-ignore Missing type defs as maps moves to Typescript import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants'; import * as i18n from './translations'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public'; import { IndexPatternSavedObject } from '../../hooks/types'; /** @@ -45,7 +40,7 @@ export const createEmbeddable = async ( endDate: number, setQuery: SetQuery, portalNode: PortalNode, - embeddableApi: EmbeddableApi + embeddableApi: EmbeddableStart ): Promise => { const factory = embeddableApi.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts index 812d327ce448..cc253beb08ea 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts @@ -9,7 +9,6 @@ import { EmbeddableInput, EmbeddableOutput, IEmbeddable, - EmbeddableFactory, } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; import { inputsModel } from '../../store/inputs'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; @@ -85,8 +84,3 @@ export interface RenderTooltipContentParams { } export type MapToolTipProps = Partial; - -export interface EmbeddableApi { - getEmbeddableFactory: (embeddableFactoryId: string) => EmbeddableFactory; - registerEmbeddableFactory: (id: string, factory: EmbeddableFactory) => void; -} diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx index f22add59a95d..71fa3a54df76 100644 --- a/x-pack/legacy/plugins/siem/public/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/plugin.tsx @@ -13,7 +13,7 @@ import { } from '../../../../../src/core/public'; import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { IEmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { Start as NewsfeedStart } from '../../../../../src/plugins/newsfeed/public'; import { Start as InspectorStart } from '../../../../../src/plugins/inspector/public'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; @@ -37,7 +37,7 @@ export interface SetupPlugins { } export interface StartPlugins { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; inspector: InspectorStart; newsfeed?: NewsfeedStart; uiActions: UiActionsStart; diff --git a/x-pack/plugins/advanced_ui_actions/public/plugin.ts b/x-pack/plugins/advanced_ui_actions/public/plugin.ts index 2f6935cdf196..b9f0ce43d3cd 100644 --- a/x-pack/plugins/advanced_ui_actions/public/plugin.ts +++ b/x-pack/plugins/advanced_ui_actions/public/plugin.ts @@ -15,8 +15,8 @@ import { UiActionsStart, UiActionsSetup } from '../../../../src/plugins/ui_actio import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, - IEmbeddableSetup, - IEmbeddableStart, + EmbeddableSetup, + EmbeddableStart, } from '../../../../src/plugins/embeddable/public'; import { CustomTimeRangeAction, @@ -32,12 +32,12 @@ import { import { CommonlyUsedRange } from './types'; interface SetupDependencies { - embeddable: IEmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. + embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. uiActions: UiActionsSetup; } interface StartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; uiActions: UiActionsStart; } diff --git a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts index 789a4181c2af..3d143b0cacd0 100644 --- a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts +++ b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts @@ -8,7 +8,7 @@ import { ContainerInput, Container, ContainerOutput, - GetEmbeddableFactory, + EmbeddableStart, } from '../../../../../src/plugins/embeddable/public'; import { TimeRange } from '../../../../../src/plugins/data/public'; @@ -37,7 +37,7 @@ export class TimeRangeContainer extends Container< public readonly type = TIME_RANGE_CONTAINER; constructor( initialInput: ContainerTimeRangeInput, - getFactory: GetEmbeddableFactory, + getFactory: EmbeddableStart['getEmbeddableFactory'], parent?: Container ) { super(initialInput, { embeddableLoaded: {} }, getFactory, parent); diff --git a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts index efbf7a3bd2dc..311d3357476b 100644 --- a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts +++ b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts @@ -19,7 +19,7 @@ interface EmbeddableTimeRangeInput extends EmbeddableInput { export class TimeRangeEmbeddableFactory extends EmbeddableFactory { public readonly type = TIME_RANGE_EMBEDDABLE; - public isEditable() { + public async isEditable() { return true; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts index f5d1aad93ed5..c8e038869efc 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts @@ -15,7 +15,7 @@ import { ResolverEmbeddable } from './embeddable'; export class ResolverEmbeddableFactory extends EmbeddableFactory { public readonly type = 'resolver'; - public isEditable() { + public async isEditable() { return true; } diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts index 155d709042fe..2759db26bb6c 100644 --- a/x-pack/plugins/endpoint/public/plugin.ts +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -5,7 +5,7 @@ */ import { Plugin, CoreSetup, AppMountParameters, CoreStart } from 'kibana/public'; -import { IEmbeddableSetup } from 'src/plugins/embeddable/public'; +import { EmbeddableSetup } from 'src/plugins/embeddable/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { i18n } from '@kbn/i18n'; import { ResolverEmbeddableFactory } from './embeddables/resolver'; @@ -13,7 +13,7 @@ import { ResolverEmbeddableFactory } from './embeddables/resolver'; export type EndpointPluginStart = void; export type EndpointPluginSetup = void; export interface EndpointPluginSetupDependencies { - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; data: DataPublicPluginStart; } export interface EndpointPluginStartDependencies { diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts index 045cc56238ac..502164554c71 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts @@ -6,13 +6,13 @@ import { Plugin, CoreSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { IEmbeddable, IEmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; +import { IEmbeddable, EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; export type ResolverTestPluginSetup = void; export type ResolverTestPluginStart = void; export interface ResolverTestPluginSetupDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface export interface ResolverTestPluginStartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; } export class ResolverTestPlugin @@ -41,7 +41,9 @@ export class ResolverTestPlugin (async () => { const [, { embeddable }] = await core.getStartServices(); const factory = embeddable.getEmbeddableFactory('resolver'); - resolveEmbeddable!(factory.create({ id: 'test basic render' })); + if (factory) { + resolveEmbeddable!(factory.create({ id: 'test basic render' })); + } })(); const { renderApp } = await import('./applications/resolver_test'); From 77a859d43db820b468e45cd1628de63fbc9d3585 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 16 Mar 2020 15:46:17 -0400 Subject: [PATCH 018/115] [Remote clusters] Add support for proxy mode (#59221) --- .../remote_clusters_add.test.js | 50 + .../remote_clusters_list.test.js | 53 +- .../remote_clusters/common/constants.ts | 3 + .../common/lib/cluster_serialization.test.ts | 45 + .../common/lib/cluster_serialization.ts | 110 +- .../remote_clusters/common/lib/index.ts | 2 +- .../fixtures/remote_cluster.js | 10 + .../remote_cluster_form.test.js.snap | 1534 ++++++++++++++++- .../remote_cluster_form.js | 275 ++- .../remote_cluster_form.test.js | 10 + .../__snapshots__/validate_proxy.test.js.snap | 25 + .../remote_cluster_form/validators/index.js | 3 +- .../validators/validate_proxy.js | 44 + .../validators/validate_proxy.test.js | 25 + .../validators/validate_seed.js | 8 +- .../remote_cluster_edit.js | 40 +- .../connection_status/connection_status.js | 13 +- .../detail_panel/detail_panel.js | 425 +++-- .../remote_cluster_table.js | 84 +- .../application/services/documentation.ts | 2 + .../public/application/services/index.js | 2 +- ...idate_seed_node.js => validate_address.js} | 4 +- ..._node.test.js => validate_address.test.js} | 34 +- .../server/routes/api/add_route.test.ts | 91 +- .../server/routes/api/add_route.ts | 15 +- .../server/routes/api/delete_route.test.ts | 24 +- .../server/routes/api/get_route.test.ts | 3 + .../server/routes/api/get_route.ts | 17 +- .../server/routes/api/update_route.test.ts | 28 +- .../server/routes/api/update_route.ts | 17 +- .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - .../remote_clusters.helpers.js | 3 +- .../remote_clusters/remote_clusters.js | 7 + 34 files changed, 2765 insertions(+), 251 deletions(-) create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/__snapshots__/validate_proxy.test.js.snap create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.js create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.test.js rename x-pack/plugins/remote_clusters/public/application/services/{validate_seed_node.js => validate_address.js} (91%) rename x-pack/plugins/remote_clusters/public/application/services/{validate_seed_node.test.js => validate_address.test.js} (55%) diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_add.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_add.test.js index 8f34e7d84a08..78482198b1a5 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_add.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_add.test.js @@ -53,6 +53,17 @@ describe('Create Remote cluster', () => { expect(find('remoteClusterFormSkipUnavailableFormToggle').props()['aria-checked']).toBe(true); }); + test('should have a toggle to enable "proxy" mode for a remote cluster', () => { + expect(exists('remoteClusterFormConnectionModeToggle')).toBe(true); + + // By default it should be set to "false" + expect(find('remoteClusterFormConnectionModeToggle').props()['aria-checked']).toBe(false); + + form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle'); + + expect(find('remoteClusterFormConnectionModeToggle').props()['aria-checked']).toBe(true); + }); + test('should display errors and disable the save button when clicking "save" without filling the form', () => { expect(exists('remoteClusterFormGlobalError')).toBe(false); expect(find('remoteClusterFormSaveButton').props().disabled).toBe(false); @@ -144,5 +155,44 @@ describe('Create Remote cluster', () => { expect(form.getErrorsMessages()).toContain('A port is required.'); }); }); + + describe('proxy address', () => { + let actions; + let form; + + beforeEach(async () => { + ({ form, actions } = setup()); + + // Enable "proxy" mode + form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle'); + }); + + test('should only allow alpha-numeric characters and "-" (dash) in the proxy address "host" part', () => { + actions.clickSaveForm(); // display form errors + + const notInArray = array => value => array.indexOf(value) < 0; + + const expectInvalidChar = char => { + form.setInputValue('remoteClusterFormProxyAddressInput', `192.16${char}:3000`); + expect(form.getErrorsMessages()).toContain( + 'Address must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.' + ); + }; + + [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS] + .filter(notInArray(['-', '_', ':'])) + .forEach(expectInvalidChar); + }); + + test('should require a numeric "port" to be set', () => { + actions.clickSaveForm(); + + form.setInputValue('remoteClusterFormProxyAddressInput', '192.168.1.1'); + expect(form.getErrorsMessages()).toContain('A port is required.'); + + form.setInputValue('remoteClusterFormProxyAddressInput', '192.168.1.1:abc'); + expect(form.getErrorsMessages()).toContain('A port is required.'); + }); + }); }); }); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js index 1b7c600218ce..954deb8b98d3 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js @@ -15,6 +15,8 @@ import { import { getRouter } from '../../public/application/services'; import { getRemoteClusterMock } from '../../fixtures/remote_cluster'; +import { PROXY_MODE } from '../../common/constants'; + jest.mock('ui/new_platform'); const { setup } = pageHelpers.remoteClustersList; @@ -84,12 +86,26 @@ describe('', () => { const remoteCluster2 = getRemoteClusterMock({ name: `b${getRandomString()}`, isConnected: false, - connectedNodesCount: 0, - seeds: ['localhost:9500'], + connectedSocketsCount: 0, + proxyAddress: 'localhost:9500', isConfiguredByNode: true, + mode: PROXY_MODE, + seeds: null, + connectedNodesCount: null, + }); + const remoteCluster3 = getRemoteClusterMock({ + name: `c${getRandomString()}`, + isConnected: false, + connectedSocketsCount: 0, + proxyAddress: 'localhost:9500', + isConfiguredByNode: false, + mode: PROXY_MODE, + hasDeprecatedProxySetting: true, + seeds: null, + connectedNodesCount: null, }); - const remoteClusters = [remoteCluster1, remoteCluster2]; + const remoteClusters = [remoteCluster1, remoteCluster2, remoteCluster3]; beforeEach(async () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); @@ -118,17 +134,28 @@ describe('', () => { [ '', // Empty because the first column is the checkbox to select the row remoteCluster1.name, - remoteCluster1.seeds.join(', '), 'Connected', + 'default', + remoteCluster1.seeds.join(', '), remoteCluster1.connectedNodesCount.toString(), '', // Empty because the last column is for the "actions" on the resource ], [ '', remoteCluster2.name, - remoteCluster2.seeds.join(', '), 'Not connected', - remoteCluster2.connectedNodesCount.toString(), + PROXY_MODE, + remoteCluster2.proxyAddress, + remoteCluster2.connectedSocketsCount.toString(), + '', + ], + [ + '', + remoteCluster3.name, + 'Not connected', + PROXY_MODE, + remoteCluster2.proxyAddress, + remoteCluster2.connectedSocketsCount.toString(), '', ], ]); @@ -141,6 +168,14 @@ describe('', () => { ).toBe(1); }); + test('should have a tooltip to indicate that the cluster has a deprecated setting', () => { + const secondRow = rows[2].reactWrapper; // The third cluster has been defined with deprecated setting + expect( + findTestSubject(secondRow, 'remoteClustersTableListClusterWithDeprecatedSettingTooltip') + .length + ).toBe(1); + }); + describe('bulk delete button', () => { test('should be visible when a remote cluster is selected', () => { expect(exists('remoteClusterBulkDeleteButton')).toBe(false); @@ -199,8 +234,8 @@ describe('', () => { errors: [], }); - // Make sure that we have our 2 remote clusters in the table - expect(rows.length).toBe(2); + // Make sure that we have our 3 remote clusters in the table + expect(rows.length).toBe(3); actions.selectRemoteClusterAt(0); actions.clickBulkDeleteButton(); @@ -211,7 +246,7 @@ describe('', () => { ({ rows } = table.getMetaData('remoteClusterListTable')); - expect(rows.length).toBe(1); + expect(rows.length).toBe(2); expect(rows[0].columns[1].value).toEqual(remoteCluster2.name); }); }); diff --git a/x-pack/plugins/remote_clusters/common/constants.ts b/x-pack/plugins/remote_clusters/common/constants.ts index 353160de8bf4..20ad6da227c5 100644 --- a/x-pack/plugins/remote_clusters/common/constants.ts +++ b/x-pack/plugins/remote_clusters/common/constants.ts @@ -20,3 +20,6 @@ export const PLUGIN = { }; export const API_BASE_PATH = '/api/remote_clusters'; + +export const SNIFF_MODE = 'sniff'; +export const PROXY_MODE = 'proxy'; diff --git a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts index 476fbee7fb6a..5be6ed8828e6 100644 --- a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts +++ b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts @@ -9,6 +9,7 @@ import { deserializeCluster, serializeCluster } from './cluster_serialization'; describe('cluster_serialization', () => { describe('deserializeCluster()', () => { it('should throw an error for invalid arguments', () => { + // @ts-ignore expect(() => deserializeCluster('foo', 'bar')).toThrowError(); }); @@ -60,6 +61,39 @@ describe('cluster_serialization', () => { }); }); + it('should deserialize a cluster that contains a deprecated proxy address', () => { + expect( + deserializeCluster( + 'test_cluster', + { + seeds: ['localhost:9300'], + connected: true, + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + transport: { + ping_schedule: '-1', + compress: false, + }, + }, + 'localhost:9300' + ) + ).toEqual({ + name: 'test_cluster', + proxyAddress: 'localhost:9300', + mode: 'proxy', + hasDeprecatedProxySetting: true, + isConnected: true, + connectedNodesCount: 1, + maxConnectionsPerCluster: 3, + initialConnectTimeout: '30s', + skipUnavailable: false, + transportPingSchedule: '-1', + transportCompress: false, + }); + }); + it('should deserialize a cluster object with arbitrary missing properties', () => { expect( deserializeCluster('test_cluster', { @@ -84,6 +118,7 @@ describe('cluster_serialization', () => { describe('serializeCluster()', () => { it('should throw an error for invalid arguments', () => { + // @ts-ignore expect(() => serializeCluster('foo')).toThrowError(); }); @@ -105,8 +140,13 @@ describe('cluster_serialization', () => { cluster: { remote: { test_cluster: { + mode: null, + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, seeds: ['localhost:9300'], skip_unavailable: false, + server_name: null, }, }, }, @@ -125,8 +165,13 @@ describe('cluster_serialization', () => { cluster: { remote: { test_cluster: { + mode: null, + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, seeds: ['localhost:9300'], skip_unavailable: null, + server_name: null, }, }, }, diff --git a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts index 07ea79d42b80..53dc72eb1695 100644 --- a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts +++ b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts @@ -4,29 +4,96 @@ * you may not use this file except in compliance with the Elastic License. */ -export function deserializeCluster(name: string, esClusterObject: any): any { +import { PROXY_MODE } from '../constants'; + +export interface ClusterEs { + seeds?: string[]; + mode?: 'proxy' | 'sniff'; + connected?: boolean; + num_nodes_connected?: number; + max_connections_per_cluster?: number; + initial_connect_timeout?: string; + skip_unavailable?: boolean; + transport?: { + ping_schedule?: string; + compress?: boolean; + }; + address?: string; + max_socket_connections?: number; + num_sockets_connected?: number; +} + +export interface Cluster { + name: string; + seeds?: string[]; + skipUnavailable?: boolean; + nodeConnections?: number; + proxyAddress?: string; + proxySocketConnections?: number; + serverName?: string; + mode?: 'proxy' | 'sniff'; + isConnected?: boolean; + transportPingSchedule?: string; + transportCompress?: boolean; + connectedNodesCount?: number; + maxConnectionsPerCluster?: number; + initialConnectTimeout?: string; + connectedSocketsCount?: number; + hasDeprecatedProxySetting?: boolean; +} +export interface ClusterPayload { + persistent: { + cluster: { + remote: { + [key: string]: { + skip_unavailable?: boolean | null; + mode?: 'sniff' | 'proxy' | null; + proxy_address?: string | null; + proxy_socket_connections?: number | null; + server_name?: string | null; + seeds?: string[] | null; + node_connections?: number | null; + }; + }; + }; + }; +} + +export function deserializeCluster( + name: string, + esClusterObject: ClusterEs, + deprecatedProxyAddress?: string | undefined +): Cluster { if (!name || !esClusterObject || typeof esClusterObject !== 'object') { throw new Error('Unable to deserialize cluster'); } const { seeds, + mode, connected: isConnected, num_nodes_connected: connectedNodesCount, max_connections_per_cluster: maxConnectionsPerCluster, initial_connect_timeout: initialConnectTimeout, skip_unavailable: skipUnavailable, transport, + address: proxyAddress, + max_socket_connections: proxySocketConnections, + num_sockets_connected: connectedSocketsCount, } = esClusterObject; - let deserializedClusterObject: any = { + let deserializedClusterObject: Cluster = { name, - seeds, + mode, isConnected, connectedNodesCount, maxConnectionsPerCluster, initialConnectTimeout, skipUnavailable, + seeds, + proxyAddress, + proxySocketConnections, + connectedSocketsCount, }; if (transport) { @@ -39,30 +106,57 @@ export function deserializeCluster(name: string, esClusterObject: any): any { }; } + // If a user has a remote cluster with the deprecated proxy setting, + // we transform the data to support the new implementation and also flag the deprecation + if (deprecatedProxyAddress) { + deserializedClusterObject = { + ...deserializedClusterObject, + proxyAddress: deprecatedProxyAddress, + seeds: undefined, + hasDeprecatedProxySetting: true, + mode: PROXY_MODE, + }; + } + // It's unnecessary to send undefined values back to the client, so we can remove them. Object.keys(deserializedClusterObject).forEach(key => { - if (deserializedClusterObject[key] === undefined) { - delete deserializedClusterObject[key]; + if (deserializedClusterObject[key as keyof Cluster] === undefined) { + delete deserializedClusterObject[key as keyof Cluster]; } }); return deserializedClusterObject; } -export function serializeCluster(deserializedClusterObject: any): any { +export function serializeCluster(deserializedClusterObject: Cluster): ClusterPayload { if (!deserializedClusterObject || typeof deserializedClusterObject !== 'object') { throw new Error('Unable to serialize cluster'); } - const { name, seeds, skipUnavailable } = deserializedClusterObject; + const { + name, + seeds, + skipUnavailable, + mode, + nodeConnections, + proxyAddress, + proxySocketConnections, + serverName, + } = deserializedClusterObject; return { + // Background on why we only save as persistent settings detailed here: https://github.com/elastic/kibana/pull/26067#issuecomment-441848124 persistent: { cluster: { remote: { [name]: { - seeds: seeds ? seeds : null, skip_unavailable: skipUnavailable !== undefined ? skipUnavailable : null, + mode: mode ?? null, + proxy_address: proxyAddress ?? null, + proxy_socket_connections: proxySocketConnections ?? null, + server_name: serverName ?? null, + seeds: seeds ?? null, + node_connections: nodeConnections ?? null, }, }, }, diff --git a/x-pack/plugins/remote_clusters/common/lib/index.ts b/x-pack/plugins/remote_clusters/common/lib/index.ts index bc67bf21af03..52a0536bfd55 100644 --- a/x-pack/plugins/remote_clusters/common/lib/index.ts +++ b/x-pack/plugins/remote_clusters/common/lib/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { deserializeCluster, serializeCluster } from './cluster_serialization'; +export { deserializeCluster, serializeCluster, Cluster, ClusterEs } from './cluster_serialization'; diff --git a/x-pack/plugins/remote_clusters/fixtures/remote_cluster.js b/x-pack/plugins/remote_clusters/fixtures/remote_cluster.js index e3e087548cf0..6a3bcba21d77 100644 --- a/x-pack/plugins/remote_clusters/fixtures/remote_cluster.js +++ b/x-pack/plugins/remote_clusters/fixtures/remote_cluster.js @@ -5,12 +5,18 @@ */ import { getRandomString } from '../../../test_utils'; +import { SNIFF_MODE } from '../common/constants'; + export const getRemoteClusterMock = ({ name = getRandomString(), isConnected = true, connectedNodesCount = 1, + connectedSocketsCount, seeds = ['localhost:9400'], isConfiguredByNode = false, + mode = SNIFF_MODE, + proxyAddress, + hasDeprecatedProxySetting = false, } = {}) => ({ name, seeds, @@ -20,4 +26,8 @@ export const getRemoteClusterMock = ({ maxConnectionsPerCluster: 3, initialConnectTimeout: '30s', skipUnavailable: false, + mode, + connectedSocketsCount, + proxyAddress, + hasDeprecatedProxySetting, }); 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 8d6c5b040ce8..88b869b1d1d8 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 @@ -1,5 +1,1429 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`RemoteClusterForm proxy mode renders correct connection settings when user enables proxy mode 1`] = ` + + +
+ + } + fullWidth={true} + title={ + +

+ +

+
+ } + > + +
+ + + Name + + + +
+ +
+ + + + +
+ +
+ + A unique name for the remote cluster. + +
+
+
+
+
+
+ +
+ + } + fullWidth={true} + hasChildLabel={true} + hasEmptyLabelSpace={false} + helpText={ + + } + isInvalid={false} + label={ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ + Name can only contain letters, numbers, underscores, and dashes. + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + , + } + } + /> + } + labelType="label" + > + + } + onChange={[Function]} + /> + + + } + fullWidth={true} + title={ + +

+ +

+
+ } + > + +
+ + + Connection mode + + + +
+ +
+ + + + +
+ +
+ + Remote cluster connections work by configuring a remote cluster and connecting only to a limited number of nodes in that remote cluster. + + + + , + } + } + /> + } + labelType="label" + > +
+
+ + } + onBlur={[Function]} + onChange={[Function]} + onFocus={[Function]} + > +
+ + + + Use proxy mode + + +
+
+ +
+ + + , + } + } + > + Configure a remote cluster with a single proxy address. + + + + +
+
+
+
+
+
+
+
+
+
+
+ +
+ + } + fullWidth={true} + hasChildLabel={true} + hasEmptyLabelSpace={false} + helpText={ + + } + isInvalid={false} + label={ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ + The address used for all remote connections. + +
+
+
+
+
+ + } + label={ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ + The number of socket connections to open per remote cluster. + +
+
+
+
+
+ + } + label={ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ + An optional hostname string which will be sent in the server_name field of the TLS Server Name Indication extension if TLS is enabled. + +
+
+
+
+
+
+
+
+
+
+
+
+ +

+ + + , + "optionName": + + , + } + } + /> +

+ + } + fullWidth={true} + title={ + +

+ +

+
+ } + > + +
+ + + Make remote cluster optional + + + +
+ +
+ + + + +
+ +
+

+ + + , + "optionName": + + , + } + } + > + 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 + + + Skip if unavailable + + + . + + + + +

+
+
+
+
+
+
+ +
+ +
+
+ +
+ + + Skip if unavailable + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+ +
+ +
+ +
+ + + +
+
+
+
+
+
+ +
+ + + +
+
+
+
+ +`; + exports[`RemoteClusterForm renders untouched state 1`] = ` Array [
@@ -191,7 +1676,48 @@ Array [ > transport port - of the remote cluster. + of the remote cluster. Specify multiple seed nodes so discovery doesn't fail if a node is unavailable. +
+ + +
+
+ +
+
+
+
+ +
+
+
+ The number of gateway nodes to connect to.
@@ -490,7 +2016,7 @@ Array [ > transport port - of the remote cluster. + of the remote cluster. Specify multiple seed nodes so discovery doesn't fail if a node is 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 08cd01496a8b..358ffc03da78 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 @@ -15,6 +15,7 @@ import { EuiCallOut, EuiComboBox, EuiDescribedFormGroup, + EuiFieldNumber, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -33,16 +34,27 @@ import { htmlIdGenerator, } from '@elastic/eui'; -import { skippingDisconnectedClustersUrl, transportPortUrl } from '../../../services/documentation'; +import { + skippingDisconnectedClustersUrl, + transportPortUrl, + proxyModeUrl, +} from '../../../services/documentation'; import { RequestFlyout } from './request_flyout'; -import { validateName, validateSeeds, validateSeed } from './validators'; +import { validateName, validateSeeds, validateProxy, validateSeed } from './validators'; + +import { SNIFF_MODE, PROXY_MODE } from '../../../../../common/constants'; const defaultFields = { name: '', seeds: [], skipUnavailable: false, + mode: SNIFF_MODE, + nodeConnections: 3, + proxyAddress: '', + proxySocketConnections: 18, + serverName: '', }; const ERROR_TITLE_ID = 'removeClustersErrorTitle'; @@ -88,10 +100,12 @@ export class RemoteClusterForm extends Component { }; getFieldsErrors(fields, seedInput = '') { - const { name, seeds } = fields; + const { name, seeds, mode, proxyAddress } = fields; + return { name: validateName(name), - seeds: validateSeeds(seeds, seedInput), + seeds: mode === SNIFF_MODE ? validateSeeds(seeds, seedInput) : null, + proxyAddress: mode === PROXY_MODE ? validateProxy(proxyAddress) : null, }; } @@ -110,13 +124,38 @@ export class RemoteClusterForm extends Component { getAllFields() { const { - fields: { name, seeds, skipUnavailable }, + fields: { + name, + mode, + seeds, + nodeConnections, + proxyAddress, + proxySocketConnections, + serverName, + skipUnavailable, + }, } = this.state; + let modeSettings; + + if (mode === PROXY_MODE) { + modeSettings = { + proxyAddress, + proxySocketConnections, + serverName, + }; + } else { + modeSettings = { + seeds, + nodeConnections, + }; + } + return { name, - seeds, skipUnavailable, + mode, + ...modeSettings, }; } @@ -215,10 +254,10 @@ export class RemoteClusterForm extends Component { return hasErrors; }; - renderSeeds() { + renderSniffModeSettings() { const { areErrorsVisible, - fields: { seeds }, + fields: { seeds, nodeConnections }, fieldsErrors: { seeds: errorsSeeds }, localSeedErrors, } = this.state; @@ -231,26 +270,7 @@ export class RemoteClusterForm extends Component { const formattedSeeds = seeds.map(seed => ({ label: seed })); return ( - -

- -

- - } - description={ - - } - fullWidth - > + <> @@ -296,6 +316,187 @@ export class RemoteClusterForm extends Component { data-test-subj="remoteClusterFormSeedsInput" /> + + + } + helpText={ + + } + fullWidth + > + this.onFieldsChange({ nodeConnections: Number(e.target.value) || null })} + fullWidth + /> + + + ); + } + + renderProxyModeSettings() { + const { + areErrorsVisible, + fields: { proxyAddress, proxySocketConnections, serverName }, + fieldsErrors: { proxyAddress: errorProxyAddress }, + } = this.state; + + return ( + <> + + } + helpText={ + + } + isInvalid={Boolean(areErrorsVisible && errorProxyAddress)} + error={errorProxyAddress} + fullWidth + > + this.onFieldsChange({ proxyAddress: e.target.value })} + isInvalid={Boolean(areErrorsVisible && errorProxyAddress)} + data-test-subj="remoteClusterFormProxyAddressInput" + fullWidth + /> + + + + } + helpText={ + + } + fullWidth + > + + this.onFieldsChange({ proxySocketConnections: Number(e.target.value) || null }) + } + fullWidth + /> + + + } + helpText={ + + } + fullWidth + > + this.onFieldsChange({ serverName: e.target.value })} + fullWidth + /> + + + ); + } + + renderMode() { + const { + fields: { mode }, + } = this.state; + + return ( + +

+ +

+ + } + description={ + <> + + + + + ), + }} + /> + } + > + + } + checked={mode === PROXY_MODE} + data-test-subj="remoteClusterFormConnectionModeToggle" + onChange={e => + this.onFieldsChange({ mode: e.target.checked ? PROXY_MODE : SNIFF_MODE }) + } + /> + + + } + fullWidth + > + {mode === PROXY_MODE ? this.renderProxyModeSettings() : this.renderSniffModeSettings()}
); } @@ -522,7 +723,7 @@ export class RemoteClusterForm extends Component { renderErrors = () => { const { areErrorsVisible, - fieldsErrors: { name: errorClusterName, seeds: errorsSeeds }, + fieldsErrors: { name: errorClusterName, seeds: errorsSeeds, proxyAddress: errorProxyAddress }, localSeedErrors, } = this.state; @@ -564,6 +765,16 @@ export class RemoteClusterForm extends Component { }); } + if (errorProxyAddress) { + errorExplanations.push({ + key: 'seedsExplanation', + field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage', { + defaultMessage: 'The "Proxy address" field is invalid.', + }), + error: errorProxyAddress, + }); + } + const messagesToBeRendered = errorExplanations.length && (
@@ -662,7 +873,7 @@ export class RemoteClusterForm extends Component { - {this.renderSeeds()} + {this.renderMode()} {this.renderSkipUnavailable()} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js index 799bf1f4fd05..907fd2183265 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js @@ -21,6 +21,16 @@ describe('RemoteClusterForm', () => { expect(component).toMatchSnapshot(); }); + describe('proxy mode', () => { + test('renders correct connection settings when user enables proxy mode', () => { + const component = mountWithIntl( {}} />); + + findTestSubject(component, 'remoteClusterFormConnectionModeToggle').simulate('click'); + + expect(component).toMatchSnapshot(); + }); + }); + describe('validation', () => { test('renders invalid state and a global form error when the user tries to submit an invalid form', () => { const component = mountWithIntl( {}} />); diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/__snapshots__/validate_proxy.test.js.snap b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/__snapshots__/validate_proxy.test.js.snap new file mode 100644 index 000000000000..646b0b509f4e --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/__snapshots__/validate_proxy.test.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`validateProxy rejects proxy address when the address is invalid 1`] = ` + +`; + +exports[`validateProxy rejects proxy address when the port is invalid 1`] = ` + +`; + +exports[`validateProxy rejects proxy address when there's no input 1`] = ` + +`; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.js index 66a1016c7fcc..ec5f0b1166ce 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.js @@ -5,5 +5,6 @@ */ export { validateName } from './validate_name'; -export { validateSeed } from './validate_seed'; +export { validateProxy } from './validate_proxy'; export { validateSeeds } from './validate_seeds'; +export { validateSeed } from './validate_seed'; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.js new file mode 100644 index 000000000000..9648bd36c1a4 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.js @@ -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 React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { isAddressValid, isPortValid } from '../../../../services'; + +export function validateProxy(proxy) { + if (!proxy) { + return ( + + ); + } + + const isValid = isAddressValid(proxy); + + if (!isValid) { + return ( + + ); + } + + if (!isPortValid(proxy)) { + return ( + + ); + } + + return null; +} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.test.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.test.js new file mode 100644 index 000000000000..e6e69849e13a --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.test.js @@ -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 { validateProxy } from './validate_proxy'; + +describe('validateProxy', () => { + test(`rejects proxy address when there's no input`, () => { + expect(validateProxy(undefined)).toMatchSnapshot(); + }); + + test(`rejects proxy address when the address is invalid`, () => { + expect(validateProxy('___')).toMatchSnapshot(); + }); + + test(`rejects proxy address when the port is invalid`, () => { + expect(validateProxy('noport')).toMatchSnapshot(); + }); + + test(`accepts valid proxy address`, () => { + expect(validateProxy('localhost:3000')).toBe(null); + }); +}); diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.js index e2260504cc03..e312e77972fb 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.js @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { isSeedNodeValid, isSeedNodePortValid } from '../../../../services'; +import { isAddressValid, isPortValid } from '../../../../services'; export function validateSeed(seed) { const errors = []; @@ -15,7 +15,7 @@ export function validateSeed(seed) { return errors; } - const isValid = isSeedNodeValid(seed); + const isValid = isAddressValid(seed); if (!isValid) { errors.push( @@ -30,9 +30,7 @@ export function validateSeed(seed) { ); } - const isPortValid = isSeedNodePortValid(seed); - - if (!isPortValid) { + if (!isPortValid(seed)) { errors.push( i18n.translate('xpack.remoteClusters.remoteClusterForm.localSeedError.invalidPortMessage', { defaultMessage: 'A port is required.', diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js index f48d854da725..2c0936b319d0 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js @@ -158,7 +158,7 @@ export class RemoteClusterEdit extends Component { ); } - const { isConfiguredByNode } = cluster; + const { isConfiguredByNode, hasDeprecatedProxySetting } = cluster; if (isConfiguredByNode) { return ( @@ -178,14 +178,36 @@ export class RemoteClusterEdit extends Component { } return ( - + <> + {hasDeprecatedProxySetting ? ( + <> + + } + color="warning" + iconType="help" + > + + + + + ) : null} + + ); } diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/connection_status/connection_status.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/connection_status/connection_status.js index d6d3272c2abe..f032636af0bc 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/connection_status/connection_status.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/connection_status/connection_status.js @@ -10,7 +10,9 @@ import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiIconTip, EuiText } from '@elastic/eui'; -export function ConnectionStatus({ isConnected }) { +import { SNIFF_MODE, PROXY_MODE } from '../../../../../../common/constants'; + +export function ConnectionStatus({ isConnected, mode }) { let icon; let message; @@ -47,13 +49,16 @@ export function ConnectionStatus({ isConnected }) { - - - + {!isConnected && mode === SNIFF_MODE && ( + + + + )} ); } ConnectionStatus.propTypes = { isConnected: PropTypes.bool, + mode: PropTypes.oneOf([SNIFF_MODE, PROXY_MODE]), }; 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 1c8ba372aa74..89a48927f683 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 @@ -7,10 +7,13 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { + EuiBadge, EuiButton, EuiButtonEmpty, + EuiCallOut, EuiDescriptionList, EuiDescriptionListDescription, EuiDescriptionListTitle, @@ -21,6 +24,7 @@ import { EuiFlyoutFooter, EuiFlyoutHeader, EuiIcon, + EuiLink, EuiSpacer, EuiText, EuiTextColor, @@ -28,9 +32,11 @@ import { } from '@elastic/eui'; import { CRUD_APP_BASE_PATH } from '../../../constants'; +import { PROXY_MODE } from '../../../../../common/constants'; import { getRouterLinkProps } from '../../../services'; import { ConfiguredByNodeWarning } from '../../components'; import { ConnectionStatus, RemoveClusterButtonProvider } from '../components'; +import { proxyModeUrl } from '../../../services/documentation'; export class DetailPanel extends Component { static propTypes = { @@ -106,139 +112,312 @@ export class DetailPanel extends Component { ); } - renderCluster({ + renderClusterWithDeprecatedSettingWarning( + { hasDeprecatedProxySetting, isConfiguredByNode }, + clusterName + ) { + if (!hasDeprecatedProxySetting) { + return null; + } + return ( + <> + + } + color="warning" + iconType="help" + > + + + + ) : ( + + + + ), + }} + /> + + + + ); + } + + renderSniffModeDescriptionList({ isConnected, connectedNodesCount, skipUnavailable, seeds, maxConnectionsPerCluster, initialConnectTimeout, + mode, }) { return ( -
- -

- -

-
+ + + + + + + + + + + + + + + + + + + + + + + {connectedNodesCount} + + + - - - - - - - - + + + + + + + + + + {seeds.map(seed => ( + {seed} + ))} + + - - - - + + + + + + + + + {this.renderSkipUnavailableValue(skipUnavailable)} + + + - - - - - - + - - {connectedNodesCount} - - - + + + + + + + + + + {maxConnectionsPerCluster} + + - + + + + + + + + + {initialConnectTimeout} + + + + + ); + } - - - - - - - - - - {seeds.map(seed => ( - {seed} - ))} - - + renderProxyModeDescriptionList({ + isConnected, + skipUnavailable, + initialConnectTimeout, + proxyAddress, + proxySocketConnections, + connectedSocketsCount, + mode, + }) { + return ( + + + + + + + + + + + + + - - - - - - + + + + + + + + + {connectedSocketsCount ? connectedSocketsCount : '-'} + + + - - {this.renderSkipUnavailableValue(skipUnavailable)} - - - + - + + + + + + + + + + {proxyAddress} + + - - - - - - - + + + + + + + + + {this.renderSkipUnavailableValue(skipUnavailable)} + + + - - {maxConnectionsPerCluster} - - + - - - - - - + + + + + + + + + + {proxySocketConnections ? proxySocketConnections : '-'} + + - - {initialConnectTimeout} - - - - + + + + + + + + + {initialConnectTimeout} + + + + + ); + } + + renderCluster(cluster) { + return ( +
+ +

+ +

+
+ + + + {cluster.mode === PROXY_MODE + ? this.renderProxyModeDescriptionList(cluster) + : this.renderSniffModeDescriptionList(cluster)}
); } renderFlyoutBody() { - const { cluster } = this.props; + const { cluster, clusterName } = this.props; return ( @@ -246,6 +425,7 @@ export class DetailPanel extends Component { {cluster && ( {this.renderClusterConfiguredByNodeWarning(cluster)} + {this.renderClusterWithDeprecatedSettingWarning(cluster, clusterName)} {this.renderCluster(cluster)} )} @@ -315,7 +495,7 @@ export class DetailPanel extends Component { } render() { - const { isOpen, closeDetailPanel, clusterName } = this.props; + const { isOpen, closeDetailPanel, clusterName, cluster } = this.props; if (!isOpen) { return null; @@ -327,16 +507,33 @@ export class DetailPanel extends Component { onClose={closeDetailPanel} aria-labelledby="remoteClusterDetailsFlyoutTitle" size="m" - maxWidth={400} + maxWidth={550} > - -

{clusterName}

-
+ + + +

{clusterName}

+
+
+ {cluster && cluster.mode === PROXY_MODE ? ( + + {' '} + + {cluster.mode} + + + ) : null} +
{this.renderFlyoutBody()} 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 62c417b19904..ec20805ccd91 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 @@ -21,6 +21,7 @@ import { } from '@elastic/eui'; import { CRUD_APP_BASE_PATH, UIM_SHOW_DETAILS_CLICK } from '../../../constants'; +import { PROXY_MODE } from '../../../../../common/constants'; import { getRouterLinkProps, trackUiMetric, METRIC_TYPE } from '../../../services'; import { ConnectionStatus, RemoveClusterButtonProvider } from '../components'; @@ -83,7 +84,7 @@ export class RemoteClusterTable extends Component { }), sortable: true, truncateText: false, - render: (name, { isConfiguredByNode }) => { + render: (name, { isConfiguredByNode, hasDeprecatedProxySetting }) => { const link = ( + + {link} + + + + + } + /> + + + ); + } + return link; }, }, - { - field: 'seeds', - name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.seedsColumnTitle', { - defaultMessage: 'Seeds', - }), - truncateText: true, - render: seeds => seeds.join(', '), - }, { field: 'isConnected', name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.connectedColumnTitle', { - defaultMessage: 'Connection', + defaultMessage: 'Status', }), sortable: true, - render: isConnected => , + render: (isConnected, { mode }) => ( + + ), width: '240px', }, { - field: 'connectedNodesCount', + field: 'mode', + name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.modeColumnTitle', { + defaultMessage: 'Mode', + }), + sortable: true, + render: mode => + mode === PROXY_MODE + ? mode + : i18n.translate('xpack.remoteClusters.remoteClusterList.table.sniffModeDescription', { + defaultMessage: 'default', + }), + }, + { + field: 'mode', + name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.addressesColumnTitle', { + defaultMessage: 'Addresses', + }), + truncateText: true, + render: (mode, { seeds, proxyAddress }) => { + if (mode === PROXY_MODE) { + return proxyAddress; + } + return seeds.join(', '); + }, + }, + { + field: 'mode', name: i18n.translate( - 'xpack.remoteClusters.remoteClusterList.table.connectedNodesColumnTitle', + 'xpack.remoteClusters.remoteClusterList.table.connectionsColumnTitle', { - defaultMessage: 'Connected nodes', + defaultMessage: 'Connections', } ), sortable: true, width: '160px', + render: (mode, { connectedNodesCount, connectedSocketsCount }) => { + if (mode === PROXY_MODE) { + return connectedSocketsCount; + } + return connectedNodesCount; + }, }, { name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.actionsColumnTitle', { 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 38cf2223a313..f6f5dc987c2e 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/documentation.ts +++ b/x-pack/plugins/remote_clusters/public/application/services/documentation.ts @@ -9,6 +9,7 @@ import { DocLinksStart } from 'kibana/public'; export let skippingDisconnectedClustersUrl: string; export let remoteClustersUrl: string; export let transportPortUrl: string; +export let proxyModeUrl: string; export function init(docLinks: DocLinksStart): void { const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; @@ -17,4 +18,5 @@ export function init(docLinks: DocLinksStart): void { skippingDisconnectedClustersUrl = `${esDocBasePath}/modules-cross-cluster-search.html#_skipping_disconnected_clusters`; remoteClustersUrl = `${esDocBasePath}/modules-remote-clusters.html`; transportPortUrl = `${esDocBasePath}/modules-transport.html`; + proxyModeUrl = `${esDocBasePath}/modules-remote-clusters.html#proxy-mode`; } diff --git a/x-pack/plugins/remote_clusters/public/application/services/index.js b/x-pack/plugins/remote_clusters/public/application/services/index.js index 031770d9500e..387a04b6e5d8 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/index.js +++ b/x-pack/plugins/remote_clusters/public/application/services/index.js @@ -10,7 +10,7 @@ export { showApiError, showApiWarning } from './api_errors'; export { initRedirect, redirect } from './redirect'; -export { isSeedNodeValid, isSeedNodePortValid } from './validate_seed_node'; +export { isAddressValid, isPortValid } from './validate_address'; export { extractQueryParams } from './query_params'; diff --git a/x-pack/plugins/remote_clusters/public/application/services/validate_seed_node.js b/x-pack/plugins/remote_clusters/public/application/services/validate_address.js similarity index 91% rename from x-pack/plugins/remote_clusters/public/application/services/validate_seed_node.js rename to x-pack/plugins/remote_clusters/public/application/services/validate_address.js index 714b5cf44de2..7e12b9c06595 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/validate_seed_node.js +++ b/x-pack/plugins/remote_clusters/public/application/services/validate_address.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export function isSeedNodeValid(seedNode) { +export function isAddressValid(seedNode) { if (!seedNode) { return false; } @@ -23,7 +23,7 @@ export function isSeedNodeValid(seedNode) { return !containsInvalidCharacters; } -export function isSeedNodePortValid(seedNode) { +export function isPortValid(seedNode) { if (!seedNode) { return false; } diff --git a/x-pack/plugins/remote_clusters/public/application/services/validate_seed_node.test.js b/x-pack/plugins/remote_clusters/public/application/services/validate_address.test.js similarity index 55% rename from x-pack/plugins/remote_clusters/public/application/services/validate_seed_node.test.js rename to x-pack/plugins/remote_clusters/public/application/services/validate_address.test.js index 36e989a41b06..2551f4fac790 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/validate_seed_node.test.js +++ b/x-pack/plugins/remote_clusters/public/application/services/validate_address.test.js @@ -4,75 +4,75 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isSeedNodeValid, isSeedNodePortValid } from './validate_seed_node'; +import { isAddressValid, isPortValid } from './validate_address'; -describe('Validate seed node', () => { +describe('Validate address', () => { describe('isSeedNodeValid', () => { describe('rejects', () => { it('adjacent periods', () => { - expect(isSeedNodeValid('a..b')).toBe(false); + expect(isAddressValid('a..b')).toBe(false); }); it('underscores', () => { - expect(isSeedNodeValid('____')).toBe(false); + expect(isAddressValid('____')).toBe(false); }); ['/', '\\', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '=', '+', '?'].forEach(char => { it(char, () => { - expect(isSeedNodeValid(char)).toBe(false); + expect(isAddressValid(char)).toBe(false); }); }); }); describe('accepts', () => { it('uppercase letters', () => { - expect(isSeedNodeValid('A.B.C.D')).toBe(true); + expect(isAddressValid('A.B.C.D')).toBe(true); }); it('lowercase letters', () => { - expect(isSeedNodeValid('a')).toBe(true); + expect(isAddressValid('a')).toBe(true); }); it('numbers', () => { - expect(isSeedNodeValid('56546354')).toBe(true); + expect(isAddressValid('56546354')).toBe(true); }); it('dashes', () => { - expect(isSeedNodeValid('----')).toBe(true); + expect(isAddressValid('----')).toBe(true); }); it('many parts', () => { - expect(isSeedNodeValid('abcd.efgh.ijkl.mnop.qrst.uvwx.yz')).toBe(true); + expect(isAddressValid('abcd.efgh.ijkl.mnop.qrst.uvwx.yz')).toBe(true); }); }); }); - describe('isSeedNodePortValid', () => { + describe('isPortValid', () => { describe('rejects', () => { it('missing port', () => { - expect(isSeedNodePortValid('abcd')).toBe(false); + expect(isPortValid('abcd')).toBe(false); }); it('empty port', () => { - expect(isSeedNodePortValid('abcd:')).toBe(false); + expect(isPortValid('abcd:')).toBe(false); }); it('letters', () => { - expect(isSeedNodePortValid('ab:cd')).toBe(false); + expect(isPortValid('ab:cd')).toBe(false); }); it('non-numbers', () => { - expect(isSeedNodePortValid('ab:5 0')).toBe(false); + expect(isPortValid('ab:5 0')).toBe(false); }); it('multiple ports', () => { - expect(isSeedNodePortValid('ab:cd:9000')).toBe(false); + expect(isPortValid('ab:cd:9000')).toBe(false); }); }); describe('accepts', () => { it('a single numeric port, even beyond the standard port range', () => { - expect(isSeedNodePortValid('abcd:100000000')).toBe(true); + expect(isPortValid('abcd:100000000')).toBe(true); }); }); }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts index a6edd15995d7..34d741aa4b7d 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts @@ -80,7 +80,7 @@ describe('ADD remote clusters', () => { }; describe('success', () => { - addRemoteClustersTest('adds remote cluster', { + addRemoteClustersTest(`adds remote cluster with "sniff" mode`, { apiResponses: [ async () => ({}), async () => ({ @@ -106,6 +106,7 @@ describe('ADD remote clusters', () => { payload: { name: 'test', seeds: ['127.0.0.1:9300'], + mode: 'sniff', skipUnavailable: false, }, asserts: { @@ -117,7 +118,79 @@ describe('ADD remote clusters', () => { body: { persistent: { cluster: { - remote: { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false } }, + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + mode: 'sniff', + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + }, + }, + }, + }, + }, + }, + ], + ], + statusCode: 200, + result: { + acknowledged: true, + }, + }, + }); + addRemoteClustersTest(`adds remote cluster with "proxy" mode`, { + apiResponses: [ + async () => ({}), + async () => ({ + acknowledged: true, + persistent: { + cluster: { + remote: { + test: { + connected: true, + mode: 'proxy', + seeds: ['127.0.0.1:9300'], + num_sockets_connected: 1, + max_socket_connections: 18, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }, + }, + }, + transient: {}, + }), + ], + payload: { + name: 'test', + proxyAddress: '127.0.0.1:9300', + mode: 'proxy', + skipUnavailable: false, + serverName: 'foobar', + }, + asserts: { + apiArguments: [ + ['cluster.remoteInfo'], + [ + 'cluster.putSettings', + { + body: { + persistent: { + cluster: { + remote: { + test: { + seeds: null, + skip_unavailable: false, + mode: 'proxy', + node_connections: null, + proxy_address: '127.0.0.1:9300', + proxy_socket_connections: null, + server_name: 'foobar', + }, + }, }, }, }, @@ -151,6 +224,7 @@ describe('ADD remote clusters', () => { name: 'test', seeds: ['127.0.0.1:9300'], skipUnavailable: false, + mode: 'sniff', }, asserts: { apiArguments: [['cluster.remoteInfo']], @@ -167,6 +241,7 @@ describe('ADD remote clusters', () => { name: 'test', seeds: ['127.0.0.1:9300'], skipUnavailable: false, + mode: 'sniff', }, asserts: { apiArguments: [ @@ -177,7 +252,17 @@ describe('ADD remote clusters', () => { body: { persistent: { cluster: { - remote: { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false } }, + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + mode: 'sniff', + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + }, + }, }, }, }, diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts index e4ede01ca23e..5e0fce82376e 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts @@ -9,17 +9,22 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { RequestHandler } from 'src/core/server'; -import { serializeCluster } from '../../../common/lib'; +import { serializeCluster, Cluster } from '../../../common/lib'; import { doesClusterExist } from '../../lib/does_cluster_exist'; -import { API_BASE_PATH } from '../../../common/constants'; +import { API_BASE_PATH, PROXY_MODE, SNIFF_MODE } from '../../../common/constants'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { isEsError } from '../../lib/is_es_error'; import { RouteDependencies } from '../../types'; const bodyValidation = schema.object({ name: schema.string(), - seeds: schema.arrayOf(schema.string()), skipUnavailable: schema.boolean(), + mode: schema.oneOf([schema.literal(PROXY_MODE), schema.literal(SNIFF_MODE)]), + seeds: schema.nullable(schema.arrayOf(schema.string())), + nodeConnections: schema.nullable(schema.number()), + proxyAddress: schema.nullable(schema.string()), + proxySocketConnections: schema.nullable(schema.number()), + serverName: schema.nullable(schema.string()), }); type RouteBody = TypeOf; @@ -33,7 +38,7 @@ export const register = (deps: RouteDependencies): void => { try { const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; - const { name, seeds, skipUnavailable } = request.body; + const { name } = request.body; // Check if cluster already exists. const existingCluster = await doesClusterExist(callAsCurrentUser, name); @@ -50,7 +55,7 @@ export const register = (deps: RouteDependencies): void => { }); } - const addClusterPayload = serializeCluster({ name, seeds, skipUnavailable }); + const addClusterPayload = serializeCluster(request.body as Cluster); const updateClusterResponse = await callAsCurrentUser('cluster.putSettings', { body: addClusterPayload, }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts index 04deb62d2c2d..cf14f8a67054 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts @@ -113,7 +113,17 @@ describe('DELETE remote clusters', () => { body: { persistent: { cluster: { - remote: { test: { seeds: null, skip_unavailable: null } }, + remote: { + test: { + seeds: null, + skip_unavailable: null, + mode: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + node_connections: null, + }, + }, }, }, }, @@ -211,7 +221,17 @@ describe('DELETE remote clusters', () => { body: { persistent: { cluster: { - remote: { test: { seeds: null, skip_unavailable: null } }, + remote: { + test: { + seeds: null, + skip_unavailable: null, + mode: null, + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + }, + }, }, }, }, diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts index 90955be85859..d81b50f1148d 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts @@ -89,6 +89,7 @@ describe('GET remote clusters', () => { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false, + mode: 'sniff', }, }, }, @@ -120,6 +121,7 @@ describe('GET remote clusters', () => { initialConnectTimeout: '30s', skipUnavailable: false, isConfiguredByNode: false, + mode: 'sniff', }, ], }, @@ -170,6 +172,7 @@ describe('GET remote clusters', () => { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false, + mode: 'sniff', }, }, }, diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts index 44b6284109ac..abd44977d8e4 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts @@ -33,13 +33,28 @@ export const register = (deps: RouteDependencies): void => { const cluster = clustersByName[clusterName]; const isTransient = transientClusterNames.includes(clusterName); const isPersistent = persistentClusterNames.includes(clusterName); + // If the cluster hasn't been stored in the cluster state, then it's defined by the // node's config file. const isConfiguredByNode = !isTransient && !isPersistent; + // Pre-7.6, ES supported an undocumented "proxy" field + // ES does not handle migrating this to the new implementation, so we need to surface it in the UI + // This value is not available via the GET /_remote/info API, so we get it from the cluster settings + const deprecatedProxyAddress = isPersistent + ? get(clusterSettings, `persistent.cluster.remote[${clusterName}].proxy`, undefined) + : undefined; + + // server_name is not available via the GET /_remote/info API, so we get it from the cluster settings + // Per https://github.com/elastic/kibana/pull/26067#issuecomment-441848124, we only look at persistent settings + const serverName = isPersistent + ? get(clusterSettings, `persistent.cluster.remote[${clusterName}].server_name`, undefined) + : undefined; + return { - ...deserializeCluster(clusterName, cluster), + ...deserializeCluster(clusterName, cluster, deprecatedProxyAddress), isConfiguredByNode, + serverName, }; }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts index 9ba239c3ff66..84ba9587ddfa 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts @@ -129,6 +129,7 @@ describe('UPDATE remote clusters', () => { payload: { seeds: ['127.0.0.1:9300'], skipUnavailable: true, + mode: 'sniff', }, asserts: { apiArguments: [ @@ -139,7 +140,17 @@ describe('UPDATE remote clusters', () => { body: { persistent: { cluster: { - remote: { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: true } }, + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: true, + mode: 'sniff', + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + }, + }, }, }, }, @@ -156,6 +167,7 @@ describe('UPDATE remote clusters', () => { name: 'test', seeds: ['127.0.0.1:9300'], skipUnavailable: true, + mode: 'sniff', }, }, }); @@ -167,6 +179,7 @@ describe('UPDATE remote clusters', () => { payload: { seeds: ['127.0.0.1:9300'], skipUnavailable: false, + mode: 'sniff', }, params: { name: 'test', @@ -198,6 +211,7 @@ describe('UPDATE remote clusters', () => { payload: { seeds: ['127.0.0.1:9300'], skipUnavailable: false, + mode: 'sniff', }, params: { name: 'test', @@ -211,7 +225,17 @@ describe('UPDATE remote clusters', () => { body: { persistent: { cluster: { - remote: { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false } }, + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + mode: 'sniff', + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + }, + }, }, }, }, diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts index ed584307d84c..14b161b6f26b 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts @@ -9,16 +9,21 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { RequestHandler } from 'src/core/server'; -import { API_BASE_PATH } from '../../../common/constants'; -import { serializeCluster, deserializeCluster } from '../../../common/lib'; +import { API_BASE_PATH, SNIFF_MODE, PROXY_MODE } from '../../../common/constants'; +import { serializeCluster, deserializeCluster, Cluster, ClusterEs } from '../../../common/lib'; import { doesClusterExist } from '../../lib/does_cluster_exist'; import { RouteDependencies } from '../../types'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { isEsError } from '../../lib/is_es_error'; const bodyValidation = schema.object({ - seeds: schema.arrayOf(schema.string()), skipUnavailable: schema.boolean(), + mode: schema.oneOf([schema.literal(PROXY_MODE), schema.literal(SNIFF_MODE)]), + seeds: schema.nullable(schema.arrayOf(schema.string())), + nodeConnections: schema.nullable(schema.number()), + proxyAddress: schema.nullable(schema.string()), + proxySocketConnections: schema.nullable(schema.number()), + serverName: schema.nullable(schema.string()), }); const paramsValidation = schema.object({ @@ -39,7 +44,6 @@ export const register = (deps: RouteDependencies): void => { const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; const { name } = request.params; - const { seeds, skipUnavailable } = request.body; // Check if cluster does exist. const existingCluster = await doesClusterExist(callAsCurrentUser, name); @@ -57,13 +61,14 @@ export const register = (deps: RouteDependencies): void => { } // Update cluster as new settings - const updateClusterPayload = serializeCluster({ name, seeds, skipUnavailable }); + const updateClusterPayload = serializeCluster({ ...request.body, name } as Cluster); + const updateClusterResponse = await callAsCurrentUser('cluster.putSettings', { body: updateClusterPayload, }); const acknowledged = get(updateClusterResponse, 'acknowledged'); - const cluster = get(updateClusterResponse, `persistent.cluster.remote.${name}`); + const cluster = get(updateClusterResponse, `persistent.cluster.remote.${name}`) as ClusterEs; if (acknowledged && cluster) { const body = { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ad49f0242e8e..9b842f736b8b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10081,10 +10081,7 @@ "xpack.remoteClusters.remoteClusterForm.saveButtonLabel": "保存", "xpack.remoteClusters.remoteClusterForm.sectionNameDescription": "リモートクラスターの固有の名前です。", "xpack.remoteClusters.remoteClusterForm.sectionNameTitle": "名前", - "xpack.remoteClusters.remoteClusterForm.sectionSeedsDescription1": "クラスターステータスのクエリを実行するリモートクラスターノードのリストです。1 つのノードが利用できない場合にディスカバリが失敗しないよう、複数シードノードを指定してください。", - "xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText": "リモートクラスターの {transportPort} の前にくる IP アドレスまたはホスト名です。", "xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText.transportPortLinkText": "トランスポートポート", - "xpack.remoteClusters.remoteClusterForm.sectionSeedsTitle": "クラスターディスカバリのシードノード", "xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription": "デフォルトで、リクエストのリモートクラスターのどれかが利用できないと、リクエストは失敗となります。このクラスターが利用できない場合にリクエストを他のリモートクラスターに送信し続けるには、{optionName} を有効にします。{learnMoreLink}", "xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.learnMoreLinkLabel": "詳細", "xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.optionNameLabel": "利用不可の場合スキップ", @@ -10106,11 +10103,9 @@ "xpack.remoteClusters.remoteClusterList.table.actionEditDescription": "リモートクラスターを編集します", "xpack.remoteClusters.remoteClusterList.table.actionsColumnTitle": "アクション", "xpack.remoteClusters.remoteClusterList.table.connectedColumnTitle": "接続", - "xpack.remoteClusters.remoteClusterList.table.connectedNodesColumnTitle": "接続済みのノード", "xpack.remoteClusters.remoteClusterList.table.isConfiguredByNodeMessage": "elasticsearch.yml で定義されています", "xpack.remoteClusters.remoteClusterList.table.nameColumnTitle": "名前", "xpack.remoteClusters.remoteClusterList.table.removeButtonLabel": "{count, plural, one {リモートクラスター} other {{count}リモートクラスター}}を削除", - "xpack.remoteClusters.remoteClusterList.table.seedsColumnTitle": "シード", "xpack.remoteClusters.remoteClusterListTitle": "リモートクラスター", "xpack.remoteClusters.removeAction.errorMultipleNotificationTitle": "「{count}」リモートクラスターの削除中にエラーが発生", "xpack.remoteClusters.removeAction.errorSingleNotificationTitle": "リモートクラスター「{name}」の削除中にエラーが発生", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 76ecf333eb10..97c30f180dfe 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10081,10 +10081,7 @@ "xpack.remoteClusters.remoteClusterForm.saveButtonLabel": "保存", "xpack.remoteClusters.remoteClusterForm.sectionNameDescription": "远程集群的唯一名称。", "xpack.remoteClusters.remoteClusterForm.sectionNameTitle": "名称", - "xpack.remoteClusters.remoteClusterForm.sectionSeedsDescription1": "要查询集群状态的远程集群节点的列表。指定多个种子节点,以便在节点不可用时发现不会失败。", - "xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText": "IP 地址或主机名,后跟远程集群的 {transportPort}。", "xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText.transportPortLinkText": "传输端口", - "xpack.remoteClusters.remoteClusterForm.sectionSeedsTitle": "用于集群发现的种子节点", "xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription": "默认情况下,如果任何查询的远程集群不可用,请求将失败。要在此集群不可用时继续向其他远程集群发送请求,请启用 {optionName}。{learnMoreLink}", "xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.learnMoreLinkLabel": "了解详情。", "xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.optionNameLabel": "如果不可用,则跳过", @@ -10106,11 +10103,9 @@ "xpack.remoteClusters.remoteClusterList.table.actionEditDescription": "编辑远程集群", "xpack.remoteClusters.remoteClusterList.table.actionsColumnTitle": "操作", "xpack.remoteClusters.remoteClusterList.table.connectedColumnTitle": "连接", - "xpack.remoteClusters.remoteClusterList.table.connectedNodesColumnTitle": "已连接节点", "xpack.remoteClusters.remoteClusterList.table.isConfiguredByNodeMessage": "在 elasticsearch.yml 中定义", "xpack.remoteClusters.remoteClusterList.table.nameColumnTitle": "名称", "xpack.remoteClusters.remoteClusterList.table.removeButtonLabel": "删除 {count, plural, one { 个远程集群} other {{count} 个远程集群}}", - "xpack.remoteClusters.remoteClusterList.table.seedsColumnTitle": "种子", "xpack.remoteClusters.remoteClusterListTitle": "远程集群", "xpack.remoteClusters.removeAction.errorMultipleNotificationTitle": "删除 “{count}” 个远程集群时出错", "xpack.remoteClusters.removeAction.errorSingleNotificationTitle": "删除远程集群 “{name}” 时出错", diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/remote_clusters.helpers.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/remote_clusters.helpers.js index d8cee1db9a2b..4462fcf75d5d 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/remote_clusters.helpers.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/remote_clusters.helpers.js @@ -25,7 +25,8 @@ export const registerHelpers = supertest => { .post(`${REMOTE_CLUSTERS_API_BASE_PATH}`) .set('kbn-xsrf', 'xxx') .send({ - name: name, + name, + mode: 'sniff', seeds: [`localhost:${esTransportPort}`], skipUnavailable: true, }); diff --git a/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js b/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js index 677d22ff7498..7921186000e1 100644 --- a/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js +++ b/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js @@ -40,6 +40,7 @@ export default function({ getService }) { name: 'test_cluster', seeds: [NODE_SEED], skipUnavailable: true, + mode: 'sniff', }) .expect(200); @@ -58,6 +59,7 @@ export default function({ getService }) { name: 'test_cluster', seeds: [NODE_SEED], skipUnavailable: false, + mode: 'sniff', }) .expect(409); @@ -79,6 +81,7 @@ export default function({ getService }) { .send({ skipUnavailable: false, seeds: [NODE_SEED], + mode: 'sniff', }) .expect(200); @@ -87,6 +90,7 @@ export default function({ getService }) { skipUnavailable: 'false', // ES issue #35671 seeds: [NODE_SEED], isConfiguredByNode: false, + mode: 'sniff', }); }); }); @@ -109,6 +113,7 @@ export default function({ getService }) { initialConnectTimeout: '30s', skipUnavailable: false, isConfiguredByNode: false, + mode: 'sniff', }, ]); }); @@ -139,6 +144,7 @@ export default function({ getService }) { name: 'test_cluster1', seeds: [NODE_SEED], skipUnavailable: true, + mode: 'sniff', }) .expect(200); @@ -149,6 +155,7 @@ export default function({ getService }) { name: 'test_cluster2', seeds: [NODE_SEED], skipUnavailable: true, + mode: 'sniff', }) .expect(200); From 93914b6cb5afbc382699818c1beab6568a5dabac Mon Sep 17 00:00:00 2001 From: marshallmain <55718608+marshallmain@users.noreply.github.com> Date: Mon, 16 Mar 2020 15:53:49 -0400 Subject: [PATCH 019/115] [Endpoint] Sample data generator CLI script (#59952) * start on cli * make it work * cleanup * remove failed attempt code * update package and tsconfig * remove empty file * generate resolver events from multiple endpoints * re-add child randomization * align index names with real plugin * remove duplication * better naming * add temporary mapping to sample data generator * error handling, move tsconfig * add readme * Update README.md * move mapping from common to scripts * make delete index option * remove unnecessary map call * fix import style Co-authored-by: Elastic Machine --- .../endpoint/common/generate_data.test.ts | 2 +- .../plugins/endpoint/common/generate_data.ts | 111 +- x-pack/plugins/endpoint/package.json | 7 +- .../endpoint/store/managing/index.test.ts | 2 +- .../store/managing/middleware.test.ts | 2 +- x-pack/plugins/endpoint/scripts/README.md | 46 + .../endpoint/scripts/cli_tsconfig.json | 8 + x-pack/plugins/endpoint/scripts/mapping.json | 2367 +++++++++++++++++ .../endpoint/scripts/resolver_generator.ts | 161 ++ 9 files changed, 2634 insertions(+), 72 deletions(-) create mode 100644 x-pack/plugins/endpoint/scripts/README.md create mode 100644 x-pack/plugins/endpoint/scripts/cli_tsconfig.json create mode 100644 x-pack/plugins/endpoint/scripts/mapping.json create mode 100644 x-pack/plugins/endpoint/scripts/resolver_generator.ts diff --git a/x-pack/plugins/endpoint/common/generate_data.test.ts b/x-pack/plugins/endpoint/common/generate_data.test.ts index ebe3c25eef82..e14f506c825f 100644 --- a/x-pack/plugins/endpoint/common/generate_data.test.ts +++ b/x-pack/plugins/endpoint/common/generate_data.test.ts @@ -151,7 +151,7 @@ describe('data generator', () => { const timestamp = new Date().getTime(); const root = generator.generateEvent({ timestamp }); const generations = 2; - const events = generator.generateDescendantsTree(root, generations); + const events = [root, ...generator.generateDescendantsTree(root, generations)]; const rootNode = buildResolverTree(events); const visitedEvents = countResolverEvents(rootNode, generations); expect(visitedEvents).toEqual(events.length); diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index a91cf0ffca78..b539e309d76f 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -6,7 +6,7 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; -import { AlertEvent, EndpointEvent, EndpointMetadata, OSFields } from './types'; +import { AlertEvent, EndpointEvent, EndpointMetadata, OSFields, HostFields } from './types'; export type Event = AlertEvent | EndpointEvent; @@ -67,31 +67,51 @@ const FILE_OPERATIONS: string[] = ['creation', 'open', 'rename', 'execution', 'd // These are from the v1 schemas and aren't all valid ECS event categories, still in flux const OTHER_EVENT_CATEGORIES: string[] = ['driver', 'file', 'library', 'network', 'registry']; +interface HostInfo { + agent: { + version: string; + id: string; + }; + host: HostFields; + endpoint: { + policy: { + id: string; + }; + }; +} + export class EndpointDocGenerator { - agentId: string; - hostId: string; - hostname: string; - macAddress: string[]; - ip: string[]; - agentVersion: string; - os: OSFields; - policy: { name: string; id: string }; + commonInfo: HostInfo; random: seedrandom.prng; constructor(seed = Math.random().toString()) { this.random = seedrandom(seed); - this.hostId = this.seededUUIDv4(); - this.agentId = this.seededUUIDv4(); - this.hostname = this.randomHostname(); - this.ip = this.randomArray(3, () => this.randomIP()); - this.macAddress = this.randomArray(3, () => this.randomMac()); - this.agentVersion = this.randomVersion(); - this.os = this.randomChoice(OS); - this.policy = this.randomChoice(POLICIES); + this.commonInfo = this.createHostData(); } - public randomizeIPs() { - this.ip = this.randomArray(3, () => this.randomIP()); + // This function will create new values for all the host fields, so documents from a different endpoint can be created + // This provides a convenient way to make documents from multiple endpoints that are all tied to a single seed value + public randomizeHostData() { + this.commonInfo = this.createHostData(); + } + + private createHostData(): HostInfo { + return { + agent: { + version: this.randomVersion(), + id: this.seededUUIDv4(), + }, + host: { + id: this.seededUUIDv4(), + hostname: this.randomHostname(), + ip: this.randomArray(3, () => this.randomIP()), + mac: this.randomArray(3, () => this.randomMac()), + os: this.randomChoice(OS), + }, + endpoint: { + policy: this.randomChoice(POLICIES), + }, + }; } public generateEndpointMetadata(ts = new Date().getTime()): EndpointMetadata { @@ -100,22 +120,7 @@ export class EndpointDocGenerator { event: { created: ts, }, - endpoint: { - policy: { - id: this.policy.id, - }, - }, - agent: { - version: this.agentVersion, - id: this.agentId, - }, - host: { - id: this.hostId, - hostname: this.hostname, - ip: this.ip, - mac: this.macAddress, - os: this.os, - }, + ...this.commonInfo, }; } @@ -125,11 +130,8 @@ export class EndpointDocGenerator { parentEntityID?: string ): AlertEvent { return { + ...this.commonInfo, '@timestamp': ts, - agent: { - id: this.agentId, - version: this.agentVersion, - }, event: { action: this.randomChoice(FILE_OPERATIONS), kind: 'alert', @@ -139,11 +141,6 @@ export class EndpointDocGenerator { module: 'endpoint', type: 'creation', }, - endpoint: { - policy: { - id: this.policy.id, - }, - }, file: { owner: 'SYSTEM', name: 'fake_malware.exe', @@ -169,13 +166,6 @@ export class EndpointDocGenerator { }, temp_file_path: 'C:/temp/fake_malware.exe', }, - host: { - id: this.hostId, - hostname: this.hostname, - ip: this.ip, - mac: this.macAddress, - os: this.os, - }, process: { pid: 2, name: 'malware writer', @@ -243,11 +233,7 @@ export class EndpointDocGenerator { public generateEvent(options: EventOptions = {}): EndpointEvent { return { '@timestamp': options.timestamp ? options.timestamp : new Date().getTime(), - agent: { - id: this.agentId, - version: this.agentVersion, - type: 'endpoint', - }, + agent: { ...this.commonInfo.agent, type: 'endgame' }, ecs: { version: '1.4.0', }, @@ -257,13 +243,7 @@ export class EndpointDocGenerator { type: options.eventType ? options.eventType : 'creation', id: this.seededUUIDv4(), }, - host: { - id: this.hostId, - hostname: this.hostname, - ip: this.ip, - mac: this.macAddress, - os: this.os, - }, + host: this.commonInfo.host, process: { entity_id: options.entityID ? options.entityID : this.randomString(10), parent: options.parentEntityID ? { entity_id: options.parentEntityID } : undefined, @@ -323,14 +303,13 @@ export class EndpointDocGenerator { percentNodesWithRelated = 100, percentChildrenTerminated = 100 ): Event[] { - let events: Event[] = [root]; + let events: Event[] = []; let parents = [root]; let timestamp = root['@timestamp']; for (let i = 0; i < generations; i++) { const newParents: EndpointEvent[] = []; parents.forEach(element => { - // const numChildren = randomN(maxChildrenPerNode); - const numChildren = maxChildrenPerNode; + const numChildren = this.randomN(maxChildrenPerNode); for (let j = 0; j < numChildren; j++) { timestamp = timestamp + 1000; const child = this.generateEvent({ diff --git a/x-pack/plugins/endpoint/package.json b/x-pack/plugins/endpoint/package.json index c7ba8b3fb419..fc4f4bd586be 100644 --- a/x-pack/plugins/endpoint/package.json +++ b/x-pack/plugins/endpoint/package.json @@ -4,10 +4,11 @@ "version": "0.0.0", "private": true, "license": "Elastic-License", - "scripts": {}, + "scripts": { + "test:generate": "ts-node --project scripts/cli_tsconfig.json scripts/resolver_generator.ts" + }, "dependencies": { - "react-redux": "^7.1.0", - "seedrandom": "^3.0.5" + "react-redux": "^7.1.0" }, "devDependencies": { "@types/seedrandom": ">=2.0.0 <4.0.0", diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts index fba1dacb0d3b..e435fded13f4 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts @@ -20,7 +20,7 @@ describe('endpoint_list store concerns', () => { dispatch = store.dispatch; }; const generateEndpoint = (): EndpointMetadata => { - return generator.generateEndpointMetadata(new Date().getTime()); + return generator.generateEndpointMetadata(); }; const loadDataToStore = () => { dispatch({ diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts index d98dc8262414..459a1789a58d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts @@ -27,7 +27,7 @@ describe('endpoint list saga', () => { const generator = new EndpointDocGenerator(); // https://github.com/elastic/endpoint-app-team/issues/131 const generateEndpoint = (): EndpointMetadata => { - return generator.generateEndpointMetadata(new Date().getTime()); + return generator.generateEndpointMetadata(); }; let history: History; diff --git a/x-pack/plugins/endpoint/scripts/README.md b/x-pack/plugins/endpoint/scripts/README.md new file mode 100644 index 000000000000..f0c8c5a9b0b6 --- /dev/null +++ b/x-pack/plugins/endpoint/scripts/README.md @@ -0,0 +1,46 @@ +This script makes it easy to create the endpoint metadata, alert, and event documents needed to test Resolver in Kibana. +The default behavior is to create 1 endpoint with 1 alert and a moderate number of events (random, typically on the order of 20). +A seed value can be provided as a string for the random number generator for repeatable behavior, useful for demos etc. +Use the `-d` option if you want to delete and remake the indices, otherwise it will add documents to existing indices. + +The sample data generator script depends on ts-node, install with npm: + +```npm install -g ts-node``` + +Example command sequence to get ES and kibana running with sample data after installing ts-node: + +```yarn es snapshot``` -> starts ES + +```npx yarn start --xpack.endpoint.enabled=true --no-base-path``` -> starts kibana + +```cd ~/path/to/kibana/x-pack/plugins/endpoint``` + +```yarn test:generate --auth elastic:changeme``` -> run the resolver_generator.ts script + +Resolver generator CLI options: +```--help Show help [boolean] + --seed, -s random seed to use for document generator [string] + --node, -n elasticsearch node url + [string] [default: "http://localhost:9200"] + --eventIndex, --ei index to store events in + [string] [default: "events-endpoint-1"] + --metadataIndex, --mi index to store endpoint metadata in + [string] [default: "endpoint-agent-1"] + --auth elasticsearch username and password, separated by + a colon [string] + --ancestors, --anc number of ancestors of origin to create + [number] [default: 3] + --generations, --gen number of child generations to create + [number] [default: 3] + --children, --ch maximum number of children per node + [number] [default: 3] + --relatedEvents, --related number of related events to create for each + process event [number] [default: 5] + --percentWithRelated, --pr percent of process events to add related events to + [number] [default: 30] + --percentTerminated, --pt percent of process events to add termination event + for [number] [default: 30] + --numEndpoints, --ne number of different endpoints to generate alerts + for [number] [default: 1] + --alertsPerEndpoint, --ape number of resolver trees to make for each endpoint + [number] [default: 1]``` diff --git a/x-pack/plugins/endpoint/scripts/cli_tsconfig.json b/x-pack/plugins/endpoint/scripts/cli_tsconfig.json new file mode 100644 index 000000000000..25afe109a42e --- /dev/null +++ b/x-pack/plugins/endpoint/scripts/cli_tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "target": "es2019", + "resolveJsonModule": true + } + } + \ No newline at end of file diff --git a/x-pack/plugins/endpoint/scripts/mapping.json b/x-pack/plugins/endpoint/scripts/mapping.json new file mode 100644 index 000000000000..34c039d64351 --- /dev/null +++ b/x-pack/plugins/endpoint/scripts/mapping.json @@ -0,0 +1,2367 @@ +{ + "mappings": { + "_meta": { + "version": "1.5.0-dev" + }, + "date_detection": false, + "dynamic": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "compile_time": { + "type": "date" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classifier": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "endpoint": { + "properties": { + "artifact": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "policy": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "entry_modified": { + "type": "double" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "macro": { + "properties": { + "code_page": { + "type": "long" + }, + "collection": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + }, + "type": "object" + }, + "errors": { + "properties": { + "count": { + "type": "long" + }, + "error_type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "file_extension": { + "type": "long" + }, + "project_file": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + }, + "type": "object" + }, + "stream": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "raw_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "raw_code_size": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + } + } + }, + "malware_classifier": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "temp_file_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_percent": { + "type": "double" + }, + "cwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "env_variables": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "handles": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classifier": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "memory_percent": { + "type": "double" + }, + "memory_region": { + "properties": { + "allocation_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "allocation_protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram": { + "properties": { + "histogram_array": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_flavor": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_resolution": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "length": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "permission": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_tag": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "unbacked_on_disk": { + "type": "boolean" + } + }, + "type": "nested" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "num_threads": { + "type": "long" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "phys_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "session_id": { + "type": "long" + }, + "short_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tty_device": { + "properties": { + "major_number": { + "type": "integer" + }, + "minor_number": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "virt_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "target": { + "properties": { + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "compile_time": { + "type": "date" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classifier": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_percent": { + "type": "double" + }, + "cwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "env_variables": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "handles": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classifier": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "memory_percent": { + "type": "double" + }, + "memory_region": { + "properties": { + "allocation_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "allocation_protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram": { + "properties": { + "histogram_array": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_flavor": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_resolution": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "length": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "permission": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_tag": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "unbacked_on_disk": { + "type": "boolean" + } + }, + "type": "nested" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "num_threads": { + "type": "long" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "phys_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "session_id": { + "type": "long" + }, + "short_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tty_device": { + "properties": { + "major_number": { + "type": "integer" + }, + "minor_number": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "virt_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": 10000 + } + }, + "refresh_interval": "5s" + } + } +} \ No newline at end of file diff --git a/x-pack/plugins/endpoint/scripts/resolver_generator.ts b/x-pack/plugins/endpoint/scripts/resolver_generator.ts new file mode 100644 index 000000000000..a3e56497f079 --- /dev/null +++ b/x-pack/plugins/endpoint/scripts/resolver_generator.ts @@ -0,0 +1,161 @@ +/* + * 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 yargs from 'yargs'; +import { Client, ClientOptions } from '@elastic/elasticsearch'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { EndpointDocGenerator } from '../common/generate_data'; +import { default as mapping } from './mapping.json'; + +main(); + +async function main() { + const argv = yargs.help().options({ + seed: { + alias: 's', + describe: 'random seed to use for document generator', + type: 'string', + }, + node: { + alias: 'n', + describe: 'elasticsearch node url', + default: 'http://localhost:9200', + type: 'string', + }, + eventIndex: { + alias: 'ei', + describe: 'index to store events in', + default: 'events-endpoint-1', + type: 'string', + }, + metadataIndex: { + alias: 'mi', + describe: 'index to store endpoint metadata in', + default: 'endpoint-agent-1', + type: 'string', + }, + auth: { + describe: 'elasticsearch username and password, separated by a colon', + type: 'string', + }, + ancestors: { + alias: 'anc', + describe: 'number of ancestors of origin to create', + type: 'number', + default: 3, + }, + generations: { + alias: 'gen', + describe: 'number of child generations to create', + type: 'number', + default: 3, + }, + children: { + alias: 'ch', + describe: 'maximum number of children per node', + type: 'number', + default: 3, + }, + relatedEvents: { + alias: 'related', + describe: 'number of related events to create for each process event', + type: 'number', + default: 5, + }, + percentWithRelated: { + alias: 'pr', + describe: 'percent of process events to add related events to', + type: 'number', + default: 30, + }, + percentTerminated: { + alias: 'pt', + describe: 'percent of process events to add termination event for', + type: 'number', + default: 30, + }, + numEndpoints: { + alias: 'ne', + describe: 'number of different endpoints to generate alerts for', + type: 'number', + default: 1, + }, + alertsPerEndpoint: { + alias: 'ape', + describe: 'number of resolver trees to make for each endpoint', + type: 'number', + default: 1, + }, + delete: { + alias: 'd', + describe: 'delete indices and remake them', + type: 'boolean', + default: false, + }, + }).argv; + const clientOptions: ClientOptions = { + node: argv.node, + }; + if (argv.auth) { + const [username, password]: string[] = argv.auth.split(':', 2); + clientOptions.auth = { username, password }; + } + const client = new Client(clientOptions); + if (argv.delete) { + try { + await client.indices.delete({ + index: [argv.eventIndex, argv.metadataIndex], + }); + } catch (err) { + if (err instanceof ResponseError && err.statusCode !== 404) { + // eslint-disable-next-line no-console + console.log(err); + process.exit(1); + } + } + } + try { + await client.indices.create({ + index: argv.eventIndex, + body: mapping, + }); + } catch (err) { + if ( + err instanceof ResponseError && + err.body.error.type !== 'resource_already_exists_exception' + ) { + // eslint-disable-next-line no-console + console.log(err.body); + process.exit(1); + } + } + + const generator = new EndpointDocGenerator(argv.seed); + for (let i = 0; i < argv.numEndpoints; i++) { + await client.index({ + index: argv.metadataIndex, + body: generator.generateEndpointMetadata(), + }); + for (let j = 0; j < argv.alertsPerEndpoint; j++) { + const resolverDocs = generator.generateFullResolverTree( + argv.ancestors, + argv.generations, + argv.children, + argv.relatedEvents, + argv.percentWithRelated, + argv.percentTerminated + ); + const body = resolverDocs.reduce( + (array: Array>, doc) => ( + array.push({ index: { _index: argv.eventIndex } }, doc), array + ), + [] + ); + + await client.bulk({ body }); + } + generator.randomizeHostData(); + } +} From 92aec698f5d20630ed194f55b5eea4fd00c5f231 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 16 Mar 2020 21:28:34 +0100 Subject: [PATCH 020/115] Embeddable triggers (#58440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add Embeddable.supportedTriggers() method * feat: 🎸 report supported triggers on visualization embeddable * feat: 🎸 hard-code supportedTriggers() in visualize_embed * feat: 🎸 use VIS_EVENT_TO_TRIGGER when executing trigger * perf: ⚡️ improve type * fix: 🐛 revert back trigger check to how it is done on master * chore: 🤖 remove unused import * feat: 🎸 hard-code triggers for each visualization * fix: 🐛 fix import * feat: 🎸 reshuffle vis_types in supportedTriggers() method Co-authored-by: Elastic Machine --- .../np_ready/public/embeddable/events.ts | 33 +++++++++++++++++ .../public/embeddable/visualize_embeddable.ts | 36 +++++++++++++++---- .../public/lib/embeddables/embeddable.tsx | 5 +++ .../public/lib/embeddables/i_embeddable.ts | 6 ++++ 4 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/events.ts diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/events.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/events.ts new file mode 100644 index 000000000000..53d04bf6eb04 --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/events.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 { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../../../../plugins/ui_actions/public'; + +export interface VisEventToTrigger { + ['brush']: typeof SELECT_RANGE_TRIGGER; + ['filter']: typeof VALUE_CLICK_TRIGGER; +} + +export const VIS_EVENT_TO_TRIGGER: VisEventToTrigger = { + brush: SELECT_RANGE_TRIGGER, + filter: VALUE_CLICK_TRIGGER, +}; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts index 474912ed508f..c45e6832dc83 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts @@ -36,10 +36,6 @@ import { Container, EmbeddableVisTriggerContext, } from '../../../../../../../plugins/embeddable/public'; -import { - selectRangeTrigger, - valueClickTrigger, -} from '../../../../../../../plugins/ui_actions/public'; import { dispatchRenderComplete } from '../../../../../../../plugins/kibana_utils/public'; import { IExpressionLoaderParams, @@ -50,6 +46,7 @@ import { buildPipeline } from '../legacy/build_pipeline'; import { Vis } from '../vis'; import { getExpressions, getUiActions } from '../services'; import { VisSavedObject } from '../types'; +import { VIS_EVENT_TO_TRIGGER } from './events'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -295,8 +292,8 @@ export class VisualizeEmbeddable extends Embeddable { + return []; + } } diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 62121cb0f23d..7fef80edde85 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -21,6 +21,7 @@ import { Observable } from 'rxjs'; import { Adapters } from '../types'; import { IContainer } from '../containers/i_container'; import { ViewMode } from '../types'; +import { TriggerContextMapping } from '../../../../ui_actions/public'; export interface EmbeddableInput { viewMode?: ViewMode; @@ -161,4 +162,9 @@ export interface IEmbeddable< * Cleans up subscriptions, destroy nodes mounted from calls to render. */ destroy(): void; + + /** + * List of triggers that this embeddable will execute. + */ + supportedTriggers(): Array; } From c898e799a5fd605177e823e112a4ad2f430fcabc Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Mon, 16 Mar 2020 14:33:56 -0600 Subject: [PATCH 021/115] Migrate dual validated range (#59689) * Move validated range files to new NP location * Update refs in code * Clean up old validated range files * Change relative paths to 'kibana-react'. Some clean up * Change to relative paths * Fix i18n errors * i18n clean up. Export module explicitly * Change files over to TS to prevent build issue where validated range was missing * Clean up TS conversion * More clean up. Extend EuiRangeProps * Remove unneeded ts-ignore * Review feedback and test fixes * Change double to single quotes * min and max aren't always passed, make optional * Type updates * Review feedback. Set state to empty on init and add ignore comment * Review feedback * Add back in last 2 ts-ignores. Build fails without focusable attribute on EuiDualRange & No good alternatives for spread syntax in TS components * Rollback change to state init. Initializing state to null actually triggers a react browser warning and complicates using 'prevState' in getDerivedStateFromProps Co-authored-by: Elastic Machine --- .../public/components/vis/range_control.tsx | 3 +- .../public/legacy_imports.ts | 2 - .../public/components/tag_cloud_options.tsx | 3 +- .../public/legacy_imports.ts | 1 - .../ui/public/validated_range/index.d.ts | 25 --------- src/plugins/kibana_react/public/index.ts | 1 + .../public/validated_range/index.ts} | 0 .../validated_range/is_range_valid.test.ts} | 0 .../public/validated_range/is_range_valid.ts} | 24 ++++++--- .../validated_range/validated_dual_range.tsx} | 54 ++++++++++++------- .../layer_settings/layer_settings.js | 2 +- .../components/size/size_range_selector.js | 2 +- .../translations/translations/ja-JP.json | 3 -- .../translations/translations/zh-CN.json | 3 -- 14 files changed, 58 insertions(+), 65 deletions(-) delete mode 100644 src/legacy/ui/public/validated_range/index.d.ts rename src/{legacy/ui/public/validated_range/index.js => plugins/kibana_react/public/validated_range/index.ts} (100%) rename src/{legacy/ui/public/validated_range/is_range_valid.test.js => plugins/kibana_react/public/validated_range/is_range_valid.test.ts} (100%) rename src/{legacy/ui/public/validated_range/is_range_valid.js => plugins/kibana_react/public/validated_range/is_range_valid.ts} (74%) rename src/{legacy/ui/public/validated_range/validated_dual_range.js => plugins/kibana_react/public/validated_range/validated_dual_range.tsx} (67%) diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx index cd3982afd9af..0cd2a2b33198 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx @@ -19,8 +19,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; - -import { ValidatedDualRange } from '../../legacy_imports'; +import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; import { FormRow } from './form_row'; import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts index b6c4eb28e974..8c58ac2386da 100644 --- a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts +++ b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts @@ -22,7 +22,5 @@ import { SearchSource as SearchSourceClass, ISearchSource } from '../../../../pl export { SearchSourceFields } from '../../../../plugins/data/public'; -export { ValidatedDualRange } from 'ui/validated_range'; - export type SearchSource = Class; export const SearchSource = SearchSourceClass; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index ab7c2cd980c4..a9e816f70cf5 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -20,11 +20,10 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - +import { ValidatedDualRange } from '../../../../../../src/plugins/kibana_react/public'; import { VisOptionsProps } from '../../../vis_default_editor/public'; import { SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; import { TagCloudVisParams } from '../types'; -import { ValidatedDualRange } from '../legacy_imports'; function TagCloudOptions({ stateParams, setValue, vis }: VisOptionsProps) { const handleFontSizeChange = ([minFontSize, maxFontSize]: [string | number, string | number]) => { diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts index d5b442bc5b34..0d76bc5d8b68 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts @@ -18,5 +18,4 @@ */ export { Schemas } from 'ui/agg_types'; -export { ValidatedDualRange } from 'ui/validated_range'; export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; diff --git a/src/legacy/ui/public/validated_range/index.d.ts b/src/legacy/ui/public/validated_range/index.d.ts deleted file mode 100644 index 50cacbc517be..000000000000 --- a/src/legacy/ui/public/validated_range/index.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { EuiRangeProps } from '@elastic/eui'; - -export class ValidatedDualRange extends React.Component { - allowEmptyRange?: boolean; -} diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index f04c6f1f19c3..e88ca7178cde 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -25,6 +25,7 @@ export * from './ui_settings'; export * from './field_icon'; export * from './table_list_view'; export * from './split_panel'; +export { ValidatedDualRange } from './validated_range'; export { Markdown, MarkdownSimple } from './markdown'; export { reactToUiComponent, uiToReactComponent } from './adapters'; export { useUrlTracker } from './use_url_tracker'; diff --git a/src/legacy/ui/public/validated_range/index.js b/src/plugins/kibana_react/public/validated_range/index.ts similarity index 100% rename from src/legacy/ui/public/validated_range/index.js rename to src/plugins/kibana_react/public/validated_range/index.ts diff --git a/src/legacy/ui/public/validated_range/is_range_valid.test.js b/src/plugins/kibana_react/public/validated_range/is_range_valid.test.ts similarity index 100% rename from src/legacy/ui/public/validated_range/is_range_valid.test.js rename to src/plugins/kibana_react/public/validated_range/is_range_valid.test.ts diff --git a/src/legacy/ui/public/validated_range/is_range_valid.js b/src/plugins/kibana_react/public/validated_range/is_range_valid.ts similarity index 74% rename from src/legacy/ui/public/validated_range/is_range_valid.js rename to src/plugins/kibana_react/public/validated_range/is_range_valid.ts index 9b733815a66b..1f822c0cb94b 100644 --- a/src/legacy/ui/public/validated_range/is_range_valid.js +++ b/src/plugins/kibana_react/public/validated_range/is_range_valid.ts @@ -18,14 +18,24 @@ */ import { i18n } from '@kbn/i18n'; +import { ValueMember, Value } from './validated_dual_range'; const LOWER_VALUE_INDEX = 0; const UPPER_VALUE_INDEX = 1; -export function isRangeValid(value, min, max, allowEmptyRange) { - allowEmptyRange = typeof allowEmptyRange === 'boolean' ? allowEmptyRange : true; //cannot use default props since that uses falsy check - let lowerValue = isNaN(value[LOWER_VALUE_INDEX]) ? '' : value[LOWER_VALUE_INDEX]; - let upperValue = isNaN(value[UPPER_VALUE_INDEX]) ? '' : value[UPPER_VALUE_INDEX]; +export function isRangeValid( + value: Value = [0, 0], + min: ValueMember = 0, + max: ValueMember = 0, + allowEmptyRange?: boolean +) { + allowEmptyRange = typeof allowEmptyRange === 'boolean' ? allowEmptyRange : true; // cannot use default props since that uses falsy check + let lowerValue: ValueMember = isNaN(value[LOWER_VALUE_INDEX] as number) + ? '' + : `${value[LOWER_VALUE_INDEX]}`; + let upperValue: ValueMember = isNaN(value[UPPER_VALUE_INDEX] as number) + ? '' + : `${value[UPPER_VALUE_INDEX]}`; const isLowerValueValid = lowerValue.toString() !== ''; const isUpperValueValid = upperValue.toString() !== ''; @@ -39,7 +49,7 @@ export function isRangeValid(value, min, max, allowEmptyRange) { let errorMessage = ''; const bothMustBeSetErrorMessage = i18n.translate( - 'common.ui.dualRangeControl.mustSetBothErrorMessage', + 'kibana-react.dualRangeControl.mustSetBothErrorMessage', { defaultMessage: 'Both lower and upper values must be set', } @@ -55,13 +65,13 @@ export function isRangeValid(value, min, max, allowEmptyRange) { errorMessage = bothMustBeSetErrorMessage; } else if ((isLowerValueValid && lowerValue < min) || (isUpperValueValid && upperValue > max)) { isValid = false; - errorMessage = i18n.translate('common.ui.dualRangeControl.outsideOfRangeErrorMessage', { + errorMessage = i18n.translate('kibana-react.dualRangeControl.outsideOfRangeErrorMessage', { defaultMessage: 'Values must be on or between {min} and {max}', values: { min, max }, }); } else if (isLowerValueValid && isUpperValueValid && upperValue < lowerValue) { isValid = false; - errorMessage = i18n.translate('common.ui.dualRangeControl.upperValidErrorMessage', { + errorMessage = i18n.translate('kibana-react.dualRangeControl.upperValidErrorMessage', { defaultMessage: 'Upper value must be greater or equal to lower value', }); } diff --git a/src/legacy/ui/public/validated_range/validated_dual_range.js b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx similarity index 67% rename from src/legacy/ui/public/validated_range/validated_dual_range.js rename to src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx index 3b0efba11afc..e7392eeba383 100644 --- a/src/legacy/ui/public/validated_range/validated_dual_range.js +++ b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx @@ -18,17 +18,38 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { isRangeValid } from './is_range_valid'; - import { EuiFormRow, EuiDualRange } from '@elastic/eui'; +import { EuiFormRowDisplayKeys } from '@elastic/eui/src/components/form/form_row/form_row'; +import { EuiDualRangeProps } from '@elastic/eui/src/components/form/range/dual_range'; +import { isRangeValid } from './is_range_valid'; // Wrapper around EuiDualRange that ensures onChange callback is only called when range value // is valid and within min/max -export class ValidatedDualRange extends Component { - state = {}; - static getDerivedStateFromProps(nextProps, prevState) { +export type Value = EuiDualRangeProps['value']; +export type ValueMember = EuiDualRangeProps['value'][0]; + +interface Props extends Omit { + value?: Value; + allowEmptyRange?: boolean; + label?: string; + formRowDisplay?: EuiFormRowDisplayKeys; + onChange?: (val: [string, string]) => void; + min?: ValueMember; + max?: ValueMember; +} + +interface State { + isValid?: boolean; + errorMessage?: string; + value: [ValueMember, ValueMember]; + prevValue?: Value; +} + +export class ValidatedDualRange extends Component { + static defaultProps: { fullWidth: boolean; allowEmptyRange: boolean; compressed: boolean }; + + static getDerivedStateFromProps(nextProps: Props, prevState: State) { if (nextProps.value !== prevState.prevValue) { const { isValid, errorMessage } = isRangeValid( nextProps.value, @@ -47,7 +68,10 @@ export class ValidatedDualRange extends Component { return null; } - _onChange = value => { + // @ts-ignore state populated by getDerivedStateFromProps + state: State = {}; + + _onChange = (value: Value) => { const { isValid, errorMessage } = isRangeValid( value, this.props.min, @@ -61,8 +85,8 @@ export class ValidatedDualRange extends Component { errorMessage, }); - if (isValid) { - this.props.onChange(value); + if (this.props.onChange && isValid) { + this.props.onChange([value[0] as string, value[1] as string]); } }; @@ -75,7 +99,8 @@ export class ValidatedDualRange extends Component { value, // eslint-disable-line no-unused-vars onChange, // eslint-disable-line no-unused-vars allowEmptyRange, // eslint-disable-line no-unused-vars - ...rest + // @ts-ignore + ...rest // TODO: Consider alternatives for spread operator in component } = this.props; return ( @@ -92,6 +117,7 @@ export class ValidatedDualRange extends Component { fullWidth={fullWidth} value={this.state.value} onChange={this._onChange} + // @ts-ignore focusable={false} // remove when #59039 is fixed {...rest} /> @@ -100,14 +126,6 @@ export class ValidatedDualRange extends Component { } } -ValidatedDualRange.propTypes = { - allowEmptyRange: PropTypes.bool, - fullWidth: PropTypes.bool, - compressed: PropTypes.bool, - label: PropTypes.node, - formRowDisplay: PropTypes.string, -}; - ValidatedDualRange.defaultProps = { allowEmptyRange: true, fullWidth: false, diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js index ac17915b5f27..eb23607aa215 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js @@ -11,7 +11,7 @@ import { EuiTitle, EuiPanel, EuiFormRow, EuiFieldText, EuiSpacer } from '@elasti import { ValidatedRange } from '../../../components/validated_range'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ValidatedDualRange } from 'ui/validated_range'; +import { ValidatedDualRange } from '../../../../../../../../src/plugins/kibana_react/public'; import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants'; export function LayerSettings(props) { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js index 1d5815a84920..5de7b462136e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js @@ -6,7 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { ValidatedDualRange } from 'ui/validated_range'; +import { ValidatedDualRange } from '../../../../../../../../../../src/plugins/kibana_react/public'; import { MIN_SIZE, MAX_SIZE } from '../../vector_style_defaults'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9b842f736b8b..3fdcf9b81593 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -78,9 +78,6 @@ "messages": { "common.ui.aggResponse.allDocsTitle": "すべてのドキュメント", "common.ui.directives.paginate.size.allDropDownOptionLabel": "すべて", - "common.ui.dualRangeControl.mustSetBothErrorMessage": "下と上の値の両方を設定する必要があります", - "common.ui.dualRangeControl.outsideOfRangeErrorMessage": "値は {min} と {max} の間でなければなりません", - "common.ui.dualRangeControl.upperValidErrorMessage": "上の値は下の値以上でなければなりません", "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "エラー", "common.ui.errorAutoCreateIndex.errorDescription": "Elasticsearch クラスターの {autoCreateIndexActionConfig} 設定が原因で、Kibana が保存されたオブジェクトを格納するインデックスを自動的に作成できないようです。Kibana は、保存されたオブジェクトインデックスが適切なマッピング/スキーマを使用し Kibana から Elasticsearch へのポーリングの回数を減らすための最適な手段であるため、この Elasticsearch の機能を使用します。", "common.ui.errorAutoCreateIndex.errorDisclaimer": "申し訳ございませんが、この問題が解決されるまで Kibana で何も保存することができません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 97c30f180dfe..1bcbcca055c3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -78,9 +78,6 @@ "messages": { "common.ui.aggResponse.allDocsTitle": "所有文档", "common.ui.directives.paginate.size.allDropDownOptionLabel": "全部", - "common.ui.dualRangeControl.mustSetBothErrorMessage": "下限值和上限值都须设置", - "common.ui.dualRangeControl.outsideOfRangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", - "common.ui.dualRangeControl.upperValidErrorMessage": "上限值必须大于或等于下限值", "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "错误", "common.ui.errorAutoCreateIndex.errorDescription": "似乎 Elasticsearch 集群的 {autoCreateIndexActionConfig} 设置使 Kibana 无法自动创建用于存储已保存对象的索引。Kibana 将使用此 Elasticsearch 功能,因为这是确保已保存对象索引使用正确映射/架构的最好方式,而且其允许 Kibana 较少地轮询 Elasticsearch。", "common.ui.errorAutoCreateIndex.errorDisclaimer": "但是,只有解决了此问题后,您才能在 Kibana 保存内容。", From 69ec60d744893b86449a913fa65836f2f2dd6295 Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Mon, 16 Mar 2020 17:18:49 -0400 Subject: [PATCH 022/115] EMT-248: implement ack resource to accept event payload to acknowledge agent actions (#60218) [Ingest]EMT-248: implement ack resource to accept event payload to acknowledge agent actions --- .../common/types/rest_spec/agent.ts | 7 +- .../server/routes/agent/acks_handlers.test.ts | 94 ++++++++++++ .../server/routes/agent/acks_handlers.ts | 69 +++++++++ .../server/routes/agent/handlers.ts | 36 +---- .../server/routes/agent/index.ts | 11 +- .../server/services/agents/acks.test.ts | 118 +++++++++++++++ .../server/services/agents/acks.ts | 99 +++++++++++-- .../server/services/agents/crud.ts | 2 +- .../server/types/models/agent.ts | 5 + .../server/types/rest_spec/agent.ts | 4 +- .../api_integration/apis/fleet/agents/acks.ts | 138 +++++++++++++++++- .../es_archives/fleet/agents/data.json | 12 ++ 12 files changed, 539 insertions(+), 56 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts create mode 100644 x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index af919d973b7d..7bbaf42422bb 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -69,13 +69,18 @@ export interface PostAgentEnrollResponse { export interface PostAgentAcksRequest { body: { - action_ids: string[]; + events: AgentEvent[]; }; params: { agentId: string; }; } +export interface PostAgentAcksResponse { + action: string; + success: boolean; +} + export interface PostAgentUnenrollRequest { body: { kuery: string } | { ids: string[] }; } diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts new file mode 100644 index 000000000000..84923d5c3366 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts @@ -0,0 +1,94 @@ +/* + * 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 { postAgentAcksHandlerBuilder } from './acks_handlers'; +import { + KibanaResponseFactory, + RequestHandlerContext, + SavedObjectsClientContract, +} from 'kibana/server'; +import { httpServerMock, savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { PostAgentAcksResponse } from '../../../common/types/rest_spec'; +import { AckEventSchema } from '../../types/models'; +import { AcksService } from '../../services/agents'; + +describe('test acks schema', () => { + it('validate that ack event schema expect action id', async () => { + expect(() => + AckEventSchema.validate({ + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + agent_id: 'agent', + message: 'hello', + payload: 'payload', + }) + ).toThrow(Error); + + expect( + AckEventSchema.validate({ + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + agent_id: 'agent', + action_id: 'actionId', + message: 'hello', + payload: 'payload', + }) + ).toBeTruthy(); + }); +}); + +describe('test acks handlers', () => { + let mockResponse: jest.Mocked; + let mockSavedObjectsClient: jest.Mocked; + + beforeEach(() => { + mockSavedObjectsClient = savedObjectsClientMock.create(); + mockResponse = httpServerMock.createResponseFactory(); + }); + + it('should succeed on valid agent event', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ + headers: { + authorization: 'ApiKey TmVqTDBIQUJsRkw1em52R1ZIUF86NS1NaTItdHFUTHFHbThmQW1Fb0ljUQ==', + }, + body: { + events: [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action1', + agent_id: 'agent', + message: 'message', + }, + ], + }, + }); + + const ackService: AcksService = { + acknowledgeAgentActions: jest.fn().mockReturnValueOnce([ + { + type: 'CONFIG_CHANGE', + id: 'action1', + }, + ]), + getAgentByAccessAPIKeyId: jest.fn().mockReturnValueOnce({ + id: 'agent', + }), + getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient), + saveAgentEvents: jest.fn(), + } as jest.Mocked; + + const postAgentAcksHandler = postAgentAcksHandlerBuilder(ackService); + await postAgentAcksHandler(({} as unknown) as RequestHandlerContext, mockRequest, mockResponse); + expect(mockResponse.ok.mock.calls[0][0]?.body as PostAgentAcksResponse).toEqual({ + action: 'acks', + success: true, + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts new file mode 100644 index 000000000000..53b677bb1389 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts @@ -0,0 +1,69 @@ +/* + * 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. + */ + +// handlers that handle events from agents in response to actions received + +import { RequestHandler } from 'kibana/server'; +import { TypeOf } from '@kbn/config-schema'; +import { PostAgentAcksRequestSchema } from '../../types/rest_spec'; +import * as APIKeyService from '../../services/api_keys'; +import { AcksService } from '../../services/agents'; +import { AgentEvent } from '../../../common/types/models'; +import { PostAgentAcksResponse } from '../../../common/types/rest_spec'; + +export const postAgentAcksHandlerBuilder = function( + ackService: AcksService +): RequestHandler< + TypeOf, + undefined, + TypeOf +> { + return async (context, request, response) => { + try { + const soClient = ackService.getSavedObjectsClientContract(request); + const res = APIKeyService.parseApiKey(request.headers); + const agent = await ackService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string); + const agentEvents = request.body.events as AgentEvent[]; + + // validate that all events are for the authorized agent obtained from the api key + const notAuthorizedAgentEvent = agentEvents.filter( + agentEvent => agentEvent.agent_id !== agent.id + ); + + if (notAuthorizedAgentEvent && notAuthorizedAgentEvent.length > 0) { + return response.badRequest({ + body: + 'agent events contains events with different agent id from currently authorized agent', + }); + } + + const agentActions = await ackService.acknowledgeAgentActions(soClient, agent, agentEvents); + + if (agentActions.length > 0) { + await ackService.saveAgentEvents(soClient, agentEvents); + } + + const body: PostAgentAcksResponse = { + action: 'acks', + success: true, + }; + + return response.ok({ body }); + } catch (e) { + if (e.isBoom) { + return response.customError({ + statusCode: e.output.statusCode, + body: { message: e.message }, + }); + } + + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } + }; +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index cb4e4d557d74..cf1fd2476f31 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -23,7 +23,6 @@ import { GetOneAgentEventsRequestSchema, PostAgentCheckinRequestSchema, PostAgentEnrollRequestSchema, - PostAgentAcksRequestSchema, PostAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, } from '../../types'; @@ -31,7 +30,7 @@ import * as AgentService from '../../services/agents'; import * as APIKeyService from '../../services/api_keys'; import { appContextService } from '../../services/app_context'; -function getInternalUserSOClient(request: KibanaRequest) { +export function getInternalUserSOClient(request: KibanaRequest) { // soClient as kibana internal users, be carefull on how you use it, security is not enabled return appContextService.getSavedObjects().getScopedClient(request, { excludedWrappers: ['security'], @@ -210,39 +209,6 @@ export const postAgentCheckinHandler: RequestHandler< } }; -export const postAgentAcksHandler: RequestHandler< - TypeOf, - undefined, - TypeOf -> = async (context, request, response) => { - try { - const soClient = getInternalUserSOClient(request); - const res = APIKeyService.parseApiKey(request.headers); - const agent = await AgentService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string); - - await AgentService.acknowledgeAgentActions(soClient, agent, request.body.action_ids); - - const body = { - action: 'acks', - success: true, - }; - - return response.ok({ body }); - } catch (e) { - if (e.isBoom) { - return response.customError({ - statusCode: e.output.statusCode, - body: { message: e.message }, - }); - } - - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); - } -}; - export const postAgentEnrollHandler: RequestHandler< undefined, undefined, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index 8a65fa9c50e8..c85629ea22ad 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -31,10 +31,12 @@ import { getAgentEventsHandler, postAgentCheckinHandler, postAgentEnrollHandler, - postAgentAcksHandler, postAgentsUnenrollHandler, getAgentStatusForConfigHandler, + getInternalUserSOClient, } from './handlers'; +import { postAgentAcksHandlerBuilder } from './acks_handlers'; +import * as AgentService from '../../services/agents'; export const registerRoutes = (router: IRouter) => { // Get one @@ -101,7 +103,12 @@ export const registerRoutes = (router: IRouter) => { validate: PostAgentAcksRequestSchema, options: { tags: [] }, }, - postAgentAcksHandler + postAgentAcksHandlerBuilder({ + acknowledgeAgentActions: AgentService.acknowledgeAgentActions, + getAgentByAccessAPIKeyId: AgentService.getAgentByAccessAPIKeyId, + getSavedObjectsClientContract: getInternalUserSOClient, + saveAgentEvents: AgentService.saveAgentEvents, + }) ); router.post( diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts new file mode 100644 index 000000000000..3c07463e3af5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { Agent, AgentAction, AgentEvent } from '../../../common/types/models'; +import { AGENT_TYPE_PERMANENT } from '../../../common/constants'; +import { acknowledgeAgentActions } from './acks'; +import { isBoom } from 'boom'; + +describe('test agent acks services', () => { + it('should succeed on valid and matched actions', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + const agentActions = await acknowledgeAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + actions: [ + { + type: 'CONFIG_CHANGE', + id: 'action1', + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + }, + ], + } as unknown) as Agent, + [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action1', + agent_id: 'id', + } as AgentEvent, + ] + ); + expect(agentActions).toEqual([ + ({ + type: 'CONFIG_CHANGE', + id: 'action1', + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + } as unknown) as AgentAction, + ]); + }); + + it('should fail for actions that cannot be found on agent actions list', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + try { + await acknowledgeAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + actions: [ + { + type: 'CONFIG_CHANGE', + id: 'action1', + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + }, + ], + } as unknown) as Agent, + [ + ({ + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action2', + agent_id: 'id', + } as unknown) as AgentEvent, + ] + ); + expect(true).toBeFalsy(); + } catch (e) { + expect(isBoom(e)).toBeTruthy(); + } + }); + + it('should fail for events that have types not in the allowed acknowledgement type list', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + try { + await acknowledgeAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + actions: [ + { + type: 'CONFIG_CHANGE', + id: 'action1', + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + }, + ], + } as unknown) as Agent, + [ + ({ + type: 'ACTION', + subtype: 'FAILED', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action1', + agent_id: 'id', + } as unknown) as AgentEvent, + ] + ); + expect(true).toBeFalsy(); + } catch (e) { + expect(isBoom(e)).toBeTruthy(); + } + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts index 1732ff9cf5b5..892d8cdbe657 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts @@ -4,25 +4,100 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; -import { Agent, AgentSOAttributes } from '../../types'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { + KibanaRequest, + SavedObjectsBulkCreateObject, + SavedObjectsBulkResponse, + SavedObjectsClientContract, +} from 'kibana/server'; +import Boom from 'boom'; +import { + Agent, + AgentAction, + AgentEvent, + AgentEventSOAttributes, + AgentSOAttributes, +} from '../../types'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; + +const ALLOWED_ACKNOWLEDGEMENT_TYPE: string[] = ['ACTION_RESULT']; export async function acknowledgeAgentActions( soClient: SavedObjectsClientContract, agent: Agent, - actionIds: string[] -) { + agentEvents: AgentEvent[] +): Promise { const now = new Date().toISOString(); - const updatedActions = agent.actions.map(action => { - if (action.sent_at) { - return action; + const agentActionMap: Map = new Map( + agent.actions.map(agentAction => [agentAction.id, agentAction]) + ); + + const matchedUpdatedActions: AgentAction[] = []; + + agentEvents.forEach(agentEvent => { + if (!isAllowedType(agentEvent.type)) { + throw Boom.badRequest(`${agentEvent.type} not allowed for acknowledgment only ACTION_RESULT`); + } + if (agentActionMap.has(agentEvent.action_id!)) { + const action = agentActionMap.get(agentEvent.action_id!) as AgentAction; + if (!action.sent_at) { + action.sent_at = now; + } + matchedUpdatedActions.push(action); + } else { + throw Boom.badRequest('all actions should belong to current agent'); } - return { ...action, sent_at: actionIds.indexOf(action.id) >= 0 ? now : undefined }; }); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, { - actions: updatedActions, - }); + if (matchedUpdatedActions.length > 0) { + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, { + actions: matchedUpdatedActions, + }); + } + + return matchedUpdatedActions; +} + +function isAllowedType(eventType: string): boolean { + return ALLOWED_ACKNOWLEDGEMENT_TYPE.indexOf(eventType) >= 0; +} + +export async function saveAgentEvents( + soClient: SavedObjectsClientContract, + events: AgentEvent[] +): Promise> { + const objects: Array> = events.map( + eventData => { + return { + attributes: { + ...eventData, + payload: eventData.payload ? JSON.stringify(eventData.payload) : undefined, + }, + type: AGENT_EVENT_SAVED_OBJECT_TYPE, + }; + } + ); + + return await soClient.bulkCreate(objects); +} + +export interface AcksService { + acknowledgeAgentActions: ( + soClient: SavedObjectsClientContract, + agent: Agent, + actionIds: AgentEvent[] + ) => Promise; + + getAgentByAccessAPIKeyId: ( + soClient: SavedObjectsClientContract, + accessAPIKeyId: string + ) => Promise; + + getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract; + + saveAgentEvents: ( + soClient: SavedObjectsClientContract, + events: AgentEvent[] + ) => Promise>; } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index bcd825fee872..cdbdf164e834 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -68,7 +68,7 @@ export async function getAgent(soClient: SavedObjectsClientContract, agentId: st export async function getAgentByAccessAPIKeyId( soClient: SavedObjectsClientContract, accessAPIKeyId: string -) { +): Promise { const response = await soClient.find({ type: AGENT_SAVED_OBJECT_TYPE, searchFields: ['access_api_key_id'], diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent.ts b/x-pack/plugins/ingest_manager/server/types/models/agent.ts index 276dddf9e3d1..e0d252faaaf8 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/agent.ts @@ -44,6 +44,11 @@ const AgentEventBase = { stream_id: schema.maybe(schema.string()), }; +export const AckEventSchema = schema.object({ + ...AgentEventBase, + ...{ action_id: schema.string() }, +}); + export const AgentEventSchema = schema.object({ ...AgentEventBase, }); diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index 92422274d5cf..9fe84c12521a 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -5,7 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { AgentEventSchema, AgentTypeSchema } from '../models'; +import { AckEventSchema, AgentEventSchema, AgentTypeSchema } from '../models'; export const GetAgentsRequestSchema = { query: schema.object({ @@ -45,7 +45,7 @@ export const PostAgentEnrollRequestSchema = { export const PostAgentAcksRequestSchema = { body: schema.object({ - action_ids: schema.arrayOf(schema.string()), + events: schema.arrayOf(AckEventSchema), }), params: schema.object({ agentId: schema.string(), 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 1ab54554d62f..a2eba2c23c39 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/acks.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/acks.ts @@ -59,7 +59,7 @@ export default function(providerContext: FtrProviderContext) { .expect(401); }); - it('should return a 200 if this a valid acks access', async () => { + it('should return a 200 if this a valid acks request', async () => { const { body: apiResponse } = await supertest .post(`/api/ingest_manager/fleet/agents/agent1/acks`) .set('kbn-xsrf', 'xx') @@ -68,12 +68,144 @@ export default function(providerContext: FtrProviderContext) { `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` ) .send({ - action_ids: ['action1'], + events: [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: '48cebde1-c906-4893-b89f-595d943b72a1', + agent_id: 'agent1', + message: 'hello', + payload: 'payload', + }, + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-05T14:32:03.36764-05:00', + action_id: '48cebde1-c906-4893-b89f-595d943b72a2', + agent_id: 'agent1', + message: 'hello2', + payload: 'payload2', + }, + ], }) .expect(200); - expect(apiResponse.action).to.be('acks'); expect(apiResponse.success).to.be(true); + const { body: eventResponse } = await supertest + .get(`/api/ingest_manager/fleet/agents/agent1/events`) + .set('kbn-xsrf', 'xx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .expect(200); + const expectedEvents = eventResponse.list.filter( + (item: Record) => + item.action_id === '48cebde1-c906-4893-b89f-595d943b72a1' || + item.action_id === '48cebde1-c906-4893-b89f-595d943b72a2' + ); + expect(expectedEvents.length).to.eql(2); + const expectedEvent = expectedEvents.find( + (item: Record) => item.action_id === '48cebde1-c906-4893-b89f-595d943b72a1' + ); + expect(expectedEvent).to.eql({ + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: '48cebde1-c906-4893-b89f-595d943b72a1', + agent_id: 'agent1', + message: 'hello', + payload: 'payload', + }); + }); + + it('should return a 400 when request event list contains event for another agent id', async () => { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/fleet/agents/agent1/acks`) + .set('kbn-xsrf', 'xx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .send({ + events: [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: '48cebde1-c906-4893-b89f-595d943b72a1', + agent_id: 'agent2', + message: 'hello', + payload: 'payload', + }, + ], + }) + .expect(400); + expect(apiResponse.message).to.eql( + 'agent events contains events with different agent id from currently authorized agent' + ); + }); + + it('should return a 400 when request event list contains action that does not belong to agent current actions', async () => { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/fleet/agents/agent1/acks`) + .set('kbn-xsrf', 'xx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .send({ + events: [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: '48cebde1-c906-4893-b89f-595d943b72a1', + agent_id: 'agent1', + message: 'hello', + payload: 'payload', + }, + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'does-not-exist', + agent_id: 'agent1', + message: 'hello', + payload: 'payload', + }, + ], + }) + .expect(400); + expect(apiResponse.message).to.eql('all actions should belong to current agent'); + }); + + it('should return a 400 when request event list contains action types that are not allowed for acknowledgement', async () => { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/fleet/agents/agent1/acks`) + .set('kbn-xsrf', 'xx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .send({ + events: [ + { + type: 'ACTION', + subtype: 'FAILED', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: '48cebde1-c906-4893-b89f-595d943b72a1', + agent_id: 'agent1', + message: 'hello', + payload: 'payload', + }, + ], + }) + .expect(400); + expect(apiResponse.message).to.eql( + 'ACTION not allowed for acknowledgment only ACTION_RESULT' + ); }); }); } diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index 36928018d15a..9b29767d5162 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -23,6 +23,18 @@ "type": "PAUSE", "created_at": "2019-09-04T15:01:07+0000", "sent_at": "2019-09-04T15:03:07+0000" + }, + { + "created_at" : "2020-03-15T03:47:15.129Z", + "id" : "48cebde1-c906-4893-b89f-595d943b72a1", + "type" : "CONFIG_CHANGE", + "sent_at": "2020-03-04T15:03:07+0000" + }, + { + "created_at" : "2020-03-16T03:47:15.129Z", + "id" : "48cebde1-c906-4893-b89f-595d943b72a2", + "type" : "CONFIG_CHANGE", + "sent_at": "2020-03-04T15:03:07+0000" }] } } From 132383c28ca53e9e348863e198bf01300d39d122 Mon Sep 17 00:00:00 2001 From: Eric Davis Date: Mon, 16 Mar 2020 17:25:14 -0400 Subject: [PATCH 023/115] [Endpoint] TEST: verify alerts page header says 'Alerts' (#60206) * test to verify alerts page header says alerts * updating test with pr feedback * updating test with pr feedback and better verbiage * updating test with pr feedback for better test titling Co-authored-by: Elastic Machine --- .../public/applications/endpoint/view/alerts/index.tsx | 2 +- x-pack/test/functional/apps/endpoint/alert_list.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx index 9718b4e4ef8c..b900a0a35dbf 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx @@ -233,7 +233,7 @@ export const AlertIndex = memo(() => { -

+

{ await esArchiver.load('endpoint/alerts/api_feature'); await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/alerts'); }); - it('loads the Alert List Page', async () => { + it('loads in the browser', async () => { await testSubjects.existOrFail('alertListPage'); }); + it('contains the Alert List Page title', async () => { + const alertsTitle = await testSubjects.getVisibleText('alertsViewTitle'); + expect(alertsTitle).to.equal('Alerts'); + }); it('includes alerts search bar', async () => { await testSubjects.existOrFail('alertsSearchBar'); }); From 537fa8c1eb6b959625ddc0ec9599827ad07e0635 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 16 Mar 2020 14:26:47 -0700 Subject: [PATCH 024/115] [Reporting] Fix error handling for job handler in route (#60161) * fix bogus rison error * add generate route test * update test name --- .../server/routes/generate_from_jobparams.ts | 9 +- .../server/routes/generation.test.ts | 140 ++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 x-pack/legacy/plugins/reporting/server/routes/generation.test.ts diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts index 49868bb7ad5d..56622617586f 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -82,16 +82,21 @@ export function registerGenerateFromJobParams( } const { exportType } = request.params; + let jobParams; let response; try { - const jobParams = rison.decode(jobParamsRison) as object | null; + jobParams = rison.decode(jobParamsRison) as object | null; if (!jobParams) { throw new Error('missing jobParams!'); } - response = await handler(exportType, jobParams, legacyRequest, h); } catch (err) { throw boom.badRequest(`invalid rison: ${jobParamsRison}`); } + try { + response = await handler(exportType, jobParams, legacyRequest, h); + } catch (err) { + throw handleError(exportType, err); + } return response; }, }); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts new file mode 100644 index 000000000000..54d9671692c5 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts @@ -0,0 +1,140 @@ +/* + * 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 Hapi from 'hapi'; +import { createMockReportingCore } from '../../test_helpers'; +import { Logger, ServerFacade } from '../../types'; +import { ReportingCore, ReportingSetupDeps } from '../../server/types'; + +jest.mock('./lib/authorized_user_pre_routing', () => ({ + authorizedUserPreRoutingFactory: () => () => ({}), +})); +jest.mock('./lib/reporting_feature_pre_routing', () => ({ + reportingFeaturePreRoutingFactory: () => () => () => ({ + jobTypes: ['unencodedJobType', 'base64EncodedJobType'], + }), +})); + +import { registerJobGenerationRoutes } from './generation'; + +let mockServer: Hapi.Server; +let mockReportingPlugin: ReportingCore; +const mockLogger = ({ + error: jest.fn(), + debug: jest.fn(), +} as unknown) as Logger; + +beforeEach(async () => { + mockServer = new Hapi.Server({ + debug: false, + port: 8080, + routes: { log: { collect: true } }, + }); + mockServer.config = () => ({ get: jest.fn(), has: jest.fn() }); + mockReportingPlugin = await createMockReportingCore(); + mockReportingPlugin.getEnqueueJob = async () => + jest.fn().mockImplementation(() => ({ toJSON: () => '{ "job": "data" }' })); +}); + +const mockPlugins = { + elasticsearch: { + adminClient: { callAsInternalUser: jest.fn() }, + }, + security: null, +}; + +const getErrorsFromRequest = (request: Hapi.Request) => { + // @ts-ignore error property doesn't exist on RequestLog + return request.logs.filter(log => log.tags.includes('error')).map(log => log.error); // NOTE: error stack is available +}; + +test(`returns 400 if there are no job params`, async () => { + registerJobGenerationRoutes( + mockReportingPlugin, + (mockServer as unknown) as ServerFacade, + (mockPlugins as unknown) as ReportingSetupDeps, + mockLogger + ); + + const options = { + method: 'POST', + url: '/api/reporting/generate/printablePdf', + }; + + const { payload, request } = await mockServer.inject(options); + expect(payload).toMatchInlineSnapshot( + `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"A jobParams RISON string is required\\"}"` + ); + + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toMatchInlineSnapshot(` + Array [ + [Error: A jobParams RISON string is required], + ] + `); +}); + +test(`returns 400 if job params is invalid`, async () => { + registerJobGenerationRoutes( + mockReportingPlugin, + (mockServer as unknown) as ServerFacade, + (mockPlugins as unknown) as ReportingSetupDeps, + mockLogger + ); + + const options = { + method: 'POST', + url: '/api/reporting/generate/printablePdf', + payload: { jobParams: `foo:` }, + }; + + const { payload, request } = await mockServer.inject(options); + expect(payload).toMatchInlineSnapshot( + `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"invalid rison: foo:\\"}"` + ); + + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toMatchInlineSnapshot(` + Array [ + [Error: invalid rison: foo:], + ] + `); +}); + +test(`returns 500 if job handler throws an error`, async () => { + mockReportingPlugin.getEnqueueJob = async () => + jest.fn().mockImplementation(() => ({ + toJSON: () => { + throw new Error('you found me'); + }, + })); + + registerJobGenerationRoutes( + mockReportingPlugin, + (mockServer as unknown) as ServerFacade, + (mockPlugins as unknown) as ReportingSetupDeps, + mockLogger + ); + + const options = { + method: 'POST', + url: '/api/reporting/generate/printablePdf', + payload: { jobParams: `abc` }, + }; + + const { payload, request } = await mockServer.inject(options); + expect(payload).toMatchInlineSnapshot( + `"{\\"statusCode\\":500,\\"error\\":\\"Internal Server Error\\",\\"message\\":\\"An internal server error occurred\\"}"` + ); + + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toMatchInlineSnapshot(` + Array [ + [Error: you found me], + [Error: you found me], + ] + `); +}); From ef3261132a43342c4709efdba235a2cdb9ed1e98 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 16 Mar 2020 15:40:52 -0600 Subject: [PATCH 025/115] [Maps] move MapSavedObject type out of telemetry (#60127) * [Maps] move MapSavedObject type out of telemetry * move SavedObject from server to core/types * review feedback * results from check_published_api_changes --- .../kibana-plugin-core-public.savedobject.md | 1 - .../kibana-plugin-core-server.savedobject.md | 1 - src/core/public/public.api.md | 2 + src/core/public/saved_objects/index.ts | 13 ++-- src/core/server/saved_objects/types.ts | 59 ++----------------- src/core/server/server.api.md | 2 + src/core/types/saved_objects.ts | 51 ++++++++++++++++ .../server/maps_telemetry/maps_telemetry.ts | 32 +--------- .../maps/common/map_saved_object_type.ts | 22 +++++++ 9 files changed, 93 insertions(+), 90 deletions(-) create mode 100644 x-pack/plugins/maps/common/map_saved_object_type.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.md index 9ced619ad4bf..c6bc13b98bc0 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.md @@ -4,7 +4,6 @@ ## SavedObject interface - Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.md index ebb105c846af..0df97b0d4221 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.md @@ -4,7 +4,6 @@ ## SavedObject interface - Signature: ```typescript diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 46667230edc3..fa5dc745e693 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -944,6 +944,8 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T ext [K in keyof T]: RecursiveReadonly; }> : T; +// Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export interface SavedObject { attributes: T; diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index 5015a9c3db78..13b4a1289366 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -32,11 +32,6 @@ export { export { SimpleSavedObject } from './simple_saved_object'; export { SavedObjectsStart, SavedObjectsService } from './saved_objects_service'; export { - SavedObject, - SavedObjectAttribute, - SavedObjectAttributes, - SavedObjectAttributeSingle, - SavedObjectReference, SavedObjectsBaseOptions, SavedObjectsFindOptions, SavedObjectsMigrationVersion, @@ -48,3 +43,11 @@ export { SavedObjectsImportError, SavedObjectsImportRetry, } from '../../server/types'; + +export { + SavedObject, + SavedObjectAttribute, + SavedObjectAttributes, + SavedObjectAttributeSingle, + SavedObjectReference, +} from '../../types'; diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 1d927211b43e..962965a08f8b 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -35,66 +35,17 @@ export { import { LegacyConfig } from '../legacy'; import { SavedObjectUnsanitizedDoc } from './serialization'; import { SavedObjectsMigrationLogger } from './migrations/core/migration_logger'; +import { SavedObject } from '../../types'; + export { SavedObjectAttributes, SavedObjectAttribute, SavedObjectAttributeSingle, + SavedObject, + SavedObjectReference, + SavedObjectsMigrationVersion, } from '../../types'; -/** - * Information about the migrations that have been applied to this SavedObject. - * When Kibana starts up, KibanaMigrator detects outdated documents and - * migrates them based on this value. For each migration that has been applied, - * the plugin's name is used as a key and the latest migration version as the - * value. - * - * @example - * migrationVersion: { - * dashboard: '7.1.1', - * space: '6.6.6', - * } - * - * @public - */ -export interface SavedObjectsMigrationVersion { - [pluginName: string]: string; -} - -/** - * @public - */ -export interface SavedObject { - /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ - id: string; - /** The type of Saved Object. Each plugin can define it's own custom Saved Object types. */ - type: string; - /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ - version?: string; - /** Timestamp of the last time this document had been updated. */ - updated_at?: string; - error?: { - message: string; - statusCode: number; - }; - /** {@inheritdoc SavedObjectAttributes} */ - attributes: T; - /** {@inheritdoc SavedObjectReference} */ - references: SavedObjectReference[]; - /** {@inheritdoc SavedObjectsMigrationVersion} */ - migrationVersion?: SavedObjectsMigrationVersion; -} - -/** - * A reference to another saved object. - * - * @public - */ -export interface SavedObjectReference { - name: string; - type: string; - id: string; -} - /** * * @public diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 84fe081adaae..229ffc4d2157 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1595,6 +1595,8 @@ export interface RouteValidatorOptions { // @public export type SafeRouteMethod = 'get' | 'options'; +// Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export interface SavedObject { attributes: T; diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index 73eb2db11d62..d3faab6c557c 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -46,3 +46,54 @@ export type SavedObjectAttribute = SavedObjectAttributeSingle | SavedObjectAttri export interface SavedObjectAttributes { [key: string]: SavedObjectAttribute; } + +/** + * A reference to another saved object. + * + * @public + */ +export interface SavedObjectReference { + name: string; + type: string; + id: string; +} + +/** + * Information about the migrations that have been applied to this SavedObject. + * When Kibana starts up, KibanaMigrator detects outdated documents and + * migrates them based on this value. For each migration that has been applied, + * the plugin's name is used as a key and the latest migration version as the + * value. + * + * @example + * migrationVersion: { + * dashboard: '7.1.1', + * space: '6.6.6', + * } + * + * @public + */ +export interface SavedObjectsMigrationVersion { + [pluginName: string]: string; +} + +export interface SavedObject { + /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ + id: string; + /** The type of Saved Object. Each plugin can define it's own custom Saved Object types. */ + type: string; + /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ + version?: string; + /** Timestamp of the last time this document had been updated. */ + updated_at?: string; + error?: { + message: string; + statusCode: number; + }; + /** {@inheritdoc SavedObjectAttributes} */ + attributes: T; + /** {@inheritdoc SavedObjectReference} */ + references: SavedObjectReference[]; + /** {@inheritdoc SavedObjectsMigrationVersion} */ + migrationVersion?: SavedObjectsMigrationVersion; +} diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 3ce46e2955f5..6a363af9e57d 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -19,6 +19,7 @@ import { // @ts-ignore } from '../../common/constants'; import { LayerDescriptor } from '../../common/descriptor_types'; +import { MapSavedObject } from '../../../../../plugins/maps/common/map_saved_object_type'; interface IStats { [key: string]: { @@ -32,33 +33,6 @@ interface ILayerTypeCount { [key: string]: number; } -interface IMapSavedObject { - [key: string]: any; - fields: IFieldType[]; - title: string; - id?: string; - type?: string; - timeFieldName?: string; - fieldFormatMap?: Record< - string, - { - id: string; - params: unknown; - } - >; - attributes?: { - title?: string; - description?: string; - mapStateJSON?: string; - layerListJSON?: string; - uiStateJSON?: string; - bounds?: { - type?: string; - coordinates?: []; - }; - }; -} - function getUniqueLayerCounts(layerCountsList: ILayerTypeCount[], mapsCount: number) { const uniqueLayerTypes = _.uniq(_.flatten(layerCountsList.map(lTypes => Object.keys(lTypes)))); @@ -102,7 +76,7 @@ export function buildMapsTelemetry({ indexPatternSavedObjects, settings, }: { - mapSavedObjects: IMapSavedObject[]; + mapSavedObjects: MapSavedObject[]; indexPatternSavedObjects: IIndexPattern[]; settings: SavedObjectAttribute; }): SavedObjectAttributes { @@ -183,7 +157,7 @@ export async function getMapsTelemetry( savedObjectsClient: SavedObjectsClientContract, config: Function ) { - const mapSavedObjects: IMapSavedObject[] = await getMapSavedObjects(savedObjectsClient); + const mapSavedObjects: MapSavedObject[] = await getMapSavedObjects(savedObjectsClient); const indexPatternSavedObjects: IIndexPattern[] = await getIndexPatternSavedObjects( savedObjectsClient ); diff --git a/x-pack/plugins/maps/common/map_saved_object_type.ts b/x-pack/plugins/maps/common/map_saved_object_type.ts new file mode 100644 index 000000000000..e5b4876186fd --- /dev/null +++ b/x-pack/plugins/maps/common/map_saved_object_type.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { SavedObject } from '../../../../src/core/types/saved_objects'; + +export type MapSavedObjectAttributes = { + title?: string; + description?: string; + mapStateJSON?: string; + layerListJSON?: string; + uiStateJSON?: string; + bounds?: { + type?: string; + coordinates?: []; + }; +}; + +export type MapSavedObject = SavedObject; From 4a8fd0afee9262916348c51e98f2ac955e25b2ac Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 16 Mar 2020 15:58:53 -0700 Subject: [PATCH 026/115] Revert "adds new test (#60064)" This reverts commit a946adbf10b0148bf510b0588713dd7a2a04579f. --- .../cypress/integration/detections.spec.ts | 43 +------------------ .../siem/cypress/screens/detections.ts | 4 +- .../plugins/siem/cypress/tasks/detections.ts | 7 --- 3 files changed, 2 insertions(+), 52 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts index c048510c50c3..1624586d4ca1 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts @@ -5,14 +5,12 @@ */ import { NUMBER_OF_SIGNALS, - OPEN_CLOSE_SIGNALS_BTN, SELECTED_SIGNALS, SHOWING_SIGNALS, SIGNALS, } from '../screens/detections'; import { - closeFirstSignal, closeSignals, goToClosedSignals, goToOpenedSignals, @@ -28,7 +26,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS } from '../urls/navigation'; describe('Detections', () => { - beforeEach(() => { + before(() => { esArchiverLoad('signals'); loginAndWaitForPage(DETECTIONS); }); @@ -113,43 +111,4 @@ describe('Detections', () => { .should('eql', expectedNumberOfOpenedSignals.toString()); }); }); - - it('Closes one signal when more than one opened signals are selected', () => { - waitForSignalsToBeLoaded(); - - cy.get(NUMBER_OF_SIGNALS) - .invoke('text') - .then(numberOfSignals => { - const numberOfSignalsToBeClosed = 1; - const numberOfSignalsToBeSelected = 3; - - cy.get(OPEN_CLOSE_SIGNALS_BTN).should('have.attr', 'disabled'); - selectNumberOfSignals(numberOfSignalsToBeSelected); - cy.get(OPEN_CLOSE_SIGNALS_BTN).should('not.have.attr', 'disabled'); - - closeFirstSignal(); - cy.reload(); - waitForSignalsToBeLoaded(); - waitForSignals(); - - const expectedNumberOfSignals = +numberOfSignals - numberOfSignalsToBeClosed; - cy.get(NUMBER_OF_SIGNALS) - .invoke('text') - .should('eq', expectedNumberOfSignals.toString()); - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfSignals.toString()} signals`); - - goToClosedSignals(); - waitForSignals(); - - cy.get(NUMBER_OF_SIGNALS) - .invoke('text') - .should('eql', numberOfSignalsToBeClosed.toString()); - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should('eql', `Showing ${numberOfSignalsToBeClosed.toString()} signal`); - cy.get(SIGNALS).should('have.length', numberOfSignalsToBeClosed); - }); - }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts index f388ac1215d0..8b5ba2357880 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts @@ -12,9 +12,7 @@ export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal export const NUMBER_OF_SIGNALS = '[data-test-subj="server-side-event-count"]'; -export const OPEN_CLOSE_SIGNAL_BTN = '[data-test-subj="update-signal-status-button"]'; - -export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] button'; +export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] .siemLinkIcon__label'; export const OPENED_SIGNALS_BTN = '[data-test-subj="openSignals"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts index 3416e3eb81de..21a0c136b90d 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts @@ -8,7 +8,6 @@ import { CLOSED_SIGNALS_BTN, LOADING_SIGNALS_PANEL, MANAGE_SIGNAL_DETECTION_RULES_BTN, - OPEN_CLOSE_SIGNAL_BTN, OPEN_CLOSE_SIGNALS_BTN, OPENED_SIGNALS_BTN, SIGNALS, @@ -16,12 +15,6 @@ import { } from '../screens/detections'; import { REFRESH_BUTTON } from '../screens/siem_header'; -export const closeFirstSignal = () => { - cy.get(OPEN_CLOSE_SIGNAL_BTN) - .first() - .click({ force: true }); -}; - export const closeSignals = () => { cy.get(OPEN_CLOSE_SIGNALS_BTN).click({ force: true }); }; From 4ebdc4edadc5def675c6ddca1444695257976b66 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 16 Mar 2020 17:02:41 -0700 Subject: [PATCH 027/115] Added message variables button for Webhook body form field (#60174) * Added message variables button for Webhook body form field * Fixed test issue --- .../builtin_action_types/webhook.test.tsx | 1 + .../builtin_action_types/webhook.tsx | 43 ++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx index 5e6c0404db53..fdb60bd2d914 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx @@ -160,6 +160,7 @@ describe('WebhookParamsFields renders', () => { .first() .prop('value') ).toStrictEqual('test message'); + expect(wrapper.find('[data-test-subj="webhookAddVariableButton"]').length > 0).toBeTruthy(); }); test('params validation fails when body is not valid', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx index 862548728288..5d07483c8a98 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx @@ -22,6 +22,9 @@ import { EuiCodeEditor, EuiSwitch, EuiButtonEmpty, + EuiContextMenuItem, + EuiPopover, + EuiContextMenuPanel, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { @@ -454,10 +457,24 @@ const WebhookParamsFields: React.FunctionComponent { const { body } = actionParams; - + const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); + const messageVariablesItems = messageVariables?.map((variable: string, i: number) => ( + { + editAction('body', (body ?? '').concat(` {{${variable}}}`), index); + setIsVariablesPopoverOpen(false); + }} + > + {`{{${variable}}}`} + + )); return ( 0 && body !== undefined} fullWidth error={errors.body} + labelAppend={ + // TODO: replace this button with a proper Eui component, when it will be ready + setIsVariablesPopoverOpen(true)} + iconType="indexOpen" + aria-label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariablePopoverButton', + { + defaultMessage: 'Add variable', + } + )} + /> + } + isOpen={isVariablesPopoverOpen} + closePopover={() => setIsVariablesPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + } > Date: Mon, 16 Mar 2020 17:05:11 -0700 Subject: [PATCH 028/115] [EPM] Packages list tabs (#60167) * Memo'ize some layout and EPM header components * Split EPM home page into two tabs * Clean up dead files and import paths * Add empty state * Use react routing for rendering tab content * Fix import paths (again) --- .../common/types/models/agent.ts | 2 +- .../common/types/models/agent_config.ts | 2 +- .../common/types/models/enrollment_api_key.ts | 2 +- .../ingest_manager/common/types/models/epm.ts | 6 +- .../ingest_manager/components/header.tsx | 14 +- .../ingest_manager/constants/index.ts | 2 + .../epm => }/hooks/use_breadcrumbs.tsx | 4 +- .../ingest_manager/hooks/use_core.ts | 2 +- .../hooks/use_request/agent_config.ts | 2 +- .../ingest_manager/hooks/use_request/epm.ts | 2 +- .../hooks/use_request/use_request.ts | 2 +- .../applications/ingest_manager/index.tsx | 2 +- .../epm/components/package_list_grid.tsx | 88 ++++++- .../sections/epm/hooks/index.tsx | 3 +- .../sections/epm/hooks/use_local_search.tsx | 27 +++ .../ingest_manager/sections/epm/index.tsx | 6 +- .../sections/epm/screens/detail/header.tsx | 2 - .../epm/screens/home/category_facets.tsx | 29 ++- .../sections/epm/screens/home/header.tsx | 15 +- .../sections/epm/screens/home/hooks.tsx | 47 ---- .../sections/epm/screens/home/index.tsx | 218 ++++++++++-------- x-pack/plugins/ingest_manager/public/index.ts | 2 +- .../plugins/ingest_manager/public/plugin.ts | 2 +- x-pack/plugins/ingest_manager/server/index.ts | 2 +- .../plugins/ingest_manager/server/plugin.ts | 2 +- .../server/routes/agent/handlers.ts | 2 +- .../server/routes/agent/index.ts | 2 +- .../server/routes/agent_config/handlers.ts | 2 +- .../server/routes/agent_config/index.ts | 2 +- .../server/routes/datasource/handlers.ts | 2 +- .../server/routes/datasource/index.ts | 2 +- .../routes/enrollment_api_key/handler.ts | 2 +- .../server/routes/enrollment_api_key/index.ts | 2 +- .../server/routes/epm/handlers.ts | 2 +- .../ingest_manager/server/routes/epm/index.ts | 2 +- .../server/routes/install_script/index.ts | 2 +- .../server/routes/setup/handlers.ts | 2 +- .../server/routes/setup/index.ts | 2 +- .../server/services/agent_config.ts | 2 +- .../server/services/agent_config_update.ts | 2 +- .../server/services/agents/acks.ts | 2 +- .../server/services/agents/checkin.ts | 2 +- .../server/services/agents/crud.ts | 2 +- .../server/services/agents/enroll.ts | 2 +- .../server/services/agents/events.ts | 2 +- .../server/services/agents/saved_objects.ts | 2 +- .../server/services/agents/status.ts | 2 +- .../server/services/agents/unenroll.ts | 2 +- .../server/services/agents/update.ts | 2 +- .../services/api_keys/enrollment_api_key.ts | 2 +- .../server/services/api_keys/index.ts | 2 +- .../server/services/api_keys/security.ts | 2 +- .../server/services/app_context.ts | 2 +- .../server/services/datasource.ts | 2 +- .../epm/kibana/index_pattern/install.ts | 2 +- .../server/services/epm/packages/get.ts | 2 +- .../services/epm/packages/get_objects.ts | 2 +- .../server/services/epm/packages/index.ts | 2 +- .../server/services/epm/packages/install.ts | 2 +- .../server/services/epm/packages/remove.ts | 2 +- .../ingest_manager/server/services/output.ts | 2 +- .../ingest_manager/server/services/setup.ts | 2 +- .../ingest_manager/server/types/index.tsx | 2 +- 63 files changed, 313 insertions(+), 248 deletions(-) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/{sections/epm => }/hooks/use_breadcrumbs.tsx (76%) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_local_search.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/hooks.tsx diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index a0575c71d3ab..ad06e8d3c9c1 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectAttributes } from '../../../../../../src/core/public'; +import { SavedObjectAttributes } from 'src/core/public'; import { AGENT_TYPE_EPHEMERAL, AGENT_TYPE_PERMANENT, AGENT_TYPE_TEMPORARY } from '../../constants'; export type AgentType = diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts index c63e496273ad..002c3784446a 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectAttributes } from '../../../../../../src/core/public'; +import { SavedObjectAttributes } from 'src/core/public'; import { Datasource, DatasourcePackage, diff --git a/x-pack/plugins/ingest_manager/common/types/models/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/common/types/models/enrollment_api_key.ts index 35cb851a7293..204ce4b15ea5 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/enrollment_api_key.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 { SavedObjectAttributes } from '../../../../../../src/core/public'; +import { SavedObjectAttributes } from 'src/core/public'; export interface EnrollmentAPIKey { id: string; diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index a1a39444c3b5..6b8403b74a75 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -6,11 +6,7 @@ // Follow pattern from https://github.com/elastic/kibana/pull/52447 // TODO: Update when https://github.com/elastic/kibana/issues/53021 is closed -import { - SavedObject, - SavedObjectAttributes, - SavedObjectReference, -} from '../../../../../../src/core/public'; +import { SavedObject, SavedObjectAttributes, SavedObjectReference } from 'src/core/public'; export enum InstallationStatus { installed = 'installed', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx index c87a77320d3f..e1f29fdbeb32 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.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 from 'react'; +import React, { memo } from 'react'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; @@ -35,13 +35,17 @@ export interface HeaderProps { tabs?: EuiTabProps[]; } +const HeaderColumns: React.FC> = memo(({ leftColumn, rightColumn }) => ( + + {leftColumn ? {leftColumn} : null} + {rightColumn ? {rightColumn} : null} + +)); + export const Header: React.FC = ({ leftColumn, rightColumn, tabs }) => ( - - {leftColumn ? {leftColumn} : null} - {rightColumn ? {rightColumn} : null} - + {tabs ? ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts index 1ac5bef629fd..b313dbf629f3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts @@ -7,6 +7,8 @@ export { PLUGIN_ID, EPM_API_ROUTES, AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../.. export const BASE_PATH = '/app/ingestManager'; export const EPM_PATH = '/epm'; +export const EPM_LIST_ALL_PACKAGES_PATH = EPM_PATH; +export const EPM_LIST_INSTALLED_PACKAGES_PATH = `${EPM_PATH}/installed`; export const EPM_DETAIL_VIEW_PATH = `${EPM_PATH}/detail/:pkgkey/:panel?`; export const AGENT_CONFIG_PATH = '/configs'; export const AGENT_CONFIG_DETAILS_PATH = `${AGENT_CONFIG_PATH}/`; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_breadcrumbs.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx similarity index 76% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_breadcrumbs.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx index 6222d346432c..ff6656e969c9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ChromeBreadcrumb } from '../../../../../../../../../src/core/public'; -import { useCore } from '../../../hooks'; +import { ChromeBreadcrumb } from 'src/core/public'; +import { useCore } from './use_core'; export function useBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]) { const { chrome } = useCore(); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_core.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_core.ts index c6e91444d21f..f4e9a032b925 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_core.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_core.ts @@ -5,7 +5,7 @@ */ import React, { useContext } from 'react'; -import { CoreStart } from 'kibana/public'; +import { CoreStart } from 'src/core/public'; export const CoreContext = React.createContext(null); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts index fe3fb4aa3296..d44cc67e2dc4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.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 { HttpFetchQuery } from 'kibana/public'; +import { HttpFetchQuery } from 'src/core/public'; import { useRequest, sendRequest } from './use_request'; import { agentConfigRouteService } from '../../services'; import { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts index 02865ffe6fb1..128ef8de68aa 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpFetchQuery } from 'kibana/public'; +import { HttpFetchQuery } from 'src/core/public'; import { useRequest, sendRequest } from './use_request'; import { epmRouteService } from '../../services'; import { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts index 4b434bd1a149..c63383637e79 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.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 { HttpSetup } from 'kibana/public'; +import { HttpSetup } from 'src/core/public'; import { SendRequestConfig, SendRequestResponse, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 9a85358a2a69..f7c2805c6ea7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -9,7 +9,7 @@ import { useObservable } from 'react-use'; import { HashRouter as Router, Redirect, Switch, Route, RouteProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiErrorBoundary } from '@elastic/eui'; -import { CoreStart, AppMountParameters } from 'kibana/public'; +import { CoreStart, AppMountParameters } from 'src/core/public'; import { EuiThemeProvider } from '../../../../../legacy/common/eui_styled_components'; import { IngestManagerSetupDeps, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx index 34e1763c4425..2ca49298decf 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx @@ -3,25 +3,76 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; -import React, { Fragment, ReactNode } from 'react'; +import React, { Fragment, ReactNode, useState } from 'react'; +import { + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, + // @ts-ignore + EuiSearchBar, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Loading } from '../../../components'; import { PackageList } from '../../../types'; +import { useLocalSearch, searchIdField } from '../hooks'; import { BadgeProps, PackageCard } from './package_card'; type ListProps = { + isLoading?: boolean; controls?: ReactNode; title: string; list: PackageList; } & BadgeProps; -export function PackageListGrid({ controls, title, list, showInstalledBadge }: ListProps) { +export function PackageListGrid({ + isLoading, + controls, + title, + list, + showInstalledBadge, +}: ListProps) { + const [searchTerm, setSearchTerm] = useState(''); + const localSearchRef = useLocalSearch(list); + const controlsContent = ; - const gridContent = ; + let gridContent: JSX.Element; + + if (isLoading || !localSearchRef.current) { + gridContent = ; + } else { + const filteredList = searchTerm + ? list.filter(item => + (localSearchRef.current!.search(searchTerm) as PackageList) + .map(match => match[searchIdField]) + .includes(item[searchIdField]) + ) + : list; + gridContent = ; + } return ( - + {controlsContent} - {gridContent} + + { + setSearchTerm(userInput); + }} + /> + + {gridContent} + ); } @@ -34,9 +85,9 @@ interface ControlsColumnProps { function ControlsColumn({ controls, title }: ControlsColumnProps) { return ( - +

{title}

-
+ {controls} @@ -53,11 +104,24 @@ type GridColumnProps = { function GridColumn({ list }: GridColumnProps) { return ( - {list.map(item => ( - - + {list.length ? ( + list.map(item => ( + + + + )) + ) : ( + + +

+ +

+
- ))} + )}
); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/index.tsx index 589ce5f5dbd2..48986481b606 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/index.tsx @@ -3,9 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -// export { useBreadcrumbs } from './use_breadcrumbs'; export { useLinks } from './use_links'; +export { useLocalSearch, searchIdField } from './use_local_search'; export { PackageInstallProvider, useDeletePackage, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_local_search.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_local_search.tsx new file mode 100644 index 000000000000..26f1ef6a8027 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_local_search.tsx @@ -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 { Search as LocalSearch } from 'js-search'; +import { useEffect, useRef } from 'react'; +import { PackageList, PackageListItem } from '../../../types'; + +export type SearchField = keyof PackageListItem; +export const searchIdField: SearchField = 'name'; +export const fieldsToSearch: SearchField[] = ['description', 'name', 'title']; + +export function useLocalSearch(packageList: PackageList) { + const localSearchRef = useRef(null); + + useEffect(() => { + if (!packageList.length) return; + + const localSearch = new LocalSearch(searchIdField); + fieldsToSearch.forEach(field => localSearch.addIndex(field)); + localSearch.addDocuments(packageList); + localSearchRef.current = localSearch; + }, [packageList]); + + return localSearchRef; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx index b8dd08eb46a5..2c8ee7ca2fcf 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { HashRouter as Router, Switch, Route } from 'react-router-dom'; import { useConfig } from '../../hooks'; import { CreateDatasourcePage } from '../agent_config/create_datasource_page'; -import { Home } from './screens/home'; +import { EPMHomePage } from './screens/home'; import { Detail } from './screens/detail'; export const EPMApp: React.FunctionComponent = () => { @@ -23,8 +23,8 @@ export const EPMApp: React.FunctionComponent = () => { - - + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx index 5a51515d4948..a7204dd72260 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx @@ -36,8 +36,6 @@ export function Header(props: HeaderProps) { const { iconType, name, title, version } = props; const hasWriteCapabilites = useCapabilities().write; const { toListView } = useLinks(); - // useBreadcrumbs([{ text: PLUGIN.TITLE, href: toListView() }, { text: title }]); - const ADD_DATASOURCE_URI = useLink(`${EPM_PATH}/${name}-${version}/add-datasource`); return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/category_facets.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/category_facets.tsx index e138f9f531a3..52730664aac0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/category_facets.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/category_facets.tsx @@ -5,30 +5,37 @@ */ import { EuiFacetButton, EuiFacetGroup } from '@elastic/eui'; import React from 'react'; +import { Loading } from '../../../../components'; import { CategorySummaryItem, CategorySummaryList } from '../../../../types'; export function CategoryFacets({ + isLoading, categories, selectedCategory, onCategoryChange, }: { + isLoading?: boolean; categories: CategorySummaryList; selectedCategory: string; onCategoryChange: (category: CategorySummaryItem) => unknown; }) { const controls = ( - {categories.map(category => ( - onCategoryChange(category)} - > - {category.title} - - ))} + {isLoading ? ( + + ) : ( + categories.map(category => ( + onCategoryChange(category)} + > + {category.title} + + )) + )} ); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx index 2cb5aca39c80..4230775c04e0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx @@ -3,14 +3,13 @@ * 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 styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; -import styled from 'styled-components'; import { useLinks } from '../../hooks'; -export function HeroCopy() { +export const HeroCopy = memo(() => { return ( @@ -35,12 +34,12 @@ export function HeroCopy() { ); -} +}); -export function HeroImage() { +export const HeroImage = memo(() => { const { toAssets } = useLinks(); const ImageWrapper = styled.div` - margin-bottom: -38px; // revert to -62px when tabs are restored + margin-bottom: -62px; `; return ( @@ -51,4 +50,4 @@ export function HeroImage() { /> ); -} +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/hooks.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/hooks.tsx deleted file mode 100644 index c3e29f723dcb..000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/hooks.tsx +++ /dev/null @@ -1,47 +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 { useEffect, useRef, useState } from 'react'; -import { PackageList } from '../../../../types'; -import { fieldsToSearch, LocalSearch, searchIdField } from './search_packages'; - -export function useAllPackages(selectedCategory: string, categoryPackages: PackageList = []) { - const [allPackages, setAllPackages] = useState([]); - - useEffect(() => { - if (!selectedCategory) setAllPackages(categoryPackages); - }, [selectedCategory, categoryPackages]); - - return [allPackages, setAllPackages] as [typeof allPackages, typeof setAllPackages]; -} - -export function useLocalSearch(allPackages: PackageList) { - const localSearchRef = useRef(null); - - useEffect(() => { - if (!allPackages.length) return; - - const localSearch = new LocalSearch(searchIdField); - fieldsToSearch.forEach(field => localSearch.addIndex(field)); - localSearch.addDocuments(allPackages); - localSearchRef.current = localSearch; - }, [allPackages]); - - return localSearchRef; -} - -export function useInstalledPackages(allPackages: PackageList) { - const [installedPackages, setInstalledPackages] = useState([]); - - useEffect(() => { - setInstalledPackages(allPackages.filter(({ status }) => status === 'installed')); - }, [allPackages]); - - return [installedPackages, setInstalledPackages] as [ - typeof installedPackages, - typeof setInstalledPackages - ]; -} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx index 640e4a30a40c..5f215b778825 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx @@ -4,136 +4,152 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { useState } from 'react'; +import { useRouteMatch, Switch, Route } from 'react-router-dom'; +import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; +import { i18n } from '@kbn/i18n'; import { - EuiHorizontalRule, - // @ts-ignore - EuiSearchBar, - EuiSpacer, -} from '@elastic/eui'; -import React, { Fragment, useState } from 'react'; -import { useGetCategories, useGetPackages } from '../../../../hooks'; + EPM_LIST_ALL_PACKAGES_PATH, + EPM_LIST_INSTALLED_PACKAGES_PATH, +} from '../../../../constants'; +import { useLink, useGetCategories, useGetPackages } from '../../../../hooks'; import { WithHeaderLayout } from '../../../../layouts'; -import { CategorySummaryItem, PackageList } from '../../../../types'; +import { CategorySummaryItem } from '../../../../types'; import { PackageListGrid } from '../../components/package_list_grid'; -// import { useBreadcrumbs, useLinks } from '../../hooks'; import { CategoryFacets } from './category_facets'; import { HeroCopy, HeroImage } from './header'; -import { useAllPackages, useInstalledPackages, useLocalSearch } from './hooks'; -import { SearchPackages } from './search_packages'; -export function Home() { - // useBreadcrumbs([{ text: PLUGIN.TITLE, href: toListView() }]); +export function EPMHomePage() { + const { + params: { tabId }, + } = useRouteMatch<{ tabId?: string }>(); - const state = useHomeState(); - const searchBar = ( - { - state.setSearchTerm(userInput); - }} - /> - ); - const body = state.searchTerm ? ( - - ) : ( - - {state.installedPackages.length ? ( - - - - - ) : null} - - - ); + const ALL_PACKAGES_URI = useLink(EPM_LIST_ALL_PACKAGES_PATH); + const INSTALLED_PACKAGES_URI = useLink(EPM_LIST_INSTALLED_PACKAGES_PATH); return ( } rightColumn={} - // tabs={[ - // { - // id: 'all_packages', - // name: 'All packages', - // isSelected: true, - // }, - // { - // id: 'installed_packages', - // name: 'Installed packages', - // }, - // ]} + tabs={ + ([ + { + id: 'all_packages', + name: i18n.translate('xpack.ingestManager.epmList.allPackagesTabText', { + defaultMessage: 'All packages', + }), + href: ALL_PACKAGES_URI, + isSelected: tabId !== 'installed', + }, + { + id: 'installed_packages', + name: i18n.translate('xpack.ingestManager.epmList.installedPackagesTabText', { + defaultMessage: 'Installed packages', + }), + href: INSTALLED_PACKAGES_URI, + isSelected: tabId === 'installed', + }, + ] as unknown) as EuiTabProps[] + } > - {searchBar} - - {body} + + + + + + + + ); } -type HomeState = ReturnType; - -export function useHomeState() { - const [searchTerm, setSearchTerm] = useState(''); +function InstalledPackages() { + const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages(); const [selectedCategory, setSelectedCategory] = useState(''); - const { data: categoriesRes } = useGetCategories(); - const categories = categoriesRes?.response; - const { data: categoryPackagesRes } = useGetPackages({ category: selectedCategory }); - const categoryPackages = categoryPackagesRes?.response; - const [allPackages, setAllPackages] = useAllPackages(selectedCategory, categoryPackages); - const localSearchRef = useLocalSearch(allPackages); - const [installedPackages, setInstalledPackages] = useInstalledPackages(allPackages); + const packages = + allPackages && allPackages.response && selectedCategory === '' + ? allPackages.response.filter(pkg => pkg.status === 'installed') + : []; - return { - searchTerm, - setSearchTerm, - selectedCategory, - setSelectedCategory, - categories, - allPackages, - setAllPackages, - installedPackages, - localSearchRef, - setInstalledPackages, - categoryPackages, - }; -} + const title = i18n.translate('xpack.ingestManager.epmList.installedPackagesTitle', { + defaultMessage: 'Installed packages', + }); -function InstalledPackages({ list }: { list: PackageList }) { - const title = 'Your Packages'; + const categories = [ + { + id: '', + title: i18n.translate('xpack.ingestManager.epmList.allPackagesFilterLinkText', { + defaultMessage: 'All', + }), + count: packages.length, + }, + { + id: 'updates_available', + title: i18n.translate('xpack.ingestManager.epmList.updatesAvailableFilterLinkText', { + defaultMessage: 'Updates available', + }), + count: 0, // TODO: Update with real count when available + }, + ]; - return ; + const controls = ( + setSelectedCategory(id)} + /> + ); + + return ( + + ); } -function AvailablePackages({ - allPackages, - categories, - categoryPackages, - selectedCategory, - setSelectedCategory, -}: HomeState) { - const title = 'Available Packages'; - const noFilter = { - id: '', - title: 'All', - count: allPackages.length, - }; +function AvailablePackages() { + const [selectedCategory, setSelectedCategory] = useState(''); + const { data: categoryPackagesRes, isLoading: isLoadingPackages } = useGetPackages({ + category: selectedCategory, + }); + const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories(); + const packages = + categoryPackagesRes && categoryPackagesRes.response ? categoryPackagesRes.response : []; + + const title = i18n.translate('xpack.ingestManager.epmList.allPackagesTitle', { + defaultMessage: 'All packages', + }); + + const categories = [ + { + id: '', + title: i18n.translate('xpack.ingestManager.epmList.allPackagesFilterLinkText', { + defaultMessage: 'All', + }), + count: packages.length, + }, + ...(categoriesRes ? categoriesRes.response : []), + ]; const controls = categories ? ( setSelectedCategory(id)} /> ) : null; - return ; + return ( + + ); } diff --git a/x-pack/plugins/ingest_manager/public/index.ts b/x-pack/plugins/ingest_manager/public/index.ts index a9e40a2a4230..aa1e0e79e548 100644 --- a/x-pack/plugins/ingest_manager/public/index.ts +++ b/x-pack/plugins/ingest_manager/public/index.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 { PluginInitializerContext } from 'kibana/public'; +import { PluginInitializerContext } from 'src/core/public'; import { IngestManagerPlugin } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => { diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index a1dc2c057e9e..99dcebd9bfba 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -9,7 +9,7 @@ import { Plugin, PluginInitializerContext, CoreStart, -} from 'kibana/public'; +} from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index b732cb8005ef..df7c3d7cf0fb 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'kibana/server'; +import { PluginInitializerContext } from 'src/core/server'; import { IngestManagerPlugin } from './plugin'; export const config = { diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index c162ea5fadab..67737c6fe502 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -11,7 +11,7 @@ import { Plugin, PluginInitializerContext, SavedObjectsServiceStart, -} from 'kibana/server'; +} from 'src/core/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { EncryptedSavedObjectsPluginStart } from '../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../security/server'; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index cf1fd2476f31..7d991f5ad2cc 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler, KibanaRequest } from 'kibana/server'; +import { RequestHandler, KibanaRequest } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; import { GetAgentsResponse, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index c85629ea22ad..414d2d79e906 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -9,7 +9,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from 'kibana/server'; +import { IRouter } from 'src/core/server'; import { PLUGIN_ID, AGENT_API_ROUTES } from '../../constants'; import { GetAgentsRequestSchema, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts index f670a797c3fb..67f758c2c126 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeOf } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { RequestHandler } from 'src/core/server'; import bluebird from 'bluebird'; import { appContextService, agentConfigService, datasourceService } from '../../services'; import { listAgents } from '../../services/agents'; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts index c3b3c00a9574..b8e827974ff8 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/index.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 { IRouter } from 'kibana/server'; +import { IRouter } from 'src/core/server'; import { PLUGIN_ID, AGENT_CONFIG_API_ROUTES } from '../../constants'; import { GetAgentConfigsRequestSchema, diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts index 349e88d8fb59..7ae562cf130a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeOf } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { RequestHandler } from 'src/core/server'; import { appContextService, datasourceService } from '../../services'; import { ensureInstalledPackage } from '../../services/epm/packages'; import { diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/index.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/index.ts index e5891cc7377e..7217f28053cf 100644 --- a/x-pack/plugins/ingest_manager/server/routes/datasource/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/index.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 { IRouter } from 'kibana/server'; +import { IRouter } from 'src/core/server'; import { PLUGIN_ID, DATASOURCE_API_ROUTES } from '../../constants'; import { GetDatasourcesRequestSchema, diff --git a/x-pack/plugins/ingest_manager/server/routes/enrollment_api_key/handler.ts b/x-pack/plugins/ingest_manager/server/routes/enrollment_api_key/handler.ts index 478078a93418..9d3eb5360dbe 100644 --- a/x-pack/plugins/ingest_manager/server/routes/enrollment_api_key/handler.ts +++ b/x-pack/plugins/ingest_manager/server/routes/enrollment_api_key/handler.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'kibana/server'; +import { RequestHandler } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; import { GetEnrollmentAPIKeysRequestSchema, diff --git a/x-pack/plugins/ingest_manager/server/routes/enrollment_api_key/index.ts b/x-pack/plugins/ingest_manager/server/routes/enrollment_api_key/index.ts index 6df5299d30bd..9d0ff65ab0b3 100644 --- a/x-pack/plugins/ingest_manager/server/routes/enrollment_api_key/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/enrollment_api_key/index.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 { IRouter } from 'kibana/server'; +import { IRouter } from 'src/core/server'; import { PLUGIN_ID, ENROLLMENT_API_KEY_ROUTES } from '../../constants'; import { GetEnrollmentAPIKeysRequestSchema, diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index 6b1dde92ec0e..8623d02e7286 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeOf } from '@kbn/config-schema'; -import { RequestHandler, CustomHttpResponseOptions } from 'kibana/server'; +import { RequestHandler, CustomHttpResponseOptions } from 'src/core/server'; import { GetPackagesRequestSchema, GetFileRequestSchema, diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts index cb9ec5cc532c..fcf81f9894d5 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/index.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 { IRouter } from 'kibana/server'; +import { IRouter } from 'src/core/server'; import { PLUGIN_ID, EPM_API_ROUTES } from '../../constants'; import { getCategoriesHandler, diff --git a/x-pack/plugins/ingest_manager/server/routes/install_script/index.ts b/x-pack/plugins/ingest_manager/server/routes/install_script/index.ts index 5470df31adbd..b007e61594e9 100644 --- a/x-pack/plugins/ingest_manager/server/routes/install_script/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/install_script/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import url from 'url'; -import { IRouter, BasePath, HttpServerInfo, KibanaRequest } from 'kibana/server'; +import { IRouter, BasePath, HttpServerInfo, KibanaRequest } from 'src/core/server'; import { INSTALL_SCRIPT_API_ROUTES } from '../../constants'; import { getScript } from '../../services/install_script'; import { InstallScriptRequestSchema } from '../../types'; diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts index 30e725bb5ad4..38188bc76f5f 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeOf } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { RequestHandler } from 'src/core/server'; import { outputService, agentConfigService } from '../../services'; import { CreateFleetSetupRequestSchema, CreateFleetSetupResponse } from '../../types'; import { setup } from '../../services/setup'; diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts index 7e09d8dbef1f..a2c641503e82 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/index.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 { IRouter } from 'kibana/server'; +import { IRouter } from 'src/core/server'; import { PLUGIN_ID, FLEET_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; import { GetFleetSetupRequestSchema, CreateFleetSetupRequestSchema } from '../../types'; import { diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index e5b20de3bf91..a941494072ae 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { uniq } from 'lodash'; -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; import { DEFAULT_AGENT_CONFIG, AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../constants'; import { diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts b/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts index 38894ff321a0..8c0e73201e1f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { generateEnrollmentAPIKey, deleteEnrollmentApiKeyForConfigId } from './api_keys'; import { updateAgentsForConfigId, unenrollForConfigId } from './agents'; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts index 892d8cdbe657..98a5f69f9d2b 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts @@ -9,7 +9,7 @@ import { SavedObjectsBulkCreateObject, SavedObjectsBulkResponse, SavedObjectsClientContract, -} from 'kibana/server'; +} from 'src/core/server'; import Boom from 'boom'; import { Agent, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts index 76dfc0867fb4..0ff4af4ffe35 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, SavedObjectsBulkCreateObject } from 'kibana/server'; +import { SavedObjectsClientContract, SavedObjectsBulkCreateObject } from 'src/core/server'; import uuid from 'uuid'; import { Agent, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index cdbdf164e834..41bd2476c99a 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts index b48d311da444..0f73f71817eb 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { AgentType, Agent, AgentSOAttributes } from '../../types'; import { savedObjectToAgent } from './saved_objects'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/events.ts b/x-pack/plugins/ingest_manager/server/services/agents/events.ts index 908d289fbc4b..707229845531 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/events.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/events.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentEventSOAttributes, AgentEvent } from '../../types'; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts index adb096a44490..dbe268818713 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject } from 'kibana/server'; +import { SavedObject } from 'src/core/server'; import { Agent, AgentSOAttributes } from '../../types'; export function savedObjectToAgent(so: SavedObject): Agent { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts index f6477bf1c733..21e200d701e6 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { listAgents } from './crud'; import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentStatus, Agent } from '../../types'; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts index e45620c3cf58..bf6f6526be06 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { AgentSOAttributes } from '../../types'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/update.ts b/x-pack/plugins/ingest_manager/server/services/agents/update.ts index 8452c05d53a1..9eabf0944bdc 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/update.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { listAgents } from './crud'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { unenrollAgents } from './unenroll'; 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 9a1a91f9ed8a..d81b998d5a75 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 @@ -5,7 +5,7 @@ */ import uuid from 'uuid'; -import { SavedObjectsClientContract, SavedObject } from 'kibana/server'; +import { SavedObjectsClientContract, SavedObject } from 'src/core/server'; import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types'; import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { createAPIKey, invalidateAPIKey } from './security'; 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 a7c74f279d16..9b0182b86fc8 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, SavedObject, KibanaRequest } from 'kibana/server'; +import { SavedObjectsClientContract, SavedObject, KibanaRequest } from 'src/core/server'; import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { EnrollmentAPIKeySOAttributes, EnrollmentAPIKey } from '../../types'; import { createAPIKey } from './security'; diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/security.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/security.ts index ffc269bca94e..dfd53d55fbbf 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/security.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/security.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, FakeRequest, SavedObjectsClientContract } from 'kibana/server'; +import { KibanaRequest, FakeRequest, SavedObjectsClientContract } from 'src/core/server'; import { CallESAsCurrentUser } from '../../types'; import { appContextService } from '../app_context'; import { outputService } from '../output'; diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index c06b282389fc..a0a7c8dd7c05 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -5,7 +5,7 @@ */ import { BehaviorSubject, Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { SavedObjectsServiceStart } from 'kibana/server'; +import { SavedObjectsServiceStart } from 'src/core/server'; import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../../security/server'; import { IngestManagerConfigType } from '../../common'; diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts index 444937343e31..8fa1428f3a05 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.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 { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; import { DeleteDatasourcesResponse, packageToConfigDatasource } from '../../common'; import { DATASOURCE_SAVED_OBJECT_TYPE } from '../constants'; 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 264000f9892b..1f1113636046 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../../constants'; import * as Registry from '../../registry'; import { loadFieldsFromYaml, Fields, Field } from '../../fields/field'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index 58416b7f66d2..d655b81f8cde 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server/'; +import { SavedObjectsClientContract } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { Installation, InstallationStatus, PackageInfo } from '../../../types'; import * as Registry from '../registry'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts index e0424aa8a36f..b924c045870f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject, SavedObjectsBulkCreateObject } from 'src/core/server/'; +import { SavedObject, SavedObjectsBulkCreateObject } from 'src/core/server'; import { AssetType } from '../../../types'; import * as Registry from '../registry'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index 2f84ea5b6f8d..79259ce79ff4 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject } from '../../../../../../../src/core/server'; +import { SavedObject } from 'src/core/server'; import { AssetType, Installable, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index acf77998fdb3..3cce238f582f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject, SavedObjectsClientContract } from 'src/core/server/'; +import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index e57729a7ab2b..2e73160453c2 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server/'; +import { SavedObjectsClientContract } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, AssetType, ElasticsearchAssetType } from '../../../types'; import { CallESAsCurrentUser } from '../../../types'; diff --git a/x-pack/plugins/ingest_manager/server/services/output.ts b/x-pack/plugins/ingest_manager/server/services/output.ts index 066f8e8a316a..8503bbb56ee8 100644 --- a/x-pack/plugins/ingest_manager/server/services/output.ts +++ b/x-pack/plugins/ingest_manager/server/services/output.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 { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { NewOutput, Output } from '../types'; import { DEFAULT_OUTPUT, OUTPUT_SAVED_OBJECT_TYPE } from '../constants'; import { appContextService } from './app_context'; diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 4b79cd639b61..7f72cdb88463 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { CallESAsCurrentUser } from '../types'; import { agentConfigService } from './agent_config'; import { outputService } from './output'; diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index c9a4bf79f351..59c7f152e5cb 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/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 { ScopedClusterClient } from 'src/core/server/'; +import { ScopedClusterClient } from 'src/core/server'; export { // Object types From 59551e7e81d245b52628dd350710ced406f28a1f Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 16 Mar 2020 18:46:38 -0700 Subject: [PATCH 029/115] Closes 59786 by removing the update toast (#60172) Co-authored-by: Elastic Machine --- .../components/app/ServiceMap/index.tsx | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) 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 2942ce64729e..7bbb77a49c84 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,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { ElementDefinition } from 'cytoscape'; @@ -16,7 +15,6 @@ import React, { useRef, useState } from 'react'; -import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; 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'; @@ -78,7 +76,6 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { }); const renderedElements = useRef([]); - const openToast = useRef(null); const [responses, setResponses] = useState([]); @@ -160,41 +157,11 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { return !find(renderedElements.current, el => isEqual(el, element)); }); - const updateMap = () => { + if (newElements.length > 0 && renderedElements.current.length > 0) { renderedElements.current = elements; - if (openToast.current) { - notifications.toasts.remove(openToast.current); - } forceUpdate(); - }; - - if (newElements.length > 0 && renderedElements.current.length > 0) { - openToast.current = notifications.toasts.add({ - title: i18n.translate('xpack.apm.newServiceMapData', { - defaultMessage: `Newly discovered connections are available.` - }), - onClose: () => { - openToast.current = null; - }, - toastLifeTimeMs: 24 * 60 * 60 * 1000, - text: toMountPoint( - - {i18n.translate('xpack.apm.updateServiceMap', { - defaultMessage: 'Update map' - })} - - ) - }).id; } - - return () => { - if (openToast.current) { - notifications.toasts.remove(openToast.current); - } - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [elements]); + }, [elements, forceUpdate]); const { ref: wrapperRef, width, height } = useRefDimensions(); From 35d6a0a635edca1781e7f3f96b5459be9dfebaad Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Mon, 16 Mar 2020 22:20:51 -0400 Subject: [PATCH 030/115] adds test that action vars are rendered for alert action parms (#60310) resolves https://github.com/elastic/kibana/issues/60083 --- .../apps/triggers_actions_ui/alerts.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 75ae6b9ea7c2..791712fa2448 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 @@ -80,10 +80,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await loggingMessageInput.click(); await loggingMessageInput.clearValue(); await loggingMessageInput.type('test message'); - // TODO: uncomment variables test when server API will be ready - // await testSubjects.click('slackAddVariableButton'); - // const variableMenuButton = await testSubjects.find('variableMenuButton-0'); - // await variableMenuButton.click(); + await testSubjects.click('slackAddVariableButton'); + const variableMenuButton = await testSubjects.find('variableMenuButton-0'); + await variableMenuButton.click(); await find.clickByCssSelector('[data-test-subj="saveAlertButton"]'); const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql(`Saved '${alertName}'`); From 90f3778bc69fa5f7af18294c5ecb98e2e842015e Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 16 Mar 2020 19:39:21 -0700 Subject: [PATCH 031/115] Added variables button for text fields in Pagerduty component. (#60189) * Added variables button for text fields in Pagerduty component. Fixed bugs mentioned in https://github.com/elastic/kibana/issues/60067 * Fixed due to comments * fixed language check issue * Fixed tests * Fixed due to comments --- .../builtin_action_types/pagerduty.test.tsx | 1 + .../builtin_action_types/pagerduty.tsx | 208 ++++++++++++++++-- 2 files changed, 193 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx index 31a69f9fd94a..bb06f7961b20 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx @@ -183,5 +183,6 @@ describe('PagerDutyParamsFields renders', () => { expect(wrapper.find('[data-test-subj="groupInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="sourceInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="pagerdutySummaryInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="dedupKeyAddVariableButton"]').length > 0).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx index 3c1b1d258cfe..7666129e4abd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.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, { Fragment } from 'react'; +import React, { Fragment, useState } from 'react'; import { EuiFieldText, EuiFlexGroup, @@ -11,6 +11,10 @@ import { EuiFormRow, EuiSelect, EuiLink, + EuiContextMenuItem, + EuiPopover, + EuiButtonIcon, + EuiContextMenuPanel, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -158,6 +162,7 @@ const PagerDutyParamsFields: React.FunctionComponent { const { @@ -171,16 +176,124 @@ const PagerDutyParamsFields: React.FunctionComponent>({ + dedupKey: false, + summary: false, + source: false, + timestamp: false, + component: false, + group: false, + class: false, + }); + // TODO: replace this button with a proper Eui component, when it will be ready + const getMessageVariables = (paramsProperty: string) => + messageVariables?.map((variable: string) => ( + { + editAction( + paramsProperty, + ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), + index + ); + setIsVariablesPopoverOpen({ ...isVariablesPopoverOpen, [paramsProperty]: false }); + }} + > + {`{{${variable}}}`} + + )); + + const getAddVariableComponent = (paramsProperty: string, buttonName: string) => { + return ( + + setIsVariablesPopoverOpen({ ...isVariablesPopoverOpen, [paramsProperty]: true }) + } + iconType="indexOpen" + aria-label={buttonName} + /> + } + isOpen={isVariablesPopoverOpen[paramsProperty]} + closePopover={() => + setIsVariablesPopoverOpen({ ...isVariablesPopoverOpen, [paramsProperty]: false }) + } + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + ); + }; return ( @@ -190,7 +303,7 @@ const PagerDutyParamsFields: React.FunctionComponent @@ -211,7 +324,7 @@ const PagerDutyParamsFields: React.FunctionComponent @@ -237,6 +350,15 @@ const PagerDutyParamsFields: React.FunctionComponent { - if (!index) { + if (!dedupKey) { editAction('dedupKey', '', index); } }} @@ -263,6 +385,15 @@ const PagerDutyParamsFields: React.FunctionComponent { - if (!index) { + if (!timestamp) { editAction('timestamp', '', index); } }} @@ -289,6 +420,15 @@ const PagerDutyParamsFields: React.FunctionComponent { - if (!index) { + if (!component) { editAction('component', '', index); } }} @@ -313,6 +453,15 @@ const PagerDutyParamsFields: React.FunctionComponent { - if (!index) { + if (!group) { editAction('group', '', index); } }} @@ -337,6 +486,15 @@ const PagerDutyParamsFields: React.FunctionComponent { - if (!index) { + if (!source) { editAction('source', '', index); } }} @@ -364,6 +522,15 @@ const PagerDutyParamsFields: React.FunctionComponent Date: Tue, 17 Mar 2020 00:29:33 -0400 Subject: [PATCH 032/115] resolves https://github.com/elastic/kibana/issues/58905 (#60120) The current index threshold alert uses a `size` limit on term aggregation, when used, but does not sort the buckets, so it's just using descending count on the grouped buckets as the sort to determine what to return. The watcher API for the index threshold notes this as "top N of", implying a sort. This PR applies sorting when the using `groupBy: top`, and the `aggType != count`. For count, ES is already sorting the way we want. The sort is calculated as a separate agg beside the date_range aggregation, which is the same metrics agg specified in the query - `aggType(aggField)`. This field is then referenced in a new `order` property in the terms agg, using 'asc' sorting for `min`, and `desc` sorting for `avg`, `max`, and `sum`. This doesn't change the shape of the output at all, just changes which term buckets will be returned, if there are more term buckets than requested with the `termSize` parameter. --- .../index_threshold/lib/time_series_query.ts | 26 +++++++++++ .../time_series_query_endpoint.ts | 46 +++++++++++++++---- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts index a4f64c0f37f4..0382792dafb3 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts @@ -77,6 +77,15 @@ export async function timeSeriesQuery( }, }, }; + + // if not count add an order + if (!isCountAgg) { + const sortOrder = aggType === 'min' ? 'asc' : 'desc'; + aggParent.aggs.groupAgg.terms.order = { + sortValueAgg: sortOrder, + }; + } + aggParent = aggParent.aggs.groupAgg; } @@ -89,6 +98,16 @@ export async function timeSeriesQuery( }, }, }; + + // if not count, add a sorted value agg + if (!isCountAgg) { + aggParent.aggs.sortValueAgg = { + [aggType]: { + field: aggField, + }, + }; + } + aggParent = aggParent.aggs.dateAgg; // finally, the metric aggregation, if requested @@ -106,13 +125,20 @@ export async function timeSeriesQuery( const logPrefix = 'indexThreshold timeSeriesQuery: callCluster'; logger.debug(`${logPrefix} call: ${JSON.stringify(esQuery)}`); + // note there are some commented out console.log()'s below, which are left + // in, as they are VERY useful when debugging these queries; debug logging + // isn't as nice since it's a single long JSON line. + + // console.log('time_series_query.ts request\n', JSON.stringify(esQuery, null, 4)); try { esResult = await callCluster('search', esQuery); } catch (err) { + // console.log('time_series_query.ts error\n', JSON.stringify(err, null, 4)); logger.warn(`${logPrefix} error: ${JSON.stringify(err.message)}`); throw new Error('error running search'); } + // console.log('time_series_query.ts response\n', JSON.stringify(esResult, null, 4)); logger.debug(`${logPrefix} result: ${JSON.stringify(esResult)}`); return getResultFromEs(isCountAgg, isGroupAgg, esResult); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts index ee44c7f25cf6..1aa1d3d21f00 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts @@ -196,14 +196,6 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider const expected = { results: [ - { - group: 'groupA', - metrics: [ - [START_DATE_MINUS_2INTERVALS, 4 / 1], - [START_DATE_MINUS_1INTERVALS, (4 + 2) / 2], - [START_DATE_MINUS_0INTERVALS, (4 + 2 + 1) / 3], - ], - }, { group: 'groupB', metrics: [ @@ -212,12 +204,50 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider [START_DATE_MINUS_0INTERVALS, (5 + 3 + 2) / 3], ], }, + { + group: 'groupA', + metrics: [ + [START_DATE_MINUS_2INTERVALS, 4 / 1], + [START_DATE_MINUS_1INTERVALS, (4 + 2) / 2], + [START_DATE_MINUS_0INTERVALS, (4 + 2 + 1) / 3], + ], + }, ], }; expect(await runQueryExpect(query, 200)).eql(expected); }); + it('should return correct sorted group for average', async () => { + const query = getQueryBody({ + aggType: 'avg', + aggField: 'testedValue', + groupBy: 'top', + termField: 'group', + termSize: 1, + dateStart: START_DATE_MINUS_2INTERVALS, + dateEnd: START_DATE_MINUS_0INTERVALS, + }); + const result = await runQueryExpect(query, 200); + expect(result.results.length).to.be(1); + expect(result.results[0].group).to.be('groupB'); + }); + + it('should return correct sorted group for min', async () => { + const query = getQueryBody({ + aggType: 'min', + aggField: 'testedValue', + groupBy: 'top', + termField: 'group', + termSize: 1, + dateStart: START_DATE_MINUS_2INTERVALS, + dateEnd: START_DATE_MINUS_0INTERVALS, + }); + const result = await runQueryExpect(query, 200); + expect(result.results.length).to.be(1); + expect(result.results[0].group).to.be('groupA'); + }); + it('should return an error when passed invalid input', async () => { const query = { ...getQueryBody(), aggType: 'invalid-agg-type' }; const expected = { From 56010b1c6543cf6b3b2a8e39e415a7085296eabf Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 17 Mar 2020 08:33:08 +0100 Subject: [PATCH 033/115] Give better stack traces for Unhandled Promise Rejection warnings (#60235) --- src/setup_node_env/exit_on_warning.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/setup_node_env/exit_on_warning.js b/src/setup_node_env/exit_on_warning.js index 5be5ccd72bd0..6321cd7ba8db 100644 --- a/src/setup_node_env/exit_on_warning.js +++ b/src/setup_node_env/exit_on_warning.js @@ -35,4 +35,16 @@ if (process.noProcessWarnings !== true) { process.exit(1); }); + + // While the above warning listener would also be called on + // unhandledRejection warnings, we can give a better error message if we + // handle them separately: + process.on('unhandledRejection', function(reason) { + console.error('Unhandled Promise rejection detected:'); + console.error(); + console.error(reason); + console.error(); + console.error('Terminating process...'); + process.exit(1); + }); } From 7f901f9e0355f06c9f0e2651e5f0958524a9c27b Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Tue, 17 Mar 2020 09:20:00 +0000 Subject: [PATCH 034/115] [ML] Fixes to error handling for analytics jobs and file data viz (#60249) * [ML] Fixes to error handling for analytics jobs and file data viz * [ML] Fix failing tests and address comments from review * [ML] Add key prop to error messages map * [ML] Add errors.ts --- x-pack/plugins/ml/common/types/errors.ts | 18 ++++++++ .../components/analytics_list/columns.tsx | 8 ++-- .../components/analytics_list/common.ts | 2 +- .../analytics_list/expanded_row.tsx | 12 +++++- .../create_analytics_form/messages.tsx | 41 +++++++++++-------- .../use_create_analytics_form.test.tsx | 23 +++++++---- .../use_create_analytics_form.ts | 5 +++ .../file_datavisualizer_view.js | 11 ++++- .../components/analytics_panel/table.tsx | 2 +- 9 files changed, 89 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/ml/common/types/errors.ts diff --git a/x-pack/plugins/ml/common/types/errors.ts b/x-pack/plugins/ml/common/types/errors.ts new file mode 100644 index 000000000000..63e222490082 --- /dev/null +++ b/x-pack/plugins/ml/common/types/errors.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 ErrorResponse { + body: { + statusCode: number; + error: string; + message: string; + }; + name: string; +} + +export function isErrorResponse(arg: any): arg is ErrorResponse { + return arg?.body?.error !== undefined && arg?.body?.message !== undefined; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx index 00cd9e3f1e0d..2ab8cb4a78d8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx @@ -43,13 +43,13 @@ enum TASK_STATE_COLOR { export const getTaskStateBadge = ( state: DataFrameAnalyticsStats['state'], - reason?: DataFrameAnalyticsStats['reason'] + failureReason?: DataFrameAnalyticsStats['failure_reason'] ) => { const color = TASK_STATE_COLOR[state]; - if (isDataFrameAnalyticsFailed(state) && reason !== undefined) { + if (isDataFrameAnalyticsFailed(state) && failureReason !== undefined) { return ( - + {state} @@ -229,7 +229,7 @@ export const getColumns = ( sortable: (item: DataFrameAnalyticsListRow) => item.stats.state, truncateText: true, render(item: DataFrameAnalyticsListRow) { - return getTaskStateBadge(item.stats.state, item.stats.reason); + return getTaskStateBadge(item.stats.state, item.stats.failure_reason); }, width: '100px', 'data-test-subj': 'mlAnalyticsTableColumnStatus', diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index ff7da8d67852..2c3ded52eba9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -50,7 +50,7 @@ export interface DataFrameAnalyticsStats { transport_address: string; }; progress: ProgressSection[]; - reason?: string; + failure_reason?: string; state: DATA_FRAME_TASK_STATE; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index 8772be698bf5..43ef6b36c397 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -24,6 +24,7 @@ import { loadEvalData, Eval, } from '../../../../common'; +import { getTaskStateBadge } from './columns'; import { isCompletedAnalyticsJob } from './common'; import { isRegressionAnalysis, @@ -157,8 +158,15 @@ export const ExpandedRow: FC = ({ item }) => { title: i18n.translate('xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state', { defaultMessage: 'State', }), - items: Object.entries(stateValues).map(s => { - return { title: s[0].toString(), description: getItemDescription(s[1]) }; + items: Object.entries(stateValues).map(([stateKey, stateValue]) => { + const title = stateKey.toString(); + if (title === 'state') { + return { + title, + description: getTaskStateBadge(getItemDescription(stateValue)), + }; + } + return { title, description: getItemDescription(stateValue) }; }), position: 'left', }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx index f228d8fe9009..68a9a264ddf7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx @@ -6,25 +6,34 @@ import React, { Fragment, FC } from 'react'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { FormMessage } from '../../hooks/use_create_analytics_form/state'; // State interface Props { - messages: any; // TODO: fix --> something like State['requestMessages']; + messages: FormMessage[]; } -export const Messages: FC = ({ messages }) => - messages.map((requestMessage: FormMessage, i: number) => ( - - - {requestMessage.error !== undefined ?

{requestMessage.error}

: null} -
- -
- )); +export const Messages: FC = ({ messages }) => { + return ( + <> + {messages.map((requestMessage, i) => ( + + + {requestMessage.error !== undefined && ( + + {requestMessage.error} + + )} + + + + ))} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx index 2bdcc28e31ff..1a248f8559ff 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx @@ -22,16 +22,23 @@ const getMountedHook = () => describe('getErrorMessage()', () => { test('verify error message response formats', () => { - const errorMessage = getErrorMessage(new Error('the-error-message')); - expect(errorMessage).toBe('the-error-message'); + const customError1 = { + body: { statusCode: 403, error: 'Forbidden', message: 'the-error-message' }, + }; + const errorMessage1 = getErrorMessage(customError1); + expect(errorMessage1).toBe('Forbidden: the-error-message'); - const customError1 = { customErrorMessage: 'the-error-message' }; - const errorMessageMessage1 = getErrorMessage(customError1); - expect(errorMessageMessage1).toBe('{"customErrorMessage":"the-error-message"}'); + const customError2 = new Error('the-error-message'); + const errorMessage2 = getErrorMessage(customError2); + expect(errorMessage2).toBe('the-error-message'); - const customError2 = { message: 'the-error-message' }; - const errorMessageMessage2 = getErrorMessage(customError2); - expect(errorMessageMessage2).toBe('the-error-message'); + const customError3 = { customErrorMessage: 'the-error-message' }; + const errorMessage3 = getErrorMessage(customError3); + expect(errorMessage3).toBe('{"customErrorMessage":"the-error-message"}'); + + const customError4 = { message: 'the-error-message' }; + const errorMessage4 = getErrorMessage(customError4); + expect(errorMessage4).toBe('the-error-message'); }); }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 74161d7c48c2..3b6b8538c2ff 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -9,6 +9,7 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; import { SimpleSavedObject } from 'kibana/public'; +import { isErrorResponse } from '../../../../../../../common/types/errors'; import { ml } from '../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../contexts/ml'; @@ -40,6 +41,10 @@ export interface CreateAnalyticsFormProps { } export function getErrorMessage(error: any) { + if (isErrorResponse(error)) { + return `${error.body.error}: ${error.body.message}`; + } + if (typeof error === 'object' && typeof error.message === 'string') { return error.message; } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js index a4300de5abbb..12e5a14b5187 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js @@ -19,6 +19,7 @@ import { FileCouldNotBeRead, FileTooLarge } from './file_error_callouts'; import { EditFlyout } from '../edit_flyout'; import { ImportView } from '../import_view'; import { MAX_BYTES } from '../../../../../../common/constants/file_datavisualizer'; +import { isErrorResponse } from '../../../../../../common/types/errors'; import { readFile, createUrlOverrides, @@ -177,12 +178,20 @@ export class FileDataVisualizerView extends Component { }); } catch (error) { console.error(error); + + let serverErrorMsg; + if (isErrorResponse(error) === true) { + serverErrorMsg = `${error.body.error}: ${error.body.message}`; + } else { + serverErrorMsg = JSON.stringify(error, null, 2); + } + this.setState({ results: undefined, loaded: false, loading: false, fileCouldNotBeRead: true, - serverErrorMessage: error.message, + serverErrorMessage: serverErrorMsg, }); // as long as the previous overrides are different to the current overrides, diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx index ff81f0e87aca..c3b8e8dd4e27 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx @@ -61,7 +61,7 @@ export const AnalyticsTable: FC = ({ items }) => { sortable: (item: DataFrameAnalyticsListRow) => item.stats.state, truncateText: true, render(item: DataFrameAnalyticsListRow) { - return getTaskStateBadge(item.stats.state, item.stats.reason); + return getTaskStateBadge(item.stats.state, item.stats.failure_reason); }, width: '100px', }, From 55003b61ddf11f7769c5de71704012ec482f86c1 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 17 Mar 2020 11:50:34 +0100 Subject: [PATCH 035/115] [SIEM] Fix Timeline footer styling (#59587) --- .../timeline_data_providers.spec.ts | 2 +- .../timeline_flyout_button.spec.ts | 4 +- .../plugins/siem/cypress/screens/timeline.ts | 2 + x-pack/legacy/plugins/siem/package.json | 2 +- .../public/components/charts/areachart.tsx | 4 +- .../public/components/charts/barchart.tsx | 4 +- .../drag_drop_context_wrapper.tsx | 7 +- .../drag_and_drop/draggable_wrapper.test.tsx | 12 - .../drag_and_drop/draggable_wrapper.tsx | 83 +- .../drag_and_drop/droppable_wrapper.tsx | 5 +- .../draggables/field_badge/index.tsx | 21 +- .../public/components/draggables/index.tsx | 13 +- .../embeddables/map_tool_tip/map_tool_tip.tsx | 79 +- .../components/event_details/columns.tsx | 40 +- .../event_details_width_context.tsx | 27 + .../events_viewer/events_viewer.tsx | 53 +- .../components/fields_browser/field_items.tsx | 102 +- .../flyout/__snapshots__/index.test.tsx.snap | 1 - .../public/components/flyout/header/index.tsx | 27 - .../__snapshots__/index.test.tsx.snap | 13 + .../header_with_close_button/index.test.tsx | 45 + .../flyout/header_with_close_button/index.tsx | 49 + .../header_with_close_button/translations.ts | 14 + .../public/components/flyout/index.test.tsx | 133 +- .../siem/public/components/flyout/index.tsx | 44 +- .../pane/__snapshots__/index.test.tsx.snap | 6 - .../components/flyout/pane/index.test.tsx | 139 +- .../public/components/flyout/pane/index.tsx | 162 +-- .../components/flyout/pane/translations.ts | 7 - .../siem/public/components/inspect/index.tsx | 33 +- .../delete_timeline_modal/index.tsx | 10 +- .../siem/public/components/page/index.tsx | 37 +- .../components/recent_timelines/index.tsx | 4 +- .../components/skeleton_row/index.test.tsx | 11 +- .../public/components/skeleton_row/index.tsx | 5 +- .../__snapshots__/timeline.test.tsx.snap | 1230 +++++++++-------- .../__snapshots__/index.test.tsx.snap | 2 +- .../body/column_headers/column_header.tsx | 116 +- .../timeline/body/column_headers/index.tsx | 254 ++-- .../__snapshots__/index.test.tsx.snap | 42 +- .../body/data_driven_columns/index.tsx | 49 +- .../components/timeline/body/events/index.tsx | 5 +- .../timeline/body/events/stateful_event.tsx | 149 +- .../body/events/stateful_event_child.tsx | 138 -- .../public/components/timeline/body/index.tsx | 15 +- .../get_row_renderer.test.tsx.snap | 8 +- .../plain_row_renderer.test.tsx.snap | 8 +- .../generic_row_renderer.test.tsx.snap | 6 - .../auditd/generic_row_renderer.test.tsx | 38 +- .../renderers/auditd/generic_row_renderer.tsx | 48 +- .../body/renderers/get_row_renderer.test.tsx | 19 +- .../netflow_row_renderer.test.tsx.snap | 3 - .../netflow/netflow_row_renderer.test.tsx | 17 - .../netflow/netflow_row_renderer.tsx | 126 +- .../renderers/plain_row_renderer.test.tsx | 4 +- .../body/renderers/plain_row_renderer.tsx | 2 +- .../timeline/body/renderers/row_renderer.tsx | 25 +- .../suricata_row_renderer.test.tsx.snap | 3 - .../suricata/suricata_row_renderer.test.tsx | 22 +- .../suricata/suricata_row_renderer.tsx | 16 +- .../renderers/suricata/suricata_signature.tsx | 10 +- .../generic_row_renderer.test.tsx.snap | 6 - .../system/generic_row_renderer.test.tsx | 74 +- .../renderers/system/generic_row_renderer.tsx | 161 +-- .../zeek_row_renderer.test.tsx.snap | 3 - .../renderers/zeek/zeek_row_renderer.test.tsx | 19 +- .../body/renderers/zeek/zeek_row_renderer.tsx | 11 +- .../body/renderers/zeek/zeek_signature.tsx | 10 +- .../timeline/body/stateful_body.tsx | 42 +- .../timeline/data_providers/empty.tsx | 26 +- .../provider_item_and_drag_drop.tsx | 23 +- .../timeline/expandable_event/index.tsx | 48 +- .../timeline/fetch_kql_timeline.tsx | 10 +- .../footer/__snapshots__/index.test.tsx.snap | 162 ++- .../components/timeline/footer/index.test.tsx | 10 - .../components/timeline/footer/index.tsx | 203 +-- .../header/__snapshots__/index.test.tsx.snap | 6 +- .../components/timeline/header/index.test.tsx | 21 +- .../components/timeline/header/index.tsx | 33 +- .../public/components/timeline/helpers.tsx | 19 - .../siem/public/components/timeline/index.tsx | 17 +- .../timeline/properties/helpers.tsx | 47 +- .../timeline/properties/index.test.tsx | 30 +- .../components/timeline/properties/index.tsx | 57 +- .../timeline/properties/properties_left.tsx | 12 +- .../components/timeline/properties/styles.tsx | 13 +- .../components/timeline/refetch_timeline.tsx | 23 +- .../public/components/timeline/styles.tsx | 46 +- .../components/timeline/timeline.test.tsx | 522 ++----- .../public/components/timeline/timeline.tsx | 190 +-- .../components/timeline/timeline_context.tsx | 15 +- .../components/toasters/modal_all_errors.tsx | 19 +- .../plugins/siem/public/components/utils.ts | 11 + .../siem/public/containers/timeline/index.tsx | 6 +- .../components/import_rule_modal/index.tsx | 4 +- .../plugins/siem/public/pages/home/index.tsx | 30 +- .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- yarn.lock | 8 +- 99 files changed, 2306 insertions(+), 3190 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts delete mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx diff --git a/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts index 4889d40ae7d3..aca988e19516 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts @@ -49,7 +49,7 @@ describe('timeline data providers', () => { .first() .invoke('text') .should(hostname => { - expect(dataProviderText).to.eq(`host.name: "${hostname}"`); + expect(dataProviderText).to.eq(hostname); }); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts index 1a94a4abbe5b..736eee421a30 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TIMELINE_FLYOUT_BODY, TIMELINE_NOT_READY_TO_DROP_BUTTON } from '../screens/timeline'; +import { TIMELINE_FLYOUT_HEADER, TIMELINE_NOT_READY_TO_DROP_BUTTON } from '../screens/timeline'; import { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; import { loginAndWaitForPage } from '../tasks/login'; @@ -26,7 +26,7 @@ describe('timeline flyout button', () => { it('toggles open the timeline', () => { openTimeline(); - cy.get(TIMELINE_FLYOUT_BODY).should('have.css', 'visibility', 'visible'); + cy.get(TIMELINE_FLYOUT_HEADER).should('have.css', 'visibility', 'visible'); }); it('sets the flyout button background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the flyout button', () => { diff --git a/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts b/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts index 5638b8d23e83..fbce585a70f8 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts @@ -31,6 +31,8 @@ export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContain export const TIMELINE_FIELDS_BUTTON = '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; +export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="eui-flyout-header"]'; + export const TIMELINE_FLYOUT_BODY = '[data-test-subj="eui-flyout-body"]'; export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]'; diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index ad4a6e86ffc8..472a473842f0 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -14,7 +14,7 @@ "devDependencies": { "@types/lodash": "^4.14.110", "@types/js-yaml": "^3.12.1", - "@types/react-beautiful-dnd": "^11.0.4" + "@types/react-beautiful-dnd": "^12.1.1" }, "dependencies": { "lodash": "^4.17.15", diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx index f3b2b736ed87..5c15f2d3c8d4 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx @@ -16,8 +16,8 @@ import { RecursivePartial, } from '@elastic/charts'; import { getOr, get, isNull, isNumber } from 'lodash/fp'; -import useResizeObserver from 'use-resize-observer/polyfilled'; +import { useThrottledResizeObserver } from '../utils'; import { ChartPlaceHolder } from './chart_place_holder'; import { useTimeZone } from '../../lib/kibana'; import { @@ -131,7 +131,7 @@ interface AreaChartComponentProps { } export const AreaChartComponent: React.FC = ({ areaChart, configs }) => { - const { ref: measureRef, width, height } = useResizeObserver({}); + const { ref: measureRef, width, height } = useThrottledResizeObserver(); const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); const chartHeight = getChartHeight(customHeight, height); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx index da0f3d1d0047..f53a1555fa1f 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { Chart, BarSeries, Axis, Position, ScaleType, Settings } from '@elastic/charts'; import { getOr, get, isNumber } from 'lodash/fp'; import deepmerge from 'deepmerge'; -import useResizeObserver from 'use-resize-observer/polyfilled'; +import { useThrottledResizeObserver } from '../utils'; import { useTimeZone } from '../../lib/kibana'; import { ChartPlaceHolder } from './chart_place_holder'; import { @@ -105,7 +105,7 @@ interface BarChartComponentProps { } export const BarChartComponent: React.FC = ({ barChart, configs }) => { - const { ref: measureRef, width, height } = useResizeObserver({}); + const { ref: measureRef, width, height } = useThrottledResizeObserver(); const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); const chartHeight = getChartHeight(customHeight, height); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx index 72f5a62d0af9..11db33fff6d7 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { defaultTo, noop } from 'lodash/fp'; +import { noop } from 'lodash/fp'; import React, { useCallback } from 'react'; import { DropResult, DragDropContext } from 'react-beautiful-dnd'; import { connect, ConnectedProps } from 'react-redux'; @@ -103,10 +103,7 @@ DragDropContextWrapperComponent.displayName = 'DragDropContextWrapperComponent'; const emptyDataProviders: dragAndDropModel.IdToDataProvider = {}; // stable reference const mapStateToProps = (state: State) => { - const dataProviders = defaultTo( - emptyDataProviders, - dragAndDropSelectors.dataProvidersSelector(state) - ); + const dataProviders = dragAndDropSelectors.dataProvidersSelector(state) ?? emptyDataProviders; return { dataProviders }; }; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index 9dcc335d4ff1..11891afabbf3 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -88,21 +88,9 @@ describe('DraggableWrapper', () => { describe('ConditionalPortal', () => { const mount = useMountAppended(); const props = { - usePortal: false, registerProvider: jest.fn(), - isDragging: true, }; - it(`doesn't call registerProvider is NOT isDragging`, () => { - mount( - -
- - ); - - expect(props.registerProvider.mock.calls.length).toEqual(0); - }); - it('calls registerProvider when isDragging', () => { mount( diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index b7d368639ed9..3a6a4de7984d 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Draggable, DraggableProvided, @@ -15,7 +15,6 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; -import { EuiPortal } from '@elastic/eui'; import { dragAndDropActions } from '../../store/drag_and_drop'; import { DataProvider } from '../timeline/data_providers/data_provider'; import { TruncatableText } from '../truncatable_text'; @@ -27,9 +26,6 @@ export const DragEffects = styled.div``; DragEffects.displayName = 'DragEffects'; -export const DraggablePortalContext = createContext(false); -export const useDraggablePortalContext = () => useContext(DraggablePortalContext); - /** * Wraps the `react-beautiful-dnd` error boundary. See also: * https://github.com/atlassian/react-beautiful-dnd/blob/v12.0.0/docs/guides/setup-problem-detection-and-error-recovery.md @@ -89,7 +85,6 @@ export const DraggableWrapper = React.memo( ({ dataProvider, render, truncate }) => { const [providerRegistered, setProviderRegistered] = useState(false); const dispatch = useDispatch(); - const usePortal = useDraggablePortalContext(); const registerProvider = useCallback(() => { if (!providerRegistered) { @@ -113,7 +108,26 @@ export const DraggableWrapper = React.memo( return ( - + ( + +
+ + {render(dataProvider, provided, snapshot)} + +
+
+ )} + > {droppableProvided => (
( key={getDraggableId(dataProvider.id)} > {(provided, snapshot) => ( - - - {truncate && !snapshot.isDragging ? ( - - {render(dataProvider, provided, snapshot)} - - ) : ( - - {render(dataProvider, provided, snapshot)} - - )} - - + {truncate && !snapshot.isDragging ? ( + + {render(dataProvider, provided, snapshot)} + + ) : ( + + {render(dataProvider, provided, snapshot)} + + )} + )} {droppableProvided.placeholder} @@ -178,20 +183,16 @@ DraggableWrapper.displayName = 'DraggableWrapper'; interface ConditionalPortalProps { children: React.ReactNode; - usePortal: boolean; - isDragging: boolean; registerProvider: () => void; } export const ConditionalPortal = React.memo( - ({ children, usePortal, registerProvider, isDragging }) => { + ({ children, registerProvider }) => { useEffect(() => { - if (isDragging) { - registerProvider(); - } - }, [isDragging, registerProvider]); + registerProvider(); + }, [registerProvider]); - return usePortal ? {children} : <>{children}; + return <>{children}; } ); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx index 821ef9be10e8..a81954f57564 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx @@ -6,7 +6,7 @@ import { rgba } from 'polished'; import React from 'react'; -import { Droppable } from 'react-beautiful-dnd'; +import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; import styled from 'styled-components'; interface Props { @@ -16,6 +16,7 @@ interface Props { isDropDisabled?: boolean; type?: string; render?: ({ isDraggingOver }: { isDraggingOver: boolean }) => React.ReactNode; + renderClone?: DraggableChildrenFn; } const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string }>` @@ -94,12 +95,14 @@ export const DroppableWrapper = React.memo( isDropDisabled = false, type, render = null, + renderClone, }) => ( {(provided, snapshot) => ( (({ width }) => { + if (width) { + return { + style: { + width: `${width}px`, + }, + }; + } +})` background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; border: ${({ theme }) => theme.eui.euiBorderThin}; box-shadow: 0 2px 2px -1px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)}, @@ -24,12 +36,9 @@ Field.displayName = 'Field'; * Renders a field (e.g. `event.action`) as a draggable badge */ -// Passing the styles directly to the component because the width is -// being calculated and is recommended by Styled Components for performance -// https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 -export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: string }>( +export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: number }>( ({ fieldId, fieldWidth }) => ( - + {fieldId} ) diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx index 57f047416ec1..1fe6c936d282 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiToolTip, IconType } from '@elastic/eui'; +import { EuiBadge, EuiToolTip, IconType } from '@elastic/eui'; import React from 'react'; +import styled from 'styled-components'; import { Omit } from '../../../common/utility_types'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; @@ -116,13 +117,9 @@ export const DefaultDraggable = React.memo( DefaultDraggable.displayName = 'DefaultDraggable'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const Badge = styled(EuiBadge)` -// vertical-align: top; -// `; -export const Badge = (props: EuiBadgeProps) => ( - -); +export const Badge = styled(EuiBadge)` + vertical-align: top; +`; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx index 249aae1eda0e..15c423a3b3dc 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx @@ -12,7 +12,6 @@ import { EuiOutsideClickDetector, } from '@elastic/eui'; import { FeatureGeometry, FeatureProperty, MapToolTipProps } from '../types'; -import { DraggablePortalContext } from '../../drag_and_drop/draggable_wrapper'; import { ToolTipFooter } from './tooltip_footer'; import { LineToolTipContent } from './line_tool_tip_content'; import { PointToolTipContent } from './point_tool_tip_content'; @@ -101,46 +100,44 @@ export const MapToolTipComponent = ({ ) : ( - - { - if (closeTooltip != null) { - closeTooltip(); - setFeatureIndex(0); - } - }} - > -
- {featureGeometry != null && featureGeometry.type === 'LineString' ? ( - - ) : ( - - )} - {features.length > 1 && ( - { - setFeatureIndex(featureIndex - 1); - setIsLoadingNextFeature(true); - }} - nextFeature={() => { - setFeatureIndex(featureIndex + 1); - setIsLoadingNextFeature(true); - }} - /> - )} - {isLoadingNextFeature && } -
-
-
+ { + if (closeTooltip != null) { + closeTooltip(); + setFeatureIndex(0); + } + }} + > +
+ {featureGeometry != null && featureGeometry.type === 'LineString' ? ( + + ) : ( + + )} + {features.length > 1 && ( + { + setFeatureIndex(featureIndex - 1); + setIsLoadingNextFeature(true); + }} + nextFeature={() => { + setFeatureIndex(featureIndex + 1); + setIsLoadingNextFeature(true); + }} + /> + )} + {isLoadingNextFeature && } +
+
); }; diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx index e9903ce66d79..cd94a9fdcb5a 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx @@ -115,6 +115,17 @@ export const getColumns = ({ )} isDropDisabled={true} type={DRAG_TYPE_FIELD} + renderClone={provided => ( +
+ + + +
+ )} > - {(provided, snapshot) => ( + {provided => (
- {!snapshot.isDragging ? ( - - ) : ( - - - - )} +
)}
diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx new file mode 100644 index 000000000000..86a776a0313c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx @@ -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 React, { createContext, useContext } from 'react'; +import { useThrottledResizeObserver } from '../utils'; + +const EventDetailsWidthContext = createContext(0); + +export const useEventDetailsWidthContext = () => useContext(EventDetailsWidthContext); + +export const EventDetailsWidthProvider = React.memo(({ children }) => { + const { ref, width } = useThrottledResizeObserver(); + + return ( + <> + + {children} + +
+ + ); +}); + +EventDetailsWidthProvider.displayName = 'EventDetailsWidthProvider'; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index a913186d9ad3..ea2cb661763f 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -9,7 +9,6 @@ import { getOr, isEmpty, union } from 'lodash/fp'; import React, { useMemo } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; -import useResizeObserver from 'use-resize-observer/polyfilled'; import { BrowserFields } from '../../containers/source'; import { TimelineQuery } from '../../containers/timeline'; @@ -25,8 +24,8 @@ import { OnChangeItemsPerPage } from '../timeline/events'; import { Footer, footerHeight } from '../timeline/footer'; import { combineQueries } from '../timeline/helpers'; import { TimelineRefetch } from '../timeline/refetch_timeline'; -import { isCompactFooter } from '../timeline/timeline'; import { ManageTimelineContext, TimelineTypeContextProps } from '../timeline/timeline_context'; +import { EventDetailsWidthProvider } from './event_details_width_context'; import * as i18n from './translations'; import { Filter, @@ -38,15 +37,15 @@ import { inputsModel } from '../../store'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; -const WrappedByAutoSizer = styled.div` - width: 100%; -`; // required by AutoSizer -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; - const StyledEuiPanel = styled(EuiPanel)` max-width: 100%; `; +const EventsContainerLoading = styled.div` + width: 100%; + overflow: auto; +`; + interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; @@ -94,7 +93,6 @@ const EventsViewerComponent: React.FC = ({ toggleColumn, utilityBar, }) => { - const { ref: measureRef, width = 0 } = useResizeObserver({}); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); const combinedQueries = combineQueries({ @@ -117,25 +115,25 @@ const EventsViewerComponent: React.FC = ({ ), [columnsHeader, timelineTypeContext.queryFields] ); + const sortField = useMemo( + () => ({ + sortFieldId: sort.columnId, + direction: sort.sortDirection as Direction, + }), + [sort.columnId, sort.sortDirection] + ); return ( - <> - -
- - - {combinedQueries != null ? ( + {combinedQueries != null ? ( + {({ @@ -169,15 +167,8 @@ const EventsViewerComponent: React.FC = ({ {utilityBar?.(refetch, totalCountMinusDeleted)} -
- + + = ({ />
= ({ tieBreaker={getOr(null, 'endCursor.tiebreaker', pageInfo)} /> -
+ ); }}
- ) : null} - +
+ ) : null} ); }; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx index 990c2678b100..62f9297c38ef 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx @@ -90,6 +90,13 @@ export const getFieldItems = ({ key={`field-browser-field-items-field-droppable-wrapper-${timelineId}-${categoryId}-${field.name}`} isDropDisabled={true} type={DRAG_TYPE_FIELD} + renderClone={provided => ( +
+ + + +
+ )} > - {(provided, snapshot) => ( -
- {!snapshot.isDragging ? ( - - - - c.id === field.name) !== -1} - data-test-subj={`field-${field.name}-checkbox`} - id={field.name || ''} - onChange={() => - toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field.name || '', - width: DEFAULT_COLUMN_MIN_WIDTH, - }) - } - /> - - - - - - - - + {provided => ( +
+ + + + c.id === field.name) !== -1} + data-test-subj={`field-${field.name}-checkbox`} + id={field.name || ''} + onChange={() => + toggleColumn({ + columnHeaderType: defaultColumnHeaderType, + id: field.name || '', + width: DEFAULT_COLUMN_MIN_WIDTH, + }) + } + /> + + - - + + - - - ) : ( - - - - )} + + + + + + +
)} diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap index abdc4f468129..4bf0033bcb43 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap @@ -3,7 +3,6 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = ` ( isDataInTimeline, isDatepickerLocked, title, - width = DEFAULT_TIMELINE_WIDTH, noteIds, notesById, timelineId, @@ -77,7 +75,6 @@ const StatefulFlyoutHeader = React.memo( updateTitle={updateTitle} updateNote={updateNote} usersViewing={usersViewing} - width={width} /> ); } @@ -103,7 +100,6 @@ const makeMapStateToProps = () => { kqlQuery, title = '', noteIds = emptyNotesId, - width = DEFAULT_TIMELINE_WIDTH, } = timeline; const history = emptyHistory; // TODO: get history from store via selector @@ -118,7 +114,6 @@ const makeMapStateToProps = () => { isDatepickerLocked: globalInput.linkTo.includes('timeline'), noteIds, title, - width, }; }; return mapStateToProps; @@ -126,28 +121,6 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ associateNote: (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), - applyDeltaToWidth: ({ - id, - delta, - bodyClientWidthPixels, - maxWidthPercent, - minWidthPixels, - }: { - id: string; - delta: number; - bodyClientWidthPixels: number; - maxWidthPercent: number; - minWidthPixels: number; - }) => - dispatch( - timelineActions.applyDeltaToWidth({ - id, - delta, - bodyClientWidthPixels, - maxWidthPercent, - minWidthPixels, - }) - ), createTimeline: ({ id, show }: { id: string; show?: boolean }) => dispatch( timelineActions.createTimeline({ diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000000..df96f2a1f7eb --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FlyoutHeaderWithCloseButton renders correctly against snapshot 1`] = ` + +`; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx new file mode 100644 index 000000000000..e0eace2ad5b1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.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 { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../../mock'; +import { FlyoutHeaderWithCloseButton } from '.'; + +describe('FlyoutHeaderWithCloseButton', () => { + test('renders correctly against snapshot', () => { + const EmptyComponent = shallow( + + + + ); + expect(EmptyComponent.find('FlyoutHeaderWithCloseButton')).toMatchSnapshot(); + }); + + test('it should invoke onClose when the close button is clicked', () => { + const closeMock = jest.fn(); + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="close-timeline"] button') + .first() + .simulate('click'); + + expect(closeMock).toBeCalled(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx new file mode 100644 index 000000000000..a4d9f0e8293d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import styled from 'styled-components'; + +import { FlyoutHeader } from '../header'; +import * as i18n from './translations'; + +const FlyoutHeaderContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +`; + +// manually wrap the close button because EuiButtonIcon can't be a wrapped `styled` +const WrappedCloseButton = styled.div` + margin-right: 5px; +`; + +const FlyoutHeaderWithCloseButtonComponent: React.FC<{ + onClose: () => void; + timelineId: string; + usersViewing: string[]; +}> = ({ onClose, timelineId, usersViewing }) => ( + + + + + + + + +); + +export const FlyoutHeaderWithCloseButton = React.memo(FlyoutHeaderWithCloseButtonComponent); + +FlyoutHeaderWithCloseButton.displayName = 'FlyoutHeaderWithCloseButton'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts new file mode 100644 index 000000000000..7fcffc9c1f0b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.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 { i18n } from '@kbn/i18n'; + +export const CLOSE_TIMELINE = i18n.translate( + 'xpack.siem.timeline.flyout.header.closeTimelineButtonLabel', + { + defaultMessage: 'Close timeline', + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx index 83b842956e10..ab41b4617894 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx @@ -13,9 +13,14 @@ import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mo import { createStore, State } from '../../store'; import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; -import { Flyout, FlyoutComponent, flyoutHeaderHeight } from '.'; +import { Flyout, FlyoutComponent } from '.'; import { FlyoutButton } from './button'; +jest.mock('../timeline', () => ({ + // eslint-disable-next-line react/display-name + StatefulTimeline: () =>
, +})); + const testFlyoutHeight = 980; const usersViewing = ['elastic']; @@ -26,12 +31,7 @@ describe('Flyout', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper.find('Flyout')).toMatchSnapshot(); @@ -40,12 +40,7 @@ describe('Flyout', () => { test('it renders the default flyout state as a button', () => { const wrapper = mount( - + ); @@ -57,41 +52,13 @@ describe('Flyout', () => { ).toContain('Timeline'); }); - test('it renders the title field when its state is set to flyout is true', () => { - const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="timeline-title"]') - .first() - .props().placeholder - ).toContain('Untitled Timeline'); - }); - test('it does NOT render the fly out button when its state is set to flyout is true', () => { const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); const wrapper = mount( - + ); @@ -100,31 +67,6 @@ describe('Flyout', () => { ); }); - test('it renders the flyout body', () => { - const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); - - const wrapper = mount( - - -

{'Fake flyout body'}

-
-
- ); - - expect( - wrapper - .find('[data-test-subj="eui-flyout-body"]') - .first() - .text() - ).toContain('Fake flyout body'); - }); - test('it does render the data providers badge when the number is greater than 0', () => { const stateWithDataProviders = set( 'timeline.timelineById.test.dataProviders', @@ -135,12 +77,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -157,12 +94,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -177,12 +109,7 @@ describe('Flyout', () => { test('it hides the data providers badge when the timeline does NOT have data providers', () => { const wrapper = mount( - + ); @@ -204,12 +131,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -228,7 +150,6 @@ describe('Flyout', () => { { expect(showTimeline).toBeCalled(); }); - - test('should call the onClose when the close button is clicked', () => { - const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); - - const showTimeline = (jest.fn() as unknown) as ActionCreator<{ id: string; show: boolean }>; - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="close-timeline"] button') - .first() - .simulate('click'); - - expect(showTimeline).toBeCalled(); - }); }); describe('showFlyoutButton', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index 22fc9f27ce26..44abe5b679c8 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -5,7 +5,6 @@ */ import { EuiBadge } from '@elastic/eui'; -import { defaultTo, getOr } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import styled from 'styled-components'; @@ -16,9 +15,8 @@ import { FlyoutButton } from './button'; import { Pane } from './pane'; import { timelineActions } from '../../store/actions'; import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/constants'; - -/** The height in pixels of the flyout header, exported for use in height calculations */ -export const flyoutHeaderHeight: number = 60; +import { StatefulTimeline } from '../timeline'; +import { TimelineById } from '../../store/timeline/types'; export const Badge = styled(EuiBadge)` position: absolute; @@ -38,9 +36,7 @@ const Visible = styled.div<{ show?: boolean }>` Visible.displayName = 'Visible'; interface OwnProps { - children?: React.ReactNode; flyoutHeight: number; - headerHeight: number; timelineId: string; usersViewing: string[]; } @@ -48,17 +44,7 @@ interface OwnProps { type Props = OwnProps & ProsFromRedux; export const FlyoutComponent = React.memo( - ({ - children, - dataProviders, - flyoutHeight, - headerHeight, - show, - showTimeline, - timelineId, - usersViewing, - width, - }) => { + ({ dataProviders, flyoutHeight, show, showTimeline, timelineId, usersViewing, width }) => { const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ showTimeline, timelineId, @@ -73,17 +59,15 @@ export const FlyoutComponent = React.memo( - {children} + ( FlyoutComponent.displayName = 'FlyoutComponent'; +const DEFAULT_DATA_PROVIDERS: DataProvider[] = []; +const DEFAULT_TIMELINE_BY_ID = {}; + const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timelineById = defaultTo({}, timelineSelectors.timelineByIdSelector(state)); - const dataProviders = getOr([], `${timelineId}.dataProviders`, timelineById) as DataProvider[]; - const show = getOr(false, `${timelineId}.show`, timelineById) as boolean; - const width = getOr(DEFAULT_TIMELINE_WIDTH, `${timelineId}.width`, timelineById) as number; + const timelineById: TimelineById = + timelineSelectors.timelineByIdSelector(state) ?? DEFAULT_TIMELINE_BY_ID; + /* + In case timelineById[timelineId]?.dataProviders is an empty array it will cause unnecessary rerender + of StatefulTimeline which can be expensive, so to avoid that return DEFAULT_DATA_PROVIDERS + */ + const dataProviders = timelineById[timelineId]?.dataProviders.length + ? timelineById[timelineId]?.dataProviders + : DEFAULT_DATA_PROVIDERS; + const show = timelineById[timelineId]?.show ?? false; + const width = timelineById[timelineId]?.width ?? DEFAULT_TIMELINE_WIDTH; return { dataProviders, show, width }; }; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap index efa682cd4d18..d30fd6f31012 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap @@ -3,14 +3,8 @@ exports[`Pane renders correctly against snapshot 1`] = ` diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx index 365f99c6667b..53cf8f95de0c 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -8,12 +8,10 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../mock'; -import { flyoutHeaderHeight } from '..'; import { Pane } from '.'; const testFlyoutHeight = 980; const testWidth = 640; -const usersViewing = ['elastic']; describe('Pane', () => { test('renders correctly against snapshot', () => { @@ -21,10 +19,8 @@ describe('Pane', () => { {'I am a child of flyout'} @@ -39,10 +35,8 @@ describe('Pane', () => { {'I am a child of flyout'} @@ -53,87 +47,13 @@ describe('Pane', () => { expect(wrapper.find('Resizable').get(0).props.maxWidth).toEqual('95vw'); }); - test('it applies timeline styles to the EuiFlyout', () => { - const wrapper = mount( - - - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="eui-flyout"]') - .first() - .hasClass('timeline-flyout') - ).toEqual(true); - }); - - test('it applies timeline styles to the EuiFlyoutHeader', () => { - const wrapper = mount( - - - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="eui-flyout-header"]') - .first() - .hasClass('timeline-flyout-header') - ).toEqual(true); - }); - - test('it applies timeline styles to the EuiFlyoutBody', () => { - const wrapper = mount( - - - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="eui-flyout-body"]') - .first() - .hasClass('timeline-flyout-body') - ).toEqual(true); - }); - test('it should render a resize handle', () => { const wrapper = mount( {'I am a child of flyout'} @@ -149,74 +69,19 @@ describe('Pane', () => { ).toEqual(true); }); - test('it should render an empty title', () => { + test('it should render children', () => { const wrapper = mount( - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="timeline-title"]') - .first() - .text() - ).toContain(''); - }); - - test('it should render the flyout body', () => { - const wrapper = mount( - - {'I am a mock body'} ); - expect( - wrapper - .find('[data-test-subj="eui-flyout-body"]') - .first() - .text() - ).toContain('I am a mock body'); - }); - - test('it should invoke onClose when the close button is clicked', () => { - const closeMock = jest.fn(); - const wrapper = mount( - - - {'I am a mock child'} - - - ); - wrapper - .find('[data-test-subj="close-timeline"] button') - .first() - .simulate('click'); - - expect(closeMock).toBeCalled(); + expect(wrapper.first().text()).toContain('I am a mock body'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx index 38ec4a4b6f1f..3b5041c1ee34 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx @@ -4,130 +4,85 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useState } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { EuiFlyout } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { Resizable, ResizeCallback } from 're-resizable'; -import { throttle } from 'lodash/fp'; import { TimelineResizeHandle } from './timeline_resize_handle'; -import { FlyoutHeader } from '../header'; +import { EventDetailsWidthProvider } from '../../events_viewer/event_details_width_context'; import * as i18n from './translations'; import { timelineActions } from '../../../store/actions'; const minWidthPixels = 550; // do not allow the flyout to shrink below this width (pixels) const maxWidthPercent = 95; // do not allow the flyout to grow past this percentage of the view -interface OwnProps { +interface FlyoutPaneComponentProps { children: React.ReactNode; flyoutHeight: number; - headerHeight: number; onClose: () => void; timelineId: string; - usersViewing: string[]; width: number; } -type Props = OwnProps & PropsFromRedux; - -const EuiFlyoutContainer = styled.div<{ headerHeight: number }>` +const EuiFlyoutContainer = styled.div` .timeline-flyout { min-width: 150px; width: auto; } - .timeline-flyout-header { - align-items: center; - box-shadow: none; - display: flex; - flex-direction: row; - height: ${({ headerHeight }) => `${headerHeight}px`}; - max-height: ${({ headerHeight }) => `${headerHeight}px`}; - overflow: hidden; - padding: 5px 0 0 10px; - } - .timeline-flyout-body { - overflow-y: hidden; - padding: 0; - .euiFlyoutBody__overflowContent { - padding: 0; - } - } `; -const FlyoutHeaderContainer = styled.div` - align-items: center; +const StyledResizable = styled(Resizable)` display: flex; - flex-direction: row; - justify-content: space-between; - width: 100%; -`; - -// manually wrap the close button because EuiButtonIcon can't be a wrapped `styled` -const WrappedCloseButton = styled.div` - margin-right: 5px; + flex-direction: column; `; -const FlyoutHeaderWithCloseButtonComponent: React.FC<{ - onClose: () => void; - timelineId: string; - usersViewing: string[]; -}> = ({ onClose, timelineId, usersViewing }) => ( - - - - - - - - -); - -const FlyoutHeaderWithCloseButton = React.memo( - FlyoutHeaderWithCloseButtonComponent, - (prevProps, nextProps) => - prevProps.timelineId === nextProps.timelineId && - prevProps.usersViewing === nextProps.usersViewing -); +const RESIZABLE_ENABLE = { left: true }; -const FlyoutPaneComponent: React.FC = ({ - applyDeltaToWidth, +const FlyoutPaneComponent: React.FC = ({ children, flyoutHeight, - headerHeight, onClose, timelineId, - usersViewing, width, }) => { - const [lastDelta, setLastDelta] = useState(0); + const dispatch = useDispatch(); + const onResizeStop: ResizeCallback = useCallback( (e, direction, ref, delta) => { const bodyClientWidthPixels = document.body.clientWidth; if (delta.width) { - applyDeltaToWidth({ - bodyClientWidthPixels, - delta: -(delta.width - lastDelta), - id: timelineId, - maxWidthPercent, - minWidthPixels, - }); - setLastDelta(delta.width); + dispatch( + timelineActions.applyDeltaToWidth({ + bodyClientWidthPixels, + delta: -delta.width, + id: timelineId, + maxWidthPercent, + minWidthPixels, + }) + ); } }, - [applyDeltaToWidth, maxWidthPercent, minWidthPixels, lastDelta] + [dispatch] + ); + const resizableDefaultSize = useMemo( + () => ({ + width, + height: '100%', + }), + [] + ); + const resizableHandleComponent = useMemo( + () => ({ + left: , + }), + [flyoutHeight] ); - const resetLastDelta = useCallback(() => setLastDelta(0), [setLastDelta]); - const throttledResize = throttle(100, onResizeStop); return ( - + = ({ onClose={onClose} size="l" > - - ), - }} - onResizeStart={resetLastDelta} - onResize={throttledResize} + handleComponent={resizableHandleComponent} + onResizeStop={onResizeStop} > - - - - - {children} - - + {children} + ); }; -const mapDispatchToProps = { - applyDeltaToWidth: timelineActions.applyDeltaToWidth, -}; - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const Pane = connector(React.memo(FlyoutPaneComponent)); +export const Pane = React.memo(FlyoutPaneComponent); Pane.displayName = 'Pane'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts b/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts index 4ba0307eb527..0c31cdb81e8e 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts @@ -12,10 +12,3 @@ export const TIMELINE_DESCRIPTION = i18n.translate( defaultMessage: 'Timeline Properties', } ); - -export const CLOSE_TIMELINE = i18n.translate( - 'xpack.siem.timeline.flyout.pane.closeTimelineButtonLabel', - { - defaultMessage: 'Close timeline', - } -); diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx index d6f814374535..f10a740db2b9 100644 --- a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx @@ -7,11 +7,10 @@ import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; import { getOr, omit } from 'lodash/fp'; import React, { useCallback } from 'react'; -import { connect } from 'react-redux'; -import { ActionCreator } from 'typescript-fsa'; +import { connect, ConnectedProps } from 'react-redux'; import styled, { css } from 'styled-components'; -import { inputsModel, inputsSelectors, State } from '../../store'; +import { inputsSelectors, State } from '../../store'; import { InputsModelId } from '../../store/inputs/constants'; import { inputsActions } from '../../store/inputs'; @@ -60,24 +59,7 @@ interface OwnProps { title: string | React.ReactElement | React.ReactNode; } -interface InspectButtonReducer { - id: string; - isInspected: boolean; - loading: boolean; - inspect: inputsModel.InspectQuery | null; - selectedInspectIndex: number; -} - -interface InspectButtonDispatch { - setIsInspected: ActionCreator<{ - id: string; - inputId: InputsModelId; - isInspected: boolean; - selectedInspectIndex: number; - }>; -} - -type InspectButtonProps = OwnProps & InspectButtonReducer & InspectButtonDispatch; +type InspectButtonProps = OwnProps & PropsFromRedux; const InspectButtonComponent: React.FC = ({ compact = false, @@ -175,7 +157,8 @@ const mapDispatchToProps = { setIsInspected: inputsActions.setInspectionParameter, }; -export const InspectButton = connect( - makeMapStateToProps, - mapDispatchToProps -)(React.memo(InspectButtonComponent)); +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const InspectButton = connector(React.memo(InspectButtonComponent)); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx index caa9cd0689c7..982937659c0a 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonIcon, EuiModal, EuiToolTip, EuiOverlayMask } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { DeleteTimelineModal, DELETE_TIMELINE_MODAL_WIDTH } from './delete_timeline_modal'; import * as i18n from '../translations'; @@ -23,15 +23,15 @@ export const DeleteTimelineModalButton = React.memo( ({ deleteTimelines, savedObjectId, title }) => { const [showModal, setShowModal] = useState(false); - const openModal = () => setShowModal(true); - const closeModal = () => setShowModal(false); + const openModal = useCallback(() => setShowModal(true), [setShowModal]); + const closeModal = useCallback(() => setShowModal(false), [setShowModal]); - const onDelete = () => { + const onDelete = useCallback(() => { if (deleteTimelines != null && savedObjectId != null) { deleteTimelines([savedObjectId]); } closeModal(); - }; + }, [deleteTimelines, savedObjectId, closeModal]); return ( <> diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx index 781155c3ddc3..ef6a19f4b744 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/index.tsx @@ -4,15 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { - EuiBadge, - EuiBadgeProps, - EuiDescriptionList, - EuiFlexGroup, - EuiIcon, - EuiPage, -} from '@elastic/eui'; +import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui'; import styled, { createGlobalStyle } from 'styled-components'; /* @@ -20,6 +12,12 @@ import styled, { createGlobalStyle } from 'styled-components'; and `EuiPopover`, `EuiToolTip` global styles */ export const AppGlobalStyle = createGlobalStyle` + /* dirty hack to fix draggables with tooltip on FF */ + body#siem-app { + position: static; + } + /* end of dirty hack to fix draggables with tooltip on FF */ + div.app-wrapper { background-color: rgba(0,0,0,0); } @@ -107,6 +105,7 @@ export const PageHeader = styled.div` PageHeader.displayName = 'PageHeader'; export const FooterContainer = styled.div` + flex: 0; bottom: 0; color: #666; left: 0; @@ -154,13 +153,9 @@ export const Pane1FlexContent = styled.div` Pane1FlexContent.displayName = 'Pane1FlexContent'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const Badge = styled(EuiBadge)` -// margin-left: 5px; -// `; -export const CountBadge = (props: EuiBadgeProps) => ( - -); +export const CountBadge = styled(EuiBadge)` + margin-left: 5px; +`; CountBadge.displayName = 'CountBadge'; @@ -170,13 +165,9 @@ export const Spacer = styled.span` Spacer.displayName = 'Spacer'; -// Ref: https://github.com/elastic/eui/issues/1655 -// export const Badge = styled(EuiBadge)` -// vertical-align: top; -// `; -export const Badge = (props: EuiBadgeProps) => ( - -); +export const Badge = styled(EuiBadge)` + vertical-align: top; +`; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx index 6f22287774d7..a557e4cea2df 100644 --- a/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx @@ -9,7 +9,6 @@ import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; -import { ActionCreator } from 'typescript-fsa'; import { AllTimelinesQuery } from '../../containers/timeline/all'; import { SortFieldTimeline, Direction } from '../../graphql/types'; @@ -31,14 +30,13 @@ export type Props = OwnProps & PropsFromRedux; const StatefulRecentTimelinesComponent = React.memo( ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { - const actionDispatcher = updateIsLoading as ActionCreator<{ id: string; isLoading: boolean }>; const onOpenTimeline: OnOpenTimeline = useCallback( ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { queryTimelineById({ apolloClient, duplicate, timelineId, - updateIsLoading: actionDispatcher, + updateIsLoading, updateTimeline, }); }, diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx index 1fdcd8eee941..0ee54a1a2000 100644 --- a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx @@ -26,16 +26,10 @@ describe('SkeletonRow', () => { expect(wrapper.find('.siemSkeletonRow__cell')).toHaveLength(10); }); - test('it applies row and cell styles when cellColor/cellMargin/rowHeight/rowPadding/style provided', () => { + test('it applies row and cell styles when cellColor/cellMargin/rowHeight/rowPadding provided', () => { const wrapper = mount( - + ); const siemSkeletonRow = wrapper.find('.siemSkeletonRow').first(); @@ -43,7 +37,6 @@ describe('SkeletonRow', () => { expect(siemSkeletonRow).toHaveStyleRule('height', '100px'); expect(siemSkeletonRow).toHaveStyleRule('padding', '10px'); - expect(siemSkeletonRow.props().style!.width).toBe('auto'); expect(siemSkeletonRowCell).toHaveStyleRule('background-color', 'red'); expect(siemSkeletonRowCell).toHaveStyleRule('margin-left', '10px', { modifier: '& + &', diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx index dce360877130..ae30f11d8bb1 100644 --- a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx @@ -54,11 +54,10 @@ Cell.displayName = 'Cell'; export interface SkeletonRowProps extends CellProps, RowProps { cellCount?: number; - style?: object; } export const SkeletonRow = React.memo( - ({ cellColor, cellCount = 4, cellMargin, rowHeight, rowPadding, style }) => { + ({ cellColor, cellCount = 4, cellMargin, rowHeight, rowPadding }) => { const cells = useMemo( () => [...Array(cellCount)].map( @@ -69,7 +68,7 @@ export const SkeletonRow = React.memo( ); return ( - + {cells} ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap index 372930ee3167..02938cb2b86b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -1,668 +1,702 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Timeline rendering renders correctly against snapshot 1`] = ` - - - + + + - + onChangeDataProviderKqlQuery={[MockFunction]} + onChangeDroppableAndProvider={[MockFunction]} + onDataProviderEdited={[MockFunction]} + onDataProviderRemoved={[MockFunction]} + onToggleDataProviderEnabled={[MockFunction]} + onToggleDataProviderExcluded={[MockFunction]} + show={true} + showCallOutUnauthorizedMsg={false} + /> + + - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index b8b03be4e472..03e4f4b5f0f2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -490,7 +490,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` isCombineEnabled={false} isDropDisabled={false} mode="standard" - renderClone={null} + renderClone={[Function]} type="drag-type-field" > diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx index c3f28fd513d0..e070ed8fa1d2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx @@ -4,27 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Draggable } from 'react-beautiful-dnd'; import { Resizable, ResizeCallback } from 're-resizable'; +import deepEqual from 'fast-deep-equal'; import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { DragEffects } from '../../../drag_and_drop/draggable_wrapper'; -import { getDraggableFieldId, DRAG_TYPE_FIELD } from '../../../drag_and_drop/helpers'; -import { DraggableFieldBadge } from '../../../draggables/field_badge'; +import { getDraggableFieldId } from '../../../drag_and_drop/helpers'; import { OnColumnRemoved, OnColumnSorted, OnFilterChange, OnColumnResized } from '../../events'; import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles'; import { Sort } from '../sort'; -import { DraggingContainer } from './common/dragging_container'; import { Header } from './header'; +const RESIZABLE_ENABLE = { right: true }; + interface ColumneHeaderProps { draggableIndex: number; header: ColumnHeaderOptions; onColumnRemoved: OnColumnRemoved; onColumnSorted: OnColumnSorted; onColumnResized: OnColumnResized; + isDragging: boolean; onFilterChange?: OnFilterChange; sort: Sort; timelineId: string; @@ -34,69 +35,82 @@ const ColumnHeaderComponent: React.FC = ({ draggableIndex, header, timelineId, + isDragging, onColumnRemoved, onColumnResized, onColumnSorted, onFilterChange, sort, }) => { - const [isDragging, setIsDragging] = React.useState(false); - const handleResizeStop: ResizeCallback = (e, direction, ref, delta) => { - onColumnResized({ columnId: header.id, delta: delta.width }); - }; + const resizableSize = useMemo( + () => ({ + width: header.width, + height: 'auto', + }), + [header.width] + ); + const resizableStyle: { + position: 'absolute' | 'relative'; + } = useMemo( + () => ({ + position: isDragging ? 'absolute' : 'relative', + }), + [isDragging] + ); + const resizableHandleComponent = useMemo( + () => ({ + right: , + }), + [] + ); + const handleResizeStop: ResizeCallback = useCallback( + (e, direction, ref, delta) => { + onColumnResized({ columnId: header.id, delta: delta.width }); + }, + [header.id, onColumnResized] + ); + const draggableId = useMemo( + () => + getDraggableFieldId({ + contextId: `timeline-column-headers-${timelineId}`, + fieldId: header.id, + }), + [timelineId, header.id] + ); return ( , - }} + enable={RESIZABLE_ENABLE} + size={resizableSize} + style={resizableStyle} + handleComponent={resizableHandleComponent} onResizeStop={handleResizeStop} > - {(dragProvided, dragSnapshot) => ( + {dragProvided => ( - {!dragSnapshot.isDragging ? ( - -
- - ) : ( - - - - - - )} + +
+ )} @@ -104,4 +118,16 @@ const ColumnHeaderComponent: React.FC = ({ ); }; -export const ColumnHeader = React.memo(ColumnHeaderComponent); +export const ColumnHeader = React.memo( + ColumnHeaderComponent, + (prevProps, nextProps) => + prevProps.draggableIndex === nextProps.draggableIndex && + prevProps.timelineId === nextProps.timelineId && + prevProps.isDragging === nextProps.isDragging && + prevProps.onColumnRemoved === nextProps.onColumnRemoved && + prevProps.onColumnResized === nextProps.onColumnResized && + prevProps.onColumnSorted === nextProps.onColumnSorted && + prevProps.onFilterChange === nextProps.onFilterChange && + prevProps.sort === nextProps.sort && + deepEqual(prevProps.header, nextProps.header) +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx index ab8dc629dd57..7a072f1dbf57 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx @@ -6,9 +6,12 @@ import { EuiCheckbox } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import React from 'react'; -import { Droppable } from 'react-beautiful-dnd'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; +import deepEqual from 'fast-deep-equal'; +import { DragEffects } from '../../../drag_and_drop/draggable_wrapper'; +import { DraggableFieldBadge } from '../../../draggables/field_badge'; import { BrowserFields } from '../../../../containers/source'; import { ColumnHeaderOptions } from '../../../../store/timeline/model'; import { DRAG_TYPE_FIELD, droppableTimelineColumnsPrefix } from '../../../drag_and_drop/helpers'; @@ -53,6 +56,26 @@ interface Props { toggleColumn: (column: ColumnHeaderOptions) => void; } +interface DraggableContainerProps { + children: React.ReactNode; + onMount: () => void; + onUnmount: () => void; +} + +export const DraggableContainer = React.memo( + ({ children, onMount, onUnmount }) => { + useEffect(() => { + onMount(); + + return () => onUnmount(); + }, [onMount, onUnmount]); + + return <>{children}; + } +); + +DraggableContainer.displayName = 'DraggableContainer'; + /** Renders the timeline header columns */ export const ColumnHeadersComponent = ({ actionsColumnWidth, @@ -71,86 +94,157 @@ export const ColumnHeadersComponent = ({ sort, timelineId, toggleColumn, -}: Props) => ( - - - - {showEventsSelect && ( - - - - - - )} - {showSelectAllCheckbox && ( +}: Props) => { + const [draggingIndex, setDraggingIndex] = useState(null); + + const handleSelectAllChange = useCallback( + (event: React.ChangeEvent) => { + onSelectAll({ isSelected: event.currentTarget.checked }); + }, + [onSelectAll] + ); + + const renderClone: DraggableChildrenFn = useCallback( + (dragProvided, dragSnapshot, rubric) => { + // TODO: Remove after github.com/DefinitelyTyped/DefinitelyTyped/pull/43057 is merged + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const index = (rubric as any).source.index; + const header = columnHeaders[index]; + + const onMount = () => setDraggingIndex(index); + const onUnmount = () => setDraggingIndex(null); + + return ( + + + + + + + + ); + }, + [columnHeaders, setDraggingIndex] + ); + + const ColumnHeaderList = useMemo( + () => + columnHeaders.map((header, draggableIndex) => ( + + )), + [ + columnHeaders, + timelineId, + draggingIndex, + onColumnRemoved, + onFilterChange, + onColumnResized, + sort, + ] + ); + + return ( + + + + {showEventsSelect && ( + + + + + + )} + {showSelectAllCheckbox && ( + + + + + + )} - - ) => { - onSelectAll({ isSelected: event.currentTarget.checked }); - }} + + - )} - - - - - - - - - {(dropProvided, snapshot) => ( - <> - - {columnHeaders.map((header, draggableIndex) => ( - - ))} - - {dropProvided.placeholder} - - )} - - - -); + -export const ColumnHeaders = React.memo(ColumnHeadersComponent); + + {(dropProvided, snapshot) => ( + <> + + {ColumnHeaderList} + + + )} + + + + ); +}; + +export const ColumnHeaders = React.memo( + ColumnHeadersComponent, + (prevProps, nextProps) => + prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && + prevProps.isEventViewer === nextProps.isEventViewer && + prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.onColumnRemoved === nextProps.onColumnRemoved && + prevProps.onColumnResized === nextProps.onColumnResized && + prevProps.onColumnSorted === nextProps.onColumnSorted && + prevProps.onSelectAll === nextProps.onSelectAll && + prevProps.onUpdateColumns === nextProps.onUpdateColumns && + prevProps.onFilterChange === nextProps.onFilterChange && + prevProps.showEventsSelect === nextProps.showEventsSelect && + prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && + prevProps.sort === nextProps.sort && + prevProps.timelineId === nextProps.timelineId && + prevProps.toggleColumn === nextProps.toggleColumn && + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && + deepEqual(prevProps.browserFields, nextProps.browserFields) +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap index 93e12a0ed4fc..75623252181d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap @@ -6,11 +6,7 @@ exports[`Columns it renders the expected columns 1`] = ` > ( - ({ _id, columnHeaders, columnRenderers, data, ecsData, timelineId }) => { - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - - {columnHeaders.map((header, index) => ( - - - {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ - columnName: header.id, - eventId: _id, - field: header, - linkValues: getOr([], header.linkField ?? '', ecsData), - timelineId, - truncate: true, - values: getMappedNonEcsValue({ - data, - fieldName: header.id, - }), - })} - - - ))} - - ); - } + ({ _id, columnHeaders, columnRenderers, data, ecsData, timelineId }) => ( + + {columnHeaders.map(header => ( + + + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ + columnName: header.id, + eventId: _id, + field: header, + linkValues: getOr([], header.linkField ?? '', ecsData), + timelineId, + truncate: true, + values: getMappedNonEcsValue({ + data, + fieldName: header.id, + }), + })} + + + ))} + + ) ); DataDrivenColumns.displayName = 'DataDrivenColumns'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx index 84c4253076dc..4178bc656f32 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx @@ -51,9 +51,6 @@ interface Props { updateNote: UpdateNote; } -// Passing the styles directly to the component because the width is -// being calculated and is recommended by Styled Components for performance -// https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 const EventsComponent: React.FC = ({ actionsColumnWidth, addNoteToEvent, @@ -93,7 +90,7 @@ const EventsComponent: React.FC = ({ getNotesByIds={getNotesByIds} isEventPinned={eventIsPinned({ eventId: event._id, pinnedEventIds })} isEventViewer={isEventViewer} - key={event._id} + key={`${event._id}_${event._index}`} loadingEventIds={loadingEventIds} maxDelay={maxDelay(i)} onColumnResized={onColumnResized} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx index 1f09ae4337c4..6e5c292064dc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx @@ -25,13 +25,14 @@ import { } from '../../events'; import { ExpandableEvent } from '../../expandable_event'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; -import { EventsTrGroup, EventsTrSupplement, OFFSET_SCROLLBAR } from '../../styles'; -import { useTimelineWidthContext } from '../../timeline_context'; +import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; import { getRowRenderer } from '../renderers/get_row_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { getEventType } from '../helpers'; -import { StatefulEventChild } from './stateful_event_child'; +import { NoteCards } from '../../../notes/note_cards'; +import { useEventDetailsWidthContext } from '../../../events_viewer/event_details_width_context'; +import { EventColumnView } from './event_column_view'; interface Props { actionsColumnWidth: number; @@ -89,28 +90,14 @@ const TOP_OFFSET = 50; */ const BOTTOM_OFFSET = -500; -interface AttributesProps { - children: React.ReactNode; -} - -const AttributesComponent: React.FC = ({ children }) => { - const width = useTimelineWidthContext(); +const emptyNotes: string[] = []; - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - - {children} - - ); -}; +const EventsTrSupplementContainerWrapper = React.memo(({ children }) => { + const width = useEventDetailsWidthContext(); + return {children}; +}); -const Attributes = React.memo(AttributesComponent); +EventsTrSupplementContainerWrapper.displayName = 'EventsTrSupplementContainerWrapper'; const StatefulEventComponent: React.FC = ({ actionsColumnWidth, @@ -221,60 +208,75 @@ const StatefulEventComponent: React.FC = ({ data-test-subj="event" eventType={getEventType(event.ecs)} showLeftBorder={!isEventViewer} - ref={c => { - if (c != null) { - divElement.current = c; - } - }} + ref={divElement} > - {getRowRenderer(event.ecs, rowRenderers).renderRow({ - browserFields, - data: event.ecs, - children: ( - + + + + - ), - timelineId, - })} + - - - + {getRowRenderer(event.ecs, rowRenderers).renderRow({ + browserFields, + data: event.ecs, + timelineId, + })} + + + + + )} @@ -286,10 +288,7 @@ const StatefulEventComponent: React.FC = ({ ? `${divElement.current.clientHeight}px` : DEFAULT_ROW_HEIGHT; - // height is being inlined directly in here because of performance with StyledComponents - // involving quick and constant changes to the DOM. - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ; + return ; } }} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx deleted file mode 100644 index 04f4ddf2a6ea..000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx +++ /dev/null @@ -1,138 +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 uuid from 'uuid'; - -import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; -import { Note } from '../../../../lib/note'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { NoteCards } from '../../../notes/note_cards'; -import { OnPinEvent, OnColumnResized, OnUnPinEvent, OnRowSelected } from '../../events'; -import { EventsTrSupplement, OFFSET_SCROLLBAR } from '../../styles'; -import { useTimelineWidthContext } from '../../timeline_context'; -import { ColumnRenderer } from '../renderers/column_renderer'; -import { EventColumnView } from './event_column_view'; - -interface Props { - id: string; - actionsColumnWidth: number; - addNoteToEvent: AddNoteToEvent; - onPinEvent: OnPinEvent; - columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; - data: TimelineNonEcsData[]; - ecsData: Ecs; - expanded: boolean; - eventIdToNoteIds: Readonly>; - isEventViewer?: boolean; - isEventPinned: boolean; - loading: boolean; - loadingEventIds: Readonly; - onColumnResized: OnColumnResized; - onRowSelected: OnRowSelected; - onUnPinEvent: OnUnPinEvent; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - showNotes: boolean; - timelineId: string; - updateNote: UpdateNote; - onToggleExpanded: () => void; - onToggleShowNotes: () => void; - getNotesByIds: (noteIds: string[]) => Note[]; - associateNote: (noteId: string) => void; -} - -export const getNewNoteId = (): string => uuid.v4(); - -const emptyNotes: string[] = []; - -export const StatefulEventChild = React.memo( - ({ - id, - actionsColumnWidth, - associateNote, - addNoteToEvent, - onPinEvent, - columnHeaders, - columnRenderers, - expanded, - data, - ecsData, - eventIdToNoteIds, - getNotesByIds, - isEventViewer = false, - isEventPinned = false, - loading, - loadingEventIds, - onColumnResized, - onRowSelected, - onToggleExpanded, - onUnPinEvent, - selectedEventIds, - showCheckboxes, - showNotes, - timelineId, - onToggleShowNotes, - updateNote, - }) => { - const width = useTimelineWidthContext(); - - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - <> - - - - - - - ); - } -); -StatefulEventChild.displayName = 'StatefulEventChild'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx index ea80d3351408..fac8cc61cddd 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx @@ -38,7 +38,7 @@ export interface BodyProps { columnRenderers: ColumnRenderer[]; data: TimelineItem[]; getNotesByIds: (noteIds: string[]) => Note[]; - height: number; + height?: number; id: string; isEventViewer?: boolean; isSelectAllChecked: boolean; @@ -96,9 +96,10 @@ export const Body = React.memo( }) => { const containerElementRef = useRef(null); const timelineTypeContext = useTimelineTypeContext(); - const additionalActionWidth = - timelineTypeContext.timelineActions?.reduce((acc, v) => acc + v.width, 0) ?? 0; - + const additionalActionWidth = useMemo( + () => timelineTypeContext.timelineActions?.reduce((acc, v) => acc + v.width, 0) ?? 0, + [timelineTypeContext.timelineActions] + ); const actionsColumnWidth = useMemo( () => getActionsColumnWidth(isEventViewer, showCheckboxes, additionalActionWidth), [isEventViewer, showCheckboxes, additionalActionWidth] @@ -113,11 +114,7 @@ export const Body = React.memo( return ( <> - + - - some child - - -`; +exports[`get_column_renderer renders correctly against snapshot 1`] = ``; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap index 5731921907fc..66a1b293cf8b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap @@ -1,9 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`plain_row_renderer renders correctly against snapshot 1`] = ` - - - some children - - -`; +exports[`plain_row_renderer renders correctly against snapshot 1`] = ``; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap index 0b2a1b2f2a0a..b24a90589ce6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly against snapshot 1`] = ` - - some children - - - some children - { const children = connectedToRenderer.renderRow({ browserFields, data: auditd, - children: {'some children'}, timelineId: 'test', }); @@ -66,26 +65,10 @@ describe('GenericRowRenderer', () => { } }); - test('should render children normally if it does not have a auditd object', () => { - const children = connectedToRenderer.renderRow({ - browserFields: mockBrowserFields, - data: nonAuditd, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render a auditd row', () => { const children = connectedToRenderer.renderRow({ browserFields: mockBrowserFields, data: auditd, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -94,7 +77,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' + 'Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' ); }); }); @@ -119,7 +102,6 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields, data: auditdFile, - children: {'some children'}, timelineId: 'test', }); @@ -145,26 +127,10 @@ describe('GenericRowRenderer', () => { } }); - test('should render children normally if it does not have a auditd object', () => { - const children = fileToRenderer.renderRow({ - browserFields: mockBrowserFields, - data: nonAuditd, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render a auditd row', () => { const children = fileToRenderer.renderRow({ browserFields: mockBrowserFields, data: auditdFile, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -173,7 +139,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' + 'Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx index bcf464ab6da1..4ed4ae10ed81 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx @@ -32,19 +32,16 @@ export const createGenericAuditRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -67,20 +64,17 @@ export const createGenericFileRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx index f367769b78f4..7ad8cfed5256 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -38,7 +38,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, - children: {'some child'}, timelineId: 'test', }); @@ -51,7 +50,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, - children: {'some child'}, timelineId: 'test', }); const wrapper = mount( @@ -59,7 +57,7 @@ describe('get_column_renderer', () => { {row} ); - expect(wrapper.text()).toContain('some child'); + expect(wrapper.text()).toEqual(''); }); test('should render a suricata row data when it is a suricata row', () => { @@ -67,7 +65,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -76,7 +73,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child 4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' + '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -86,7 +83,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -95,7 +91,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child 4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' + '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -105,7 +101,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: zeek, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -114,7 +109,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' + 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); @@ -124,7 +119,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: system, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -133,7 +127,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child Braden@zeek-londonattempted a login via(6278)with resultfailureSource128.199.212.120' + 'Braden@zeek-londonattempted a login via(6278)with resultfailureSource128.199.212.120' ); }); @@ -143,7 +137,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: auditd, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -152,7 +145,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child Sessionalice@zeek-sanfranin/executedgpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' + 'Sessionalice@zeek-sanfranin/executedgpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap index 4326b7372604..d7bdacbcc61e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`netflowRowRenderer renders correctly against snapshot 1`] = ` - - some children -
{ const children = netflowRowRenderer.renderRow({ browserFields, data: getMockNetflowData(), - children: {'some children'}, timelineId: 'test', }); @@ -98,26 +97,10 @@ describe('netflowRowRenderer', () => { }); }); - test('should render children normally when given non-netflow data', () => { - const children = netflowRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: justIdAndTimestamp, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render netflow data', () => { const children = netflowRowRenderer.renderRow({ browserFields: mockBrowserFields, data: getMockNetflowData(), - children: {'some children'}, timelineId: 'test', }); const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx index 754d6ad99b7f..10d80e1952f4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx @@ -78,73 +78,63 @@ export const eventActionMatches = (eventAction: string | object | undefined | nu }; export const netflowRowRenderer: RowRenderer = { - isInstance: ecs => { - return ( - eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) || - eventActionMatches(get(EVENT_ACTION_FIELD, ecs)) - ); - }, - renderRow: ({ data, children, timelineId }) => ( - <> - {children} - -
- -
-
- + isInstance: ecs => + eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) || + eventActionMatches(get(EVENT_ACTION_FIELD, ecs)), + renderRow: ({ data, timelineId }) => ( + +
+ +
+
), }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx index 50ea7ca05b92..467f507e8be7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx @@ -25,7 +25,6 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, - children: {'some children'}, timelineId: 'test', }); const wrapper = shallow({children}); @@ -40,7 +39,6 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, - children: {'some children'}, timelineId: 'test', }); const wrapper = mount( @@ -48,6 +46,6 @@ describe('plain_row_renderer', () => { {children} ); - expect(wrapper.text()).toEqual('some children'); + expect(wrapper.text()).toEqual(''); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx index 6725830c97d0..da78f41f09ed 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx @@ -12,5 +12,5 @@ import { RowRenderer } from './row_renderer'; export const plainRowRenderer: RowRenderer = { isInstance: _ => true, - renderRow: ({ children }) => <>{children}, + renderRow: () => <>, }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx index df92fc1e9f63..2d9f877fe4af 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx @@ -8,28 +8,17 @@ import React from 'react'; import { BrowserFields } from '../../../../containers/source'; import { Ecs } from '../../../../graphql/types'; -import { EventsTrSupplement, OFFSET_SCROLLBAR } from '../../styles'; -import { useTimelineWidthContext } from '../../timeline_context'; +import { EventsTrSupplement } from '../../styles'; interface RowRendererContainerProps { children: React.ReactNode; } -export const RowRendererContainer = React.memo(({ children }) => { - const width = useTimelineWidthContext(); - - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - - {children} - - ); -}); +export const RowRendererContainer = React.memo(({ children }) => ( + + {children} + +)); RowRendererContainer.displayName = 'RowRendererContainer'; export interface RowRenderer { @@ -37,12 +26,10 @@ export interface RowRenderer { renderRow: ({ browserFields, data, - children, timelineId, }: { browserFields: BrowserFields; data: Ecs; - children: React.ReactNode; timelineId: string; }) => React.ReactNode; } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap index 3608a8123467..93b3046b57ed 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`suricata_row_renderer renders correctly against snapshot 1`] = ` - - some children - { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, - children: {'some children'}, timelineId: 'test', }); @@ -45,26 +44,10 @@ describe('suricata_row_renderer', () => { expect(suricataRowRenderer.isInstance(suricata)).toBe(true); }); - test('should render children normally if it does not have a signature', () => { - const children = suricataRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: nonSuricata, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render a suricata row', () => { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -73,7 +56,7 @@ describe('suricata_row_renderer', () => { ); expect(wrapper.text()).toContain( - 'some children 4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' + '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -82,7 +65,6 @@ describe('suricata_row_renderer', () => { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, - children: {'some children'}, timelineId: 'test', }); const wrapper = mount( @@ -90,6 +72,6 @@ describe('suricata_row_renderer', () => { {children} ); - expect(wrapper.text()).toEqual('some children'); + expect(wrapper.text()).toEqual(''); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx index b227326551e0..e49a5f65b47c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx @@ -17,15 +17,9 @@ export const suricataRowRenderer: RowRenderer = { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'suricata'; }, - renderRow: ({ browserFields, data, children, timelineId }) => { - return ( - <> - {children} - - - - - - ); - }, + renderRow: ({ browserFields, data, timelineId }) => ( + + + + ), }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx index 2b9adfe21b12..9ccd1fb7a051 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; @@ -28,11 +28,9 @@ const SignatureFlexItem = styled(EuiFlexItem)` SignatureFlexItem.displayName = 'SignatureFlexItem'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const Badge = styled(EuiBadge)` -// vertical-align: top; -// `; -const Badge = (props: EuiBadgeProps) => ; +const Badge = styled(EuiBadge)` + vertical-align: top; +`; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap index 9ed658714558..6fff32925abf 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly against snapshot 1`] = ` - - some children - - - some children - { const children = connectedToRenderer.renderRow({ browserFields, data: system, - children: {'some children'}, timelineId: 'test', }); @@ -99,7 +98,6 @@ describe('GenericRowRenderer', () => { const children = connectedToRenderer.renderRow({ browserFields: mockBrowserFields, data: system, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -108,7 +106,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Evan@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' + 'Evan@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' ); }); }); @@ -133,7 +131,6 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields, data: systemFile, - children: {'some children'}, timelineId: 'test', }); @@ -162,7 +159,6 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields: mockBrowserFields, data: systemFile, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -171,7 +167,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Braden@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' + 'Braden@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' ); }); }); @@ -195,14 +191,13 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54started processMicrosoft.Photos.exe(441684)C:\\Program Files\\WindowsApps\\Microsoft.Windows.Photos_2018.18091.17210.0_x64__8wekyb3d8bbwe\\Microsoft.Photos.exe-ServerName:App.AppXzst44mncqdg84v7sv6p7yznqwssy6f7f.mcavia parent processsvchost.exe(8)d4c97ed46046893141652e2ec0056a698f6445109949d7fcabbce331146889ee12563599116157778a22600d2a163d8112aed84562d06d7235b37895b68de56687895743' + 'Arun\\Anvi-Acer@HD-obe-8bf77f54started processMicrosoft.Photos.exe(441684)C:\\Program Files\\WindowsApps\\Microsoft.Windows.Photos_2018.18091.17210.0_x64__8wekyb3d8bbwe\\Microsoft.Photos.exe-ServerName:App.AppXzst44mncqdg84v7sv6p7yznqwssy6f7f.mcavia parent processsvchost.exe(8)d4c97ed46046893141652e2ec0056a698f6445109949d7fcabbce331146889ee12563599116157778a22600d2a163d8112aed84562d06d7235b37895b68de56687895743' ); }); @@ -224,14 +219,13 @@ describe('GenericRowRenderer', () => { endgameProcessTerminationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameTerminationEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54terminated processRuntimeBroker.exe(442384)with exit code087976f3430cc99bc939e0694247c0759961a49832b87218f4313d6fc0bc3a776797255e72d5ed5c058d4785950eba7abaa057653bd4401441a21bf1abce6404f4231db4d' + 'Arun\\Anvi-Acer@HD-obe-8bf77f54terminated processRuntimeBroker.exe(442384)with exit code087976f3430cc99bc939e0694247c0759961a49832b87218f4313d6fc0bc3a776797255e72d5ed5c058d4785950eba7abaa057653bd4401441a21bf1abce6404f4231db4d' ); }); @@ -253,7 +247,6 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, - children: {'some children '}, timelineId: 'test', })} @@ -284,7 +277,6 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, - children: {'some children '}, timelineId: 'test', })} @@ -315,7 +307,6 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, - children: {'some children '}, timelineId: 'test', })} @@ -344,14 +335,13 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54created a fileinC:\\Users\\Arun\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\63d78c21-e593-4484-b7a9-db33cd522ddc.tmpviachrome.exe(11620)' + 'Arun\\Anvi-Acer@HD-obe-8bf77f54created a fileinC:\\Users\\Arun\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\63d78c21-e593-4484-b7a9-db33cd522ddc.tmpviachrome.exe(11620)' ); }); @@ -373,14 +363,13 @@ describe('GenericRowRenderer', () => { endgameFileDeleteEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileDeleteEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-v1s-d2118419deleted a filetmp000002f6inC:\\Windows\\TEMP\\tmp00000404\\tmp000002f6viaAmSvc.exe(1084)' + 'SYSTEM\\NT AUTHORITY@HD-v1s-d2118419deleted a filetmp000002f6inC:\\Windows\\TEMP\\tmp00000404\\tmp000002f6viaAmSvc.exe(1084)' ); }); @@ -402,15 +391,12 @@ describe('GenericRowRenderer', () => { fileCreatedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileCreatedEvent, - children: {'some children '}, timelineId: 'test', })} ); - expect(wrapper.text()).toEqual( - 'some children foohostcreated a filein/etc/subgidviaan unknown process' - ); + expect(wrapper.text()).toEqual('foohostcreated a filein/etc/subgidviaan unknown process'); }); test('it renders a FIM (non-endgame) file deleted event', () => { @@ -431,14 +417,13 @@ describe('GenericRowRenderer', () => { fileDeletedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileDeletedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children foohostdeleted a filein/etc/gshadow.lockviaan unknown process' + 'foohostdeleted a filein/etc/gshadow.lockviaan unknown process' ); }); @@ -460,7 +445,6 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, - children: {'some children '}, timelineId: 'test', })} @@ -491,7 +475,6 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, - children: {'some children '}, timelineId: 'test', })} @@ -522,7 +505,6 @@ describe('GenericRowRenderer', () => { fileCreatedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileCreatedEvent, - children: {'some children '}, timelineId: 'test', })} @@ -551,14 +533,13 @@ describe('GenericRowRenderer', () => { endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4ConnectionAcceptEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' + 'SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' ); }); @@ -580,14 +561,13 @@ describe('GenericRowRenderer', () => { endgameIpv6ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv6ConnectionAcceptEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' + 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' ); }); @@ -609,14 +589,13 @@ describe('GenericRowRenderer', () => { endgameIpv4DisconnectReceivedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4DisconnectReceivedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' + 'Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' ); }); @@ -638,14 +617,13 @@ describe('GenericRowRenderer', () => { endgameIpv6DisconnectReceivedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv6DisconnectReceivedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' + 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' ); }); @@ -667,14 +645,13 @@ describe('GenericRowRenderer', () => { socketOpenedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: socketOpenedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' + 'root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' ); }); @@ -696,14 +673,13 @@ describe('GenericRowRenderer', () => { socketClosedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: socketClosedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' + 'root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' ); }); @@ -725,7 +701,6 @@ describe('GenericRowRenderer', () => { endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4ConnectionAcceptEvent, - children: {'some children '}, timelineId: 'test', })} @@ -750,14 +725,13 @@ describe('GenericRowRenderer', () => { userLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogonEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-v1s-d2118419successfully logged inusing logon type5 - Service(target logon ID0x3e7)viaC:\\Windows\\System32\\services.exe(432)as requested by subjectWIN-Q3DOP1UKA81$(subject logon ID0x3e7)4624' + 'SYSTEM\\NT AUTHORITY@HD-v1s-d2118419successfully logged inusing logon type5 - Service(target logon ID0x3e7)viaC:\\Windows\\System32\\services.exe(432)as requested by subjectWIN-Q3DOP1UKA81$(subject logon ID0x3e7)4624' ); }); @@ -775,14 +749,13 @@ describe('GenericRowRenderer', () => { adminLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: adminLogonEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children With special privileges,SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54successfully logged inviaC:\\Windows\\System32\\lsass.exe(964)as requested by subjectSYSTEM\\NT AUTHORITY4672' + 'With special privileges,SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54successfully logged inviaC:\\Windows\\System32\\lsass.exe(964)as requested by subjectSYSTEM\\NT AUTHORITY4672' ); }); @@ -800,14 +773,13 @@ describe('GenericRowRenderer', () => { explicitUserLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: explicitUserLogonEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children A login was attempted using explicit credentialsArun\\Anvi-AcertoHD-55b-3ec87f66viaC:\\Windows\\System32\\svchost.exe(1736)as requested by subjectANVI-ACER$\\WORKGROUP(subject logon ID0x3e7)4648' + 'A login was attempted using explicit credentialsArun\\Anvi-AcertoHD-55b-3ec87f66viaC:\\Windows\\System32\\svchost.exe(1736)as requested by subjectANVI-ACER$\\WORKGROUP(subject logon ID0x3e7)4648' ); }); @@ -825,14 +797,13 @@ describe('GenericRowRenderer', () => { userLogoffEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogoffEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-55b-3ec87f66logged offusing logon type2 - Interactive(target logon ID0x16db41e)viaC:\\Windows\\System32\\lsass.exe(964)4634' + 'Arun\\Anvi-Acer@HD-55b-3ec87f66logged offusing logon type2 - Interactive(target logon ID0x16db41e)viaC:\\Windows\\System32\\lsass.exe(964)4634' ); }); @@ -850,7 +821,6 @@ describe('GenericRowRenderer', () => { userLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogonEvent, - children: {'some children '}, timelineId: 'test', })} @@ -874,14 +844,13 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54asked forupdate.googleapis.comwith question typeA, which resolved to10.100.197.67viaGoogleUpdate.exe(443192)3008dns' + 'SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54asked forupdate.googleapis.comwith question typeA, which resolved to10.100.197.67viaGoogleUpdate.exe(443192)3008dns' ); }); @@ -898,14 +867,13 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: dnsEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' + 'iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' ); }); @@ -928,7 +896,6 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, - children: {'some children '}, timelineId: 'test', })} @@ -956,7 +923,6 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, - children: {'some children '}, timelineId: 'test', })} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx index 3e64248d3987..523d4f3a0cfb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -35,19 +35,16 @@ export const createGenericSystemRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -68,20 +65,17 @@ export const createEndgameProcessRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -102,20 +96,17 @@ export const createFimRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -136,19 +127,16 @@ export const createGenericFileRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -163,19 +151,16 @@ export const createSocketRowRenderer = ({ const action: string | null | undefined = get('event.action[0]', ecs); return action != null && action.toLowerCase() === actionName; }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -194,18 +179,15 @@ export const createSecurityEventRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -215,18 +197,15 @@ export const createDnsRowRenderer = (): RowRenderer => ({ const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', ecs); return !isNillEmptyOrNotFinite(dnsQuestionType) && !isNillEmptyOrNotFinite(dnsQuestionName); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap index 9b59f69cad3a..460ad35b4767 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`zeek_row_renderer renders correctly against snapshot 1`] = ` - - some children - { const children = zeekRowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonZeek, - children: {'some children'}, timelineId: 'test', }); @@ -44,26 +43,10 @@ describe('zeek_row_renderer', () => { expect(zeekRowRenderer.isInstance(zeek)).toBe(true); }); - test('should render children normally if it does not have a zeek object', () => { - const children = zeekRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: nonZeek, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render a zeek row', () => { const children = zeekRowRenderer.renderRow({ browserFields: mockBrowserFields, data: zeek, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -72,7 +55,7 @@ describe('zeek_row_renderer', () => { ); expect(wrapper.text()).toContain( - 'some children C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' + 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx index fc528e33b5ab..0fca5cdd8b3d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx @@ -17,12 +17,9 @@ export const zeekRowRenderer: RowRenderer = { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'zeek'; }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx index 57e5ff19eb81..f13a236e8ec3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { get } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; @@ -19,11 +19,9 @@ import { IS_OPERATOR } from '../../../data_providers/data_provider'; import * as i18n from './translations'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const Badge = styled(EuiBadge)` -// vertical-align: top; -// `; -const Badge = (props: EuiBadgeProps) => ; +const Badge = styled(EuiBadge)` + vertical-align: top; +`; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx index d06dcbb84ad7..76f26d3dda5a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx @@ -8,6 +8,7 @@ import { noop } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import React, { useCallback, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { BrowserFields } from '../../../containers/source'; import { TimelineItem } from '../../../graphql/types'; @@ -38,9 +39,9 @@ import { plainRowRenderer } from './renderers/plain_row_renderer'; interface OwnProps { browserFields: BrowserFields; data: TimelineItem[]; + height?: number; id: string; isEventViewer?: boolean; - height: number; sort: Sort; toggleColumn: (column: ColumnHeaderOptions) => void; } @@ -101,7 +102,7 @@ const StatefulBodyComponent = React.memo( isSelected && Object.keys(selectedEventIds).length + 1 === data.length, }); }, - [id, data, selectedEventIds, timelineTypeContext.queryFields] + [setSelected, id, data, selectedEventIds, timelineTypeContext.queryFields] ); const onSelectAll: OnSelectAll = useCallback( @@ -118,7 +119,7 @@ const StatefulBodyComponent = React.memo( isSelectAllChecked: isSelected, }) : clearSelected!({ id }), - [id, data, timelineTypeContext.queryFields] + [setSelected, clearSelected, id, data, timelineTypeContext.queryFields] ); const onColumnSorted: OnColumnSorted = useCallback( @@ -189,25 +190,22 @@ const StatefulBodyComponent = React.memo( /> ); }, - (prevProps, nextProps) => { - return ( - prevProps.browserFields === nextProps.browserFields && - prevProps.columnHeaders === nextProps.columnHeaders && - prevProps.data === nextProps.data && - prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && - prevProps.notesById === nextProps.notesById && - prevProps.height === nextProps.height && - prevProps.id === nextProps.id && - prevProps.isEventViewer === nextProps.isEventViewer && - prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && - prevProps.loadingEventIds === nextProps.loadingEventIds && - prevProps.pinnedEventIds === nextProps.pinnedEventIds && - prevProps.selectedEventIds === nextProps.selectedEventIds && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showRowRenderers === nextProps.showRowRenderers && - prevProps.sort === nextProps.sort - ); - } + (prevProps, nextProps) => + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && + deepEqual(prevProps.data, nextProps.data) && + prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && + deepEqual(prevProps.notesById, nextProps.notesById) && + prevProps.height === nextProps.height && + prevProps.id === nextProps.id && + prevProps.isEventViewer === nextProps.isEventViewer && + prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.loadingEventIds === nextProps.loadingEventIds && + prevProps.pinnedEventIds === nextProps.pinnedEventIds && + prevProps.selectedEventIds === nextProps.selectedEventIds && + prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.showRowRenderers === nextProps.showRowRenderers && + prevProps.sort === nextProps.sort ); StatefulBodyComponent.displayName = 'StatefulBodyComponent'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx index a47fb932ed26..56639f90c146 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiText } from '@elastic/eui'; +import { EuiBadge, EuiText } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; @@ -21,24 +21,12 @@ const Text = styled(EuiText)` Text.displayName = 'Text'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const BadgeHighlighted = styled(EuiBadge)` -// height: 20px; -// margin: 0 5px 0 5px; -// max-width: 70px; -// min-width: 70px; -// `; -const BadgeHighlighted = (props: EuiBadgeProps) => ( - -); +const BadgeHighlighted = styled(EuiBadge)` + height: 20px; + margin: 0 5px 0 5px; + maxwidth: 85px; + minwidth: 85px; +`; BadgeHighlighted.displayName = 'BadgeHighlighted'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx index 1a1e8292b7e0..663b3dd50134 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { rgba } from 'polished'; -import React from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; import { AndOrBadge } from '../../and_or_badge'; @@ -54,13 +54,9 @@ const DropAndTargetDataProviders = styled.div<{ hasAndItem: boolean }>` DropAndTargetDataProviders.displayName = 'DropAndTargetDataProviders'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const NumberProviderAndBadge = styled(EuiBadge)` -// margin: 0px 5px; -// `; -const NumberProviderAndBadge = (props: EuiBadgeProps) => ( - -); +const NumberProviderAndBadge = styled(EuiBadge)` + margin: 0px 5px; +`; NumberProviderAndBadge.displayName = 'NumberProviderAndBadge'; @@ -89,8 +85,13 @@ export const ProviderItemAndDragDrop = React.memo( onToggleDataProviderExcluded, timelineId, }) => { - const onMouseEnter = () => onChangeDroppableAndProvider(dataProvider.id); - const onMouseLeave = () => onChangeDroppableAndProvider(''); + const onMouseEnter = useCallback(() => onChangeDroppableAndProvider(dataProvider.id), [ + onChangeDroppableAndProvider, + dataProvider.id, + ]); + const onMouseLeave = useCallback(() => onChangeDroppableAndProvider(''), [ + onChangeDroppableAndProvider, + ]); const hasAndItem = dataProvider.and.length > 0; return ( ` ${({ hideExpandButton }) => @@ -50,33 +49,26 @@ export const ExpandableEvent = React.memo( timelineId, toggleColumn, onUpdateColumns, - }) => { - const width = useTimelineWidthContext(); - // Passing the styles directly to the component of LazyAccordion because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - - ( - - )} - forceExpand={forceExpand} - paddingSize="none" - /> - - ); - } + }) => ( + + ( + + )} + forceExpand={forceExpand} + paddingSize="none" + /> + + ) ); ExpandableEvent.displayName = 'ExpandableEvent'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx index 65c539d77a16..16eaa8030820 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx @@ -6,6 +6,7 @@ import { memo, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { IIndexPattern } from 'src/plugins/data/public'; import { timelineSelectors, State } from '../../store'; @@ -39,7 +40,14 @@ const TimelineKqlFetchComponent = memo( }); }, [kueryFilterQueryDraft, kueryFilterQuery, id]); return null; - } + }, + (prevProps, nextProps) => + prevProps.id === nextProps.id && + prevProps.inputId === nextProps.inputId && + prevProps.setTimelineQuery === nextProps.setTimelineQuery && + deepEqual(prevProps.kueryFilterQuery, nextProps.kueryFilterQuery) && + deepEqual(prevProps.kueryFilterQueryDraft, nextProps.kueryFilterQueryDraft) && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) ); const makeMapStateToProps = () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap index 9cdbda757d97..eff487ceb798 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap @@ -1,94 +1,86 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Footer Timeline Component rendering it renders the default timeline footer 1`] = ` - - + - - - - - 1 rows - , - - 5 rows - , - - 10 rows - , - - 20 rows - , - ] - } - itemsCount={2} - onClick={[Function]} - serverSideEventCount={15546} - /> - - - - + 1 rows + , + + 5 rows + , + + 10 rows + , + + 20 rows + , + ] + } + itemsCount={2} + onClick={[Function]} + serverSideEventCount={15546} /> - - - - - - - - - + + + + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx index cbad2d42cf8a..d54a4cee83e5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx @@ -17,7 +17,6 @@ describe('Footer Timeline Component', () => { const loadMore = jest.fn(); const onChangeItemsPerPage = jest.fn(); const getUpdatedAt = () => 1546878704036; - const compact = true; describe('rendering', () => { test('it renders the default timeline footer', () => { @@ -36,7 +35,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -59,7 +57,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -83,7 +80,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -140,7 +136,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -164,7 +159,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -195,7 +189,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -225,7 +218,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -259,7 +251,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -285,7 +276,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx index 1fcc4382c179..7a025e96e57f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx @@ -19,7 +19,7 @@ import { EuiPopoverProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; +import React, { FC, useCallback, useEffect, useState, useMemo } from 'react'; import styled from 'styled-components'; import { LoadingPanel } from '../../loading'; @@ -28,8 +28,30 @@ import { OnChangeItemsPerPage, OnLoadMore } from '../events'; import { LastUpdatedAt } from './last_updated'; import * as i18n from './translations'; import { useTimelineTypeContext } from '../timeline_context'; +import { useEventDetailsWidthContext } from '../../events_viewer/event_details_width_context'; -const FixedWidthLastUpdated = styled.div<{ compact: boolean }>` +export const isCompactFooter = (width: number): boolean => width < 600; + +interface FixedWidthLastUpdatedContainerProps { + updatedAt: number; +} + +const FixedWidthLastUpdatedContainer = React.memo( + ({ updatedAt }) => { + const width = useEventDetailsWidthContext(); + const compact = useMemo(() => isCompactFooter(width), [width]); + + return ( + + + + ); + } +); + +FixedWidthLastUpdatedContainer.displayName = 'FixedWidthLastUpdatedContainer'; + +const FixedWidthLastUpdated = styled.div<{ compact?: boolean }>` width: ${({ compact }) => (!compact ? 200 : 25)}px; overflow: hidden; text-align: end; @@ -37,8 +59,16 @@ const FixedWidthLastUpdated = styled.div<{ compact: boolean }>` FixedWidthLastUpdated.displayName = 'FixedWidthLastUpdated'; -const FooterContainer = styled(EuiFlexGroup)<{ height: number }>` - height: ${({ height }) => height}px; +interface HeightProp { + height: number; +} + +const FooterContainer = styled(EuiFlexGroup).attrs(({ height }) => ({ + style: { + height: `${height}px`, + }, +}))` + flex: 0; `; FooterContainer.displayName = 'FooterContainer'; @@ -56,7 +86,7 @@ const LoadingPanelContainer = styled.div` LoadingPanelContainer.displayName = 'LoadingPanelContainer'; -const PopoverRowItems = styled((EuiPopover as unknown) as FunctionComponent)< +const PopoverRowItems = styled((EuiPopover as unknown) as FC)< EuiPopoverProps & { className?: string; id?: string; @@ -173,11 +203,9 @@ export const PagingControl = React.memo(PagingControlComponent); PagingControl.displayName = 'PagingControl'; interface FooterProps { - compact: boolean; getUpdatedAt: () => number; hasNextPage: boolean; height: number; - isEventViewer?: boolean; isLive: boolean; isLoading: boolean; itemsCount: number; @@ -192,11 +220,9 @@ interface FooterProps { /** Renders a loading indicator and paging controls */ export const FooterComponent = ({ - compact, getUpdatedAt, hasNextPage, height, - isEventViewer, isLive, isLoading, itemsCount, @@ -216,11 +242,13 @@ export const FooterComponent = ({ const loadMore = useCallback(() => { setPaginationLoading(true); onLoadMore(nextCursor, tieBreaker); - }, [nextCursor, tieBreaker, onLoadMore]); + }, [nextCursor, tieBreaker, onLoadMore, setPaginationLoading]); - const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); - - const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [ + isPopoverOpen, + setIsPopoverOpen, + ]); + const closePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); useEffect(() => { if (paginationLoading && !isLoading) { @@ -263,95 +291,78 @@ export const FooterComponent = ({ )); return ( - <> - + - - - - - - - - - {isLive ? ( - - - {i18n.AUTO_REFRESH_ACTIVE}{' '} - - } - type="iInCircle" - /> - - - ) : ( - - )} - - - - - - - - - - + + + + + + + + {isLive ? ( + + + {i18n.AUTO_REFRESH_ACTIVE}{' '} + + } + type="iInCircle" + /> + + + ) : ( + + )} + + + + + + + ); }; FooterComponent.displayName = 'FooterComponent'; -export const Footer = React.memo( - FooterComponent, - (prevProps, nextProps) => - prevProps.compact === nextProps.compact && - prevProps.hasNextPage === nextProps.hasNextPage && - prevProps.height === nextProps.height && - prevProps.isEventViewer === nextProps.isEventViewer && - prevProps.isLive === nextProps.isLive && - prevProps.isLoading === nextProps.isLoading && - prevProps.itemsCount === nextProps.itemsCount && - prevProps.itemsPerPage === nextProps.itemsPerPage && - prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions && - prevProps.serverSideEventCount === nextProps.serverSideEventCount -); +export const Footer = React.memo(FooterComponent); Footer.displayName = 'Footer'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap index 048ca080772f..90d0dc1a8a66 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Header rendering renders correctly against snapshot 1`] = ` - + - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx index 5af7aff4f879..317c68b63f69 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx @@ -7,13 +7,12 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { Direction } from '../../../graphql/types'; import { mockIndexPattern } from '../../../mock'; import { TestProviders } from '../../../mock/test_providers'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../utils/use_mount_appended'; -import { TimelineHeaderComponent } from '.'; +import { TimelineHeader } from '.'; jest.mock('../../../lib/kibana'); @@ -24,7 +23,7 @@ describe('Header', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - { onToggleDataProviderExcluded={jest.fn()} show={true} showCallOutUnauthorizedMsg={false} - sort={{ - columnId: '@timestamp', - sortDirection: Direction.desc, - }} /> ); expect(wrapper).toMatchSnapshot(); @@ -49,7 +44,7 @@ describe('Header', () => { test('it renders the data providers', () => { const wrapper = mount( - { onToggleDataProviderExcluded={jest.fn()} show={true} showCallOutUnauthorizedMsg={false} - sort={{ - columnId: '@timestamp', - sortDirection: Direction.desc, - }} /> ); @@ -76,7 +67,7 @@ describe('Header', () => { test('it renders the unauthorized call out providers', () => { const wrapper = mount( - { onToggleDataProviderExcluded={jest.fn()} show={true} showCallOutUnauthorizedMsg={true} - sort={{ - columnId: '@timestamp', - sortDirection: Direction.desc, - }} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx index 81eef0efbfa5..7cac03cec42b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx @@ -6,10 +6,9 @@ import { EuiCallOut } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; import { IIndexPattern } from 'src/plugins/data/public'; +import deepEqual from 'fast-deep-equal'; -import { Sort } from '../body/sort'; import { DataProviders } from '../data_providers'; import { DataProvider } from '../data_providers/data_provider'; import { @@ -38,16 +37,9 @@ interface Props { onToggleDataProviderExcluded: OnToggleDataProviderExcluded; show: boolean; showCallOutUnauthorizedMsg: boolean; - sort: Sort; } -const TimelineHeaderContainer = styled.div` - width: 100%; -`; - -TimelineHeaderContainer.displayName = 'TimelineHeaderContainer'; - -export const TimelineHeaderComponent: React.FC = ({ +const TimelineHeaderComponent: React.FC = ({ browserFields, id, indexPattern, @@ -61,7 +53,7 @@ export const TimelineHeaderComponent: React.FC = ({ show, showCallOutUnauthorizedMsg, }) => ( - + <> {showCallOutUnauthorizedMsg && ( = ({ indexPattern={indexPattern} timelineId={id} /> - + ); -export const TimelineHeader = React.memo(TimelineHeaderComponent); +export const TimelineHeader = React.memo( + TimelineHeaderComponent, + (prevProps, nextProps) => + deepEqual(prevProps.browserFields, nextProps.browserFields) && + prevProps.id === nextProps.id && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + prevProps.onChangeDataProviderKqlQuery === nextProps.onChangeDataProviderKqlQuery && + prevProps.onChangeDroppableAndProvider === nextProps.onChangeDroppableAndProvider && + prevProps.onDataProviderEdited === nextProps.onDataProviderEdited && + prevProps.onDataProviderRemoved === nextProps.onDataProviderRemoved && + prevProps.onToggleDataProviderEnabled === nextProps.onToggleDataProviderEnabled && + prevProps.onToggleDataProviderExcluded === nextProps.onToggleDataProviderExcluded && + prevProps.show === nextProps.show && + prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx index 611d08e61be2..f051bbe5b1af 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx @@ -153,25 +153,6 @@ export const combineQueries = ({ }; }; -interface CalculateBodyHeightParams { - /** The the height of the flyout container, which is typically the entire "page", not including the standard Kibana navigation */ - flyoutHeight?: number; - /** The flyout header typically contains a title and a close button */ - flyoutHeaderHeight?: number; - /** All non-body timeline content (i.e. the providers drag and drop area, and the column headers) */ - timelineHeaderHeight?: number; - /** Footer content that appears below the body (i.e. paging controls) */ - timelineFooterHeight?: number; -} - -export const calculateBodyHeight = ({ - flyoutHeight = 0, - flyoutHeaderHeight = 0, - timelineHeaderHeight = 0, - timelineFooterHeight = 0, -}: CalculateBodyHeightParams): number => - flyoutHeight - (flyoutHeaderHeight + timelineHeaderHeight + timelineFooterHeight); - /** * The CSS class name of a "stateful event", which appears in both * the `Timeline` and the `Events Viewer` widget diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index 0ce6bc16f132..35099e3836fb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -28,8 +28,8 @@ import { Timeline } from './timeline'; export interface OwnProps { id: string; - flyoutHeaderHeight: number; - flyoutHeight: number; + onClose: () => void; + usersViewing: string[]; } type Props = OwnProps & PropsFromRedux; @@ -42,14 +42,13 @@ const StatefulTimelineComponent = React.memo( eventType, end, filters, - flyoutHeaderHeight, - flyoutHeight, id, isLive, itemsPerPage, itemsPerPageOptions, kqlMode, kqlQueryExpression, + onClose, onDataProviderEdited, removeColumn, removeProvider, @@ -63,6 +62,7 @@ const StatefulTimelineComponent = React.memo( updateHighlightedDropAndProviderId, updateItemsPerPage, upsertColumn, + usersViewing, }) => { const { loading, signalIndexExists, signalIndexName } = useSignalIndex(); @@ -173,8 +173,6 @@ const StatefulTimelineComponent = React.memo( end={end} eventType={eventType} filters={filters} - flyoutHeaderHeight={flyoutHeaderHeight} - flyoutHeight={flyoutHeight} id={id} indexPattern={indexPattern} indexToAdd={indexToAdd} @@ -187,6 +185,7 @@ const StatefulTimelineComponent = React.memo( onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery} onChangeDroppableAndProvider={onChangeDroppableAndProvider} onChangeItemsPerPage={onChangeItemsPerPage} + onClose={onClose} onDataProviderEdited={onDataProviderEditedLocal} onDataProviderRemoved={onDataProviderRemoved} onToggleDataProviderEnabled={onToggleDataProviderEnabled} @@ -196,6 +195,7 @@ const StatefulTimelineComponent = React.memo( sort={sort!} start={start} toggleColumn={toggleColumn} + usersViewing={usersViewing} /> )} @@ -205,8 +205,6 @@ const StatefulTimelineComponent = React.memo( return ( prevProps.eventType === nextProps.eventType && prevProps.end === nextProps.end && - prevProps.flyoutHeaderHeight === nextProps.flyoutHeaderHeight && - prevProps.flyoutHeight === nextProps.flyoutHeight && prevProps.id === nextProps.id && prevProps.isLive === nextProps.isLive && prevProps.itemsPerPage === nextProps.itemsPerPage && @@ -219,7 +217,8 @@ const StatefulTimelineComponent = React.memo( deepEqual(prevProps.dataProviders, nextProps.dataProviders) && deepEqual(prevProps.filters, nextProps.filters) && deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && - deepEqual(prevProps.sort, nextProps.sort) + deepEqual(prevProps.sort, nextProps.sort) && + deepEqual(prevProps.usersViewing, nextProps.usersViewing) ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx index ae139c24d017..4b1fd4b5851c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -6,7 +6,6 @@ import { EuiBadge, - EuiBadgeProps, EuiButton, EuiButtonEmpty, EuiButtonIcon, @@ -18,8 +17,9 @@ import { EuiOverlayMask, EuiToolTip, } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; import uuid from 'uuid'; +import styled from 'styled-components'; import { Note } from '../../../lib/note'; import { Notes } from '../../notes'; @@ -32,13 +32,10 @@ export const historyToolTip = 'The chronological history of actions related to t export const streamLiveToolTip = 'Update the Timeline as new data arrives'; export const newTimelineToolTip = 'Create a new timeline'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const NotesCountBadge = styled(EuiBadge)` -// margin-left: 5px; -// `; -const NotesCountBadge = (props: EuiBadgeProps) => ( - -); +const NotesCountBadge = styled(EuiBadge)` + margin-left: 5px; +`; + NotesCountBadge.displayName = 'NotesCountBadge'; type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; @@ -121,20 +118,24 @@ interface NewTimelineProps { } export const NewTimeline = React.memo( - ({ createTimeline, onClosePopover, timelineId }) => ( - { - createTimeline({ id: timelineId, show: true }); - onClosePopover(); - }} - > - {i18n.NEW_TIMELINE} - - ) + ({ createTimeline, onClosePopover, timelineId }) => { + const handleClick = useCallback(() => { + createTimeline({ id: timelineId, show: true }); + onClosePopover(); + }, [createTimeline, timelineId, onClosePopover]); + + return ( + + {i18n.NEW_TIMELINE} + + ); + } ); NewTimeline.displayName = 'NewTimeline'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx index 495b94f8c02e..e942c8f36dc8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx @@ -10,11 +10,18 @@ import { Provider as ReduxStoreProvider } from 'react-redux'; import { mockGlobalState, apolloClientObservable } from '../../../mock'; import { createStore, State } from '../../../store'; +import { useThrottledResizeObserver } from '../../utils'; import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; jest.mock('../../../lib/kibana'); +let mockedWidth = 1000; +jest.mock('../../utils'); +(useThrottledResizeObserver as jest.Mock).mockImplementation(() => ({ + width: mockedWidth, +})); + describe('Properties', () => { const usersViewing = ['elastic']; @@ -24,6 +31,7 @@ describe('Properties', () => { beforeEach(() => { jest.clearAllMocks(); store = createStore(state, apolloClientObservable); + mockedWidth = 1000; }); test('renders correctly', () => { @@ -46,7 +54,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -73,7 +80,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -101,7 +107,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -131,7 +136,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -164,7 +168,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -197,7 +200,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -229,7 +231,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -243,7 +244,7 @@ describe('Properties', () => { test('it renders a description on the left when the width is at least as wide as the threshold', () => { const description = 'strange'; - const width = showDescriptionThreshold; + mockedWidth = showDescriptionThreshold; const wrapper = mount( @@ -264,7 +265,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={width} /> ); @@ -280,7 +280,7 @@ describe('Properties', () => { test('it does NOT render a description on the left when the width is less than the threshold', () => { const description = 'strange'; - const width = showDescriptionThreshold - 1; + mockedWidth = showDescriptionThreshold - 1; const wrapper = mount( @@ -301,7 +301,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={width} /> ); @@ -315,7 +314,7 @@ describe('Properties', () => { }); test('it renders a notes button on the left when the width is at least as wide as the threshold', () => { - const width = showNotesThreshold; + mockedWidth = showNotesThreshold; const wrapper = mount( @@ -336,7 +335,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={width} /> ); @@ -350,7 +348,7 @@ describe('Properties', () => { }); test('it does NOT render a a notes button on the left when the width is less than the threshold', () => { - const width = showNotesThreshold - 1; + mockedWidth = showNotesThreshold - 1; const wrapper = mount( @@ -371,7 +369,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={width} /> ); @@ -404,7 +401,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -434,7 +430,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -462,7 +457,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx index 7b69e006f48a..8549784b8ecd 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; +import { useThrottledResizeObserver } from '../../utils'; import { Note } from '../../../lib/note'; import { InputsModelId } from '../../../store/inputs/constants'; import { AssociateNote, UpdateNote } from '../../notes/helpers'; @@ -37,7 +38,6 @@ interface Props { updateNote: UpdateNote; updateTitle: UpdateTitle; usersViewing: string[]; - width: number; } const rightGutter = 60; // px @@ -49,7 +49,7 @@ const starIconWidth = 30; const nameWidth = 155; const descriptionWidth = 165; const noteWidth = 130; -const settingsWidth = 50; +const settingsWidth = 55; /** Displays the properties of a timeline, i.e. name, description, notes, etc */ export const Properties = React.memo( @@ -70,47 +70,36 @@ export const Properties = React.memo( updateNote, updateTitle, usersViewing, - width, }) => { + const { ref, width = 0 } = useThrottledResizeObserver(300); const [showActions, setShowActions] = useState(false); const [showNotes, setShowNotes] = useState(false); const [showTimelineModal, setShowTimelineModal] = useState(false); - const onButtonClick = useCallback(() => { - setShowActions(!showActions); - }, [showActions]); - - const onToggleShowNotes = useCallback(() => { - setShowNotes(!showNotes); - }, [showNotes]); - - const onClosePopover = useCallback(() => { - setShowActions(false); - }, []); - + const onButtonClick = useCallback(() => setShowActions(!showActions), [showActions]); + const onToggleShowNotes = useCallback(() => setShowNotes(!showNotes), [showNotes]); + const onClosePopover = useCallback(() => setShowActions(false), []); + const onCloseTimelineModal = useCallback(() => setShowTimelineModal(false), []); + const onToggleLock = useCallback(() => toggleLock({ linkToId: 'timeline' }), [toggleLock]); const onOpenTimelineModal = useCallback(() => { onClosePopover(); setShowTimelineModal(true); }, []); - const onCloseTimelineModal = useCallback(() => { - setShowTimelineModal(false); - }, []); - - const datePickerWidth = - width - - rightGutter - - starIconWidth - - nameWidth - - (width >= showDescriptionThreshold ? descriptionWidth : 0) - - noteWidth - - settingsWidth; + const datePickerWidth = useMemo( + () => + width - + rightGutter - + starIconWidth - + nameWidth - + (width >= showDescriptionThreshold ? descriptionWidth : 0) - + noteWidth - + settingsWidth, + [width] + ); - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 return ( - + ( showNotesFromWidth={width >= showNotesThreshold} timelineId={timelineId} title={title} - toggleLock={() => { - toggleLock({ linkToId: 'timeline' }); - }} + toggleLock={onToggleLock} updateDescription={updateDescription} updateIsFavorite={updateIsFavorite} updateNote={updateNote} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx index 21800fefb21f..3016def8a80b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx @@ -52,7 +52,15 @@ export const LockIconContainer = styled(EuiFlexItem)` LockIconContainer.displayName = 'LockIconContainer'; -export const DatePicker = styled(EuiFlexItem)` +interface WidthProp { + width: number; +} + +export const DatePicker = styled(EuiFlexItem).attrs(({ width }) => ({ + style: { + width: `${width}px`, + }, +}))` .euiSuperDatePicker__flexWrapper { max-width: none; width: auto; @@ -151,7 +159,7 @@ export const PropertiesLeft = React.memo( /> - + diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx index 3444875282ae..74653fb6cb1e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx @@ -12,17 +12,26 @@ const fadeInEffect = keyframes` to { opacity: 1; } `; +interface WidthProp { + width: number; +} + export const TimelineProperties = styled.div` + flex: 1; align-items: center; display: flex; flex-direction: row; justify-content: space-between; user-select: none; `; + TimelineProperties.displayName = 'TimelineProperties'; -export const DatePicker = styled(EuiFlexItem)<{ width: number }>` - width: ${({ width }) => `${width}px`}; +export const DatePicker = styled(EuiFlexItem).attrs(({ width }) => ({ + style: { + width: `${width}px`, + }, +}))` .euiSuperDatePicker__flexWrapper { max-width: none; width: auto; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx index 3d2ec0683f09..73c20d9b9b6b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx @@ -5,7 +5,7 @@ */ import React, { useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { inputsModel } from '../../store'; import { inputsActions } from '../../store/actions'; @@ -19,29 +19,20 @@ export interface TimelineRefetchProps { refetch: inputsModel.Refetch; } -type OwnProps = TimelineRefetchProps & PropsFromRedux; - -const TimelineRefetchComponent: React.FC = ({ +const TimelineRefetchComponent: React.FC = ({ id, inputId, inspect, loading, refetch, - setTimelineQuery, }) => { + const dispatch = useDispatch(); + useEffect(() => { - setTimelineQuery({ id, inputId, inspect, loading, refetch }); - }, [id, inputId, loading, refetch, inspect]); + dispatch(inputsActions.setQuery({ id, inputId, inspect, loading, refetch })); + }, [dispatch, id, inputId, loading, refetch, inspect]); return null; }; -const mapDispatchToProps = { - setTimelineQuery: inputsActions.setQuery, -}; - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const TimelineRefetch = connector(React.memo(TimelineRefetchComponent)); +export const TimelineRefetch = React.memo(TimelineRefetchComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx index d5e5d15eb8ad..16fb57714829 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx @@ -11,12 +11,6 @@ import styled, { createGlobalStyle } from 'styled-components'; import { EventType } from '../../store/timeline/model'; import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; -/** - * OFFSET PIXEL VALUES - */ - -export const OFFSET_SCROLLBAR = 17; - /** * TIMELINE BODY */ @@ -30,10 +24,11 @@ export const TimelineBodyGlobalStyle = createGlobalStyle` export const TimelineBody = styled.div.attrs(({ className = '' }) => ({ className: `siemTimeline__body ${className}`, -}))<{ bodyHeight: number }>` - height: ${({ bodyHeight }) => `${bodyHeight}px`}; +}))<{ bodyHeight?: number }>` + height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')}; overflow: auto; scrollbar-width: thin; + flex: 1; &::-webkit-scrollbar { height: ${({ theme }) => theme.eui.euiScrollBar}; @@ -57,10 +52,19 @@ TimelineBody.displayName = 'TimelineBody'; * EVENTS TABLE */ -export const EventsTable = styled.div.attrs(({ className = '' }) => ({ - className: `siemEventsTable ${className}`, - role: 'table', -}))``; +interface EventsTableProps { + columnWidths: number; +} + +export const EventsTable = styled.div.attrs( + ({ className = '', columnWidths }) => ({ + className: `siemEventsTable ${className}`, + role: 'table', + style: { + minWidth: `${columnWidths}px`, + }, + }) +)``; /* EVENTS HEAD */ @@ -177,6 +181,14 @@ export const EventsTrData = styled.div.attrs(({ className = '' }) => ({ display: flex; `; +const TIMELINE_EVENT_DETAILS_OFFSET = 40; + +export const EventsTrSupplementContainer = styled.div.attrs(({ width }) => ({ + style: { + width: `${width! - TIMELINE_EVENT_DETAILS_OFFSET}px`, + }, +}))``; + export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__trSupplement ${className}`, }))<{ className: string }>` @@ -200,11 +212,17 @@ export const EventsTdGroupData = styled.div.attrs(({ className = '' }) => ({ }))` display: flex; `; +interface WidthProp { + width?: number; +} -export const EventsTd = styled.div.attrs(({ className = '' }) => ({ +export const EventsTd = styled.div.attrs(({ className = '', width }) => ({ className: `siemEventsTable__td ${className}`, role: 'cell', -}))` + style: { + flexBasis: width ? `${width}px` : 'auto', + }, +}))` align-items: center; display: flex; flex-shrink: 0; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index d66bc702bae4..ea4406311d7c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -14,20 +14,17 @@ import { mockBrowserFields } from '../../containers/source/mock'; import { Direction } from '../../graphql/types'; import { defaultHeaders, mockTimelineData, mockIndexPattern } from '../../mock'; import { TestProviders } from '../../mock/test_providers'; -import { flyoutHeaderHeight } from '../flyout'; import { DELETE_CLASS_NAME, ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME, } from './data_providers/provider_item_actions'; -import { TimelineComponent } from './timeline'; +import { TimelineComponent, Props as TimelineComponentProps } from './timeline'; import { Sort } from './body/sort'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../utils/use_mount_appended'; -const testFlyoutHeight = 980; - jest.mock('../../lib/kibana'); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; @@ -35,6 +32,7 @@ jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); describe('Timeline', () => { + let props = {} as TimelineComponentProps; const sort: Sort = { columnId: '@timestamp', sortDirection: Direction.desc, @@ -50,41 +48,44 @@ describe('Timeline', () => { const mount = useMountAppended(); + beforeEach(() => { + props = { + browserFields: mockBrowserFields, + columns: defaultHeaders, + id: 'foo', + dataProviders: mockDataProviders, + end: endDate, + eventType: 'raw' as TimelineComponentProps['eventType'], + filters: [], + indexPattern, + indexToAdd: [], + isLive: false, + itemsPerPage: 5, + itemsPerPageOptions: [5, 10, 20], + kqlMode: 'search' as TimelineComponentProps['kqlMode'], + kqlQueryExpression: '', + loadingIndexName: false, + onChangeDataProviderKqlQuery: jest.fn(), + onChangeDroppableAndProvider: jest.fn(), + onChangeItemsPerPage: jest.fn(), + onClose: jest.fn(), + onDataProviderEdited: jest.fn(), + onDataProviderRemoved: jest.fn(), + onToggleDataProviderEnabled: jest.fn(), + onToggleDataProviderExcluded: jest.fn(), + show: true, + showCallOutUnauthorizedMsg: false, + start: startDate, + sort, + toggleColumn: jest.fn(), + usersViewing: ['elastic'], + }; + }); + describe('rendering', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); }); @@ -92,37 +93,7 @@ describe('Timeline', () => { const wrapper = mount( - + ); @@ -130,41 +101,28 @@ describe('Timeline', () => { expect(wrapper.find('[data-test-subj="timelineHeader"]').exists()).toEqual(true); }); + test('it renders the title field', () => { + const wrapper = mount( + + + + + + ); + + expect( + wrapper + .find('[data-test-subj="timeline-title"]') + .first() + .props().placeholder + ).toContain('Untitled Timeline'); + }); + test('it renders the timeline table', () => { const wrapper = mount( - + ); @@ -176,37 +134,7 @@ describe('Timeline', () => { const wrapper = mount( - + ); @@ -218,36 +146,7 @@ describe('Timeline', () => { const wrapper = mount( - + ); @@ -261,42 +160,10 @@ describe('Timeline', () => { describe('event wire up', () => { describe('onDataProviderRemoved', () => { test('it invokes the onDataProviderRemoved callback when the delete button on a provider is clicked', () => { - const mockOnDataProviderRemoved = jest.fn(); - const wrapper = mount( - + ); @@ -306,46 +173,16 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnDataProviderRemoved.mock.calls[0][0]).toEqual('id-Provider 1'); + expect((props.onDataProviderRemoved as jest.Mock).mock.calls[0][0]).toEqual( + 'id-Provider 1' + ); }); test('it invokes the onDataProviderRemoved callback when you click on the option "Delete" in the provider menu', () => { - const mockOnDataProviderRemoved = jest.fn(); - const wrapper = mount( - + ); @@ -361,48 +198,18 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnDataProviderRemoved.mock.calls[0][0]).toEqual('id-Provider 1'); + expect((props.onDataProviderRemoved as jest.Mock).mock.calls[0][0]).toEqual( + 'id-Provider 1' + ); }); }); describe('onToggleDataProviderEnabled', () => { test('it invokes the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => { - const mockOnToggleDataProviderEnabled = jest.fn(); - const wrapper = mount( - + ); @@ -419,7 +226,7 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnToggleDataProviderEnabled.mock.calls[0][0]).toEqual({ + expect((props.onToggleDataProviderEnabled as jest.Mock).mock.calls[0][0]).toEqual({ providerId: 'id-Provider 1', enabled: false, }); @@ -428,42 +235,10 @@ describe('Timeline', () => { describe('onToggleDataProviderExcluded', () => { test('it invokes the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => { - const mockOnToggleDataProviderExcluded = jest.fn(); - const wrapper = mount( - + ); @@ -482,7 +257,7 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnToggleDataProviderExcluded.mock.calls[0][0]).toEqual({ + expect((props.onToggleDataProviderExcluded as jest.Mock).mock.calls[0][0]).toEqual({ providerId: 'id-Provider 1', excluded: true, }); @@ -490,44 +265,14 @@ describe('Timeline', () => { }); describe('#ProviderWithAndProvider', () => { - test('Rendering And Provider', () => { - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); + const dataProviders = mockDataProviders.slice(0, 1); + dataProviders[0].and = mockDataProviders.slice(1, 3); + test('Rendering And Provider', () => { const wrapper = mount( - + ); @@ -544,44 +289,10 @@ describe('Timeline', () => { }); test('it invokes the onDataProviderRemoved callback when you click on the option "Delete" in the accordion menu', () => { - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); - const mockOnDataProviderRemoved = jest.fn(); - const wrapper = mount( - + ); @@ -600,48 +311,17 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnDataProviderRemoved.mock.calls[0]).toEqual(['id-Provider 1', 'id-Provider 2']); + expect((props.onDataProviderRemoved as jest.Mock).mock.calls[0]).toEqual([ + 'id-Provider 1', + 'id-Provider 2', + ]); }); test('it invokes the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the accordion menu', () => { - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); - const mockOnToggleDataProviderEnabled = jest.fn(); - const wrapper = mount( - + ); @@ -660,7 +340,7 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnToggleDataProviderEnabled.mock.calls[0][0]).toEqual({ + expect((props.onToggleDataProviderEnabled as jest.Mock).mock.calls[0][0]).toEqual({ andProviderId: 'id-Provider 2', enabled: false, providerId: 'id-Provider 1', @@ -668,44 +348,10 @@ describe('Timeline', () => { }); test('it invokes the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the accordion menu', () => { - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); - const mockOnToggleDataProviderExcluded = jest.fn(); - const wrapper = mount( - + ); @@ -724,7 +370,7 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnToggleDataProviderExcluded.mock.calls[0][0]).toEqual({ + expect((props.onToggleDataProviderExcluded as jest.Mock).mock.calls[0][0]).toEqual({ andProviderId: 'id-Provider 2', excluded: true, providerId: 'id-Provider 1', diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index 58bbbef328dd..098dd8279161 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui'; import { getOr, isEmpty } from 'lodash/fp'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; -import useResizeObserver from 'use-resize-observer/polyfilled'; +import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button'; import { BrowserFields } from '../../containers/source'; import { TimelineQuery } from '../../containers/timeline'; import { Direction } from '../../graphql/types'; @@ -31,38 +31,60 @@ import { import { TimelineKqlFetch } from './fetch_kql_timeline'; import { Footer, footerHeight } from './footer'; import { TimelineHeader } from './header'; -import { calculateBodyHeight, combineQueries } from './helpers'; +import { combineQueries } from './helpers'; import { TimelineRefetch } from './refetch_timeline'; import { ManageTimelineContext } from './timeline_context'; import { esQuery, Filter, IIndexPattern } from '../../../../../../../src/plugins/data/public'; -const WrappedByAutoSizer = styled.div` +const TimelineContainer = styled.div` + height: 100%; + display: flex; + flex-direction: column; +`; + +const TimelineHeaderContainer = styled.div` + margin-top: 6px; width: 100%; -`; // required by AutoSizer +`; -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; +TimelineHeaderContainer.displayName = 'TimelineHeaderContainer'; -const TimelineContainer = styled(EuiFlexGroup)` - min-height: 500px; - overflow: hidden; - padding: 0 10px 0 12px; - user-select: none; - width: 100%; +const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)` + align-items: center; + box-shadow: none; + display: flex; + flex-direction: column; + padding: 14px 10px 0 12px; `; -TimelineContainer.displayName = 'TimelineContainer'; +const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` + overflow-y: hidden; + flex: 1; -export const isCompactFooter = (width: number): boolean => width < 600; + .euiFlyoutBody__overflow { + overflow: hidden; + mask-image: none; + } -interface Props { + .euiFlyoutBody__overflowContent { + padding: 0 10px 0 12px; + height: 100%; + display: flex; + } +`; + +const StyledEuiFlyoutFooter = styled(EuiFlyoutFooter)` + background: none; + padding: 0 10px 5px 12px; +`; + +export interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; dataProviders: DataProvider[]; end: number; eventType?: EventType; filters: Filter[]; - flyoutHeaderHeight: number; - flyoutHeight: number; id: string; indexPattern: IIndexPattern; indexToAdd: string[]; @@ -75,6 +97,7 @@ interface Props { onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery; onChangeDroppableAndProvider: OnChangeDroppableAndProvider; onChangeItemsPerPage: OnChangeItemsPerPage; + onClose: () => void; onDataProviderEdited: OnDataProviderEdited; onDataProviderRemoved: OnDataProviderRemoved; onToggleDataProviderEnabled: OnToggleDataProviderEnabled; @@ -84,6 +107,7 @@ interface Props { start: number; sort: Sort; toggleColumn: (column: ColumnHeaderOptions) => void; + usersViewing: string[]; } /** The parent Timeline component */ @@ -94,8 +118,6 @@ export const TimelineComponent: React.FC = ({ end, eventType, filters, - flyoutHeaderHeight, - flyoutHeight, id, indexPattern, indexToAdd, @@ -108,6 +130,7 @@ export const TimelineComponent: React.FC = ({ onChangeDataProviderKqlQuery, onChangeDroppableAndProvider, onChangeItemsPerPage, + onClose, onDataProviderEdited, onDataProviderRemoved, onToggleDataProviderEnabled, @@ -117,10 +140,8 @@ export const TimelineComponent: React.FC = ({ start, sort, toggleColumn, + usersViewing, }) => { - const { ref: measureRef, width = 0, height: timelineHeaderHeight = 0 } = useResizeObserver< - HTMLDivElement - >({}); const kibana = useKibana(); const combinedQueries = combineQueries({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), @@ -134,45 +155,51 @@ export const TimelineComponent: React.FC = ({ end, }); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; + const timelineQueryFields = useMemo(() => columnsHeader.map(c => c.id), [columnsHeader]); + const timelineQuerySortField = useMemo( + () => ({ + sortFieldId: sort.columnId, + direction: sort.sortDirection as Direction, + }), + [sort.columnId, sort.sortDirection] + ); return ( - - }> - + + - + + + + {combinedQueries != null ? ( c.id)} + fields={timelineQueryFields} sourceId="default" limit={itemsPerPage} filterQuery={combinedQueries.filterQuery} - sortField={{ - sortFieldId: sort.columnId, - direction: sort.sortDirection as Direction, - }} + sortField={timelineQuerySortField} > {({ events, @@ -184,7 +211,7 @@ export const TimelineComponent: React.FC = ({ getUpdatedAt, refetch, }) => ( - + = ({ loading={loading} refetch={refetch} /> - -
+ + + + +
+ )} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx index 15759c2efff0..f1100e17bd3c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx @@ -11,10 +11,6 @@ const initTimelineContext = false; export const TimelineContext = createContext(initTimelineContext); export const useTimelineContext = () => useContext(TimelineContext); -const initTimelineWidth = 0; -export const TimelineWidthContext = createContext(initTimelineWidth); -export const useTimelineWidthContext = () => useContext(TimelineWidthContext); - export interface TimelineTypeContextProps { documentType?: string; footerText?: string; @@ -41,7 +37,6 @@ export const useTimelineTypeContext = () => useContext(TimelineTypeContext); interface ManageTimelineContextProps { children: React.ReactNode; loading: boolean; - width: number; type?: TimelineTypeContextProps; } @@ -50,11 +45,9 @@ interface ManageTimelineContextProps { const ManageTimelineContextComponent: React.FC = ({ children, loading, - width, type = initTimelineType, }) => { const [myLoading, setLoading] = useState(initTimelineContext); - const [myWidth, setWidth] = useState(initTimelineWidth); const [myType, setType] = useState(initTimelineType); useEffect(() => { @@ -65,15 +58,9 @@ const ManageTimelineContextComponent: React.FC = ({ setType(type); }, [type]); - useEffect(() => { - setWidth(width); - }, [width]); - return ( - - {children} - + {children} ); }; diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.tsx b/x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.tsx index dfbf09e555a7..06a46ddff107 100644 --- a/x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.tsx +++ b/x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.tsx @@ -17,7 +17,7 @@ import { EuiModalFooter, EuiAccordion, } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; import { AppToast } from '.'; @@ -29,10 +29,14 @@ interface FullErrorProps { toggle: (toast: AppToast) => void; } -export const ModalAllErrors = ({ isShowing, toast, toggle }: FullErrorProps) => - isShowing && toast != null ? ( +const ModalAllErrorsComponent: React.FC = ({ isShowing, toast, toggle }) => { + const handleClose = useCallback(() => toggle(toast), [toggle, toast]); + + if (!isShowing || toast == null) return null; + + return ( - toggle(toast)}> + {i18n.TITLE_ERROR_MODAL} @@ -55,13 +59,16 @@ export const ModalAllErrors = ({ isShowing, toast, toggle }: FullErrorProps) => - toggle(toast)} fill data-test-subj="modal-all-errors-close"> + {i18n.CLOSE_ERROR_MODAL} - ) : null; + ); +}; + +export const ModalAllErrors = React.memo(ModalAllErrorsComponent); const MyEuiCodeBlock = styled(EuiCodeBlock)` margin-top: 4px; diff --git a/x-pack/legacy/plugins/siem/public/components/utils.ts b/x-pack/legacy/plugins/siem/public/components/utils.ts index 42dd5b7c011a..ff022fd7d763 100644 --- a/x-pack/legacy/plugins/siem/public/components/utils.ts +++ b/x-pack/legacy/plugins/siem/public/components/utils.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { throttle } from 'lodash/fp'; +import { useMemo, useState } from 'react'; +import useResizeObserver from 'use-resize-observer/polyfilled'; import { niceTimeFormatByDay, timeFormatter } from '@elastic/charts'; import moment from 'moment-timezone'; @@ -22,3 +25,11 @@ export const histogramDateTimeFormatter = (domain: [number, number] | null, fixe const format = niceTimeFormatByDay(diff); return timeFormatter(format); }; + +export const useThrottledResizeObserver = (wait = 100) => { + const [size, setSize] = useState<{ width: number; height: number }>({ width: 0, height: 0 }); + const onResize = useMemo(() => throttle(wait, setSize), [wait]); + const { ref } = useResizeObserver({ onResize }); + + return { ref, ...size }; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx index ccd8babd41e6..f726ec9779dc 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; +import { getOr, uniqBy } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import React from 'react'; import { Query } from 'react-apollo'; @@ -137,10 +137,10 @@ class TimelineQueryComponent extends QueryTemplate< ...fetchMoreResult.source, Timeline: { ...fetchMoreResult.source.Timeline, - edges: [ + edges: uniqBy('node._id', [ ...prev.source.Timeline.edges, ...fetchMoreResult.source.Timeline.edges, - ], + ]), }, }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx index ef42b5097e36..49a181a1cd89 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx @@ -49,11 +49,11 @@ export const ImportRuleModalComponent = ({ const [overwrite, setOverwrite] = useState(false); const [, dispatchToaster] = useStateToaster(); - const cleanupAndCloseModal = () => { + const cleanupAndCloseModal = useCallback(() => { setIsImporting(false); setSelectedFiles(null); closeModal(); - }; + }, [setIsImporting, setSelectedFiles, closeModal]); const importRulesCallback = useCallback(async () => { if (selectedFiles != null) { diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index 605136a190c5..a8a34383585c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -4,19 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import styled from 'styled-components'; -import useResizeObserver from 'use-resize-observer/polyfilled'; +import { useThrottledResizeObserver } from '../../components/utils'; import { DragDropContextWrapper } from '../../components/drag_and_drop/drag_drop_context_wrapper'; -import { Flyout, flyoutHeaderHeight } from '../../components/flyout'; +import { Flyout } from '../../components/flyout'; import { HeaderGlobal } from '../../components/header_global'; import { HelpMenu } from '../../components/help_menu'; import { LinkToPage } from '../../components/link_to'; import { MlHostConditionalContainer } from '../../components/ml/conditional_links/ml_host_conditional_container'; import { MlNetworkConditionalContainer } from '../../components/ml/conditional_links/ml_network_conditional_container'; -import { StatefulTimeline } from '../../components/timeline'; import { AutoSaveWarningMsg } from '../../components/timeline/auto_save_warning'; import { UseUrlState } from '../../components/url_state'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; @@ -63,11 +62,15 @@ const calculateFlyoutHeight = ({ }): number => Math.max(0, windowHeight - globalHeaderSize); export const HomePage: React.FC = () => { - const { ref: measureRef, height: windowHeight = 0 } = useResizeObserver({}); - const flyoutHeight = calculateFlyoutHeight({ - globalHeaderSize: globalHeaderHeightPx, - windowHeight, - }); + const { ref: measureRef, height: windowHeight = 0 } = useThrottledResizeObserver(); + const flyoutHeight = useMemo( + () => + calculateFlyoutHeight({ + globalHeaderSize: globalHeaderHeightPx, + windowHeight, + }), + [windowHeight] + ); const [showTimeline] = useShowTimeline(); @@ -85,16 +88,9 @@ export const HomePage: React.FC = () => { - - + /> )} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3fdcf9b81593..c155344c7534 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11330,7 +11330,7 @@ "xpack.siem.timeline.expandableEvent.copyToClipboardToolTip": "クリップボードにコピー", "xpack.siem.timeline.expandableEvent.eventToolTipTitle": "イベント", "xpack.siem.timeline.fieldTooltip": "フィールド", - "xpack.siem.timeline.flyout.pane.closeTimelineButtonLabel": "タイムラインを閉じる", + "xpack.siem.timeline.flyout.header.closeTimelineButtonLabel": "タイムラインを閉じる", "xpack.siem.timeline.flyout.pane.removeColumnButtonLabel": "列を削除", "xpack.siem.timeline.flyout.pane.timelinePropertiesAriaLabel": "タイムラインのプロパティ", "xpack.siem.timeline.properties.descriptionPlaceholder": "説明", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1bcbcca055c3..ed0ac8f6f7fe 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11330,7 +11330,7 @@ "xpack.siem.timeline.expandableEvent.copyToClipboardToolTip": "复制到剪贴板", "xpack.siem.timeline.expandableEvent.eventToolTipTitle": "时间", "xpack.siem.timeline.fieldTooltip": "字段", - "xpack.siem.timeline.flyout.pane.closeTimelineButtonLabel": "关闭时间线", + "xpack.siem.timeline.flyout.header.closeTimelineButtonLabel": "关闭时间线", "xpack.siem.timeline.flyout.pane.removeColumnButtonLabel": "删除列", "xpack.siem.timeline.flyout.pane.timelinePropertiesAriaLabel": "时间线属性", "xpack.siem.timeline.properties.descriptionPlaceholder": "描述", diff --git a/yarn.lock b/yarn.lock index dcb360587e4e..1e5c160a7eb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5018,10 +5018,10 @@ dependencies: "@types/react" "*" -"@types/react-beautiful-dnd@^11.0.4": - version "11.0.4" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-11.0.4.tgz#25cdf16864df8fd1d82f9416c8c0fd957e793024" - integrity sha512-a1Nvt1AcSEA962OuXrk1gu5bJQhzu0B3qFNO999/0nmF+oAD7HIAY0DwraS3L3XM1cVuRO1+PtpTkD4CfRK2QA== +"@types/react-beautiful-dnd@^12.1.1": + version "12.1.1" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-12.1.1.tgz#149e638c0f912eee6b74ea419b26bb43d0b1da60" + integrity sha512-CPKynKgGVRK+xmywLMD0qNWamdscxhgf1Um+2oEgN6Qibn1rye3M4p2bdxAMgtOTZ2L81bYl6KGKSzJVboJWeA== dependencies: "@types/react" "*" From eddbdc896ba6ebb548fc01d32b86cf56c2922764 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Tue, 17 Mar 2020 14:02:03 +0300 Subject: [PATCH 036/115] [NP] Get rid of usage redirectWhenMissing service (#59777) * Move redirect_when_missing to kibana utils * Replace redirectWhenMissing in dashboard * Replace redirectWhenMissing in discover * Remove redirect in monitoring * Remove extra import * Move invalid vistype check into editor.js * Mock the history folder * Fix redirect when missing index or saved object * Move history to discover services * Use redirect to listing page Co-authored-by: Elastic Machine --- .../kibana/public/dashboard/legacy_imports.ts | 2 +- .../public/dashboard/np_ready/application.ts | 4 +- .../public/dashboard/np_ready/legacy_app.js | 23 ++++-- .../kibana/public/discover/build_services.ts | 4 + .../public/discover/get_inner_angular.ts | 5 +- .../kibana/public/discover/kibana_services.ts | 2 +- .../discover/np_ready/angular/discover.js | 17 ++-- .../np_ready/angular/discover_state.test.ts | 2 +- .../np_ready/angular/discover_state.ts | 10 +-- .../kibana/public/visualize/legacy_imports.ts | 2 +- .../public/visualize/np_ready/application.ts | 4 +- .../visualize/np_ready/editor/editor.js | 30 +++++-- .../np_ready/editor/visualization.js | 4 +- .../public/visualize/np_ready/legacy_app.js | 35 +++++--- .../index_patterns/index_pattern.test.ts | 2 + .../kibana_utils/public/history/index.ts | 1 + .../public/history/redirect_when_missing.tsx | 80 +++++++++++++++++++ src/plugins/kibana_utils/public/index.ts | 2 +- .../public/np_imports/angular/modules.ts | 4 +- .../public/np_imports/legacy_imports.ts | 2 +- 20 files changed, 179 insertions(+), 56 deletions(-) create mode 100644 src/plugins/kibana_utils/public/history/redirect_when_missing.tsx diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index 0c5329d8b259..b497f73f3df2 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -28,7 +28,7 @@ export { npSetup, npStart } from 'ui/new_platform'; export { KbnUrl } from 'ui/url/kbn_url'; // @ts-ignore -export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url/index'; +export { KbnUrlProvider } from 'ui/url/index'; export { IInjector } from 'ui/chrome'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index fe0e7a1d3e6d..9447b5384d17 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -35,7 +35,6 @@ import { KbnUrlProvider, PrivateProvider, PromiseServiceCreator, - RedirectWhenMissingProvider, } from '../legacy_imports'; // @ts-ignore import { initDashboardApp } from './legacy_app'; @@ -146,8 +145,7 @@ function createLocalIconModule() { function createLocalKbnUrlModule() { angular .module('app/dashboard/KbnUrl', ['app/dashboard/Private', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) - .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } function createLocalConfigModule(core: AppMountContext['core']) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index 35b510894179..f7baba663da7 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -28,6 +28,7 @@ import { initDashboardAppDirective } from './dashboard_app'; import { createDashboardEditUrl, DashboardConstants } from './dashboard_constants'; import { createKbnUrlStateStorage, + redirectWhenMissing, InvalidJSONProperty, SavedObjectNotFound, } from '../../../../../../plugins/kibana_utils/public'; @@ -136,7 +137,7 @@ export function initDashboardApp(app, deps) { }); }, resolve: { - dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { + dash: function($rootScope, $route, kbnUrl, history) { return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl).then(() => { const savedObjectsClient = deps.savedObjectsClient; const title = $route.current.params.title; @@ -171,14 +172,18 @@ export function initDashboardApp(app, deps) { controller: createNewDashboardCtrl, requireUICapability: 'dashboard.createNew', resolve: { - dash: function(redirectWhenMissing, $rootScope, kbnUrl) { + dash: function($rootScope, kbnUrl, history) { return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl) .then(() => { return deps.savedDashboards.get(); }) .catch( redirectWhenMissing({ - dashboard: DashboardConstants.LANDING_PAGE_PATH, + history, + mapping: { + dashboard: DashboardConstants.LANDING_PAGE_PATH, + }, + toastNotifications: deps.core.notifications.toasts, }) ); }, @@ -189,7 +194,7 @@ export function initDashboardApp(app, deps) { template: dashboardTemplate, controller: createNewDashboardCtrl, resolve: { - dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { + dash: function($rootScope, $route, kbnUrl, history) { const id = $route.current.params.id; return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl) @@ -207,7 +212,7 @@ export function initDashboardApp(app, deps) { .catch(error => { // A corrupt dashboard was detected (e.g. with invalid JSON properties) if (error instanceof InvalidJSONProperty) { - deps.toastNotifications.addDanger(error.message); + deps.core.notifications.toasts.addDanger(error.message); kbnUrl.redirect(DashboardConstants.LANDING_PAGE_PATH); return; } @@ -221,7 +226,7 @@ export function initDashboardApp(app, deps) { pathname: DashboardConstants.CREATE_NEW_DASHBOARD_URL, }); - deps.toastNotifications.addWarning( + deps.core.notifications.toasts.addWarning( i18n.translate('kbn.dashboard.urlWasRemovedInSixZeroWarningMessage', { defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', @@ -234,7 +239,11 @@ export function initDashboardApp(app, deps) { }) .catch( redirectWhenMissing({ - dashboard: DashboardConstants.LANDING_PAGE_PATH, + history, + mapping: { + dashboard: DashboardConstants.LANDING_PAGE_PATH, + }, + toastNotifications: deps.core.notifications.toasts, }) ); }, diff --git a/src/legacy/core_plugins/kibana/public/discover/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/build_services.ts index c58307adaf38..282eef0c983e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/build_services.ts @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { createHashHistory, History } from 'history'; + import { Capabilities, ChromeStart, @@ -46,6 +48,7 @@ export interface DiscoverServices { data: DataPublicPluginStart; docLinks: DocLinksStart; docViewsRegistry: DocViewsRegistry; + history: History; theme: ChartsPluginStart['theme']; filterManager: FilterManager; indexPatterns: IndexPatternsContract; @@ -79,6 +82,7 @@ export async function buildServices( data: plugins.data, docLinks: core.docLinks, docViewsRegistry, + history: createHashHistory(), theme: plugins.charts.theme, filterManager: plugins.data.query.filterManager, getSavedSearchById: async (id: string) => savedObjectService.get(id), diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index 76d475c4f7f9..4d871bcb7a85 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -27,7 +27,7 @@ import { CoreStart, LegacyCoreStart, IUiSettingsClient } from 'kibana/public'; // @ts-ignore import { StateManagementConfigProvider } from 'ui/state_management/config_provider'; // @ts-ignore -import { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; +import { KbnUrlProvider } from 'ui/url'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; @@ -173,8 +173,7 @@ export function initializeInnerAngularModule( function createLocalKbnUrlModule() { angular .module('discoverKbnUrl', ['discoverPrivate', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) - .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } function createLocalConfigModule(uiSettings: IUiSettingsClient) { diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 57a9e4966d6d..8202ba13b30c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -59,7 +59,7 @@ export { intervalOptions } from 'ui/agg_types'; export { subscribeWithScope } from '../../../../../plugins/kibana_legacy/public'; // @ts-ignore export { timezoneProvider } from 'ui/vis/lib/timezone'; -export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; +export { unhashUrl, redirectWhenMissing } from '../../../../../plugins/kibana_utils/public'; export { ensureDefaultIndexPattern, formatMsg, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index f3334c9211e4..6978781fe669 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -50,6 +50,7 @@ import { tabifyAggResponse, getAngularModule, ensureDefaultIndexPattern, + redirectWhenMissing, } from '../../kibana_services'; const { @@ -57,6 +58,7 @@ const { chrome, data, docTitle, + history, indexPatterns, filterManager, share, @@ -113,10 +115,10 @@ app.config($routeProvider => { template: indexTemplate, reloadOnSearch: false, resolve: { - savedObjects: function(redirectWhenMissing, $route, kbnUrl, Promise, $rootScope) { + savedObjects: function($route, kbnUrl, Promise, $rootScope) { const savedSearchId = $route.current.params.id; return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl).then(() => { - const { appStateContainer } = getState({}); + const { appStateContainer } = getState({ history }); const { index } = appStateContainer.getState(); return Promise.props({ ip: indexPatterns.getCache().then(indexPatternList => { @@ -151,9 +153,13 @@ app.config($routeProvider => { }) .catch( redirectWhenMissing({ - search: '/discover', - 'index-pattern': - '/management/kibana/objects/savedSearches/' + $route.current.params.id, + history, + mapping: { + search: '/discover', + 'index-pattern': + '/management/kibana/objects/savedSearches/' + $route.current.params.id, + }, + toastNotifications, }) ), }); @@ -207,6 +213,7 @@ function discoverController( } = getState({ defaultAppState: getStateDefaults(), storeInSessionStorage: config.get('state:storeInSessionStorage'), + history, }); if (appStateContainer.getState().index !== $scope.indexPattern.id) { //used index pattern is different than the given by url/state which is invalid diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts index af772cb5c76f..3840fd0c2e3b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts @@ -30,7 +30,7 @@ describe('Test discover state', () => { history.push('/'); state = getState({ defaultAppState: { index: 'test' }, - hashHistory: history, + history, }); await state.replaceUrlAppState({}); await state.startSync(); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts index 10e7cd1d0c49..981855d1ee77 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts @@ -17,7 +17,7 @@ * under the License. */ import { isEqual } from 'lodash'; -import { createHashHistory, History } from 'history'; +import { History } from 'history'; import { createStateContainer, createKbnUrlStateStorage, @@ -65,9 +65,9 @@ interface GetStateParams { */ storeInSessionStorage?: boolean; /** - * Browser history used for testing + * Browser history */ - hashHistory?: History; + history: History; } export interface GetStateReturn { @@ -121,11 +121,11 @@ const APP_STATE_URL_KEY = '_a'; export function getState({ defaultAppState = {}, storeInSessionStorage = false, - hashHistory, + history, }: GetStateParams): GetStateReturn { const stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, - history: hashHistory ? hashHistory : createHashHistory(), + history, }); const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index 0ddf3ee67aa9..69af466a0372 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -25,7 +25,7 @@ */ // @ts-ignore -export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; +export { KbnUrlProvider } from 'ui/url'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; export { wrapInI18nContext } from 'ui/i18n'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts index 8ef63ec5778e..c7c3286bb5c7 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts @@ -24,7 +24,6 @@ import { AppMountContext } from 'kibana/public'; import { configureAppAngularModule, KbnUrlProvider, - RedirectWhenMissingProvider, IPrivate, PrivateProvider, PromiseServiceCreator, @@ -102,8 +101,7 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav function createLocalKbnUrlModule() { angular .module('app/visualize/KbnUrl', ['app/visualize/Private', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) - .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } function createLocalPromiseModule() { diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index c023c402f5fe..1fab38027f65 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -31,6 +31,7 @@ import { getEditBreadcrumbs } from '../breadcrumbs'; import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; import { unhashUrl } from '../../../../../../../plugins/kibana_utils/public'; +import { MarkdownSimple, toMountPoint } from '../../../../../../../plugins/kibana_react/public'; import { addFatalError, kbnBaseUrl } from '../../../../../../../plugins/kibana_legacy/public'; import { SavedObjectSaveModal, @@ -75,7 +76,6 @@ function VisualizeAppController( $injector, $timeout, kbnUrl, - redirectWhenMissing, kbnUrlStateStorage, history ) { @@ -313,16 +313,33 @@ function VisualizeAppController( } ); + const stopAllSyncing = () => { + stopStateSync(); + stopSyncingQueryServiceStateWithUrl(); + stopSyncingAppFilters(); + }; + // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the // defaults applied. If the url was from a previous session which included modifications to the // appState then they won't be equal. if (!_.isEqual(stateContainer.getState().vis, stateDefaults.vis)) { try { vis.setState(stateContainer.getState().vis); - } catch { - redirectWhenMissing({ - 'index-pattern-field': '/visualize', + } catch (error) { + // stop syncing url updtes with the state to prevent extra syncing + stopAllSyncing(); + + toastNotifications.addWarning({ + title: i18n.translate('kbn.visualize.visualizationTypeInvalidNotificationMessage', { + defaultMessage: 'Invalid visualization type', + }), + text: toMountPoint({error.message}), }); + + history.replace(`${VisualizeConstants.LANDING_PAGE_PATH}?notFound=visualization`); + + // prevent further controller execution + return; } } @@ -529,9 +546,8 @@ function VisualizeAppController( unsubscribePersisted(); unsubscribeStateUpdates(); - stopStateSync(); - stopSyncingQueryServiceStateWithUrl(); - stopSyncingAppFilters(); + + stopAllSyncing(); }); $timeout(() => { diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js index 6acdb0abdd0b..c8acea168444 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js @@ -59,7 +59,9 @@ export function initVisualizationDirective(app, deps) { }); $scope.$on('$destroy', () => { - $scope._handler.destroy(); + if ($scope._handler) { + $scope._handler.destroy(); + } }); }, }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js index b9409445166b..1002f401706c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js @@ -21,7 +21,10 @@ import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { createHashHistory } from 'history'; -import { createKbnUrlStateStorage } from '../../../../../../plugins/kibana_utils/public'; +import { + createKbnUrlStateStorage, + redirectWhenMissing, +} from '../../../../../../plugins/kibana_utils/public'; import editorTemplate from './editor/editor.html'; import visualizeListingTemplate from './listing/visualize_listing.html'; @@ -100,8 +103,8 @@ export function initVisualizeApp(app, deps) { template: editorTemplate, k7Breadcrumbs: getCreateBreadcrumbs, resolve: { - savedVis: function(redirectWhenMissing, $route, $rootScope, kbnUrl) { - const { core, data, savedVisualizations, visualizations } = deps; + savedVis: function($route, $rootScope, kbnUrl, history) { + const { core, data, savedVisualizations, visualizations, toastNotifications } = deps; const visTypes = visualizations.all(); const visType = find(visTypes, { name: $route.current.params.type }); const shouldHaveIndex = visType.requiresSearch && visType.options.showIndexSelection; @@ -128,7 +131,9 @@ export function initVisualizeApp(app, deps) { }) .catch( redirectWhenMissing({ - '*': '/visualize', + history, + mapping: VisualizeConstants.LANDING_PAGE_PATH, + toastNotifications, }) ); }, @@ -139,8 +144,8 @@ export function initVisualizeApp(app, deps) { template: editorTemplate, k7Breadcrumbs: getEditBreadcrumbs, resolve: { - savedVis: function(redirectWhenMissing, $route, $rootScope, kbnUrl) { - const { chrome, core, data, savedVisualizations } = deps; + savedVis: function($route, $rootScope, kbnUrl, history) { + const { chrome, core, data, savedVisualizations, toastNotifications } = deps; return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl) .then(() => savedVisualizations.get($route.current.params.id)) .then(savedVis => { @@ -155,13 +160,17 @@ export function initVisualizeApp(app, deps) { }) .catch( redirectWhenMissing({ - visualization: '/visualize', - search: - '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, - 'index-pattern': - '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, - 'index-pattern-field': - '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + history, + mapping: { + visualization: VisualizeConstants.LANDING_PAGE_PATH, + search: + '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + 'index-pattern': + '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + 'index-pattern-field': + '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + }, + toastNotifications, }) ); }, diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts index b6ca91169a93..305aa8575e4d 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts @@ -18,6 +18,8 @@ */ import { defaults, pluck, last, get } from 'lodash'; + +jest.mock('../../../../kibana_utils/public/history'); import { IndexPattern } from './index_pattern'; import { DuplicateField } from '../../../../kibana_utils/public'; diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts index b4b5658c1c88..bb13ea09f928 100644 --- a/src/plugins/kibana_utils/public/history/index.ts +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -18,3 +18,4 @@ */ export { removeQueryParam } from './remove_query_param'; +export { redirectWhenMissing } from './redirect_when_missing'; diff --git a/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx b/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx new file mode 100644 index 000000000000..cbdeef6fbe96 --- /dev/null +++ b/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx @@ -0,0 +1,80 @@ +/* + * 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 { History } from 'history'; +import { i18n } from '@kbn/i18n'; + +import { ToastsSetup } from 'kibana/public'; +import { MarkdownSimple, toMountPoint } from '../../../kibana_react/public'; +import { SavedObjectNotFound } from '../errors'; + +interface Mapping { + [key: string]: string; +} + +/** + * Creates an error handler that will redirect to a url when a SavedObjectNotFound + * error is thrown + */ +export function redirectWhenMissing({ + history, + mapping, + toastNotifications, +}: { + history: History; + /** + * a mapping of url's to redirect to based on the saved object that + * couldn't be found, or just a string that will be used for all types + */ + mapping: string | Mapping; + /** + * Toast notifications service to show toasts in error cases. + */ + toastNotifications: ToastsSetup; +}) { + let localMappingObject: Mapping; + + if (typeof mapping === 'string') { + localMappingObject = { '*': mapping }; + } else { + localMappingObject = mapping; + } + + return (error: SavedObjectNotFound) => { + // if this error is not "404", rethrow + // we can't check "error instanceof SavedObjectNotFound" since this class can live in a separate bundle + // and the error will be an instance of other class with the same interface (actually the copy of SavedObjectNotFound class) + if (!error.savedObjectType) { + throw error; + } + + let url = localMappingObject[error.savedObjectType] || localMappingObject['*'] || '/'; + url += (url.indexOf('?') >= 0 ? '&' : '?') + `notFound=${error.savedObjectType}`; + + toastNotifications.addWarning({ + title: i18n.translate('kibana_utils.history.savedObjectIsMissingNotificationMessage', { + defaultMessage: 'Saved object is missing', + }), + text: toMountPoint({error.message}), + }); + + history.replace(url); + }; +} diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index ee38d5e8111c..47f90cbe2a62 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -73,5 +73,5 @@ export { StartSyncStateFnType, StopSyncStateFnType, } from './state_sync'; -export { removeQueryParam } from './history'; +export { removeQueryParam, redirectWhenMissing } from './history'; export { applyDiff } from './state_management/utils/diff_object'; diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts index c14b64a32fb5..b506784bf15e 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts +++ b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts @@ -19,7 +19,6 @@ import { StateManagementConfigProvider, AppStateProvider, KbnUrlProvider, - RedirectWhenMissingProvider, npStart, } from '../legacy_imports'; @@ -79,8 +78,7 @@ function createLocalStateModule() { function createLocalKbnUrlModule() { angular .module('monitoring/KbnUrl', ['monitoring/Private', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) - .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } function createLocalConfigModule(core: AppMountContext['core']) { diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts index a2ebe8231456..208b7e2acdb0 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts +++ b/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts @@ -18,5 +18,5 @@ export { AppStateProvider } from 'ui/state_management/app_state'; // @ts-ignore export { EventsProvider } from 'ui/events'; // @ts-ignore -export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; +export { KbnUrlProvider } from 'ui/url'; export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router'; From a755e55907ef0b084c850814809e77df650d07bc Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 17 Mar 2020 12:16:11 +0100 Subject: [PATCH 037/115] Fix import to timefilter from in TSVB (#60296) --- .../vis_type_timeseries/public/components/vis_editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index 0263f5b2c851..ff2546f75c51 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -50,7 +50,7 @@ export class VisEditor extends Component { visFields: props.visFields, extractedIndexPatterns: [''], }; - this.onBrush = createBrushHandler(getDataStart().query.timefilter); + this.onBrush = createBrushHandler(getDataStart().query.timefilter.timefilter); this.visDataSubject = new Rx.BehaviorSubject(this.props.visData); this.visData$ = this.visDataSubject.asObservable().pipe(share()); From e25430ba36bb450a3e93bf788258fb732bdad63b Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 17 Mar 2020 12:22:45 +0100 Subject: [PATCH 038/115] [TSVB] fix text color when using custom background color (#60261) When the user apply a background color manually from the UI, this commit adapt the current colors to have a better contrast with the chosen background color irrespective of the used dark/light theme --- package.json | 1 + .../components/vis_types/_vis_types.scss | 17 +++ .../components/vis_types/timeseries/vis.js | 7 +- .../timeseries/__mocks__/@elastic/charts.js | 2 + .../visualizations/views/timeseries/index.js | 19 ++- .../views/timeseries/utils/theme.test.ts | 44 ++++++ .../views/timeseries/utils/theme.ts | 139 ++++++++++++++++++ 7 files changed, 220 insertions(+), 9 deletions(-) create mode 100644 src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.test.ts create mode 100644 src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts diff --git a/package.json b/package.json index b3dcfb2aa3b0..261b3ad74d9b 100644 --- a/package.json +++ b/package.json @@ -314,6 +314,7 @@ "@types/cheerio": "^0.22.10", "@types/chromedriver": "^2.38.0", "@types/classnames": "^2.2.9", + "@types/color": "^3.0.0", "@types/d3": "^3.5.43", "@types/dedent": "^0.7.0", "@types/deep-freeze-strict": "^1.1.0", diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss index 90c2007b1c94..3db09bace079 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss @@ -7,4 +7,21 @@ .tvbVisTimeSeries { overflow: hidden; } + .tvbVisTimeSeriesDark { + .echReactiveChart_unavailable { + color: #DFE5EF; + } + .echLegendItem { + color: #DFE5EF; + } + } + .tvbVisTimeSeriesLight { + .echReactiveChart_unavailable { + color: #343741; + } + .echLegendItem { + color: #343741; + } + } } + diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js index 954d3d174bb8..356ba08ac242 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js @@ -33,9 +33,8 @@ import { getAxisLabelString } from '../../lib/get_axis_label_string'; import { getInterval } from '../../lib/get_interval'; import { areFieldsDifferent } from '../../lib/charts'; import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; -import { isBackgroundDark } from '../../../lib/set_is_reversed'; import { STACKED_OPTIONS } from '../../../visualizations/constants'; -import { getCoreStart } from '../../../services'; +import { getCoreStart, getUISettings } from '../../../services'; export class TimeseriesVisualization extends Component { static propTypes = { @@ -238,6 +237,7 @@ export class TimeseriesVisualization extends Component { } }); + const darkMode = getUISettings().get('theme:darkMode'); return (
null; export const AreaSeries = () => null; + +export { LIGHT_THEME, DARK_THEME } from '@elastic/charts'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js index 986111b462b3..75554a476bde 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js @@ -19,14 +19,13 @@ import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { Axis, Chart, Position, Settings, - DARK_THEME, - LIGHT_THEME, AnnotationDomainTypes, LineAnnotation, TooltipType, @@ -40,6 +39,7 @@ import { GRID_LINE_CONFIG, ICON_TYPES_MAP, STACKED_OPTIONS } from '../../constan import { AreaSeriesDecorator } from './decorators/area_decorator'; import { BarSeriesDecorator } from './decorators/bar_decorator'; import { getStackAccessors } from './utils/stack_format'; +import { getTheme, getChartClasses } from './utils/theme'; const generateAnnotationData = (values, formatter) => values.map(({ key, docs }) => ({ @@ -57,7 +57,8 @@ const handleCursorUpdate = cursor => { }; export const TimeSeries = ({ - isDarkMode, + darkMode, + backgroundColor, showGrid, legend, legendPosition, @@ -89,8 +90,13 @@ export const TimeSeries = ({ const timeZone = timezoneProvider(uiSettings)(); const hasBarChart = series.some(({ bars }) => bars.show); + // compute the theme based on the bg color + const theme = getTheme(darkMode, backgroundColor); + // apply legend style change if bgColor is configured + const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor)); + return ( - + { + it('should return the basic themes if no bg color is specified', () => { + // use original dark/light theme + expect(getTheme(false)).toEqual(LIGHT_THEME); + expect(getTheme(true)).toEqual(DARK_THEME); + + // discard any wrong/missing bg color + expect(getTheme(true, null)).toEqual(DARK_THEME); + expect(getTheme(true, '')).toEqual(DARK_THEME); + expect(getTheme(true, undefined)).toEqual(DARK_THEME); + }); + it('should return a highcontrast color theme for a different background', () => { + // red use a near full-black color + expect(getTheme(false, 'red').axes.axisTitleStyle.fill).toEqual('rgb(23,23,23)'); + + // violet increased the text color to full white for higer contrast + expect(getTheme(false, '#ba26ff').axes.axisTitleStyle.fill).toEqual('rgb(255,255,255)'); + + // light yellow, prefer the LIGHT_THEME fill color because already with a good contrast + expect(getTheme(false, '#fff49f').axes.axisTitleStyle.fill).toEqual('#333'); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts new file mode 100644 index 000000000000..a25d5e1ce1d3 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts @@ -0,0 +1,139 @@ +/* + * 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 colorJS from 'color'; +import { Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts'; + +function computeRelativeLuminosity(rgb: string) { + return colorJS(rgb).luminosity(); +} + +function computeContrast(rgb1: string, rgb2: string) { + return colorJS(rgb1).contrast(colorJS(rgb2)); +} + +function getAAARelativeLum(bgColor: string, fgColor: string, ratio = 7) { + const relLum1 = computeRelativeLuminosity(bgColor); + const relLum2 = computeRelativeLuminosity(fgColor); + if (relLum1 > relLum2) { + // relLum1 is brighter, relLum2 is darker + return (relLum1 + 0.05 - ratio * 0.05) / ratio; + } else { + // relLum1 is darker, relLum2 is brighter + return Math.min(ratio * (relLum1 + 0.05) - 0.05, 1); + } +} + +function getGrayFromRelLum(relLum: number) { + if (relLum <= 0.0031308) { + return relLum * 12.92; + } else { + return (1.0 + 0.055) * Math.pow(relLum, 1.0 / 2.4) - 0.055; + } +} + +function getGrayRGBfromGray(gray: number) { + const g = Math.round(gray * 255); + return `rgb(${g},${g},${g})`; +} + +function getAAAGray(bgColor: string, fgColor: string, ratio = 7) { + const relLum = getAAARelativeLum(bgColor, fgColor, ratio); + const gray = getGrayFromRelLum(relLum); + return getGrayRGBfromGray(gray); +} + +function findBestContrastColor( + bgColor: string, + lightFgColor: string, + darkFgColor: string, + ratio = 4.5 +) { + const lc = computeContrast(bgColor, lightFgColor); + const dc = computeContrast(bgColor, darkFgColor); + if (lc >= dc) { + if (lc >= ratio) { + return lightFgColor; + } + return getAAAGray(bgColor, lightFgColor, ratio); + } + if (dc >= ratio) { + return darkFgColor; + } + return getAAAGray(bgColor, darkFgColor, ratio); +} + +function isValidColor(color: string | null | undefined): color is string { + if (typeof color !== 'string') { + return false; + } + if (color.length === 0) { + return false; + } + try { + colorJS(color); + return true; + } catch { + return false; + } +} + +export function getTheme(darkMode: boolean, bgColor?: string | null): Theme { + if (!isValidColor(bgColor)) { + return darkMode ? DARK_THEME : LIGHT_THEME; + } + + const bgLuminosity = computeRelativeLuminosity(bgColor); + const mainTheme = bgLuminosity <= 0.179 ? DARK_THEME : LIGHT_THEME; + const color = findBestContrastColor( + bgColor, + LIGHT_THEME.axes.axisTitleStyle.fill, + DARK_THEME.axes.axisTitleStyle.fill + ); + return { + ...mainTheme, + axes: { + ...mainTheme.axes, + axisTitleStyle: { + ...mainTheme.axes.axisTitleStyle, + fill: color, + }, + tickLabelStyle: { + ...mainTheme.axes.tickLabelStyle, + fill: color, + }, + axisLineStyle: { + ...mainTheme.axes.axisLineStyle, + stroke: color, + }, + tickLineStyle: { + ...mainTheme.axes.tickLineStyle, + stroke: color, + }, + }, + }; +} + +export function getChartClasses(bgColor?: string) { + // keep the original theme color if no bg color is specified + if (typeof bgColor !== 'string') { + return; + } + const bgLuminosity = computeRelativeLuminosity(bgColor); + return bgLuminosity <= 0.179 ? 'tvbVisTimeSeriesDark' : 'tvbVisTimeSeriesLight'; +} From dd680c790cf974ae9e48364c770485302c6eafa9 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 17 Mar 2020 13:34:33 +0100 Subject: [PATCH 039/115] [ML] Adds the class_assignment_objective to classification (#60358) * [ML] add maximize_minimum_recall to classification analysis * [ML] fix mutation of the original job during the cloning --- x-pack/plugins/ml/common/types/common.ts | 12 +++++ .../analytics_list/action_clone.tsx | 44 ++++++++++++------- .../components/analytics_list/actions.tsx | 3 +- .../use_create_analytics_form/actions.ts | 5 ++- .../hooks/use_create_analytics_form/state.ts | 6 +-- .../use_create_analytics_form.ts | 3 +- 6 files changed, 49 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/ml/common/types/common.ts b/x-pack/plugins/ml/common/types/common.ts index 3f3493863e0f..691b3e9eb116 100644 --- a/x-pack/plugins/ml/common/types/common.ts +++ b/x-pack/plugins/ml/common/types/common.ts @@ -19,3 +19,15 @@ export function dictionaryToArray(dict: Dictionary): TValue[] { export type DeepPartial = { [P in keyof T]?: DeepPartial; }; + +export type DeepReadonly = T extends Array + ? ReadonlyArray> + : T extends Function + ? T + : T extends object + ? DeepReadonlyObject + : T; + +type DeepReadonlyObject = { + readonly [P in keyof T]: DeepReadonly; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index 7199453a15d7..3a0f98fc5aca 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -6,8 +6,9 @@ import { EuiButtonEmpty } from '@elastic/eui'; import React, { FC } from 'react'; -import { isEqual } from 'lodash'; +import { isEqual, cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; @@ -97,6 +98,10 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo num_top_feature_importance_values: { optional: true, }, + class_assignment_objective: { + optional: true, + defaultValue: 'maximize_minimum_recall', + }, }, } : {}), @@ -257,20 +262,25 @@ export type CloneDataFrameAnalyticsConfig = Omit< 'id' | 'version' | 'create_time' >; -export function extractCloningConfig( - originalConfig: DataFrameAnalyticsConfig -): CloneDataFrameAnalyticsConfig { - const { - // Omit non-relevant props from the configuration - id, - version, - create_time, - ...cloneConfig - } = originalConfig; - - // Reset the destination index - cloneConfig.dest.index = ''; - return cloneConfig; +/** + * Gets complete original configuration as an input + * and returns the config for cloning omitting + * non-relevant parameters and resetting the destination index. + */ +export function extractCloningConfig({ + id, + version, + create_time, + ...configToClone +}: DeepReadonly): CloneDataFrameAnalyticsConfig { + return (cloneDeep({ + ...configToClone, + dest: { + ...configToClone.dest, + // Reset the destination index + index: '', + }, + }) as unknown) as CloneDataFrameAnalyticsConfig; } export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { @@ -280,7 +290,7 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { const { actions } = createAnalyticsForm; - const onClick = async (item: DataFrameAnalyticsListRow) => { + const onClick = async (item: DeepReadonly) => { await actions.setJobClone(item.config); }; @@ -294,7 +304,7 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { } interface CloneActionProps { - item: DataFrameAnalyticsListRow; + item: DeepReadonly; createAnalyticsForm: CreateAnalyticsFormProps; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 0436bcfc3684..425e3bc903d0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission, @@ -107,7 +108,7 @@ export const getActions = (createAnalyticsForm: CreateAnalyticsFormProps) => { }, }, { - render: (item: DataFrameAnalyticsListRow) => { + render: (item: DeepReadonly) => { return ; }, }, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts index 8cedc38b1b59..66e4103f5bb4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig } from '../../../../common'; import { FormMessage, State, SourceIndexMap } from './state'; @@ -64,7 +65,7 @@ export type Action = | { type: ACTION.SET_JOB_CONFIG; payload: State['jobConfig'] } | { type: ACTION.SET_JOB_IDS; jobIds: State['jobIds'] } | { type: ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT; value: State['estimatedModelMemoryLimit'] } - | { type: ACTION.SET_JOB_CLONE; cloneJob: DataFrameAnalyticsConfig }; + | { type: ACTION.SET_JOB_CLONE; cloneJob: DeepReadonly }; // Actions wrapping the dispatcher exposed by the custom hook export interface ActionDispatchers { @@ -79,5 +80,5 @@ export interface ActionDispatchers { startAnalyticsJob: () => void; switchToAdvancedEditor: () => void; setEstimatedModelMemoryLimit: (value: State['estimatedModelMemoryLimit']) => void; - setJobClone: (cloneJob: DataFrameAnalyticsConfig) => Promise; + setJobClone: (cloneJob: DeepReadonly) => Promise; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 515e0e42bd87..719bb6c5b07c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -5,7 +5,7 @@ */ import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { DeepPartial } from '../../../../../../../common/types/common'; +import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission } from '../../../../../privilege/check_privilege'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; @@ -91,7 +91,7 @@ export interface State { jobIds: DataFrameAnalyticsId[]; requestMessages: FormMessage[]; estimatedModelMemoryLimit: string; - cloneJob?: DataFrameAnalyticsConfig; + cloneJob?: DeepReadonly; } export const getInitialState = (): State => ({ @@ -195,7 +195,7 @@ export const getJobConfigFromFormState = ( * For cloning we keep job id and destination index empty. */ export function getCloneFormStateFromJobConfig( - analyticsJobConfig: CloneDataFrameAnalyticsConfig + analyticsJobConfig: Readonly ): Partial { const jobType = Object.keys(analyticsJobConfig.analysis)[0] as ANALYSIS_CONFIG_TYPE; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 3b6b8538c2ff..86c43b232738 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { SimpleSavedObject } from 'kibana/public'; import { isErrorResponse } from '../../../../../../../common/types/errors'; +import { DeepReadonly } from '../../../../../../../common/types/common'; import { ml } from '../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../contexts/ml'; @@ -313,7 +314,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { dispatch({ type: ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT, value }); }; - const setJobClone = async (cloneJob: DataFrameAnalyticsConfig) => { + const setJobClone = async (cloneJob: DeepReadonly) => { resetForm(); await prepareFormValidation(); From 9c3c2a237278fc254bb59845cb5af155229128c2 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 17 Mar 2020 09:39:59 -0400 Subject: [PATCH 040/115] Changing default type to start and allowing it to be configured by the event category (#60323) --- .../endpoint/common/generate_data.test.ts | 6 +++-- .../plugins/endpoint/common/generate_data.ts | 24 ++++++++++++++++--- x-pack/plugins/endpoint/common/types.ts | 1 + 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/endpoint/common/generate_data.test.ts b/x-pack/plugins/endpoint/common/generate_data.test.ts index e14f506c825f..a687d7af1c59 100644 --- a/x-pack/plugins/endpoint/common/generate_data.test.ts +++ b/x-pack/plugins/endpoint/common/generate_data.test.ts @@ -62,10 +62,11 @@ describe('data generator', () => { expect(processEvent['@timestamp']).toEqual(timestamp); expect(processEvent.event.category).toEqual('process'); expect(processEvent.event.kind).toEqual('event'); - expect(processEvent.event.type).toEqual('creation'); + expect(processEvent.event.type).toEqual('start'); expect(processEvent.agent).not.toBeNull(); expect(processEvent.host).not.toBeNull(); expect(processEvent.process.entity_id).not.toBeNull(); + expect(processEvent.process.name).not.toBeNull(); }); it('creates other event documents', () => { @@ -74,10 +75,11 @@ describe('data generator', () => { expect(processEvent['@timestamp']).toEqual(timestamp); expect(processEvent.event.category).toEqual('dns'); expect(processEvent.event.kind).toEqual('event'); - expect(processEvent.event.type).toEqual('creation'); + expect(processEvent.event.type).toEqual('start'); expect(processEvent.agent).not.toBeNull(); expect(processEvent.host).not.toBeNull(); expect(processEvent.process.entity_id).not.toBeNull(); + expect(processEvent.process.name).not.toBeNull(); }); describe('creates alert ancestor tree', () => { diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index b539e309d76f..36896e5af681 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -16,6 +16,7 @@ interface EventOptions { parentEntityID?: string; eventType?: string; eventCategory?: string; + processName?: string; } const Windows: OSFields[] = [ @@ -64,8 +65,22 @@ const POLICIES: Array<{ name: string; id: string }> = [ const FILE_OPERATIONS: string[] = ['creation', 'open', 'rename', 'execution', 'deletion']; +interface EventInfo { + category: string; + /** + * This denotes the `event.type` field for when an event is created, this can be `start` or `creation` + */ + creationType: string; +} + // These are from the v1 schemas and aren't all valid ECS event categories, still in flux -const OTHER_EVENT_CATEGORIES: string[] = ['driver', 'file', 'library', 'network', 'registry']; +const OTHER_EVENT_CATEGORIES: EventInfo[] = [ + { category: 'driver', creationType: 'start' }, + { category: 'file', creationType: 'creation' }, + { category: 'library', creationType: 'start' }, + { category: 'network', creationType: 'start' }, + { category: 'registry', creationType: 'creation' }, +]; interface HostInfo { agent: { @@ -240,13 +255,14 @@ export class EndpointDocGenerator { event: { category: options.eventCategory ? options.eventCategory : 'process', kind: 'event', - type: options.eventType ? options.eventType : 'creation', + type: options.eventType ? options.eventType : 'start', id: this.seededUUIDv4(), }, host: this.commonInfo.host, process: { entity_id: options.entityID ? options.entityID : this.randomString(10), parent: options.parentEntityID ? { entity_id: options.parentEntityID } : undefined, + name: options.processName ? options.processName : 'powershell.exe', }, }; } @@ -352,12 +368,14 @@ export class EndpointDocGenerator { const ts = node['@timestamp'] + 1000; const relatedEvents: EndpointEvent[] = []; for (let i = 0; i < numRelatedEvents; i++) { + const eventInfo = this.randomChoice(OTHER_EVENT_CATEGORIES); relatedEvents.push( this.generateEvent({ timestamp: ts, entityID: node.process.entity_id, parentEntityID: node.process.parent?.entity_id, - eventCategory: this.randomChoice(OTHER_EVENT_CATEGORIES), + eventCategory: eventInfo.category, + eventType: eventInfo.creationType, }) ); } diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 1cb000b0a035..aa326c663965 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -325,6 +325,7 @@ export interface EndpointEvent { }; process: { entity_id: string; + name: string; parent?: { entity_id: string; }; From caed9ba5ac9798762e07d236a55226dce355b386 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 17 Mar 2020 09:57:52 -0400 Subject: [PATCH 041/115] [Lens] Simplify state management from visualization (#58279) * [Lens] Declarative right panel * Fix memoized operations * Add error checking * Fix dimension panel tests * More updates * Fix all editor frame tests * Fix jest tests * Fix bug with removing dimension * Update tests * Fix frame tests * Fix all tests I could find * Remove debugger * Style config panels * Update i18n * Fix dashboard test * Fix bug when switching index patterns --- .../plugins/lens/public/_config_panel.scss | 21 - .../visualization.test.tsx | 193 +- .../datatable_visualization/visualization.tsx | 123 +- .../editor_frame/_config_panel_wrapper.scss | 50 + .../editor_frame/chart_switch.test.tsx | 2 +- .../editor_frame/config_panel_wrapper.tsx | 502 ++++- .../editor_frame/editor_frame.test.tsx | 106 +- .../editor_frame/editor_frame.tsx | 132 +- .../editor_frame/frame_layout.tsx | 27 +- .../editor_frame/index.scss | 1 + .../editor_frame/save.test.ts | 4 +- .../editor_frame/suggestion_helpers.test.ts | 8 +- .../editor_frame/suggestion_panel.test.tsx | 2 +- .../editor_frame/suggestion_panel.tsx | 1 - .../editor_frame/workspace_panel.test.tsx | 4 +- .../public/editor_frame_service/mocks.tsx | 29 +- .../editor_frame_service/service.test.tsx | 4 +- x-pack/legacy/plugins/lens/public/index.scss | 2 - .../dimension_panel/_dimension_panel.scss | 9 - .../dimension_panel/_index.scss | 1 - .../dimension_panel/_popover.scss | 29 +- .../dimension_panel/dimension_panel.test.tsx | 1838 ++++++++--------- .../dimension_panel/dimension_panel.tsx | 320 +-- .../dimension_panel/popover_editor.tsx | 423 ++-- .../indexpattern.test.ts | 1 - .../indexpattern_datasource/indexpattern.tsx | 168 +- .../layerpanel.test.tsx | 1 + .../indexpattern_datasource/layerpanel.tsx | 3 +- .../metric_config_panel.test.tsx | 69 - .../metric_config_panel.tsx | 39 - .../metric_expression.tsx | 5 + .../metric_visualization.test.ts | 49 +- .../metric_visualization.tsx | 46 +- .../lens/public/metric_visualization/types.ts | 2 +- .../lens/public/multi_column_editor/index.ts | 7 - .../multi_column_editor.test.tsx | 71 - .../multi_column_editor.tsx | 63 - x-pack/legacy/plugins/lens/public/plugin.tsx | 2 +- x-pack/legacy/plugins/lens/public/types.ts | 130 +- ...est.ts.snap => to_expression.test.ts.snap} | 2 +- .../xy_visualization/to_expression.test.ts | 133 ++ .../public/xy_visualization/to_expression.ts | 199 +- .../lens/public/xy_visualization/types.ts | 4 +- .../xy_visualization/xy_config_panel.test.tsx | 156 +- .../xy_visualization/xy_config_panel.tsx | 104 +- .../public/xy_visualization/xy_expression.tsx | 4 +- .../xy_visualization/xy_suggestions.test.ts | 71 +- .../public/xy_visualization/xy_suggestions.ts | 3 +- .../xy_visualization/xy_visualization.test.ts | 195 +- .../xy_visualization/xy_visualization.tsx | 105 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../dashboard_mode/dashboard_empty_screen.js | 6 +- .../test/functional/apps/lens/smokescreen.ts | 6 +- 54 files changed, 2777 insertions(+), 2700 deletions(-) delete mode 100644 x-pack/legacy/plugins/lens/public/_config_panel.scss create mode 100644 x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss delete mode 100644 x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss delete mode 100644 x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx delete mode 100644 x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx delete mode 100644 x-pack/legacy/plugins/lens/public/multi_column_editor/index.ts delete mode 100644 x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx delete mode 100644 x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx rename x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/{xy_visualization.test.ts.snap => to_expression.test.ts.snap} (96%) create mode 100644 x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts diff --git a/x-pack/legacy/plugins/lens/public/_config_panel.scss b/x-pack/legacy/plugins/lens/public/_config_panel.scss deleted file mode 100644 index 5c6d25bf1081..000000000000 --- a/x-pack/legacy/plugins/lens/public/_config_panel.scss +++ /dev/null @@ -1,21 +0,0 @@ -.lnsConfigPanel__panel { - margin-bottom: $euiSizeS; -} - -.lnsConfigPanel__axis { - background: $euiColorLightestShade; - padding: $euiSizeS; - border-radius: $euiBorderRadius; - - // Add margin to the top of the next same panel - & + & { - margin-top: $euiSizeS; - } -} - -.lnsConfigPanel__addLayerBtn { - color: transparentize($euiColorMediumShade, .3); - // sass-lint:disable-block no-important - box-shadow: none !important; - border: 1px dashed currentColor; -} diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx index 0cba22170df1..e18190b6c2d6 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -4,18 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { createMockDatasource } from '../editor_frame_service/mocks'; -import { - DatatableVisualizationState, - datatableVisualization, - DataTableLayer, -} from './visualization'; -import { mount } from 'enzyme'; +import { DatatableVisualizationState, datatableVisualization } from './visualization'; import { Operation, DataType, FramePublicAPI, TableSuggestionColumn } from '../types'; -import { generateId } from '../id_generator'; - -jest.mock('../id_generator'); function mockFrame(): FramePublicAPI { return { @@ -34,12 +25,11 @@ function mockFrame(): FramePublicAPI { describe('Datatable Visualization', () => { describe('#initialize', () => { it('should initialize from the empty state', () => { - (generateId as jest.Mock).mockReturnValueOnce('id'); expect(datatableVisualization.initialize(mockFrame(), undefined)).toEqual({ layers: [ { layerId: 'aaa', - columns: ['id'], + columns: [], }, ], }); @@ -88,7 +78,6 @@ describe('Datatable Visualization', () => { describe('#clearLayer', () => { it('should reset the layer', () => { - (generateId as jest.Mock).mockReturnValueOnce('testid'); const state: DatatableVisualizationState = { layers: [ { @@ -101,7 +90,7 @@ describe('Datatable Visualization', () => { layers: [ { layerId: 'baz', - columns: ['testid'], + columns: [], }, ], }); @@ -214,29 +203,35 @@ describe('Datatable Visualization', () => { }); }); - describe('DataTableLayer', () => { - it('allows all kinds of operations', () => { - const setState = jest.fn(); - const datasource = createMockDatasource(); - const layer = { layerId: 'a', columns: ['b', 'c'] }; + describe('#getConfiguration', () => { + it('returns a single layer option', () => { + const datasource = createMockDatasource('test'); const frame = mockFrame(); - frame.datasourceLayers = { a: datasource.publicAPIMock }; + frame.datasourceLayers = { first: datasource.publicAPIMock }; - mount( - {} }} - frame={frame} - layer={layer} - setState={setState} - state={{ layers: [layer] }} - /> - ); + expect( + datatableVisualization.getConfiguration({ + layerId: 'first', + state: { + layers: [{ layerId: 'first', columns: [] }], + }, + frame, + }).groups + ).toHaveLength(1); + }); - expect(datasource.publicAPIMock.renderDimensionPanel).toHaveBeenCalled(); + it('allows all kinds of operations', () => { + const datasource = createMockDatasource('test'); + const frame = mockFrame(); + frame.datasourceLayers = { first: datasource.publicAPIMock }; - const filterOperations = - datasource.publicAPIMock.renderDimensionPanel.mock.calls[0][1].filterOperations; + const filterOperations = datatableVisualization.getConfiguration({ + layerId: 'first', + state: { + layers: [{ layerId: 'first', columns: [] }], + }, + frame, + }).groups[0].filterOperations; const baseOperation: Operation = { dataType: 'string', @@ -253,108 +248,80 @@ describe('Datatable Visualization', () => { ); }); - it('allows columns to be removed', () => { - const setState = jest.fn(); - const datasource = createMockDatasource(); + it('reorders the rendered colums based on the order from the datasource', () => { + const datasource = createMockDatasource('test'); const layer = { layerId: 'a', columns: ['b', 'c'] }; const frame = mockFrame(); frame.datasourceLayers = { a: datasource.publicAPIMock }; - const component = mount( - {} }} - frame={frame} - layer={layer} - setState={setState} - state={{ layers: [layer] }} - /> - ); - - const onRemove = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('onRemove') as (k: string) => {}; - - onRemove('b'); + datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); + + expect( + datatableVisualization.getConfiguration({ + layerId: 'a', + state: { layers: [layer] }, + frame, + }).groups[0].accessors + ).toEqual(['c', 'b']); + }); + }); - expect(setState).toHaveBeenCalledWith({ + describe('#removeDimension', () => { + it('allows columns to be removed', () => { + const layer = { layerId: 'layer1', columns: ['b', 'c'] }; + expect( + datatableVisualization.removeDimension({ + prevState: { layers: [layer] }, + layerId: 'layer1', + columnId: 'b', + }) + ).toEqual({ layers: [ { - layerId: 'a', + layerId: 'layer1', columns: ['c'], }, ], }); }); + }); + describe('#setDimension', () => { it('allows columns to be added', () => { - (generateId as jest.Mock).mockReturnValueOnce('d'); - const setState = jest.fn(); - const datasource = createMockDatasource(); - const layer = { layerId: 'a', columns: ['b', 'c'] }; - const frame = mockFrame(); - frame.datasourceLayers = { a: datasource.publicAPIMock }; - const component = mount( - {} }} - frame={frame} - layer={layer} - setState={setState} - state={{ layers: [layer] }} - /> - ); - - const onAdd = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('onAdd') as () => {}; - - onAdd(); - - expect(setState).toHaveBeenCalledWith({ + const layer = { layerId: 'layer1', columns: ['b', 'c'] }; + expect( + datatableVisualization.setDimension({ + prevState: { layers: [layer] }, + layerId: 'layer1', + columnId: 'd', + groupId: '', + }) + ).toEqual({ layers: [ { - layerId: 'a', + layerId: 'layer1', columns: ['b', 'c', 'd'], }, ], }); }); - it('reorders the rendered colums based on the order from the datasource', () => { - const datasource = createMockDatasource(); - const layer = { layerId: 'a', columns: ['b', 'c'] }; - const frame = mockFrame(); - frame.datasourceLayers = { a: datasource.publicAPIMock }; - const component = mount( - {} }} - frame={frame} - layer={layer} - setState={jest.fn()} - state={{ layers: [layer] }} - /> - ); - - const accessors = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('accessors') as string[]; - - expect(accessors).toEqual(['b', 'c']); - - component.setProps({ - layer: { layerId: 'a', columns: ['c', 'b'] }, + it('does not set a duplicate dimension', () => { + const layer = { layerId: 'layer1', columns: ['b', 'c'] }; + expect( + datatableVisualization.setDimension({ + prevState: { layers: [layer] }, + layerId: 'layer1', + columnId: 'b', + groupId: '', + }) + ).toEqual({ + layers: [ + { + layerId: 'layer1', + columns: ['b', 'c'], + }, + ], }); - - const newAccessors = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('accessors') as string[]; - - expect(newAccessors).toEqual(['c', 'b']); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx index 79a018635134..4248d722d554 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx @@ -4,20 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { render } from 'react-dom'; -import { EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; -import { MultiColumnEditor } from '../multi_column_editor'; -import { - SuggestionRequest, - Visualization, - VisualizationLayerConfigProps, - VisualizationSuggestion, - Operation, -} from '../types'; -import { generateId } from '../id_generator'; +import { SuggestionRequest, Visualization, VisualizationSuggestion, Operation } from '../types'; import chartTableSVG from '../assets/chart_datatable.svg'; export interface LayerState { @@ -32,58 +20,10 @@ export interface DatatableVisualizationState { function newLayerState(layerId: string): LayerState { return { layerId, - columns: [generateId()], + columns: [], }; } -function updateColumns( - state: DatatableVisualizationState, - layer: LayerState, - fn: (columns: string[]) => string[] -) { - const columns = fn(layer.columns); - const updatedLayer = { ...layer, columns }; - const layers = state.layers.map(l => (l.layerId === layer.layerId ? updatedLayer : l)); - return { ...state, layers }; -} - -const allOperations = () => true; - -export function DataTableLayer({ - layer, - frame, - state, - setState, - dragDropContext, -}: { layer: LayerState } & VisualizationLayerConfigProps) { - const datasource = frame.datasourceLayers[layer.layerId]; - - const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); - // When we add a column it could be empty, and therefore have no order - const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); - - return ( - - setState(updateColumns(state, layer, columns => [...columns, generateId()]))} - onRemove={column => - setState(updateColumns(state, layer, columns => columns.filter(c => c !== column))) - } - testSubj="datatable_columns" - data-test-subj="datatable_multicolumnEditor" - /> - - ); -} - export const datatableVisualization: Visualization< DatatableVisualizationState, DatatableVisualizationState @@ -188,17 +128,56 @@ export const datatableVisualization: Visualization< ]; }, - renderLayerConfigPanel(domElement, props) { - const layer = props.state.layers.find(l => l.layerId === props.layerId); - - if (layer) { - render( - - - , - domElement - ); + getConfiguration({ state, frame, layerId }) { + const layer = state.layers.find(l => l.layerId === layerId); + if (!layer) { + return { groups: [] }; } + + const datasource = frame.datasourceLayers[layer.layerId]; + const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); + // When we add a column it could be empty, and therefore have no order + const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); + + return { + groups: [ + { + groupId: 'columns', + groupLabel: i18n.translate('xpack.lens.datatable.columns', { + defaultMessage: 'Columns', + }), + layerId: state.layers[0].layerId, + accessors: sortedColumns, + supportsMoreColumns: true, + filterOperations: () => true, + }, + ], + }; + }, + + setDimension({ prevState, layerId, columnId }) { + return { + ...prevState, + layers: prevState.layers.map(l => { + if (l.layerId !== layerId || l.columns.includes(columnId)) { + return l; + } + return { ...l, columns: [...l.columns, columnId] }; + }), + }; + }, + removeDimension({ prevState, layerId, columnId }) { + return { + ...prevState, + layers: prevState.layers.map(l => + l.layerId === layerId + ? { + ...l, + columns: l.columns.filter(c => c !== columnId), + } + : l + ), + }; }, toExpression(state, frame) { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss new file mode 100644 index 000000000000..62a7f6b023f3 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss @@ -0,0 +1,50 @@ +.lnsConfigPanel__panel { + margin-bottom: $euiSizeS; +} + +.lnsConfigPanel__row { + background: $euiColorLightestShade; + padding: $euiSizeS; + border-radius: $euiBorderRadius; + + // Add margin to the top of the next same panel + & + & { + margin-top: $euiSizeS; + } +} + +.lnsConfigPanel__addLayerBtn { + color: transparentize($euiColorMediumShade, .3); + // Remove EuiButton's default shadow to make button more subtle + // sass-lint:disable-block no-important + box-shadow: none !important; + border: 1px dashed currentColor; +} + +.lnsConfigPanel__dimension { + @include euiFontSizeS; + background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade); + border-radius: $euiBorderRadius; + display: flex; + align-items: center; + margin-top: $euiSizeXS; + overflow: hidden; +} + +.lnsConfigPanel__trigger { + max-width: 100%; + display: block; +} + +.lnsConfigPanel__triggerLink { + padding: $euiSizeS; + width: 100%; + display: flex; + align-items: center; + min-height: $euiSizeXXL; +} + +.lnsConfigPanel__popover { + line-height: 0; + flex-grow: 1; +} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx index 1b60098fd45a..6698c9e68b98 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx @@ -84,7 +84,7 @@ describe('chart_switch', () => { } function mockDatasourceMap() { - const datasource = createMockDatasource(); + const datasource = createMockDatasource('testDatasource'); datasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ { state: {}, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx index 1422ee86be3e..c2cd0485de67 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx @@ -16,17 +16,21 @@ import { EuiToolTip, EuiButton, EuiForm, + EuiFormRow, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { NativeRenderer } from '../../native_renderer'; import { Action } from './state_management'; import { Visualization, FramePublicAPI, Datasource, - VisualizationLayerConfigProps, + VisualizationLayerWidgetProps, + DatasourceDimensionEditorProps, + StateSetter, } from '../../types'; -import { DragContext } from '../../drag_drop'; +import { DragContext, DragDrop, ChildDragDropProvider } from '../../drag_drop'; import { ChartSwitch } from './chart_switch'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { generateId } from '../../id_generator'; @@ -47,6 +51,7 @@ interface ConfigPanelWrapperProps { state: unknown; } >; + core: DatasourceDimensionEditorProps['core']; } export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { @@ -86,8 +91,7 @@ function LayerPanels( activeDatasourceId, datasourceMap, } = props; - const dragDropContext = useContext(DragContext); - const setState = useMemo( + const setVisualizationState = useMemo( () => (newState: unknown) => { props.dispatch({ type: 'UPDATE_VISUALIZATION_STATE', @@ -98,6 +102,43 @@ function LayerPanels( }, [props.dispatch, activeVisualization] ); + const updateDatasource = useMemo( + () => (datasourceId: string, newState: unknown) => { + props.dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: () => newState, + datasourceId, + clearStagedPreview: false, + }); + }, + [props.dispatch] + ); + const updateAll = useMemo( + () => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => { + props.dispatch({ + type: 'UPDATE_STATE', + subType: 'UPDATE_ALL_STATES', + updater: prevState => { + return { + ...prevState, + datasourceStates: { + ...prevState.datasourceStates, + [datasourceId]: { + state: newDatasourceState, + isLoading: false, + }, + }, + visualization: { + activeId: activeVisualization.id, + state: newVisualizationState, + }, + stagedPreview: undefined, + }; + }, + }); + }, + [props.dispatch] + ); const layerIds = activeVisualization.getLayerIds(visualizationState); return ( @@ -108,12 +149,13 @@ function LayerPanels( key={layerId} layerId={layerId} activeVisualization={activeVisualization} - dragDropContext={dragDropContext} - state={setState} - setState={setState} + visualizationState={visualizationState} + updateVisualization={setVisualizationState} + updateDatasource={updateDatasource} + updateAll={updateAll} frame={framePublicAPI} isOnlyLayer={layerIds.length === 1} - onRemove={() => { + onRemoveLayer={() => { dispatch({ type: 'UPDATE_STATE', subType: 'REMOVE_OR_CLEAR_LAYER', @@ -143,7 +185,7 @@ function LayerPanels( className="lnsConfigPanel__addLayerBtn" fullWidth size="s" - data-test-subj={`lnsXY_layer_add`} + data-test-subj="lnsXY_layer_add" aria-label={i18n.translate('xpack.lens.xyChart.addLayerButton', { defaultMessage: 'Add layer', })} @@ -174,85 +216,399 @@ function LayerPanels( } function LayerPanel( - props: ConfigPanelWrapperProps & - VisualizationLayerConfigProps & { - isOnlyLayer: boolean; - activeVisualization: Visualization; - onRemove: () => void; - } + props: Exclude & { + frame: FramePublicAPI; + layerId: string; + isOnlyLayer: boolean; + activeVisualization: Visualization; + visualizationState: unknown; + updateVisualization: StateSetter; + updateDatasource: (datasourceId: string, newState: unknown) => void; + updateAll: ( + datasourceId: string, + newDatasourcestate: unknown, + newVisualizationState: unknown + ) => void; + onRemoveLayer: () => void; + } ) { - const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemove } = props; + const dragDropContext = useContext(DragContext); + const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; - const layerConfigProps = { + if (!datasourcePublicAPI) { + return null; + } + const layerVisualizationConfigProps = { layerId, - dragDropContext: props.dragDropContext, + dragDropContext, state: props.visualizationState, - setState: props.setState, frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, }; + const datasourceId = datasourcePublicAPI.datasourceId; + const layerDatasourceState = props.datasourceStates[datasourceId].state; + const layerDatasource = props.datasourceMap[datasourceId]; - return ( - - - - - + const layerDatasourceDropProps = { + layerId, + dragDropContext, + state: layerDatasourceState, + setState: (newState: unknown) => { + props.updateDatasource(datasourceId, newState); + }, + }; - {datasourcePublicAPI && ( - - ({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + + const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); + const isEmptyLayer = !groups.some(d => d.accessors.length > 0); + + function wrapInPopover( + id: string, + groupId: string, + trigger: React.ReactElement, + panel: React.ReactElement + ) { + const noMatch = popoverState.isOpen ? !groups.some(d => d.accessors.includes(id)) : false; + return ( + { + setPopoverState({ isOpen: false, openId: null, addingToGroupId: null }); + }} + button={trigger} + anchorPosition="leftUp" + withTitle + panelPaddingSize="s" + > + {panel} + + ); + } + + return ( + + + + + - )} - - - - + {layerDatasource && ( + + { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + dateRange: props.framePublicAPI.dateRange, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter(columnId => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach(columnId => { + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + }); + }); - + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + + )} + - - - { - // If we don't blur the remove / clear button, it remains focused - // which is a strange UX in this case. e.target.blur doesn't work - // due to who knows what, but probably event re-writing. Additionally, - // activeElement does not have blur so, we need to do some casting + safeguards. - const el = (document.activeElement as unknown) as { blur: () => void }; + - if (el && el.blur) { - el.blur(); + {groups.map((group, index) => { + const newId = generateId(); + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + + <> + {group.accessors.map(accessor => ( + { + layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: accessor, + filterOperations: group.filterOperations, + }); + }} + > + {wrapInPopover( + accessor, + group.groupId, + { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: accessor, + addingToGroupId: null, // not set for existing dimension + }); + } + }, + }} + />, + + )} - onRemove(); - }} - > - {isOnlyLayer - ? i18n.translate('xpack.lens.resetLayer', { - defaultMessage: 'Reset layer', - }) - : i18n.translate('xpack.lens.deleteLayer', { - defaultMessage: 'Delete layer', - })} - - - - + { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: accessor, + prevState: layerDatasourceState, + }), + props.activeVisualization.removeDimension({ + layerId, + columnId: accessor, + prevState: props.visualizationState, + }) + ); + }} + /> + + ))} + {group.supportsMoreColumns ? ( + { + const dropSuccess = layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: newId, + filterOperations: group.filterOperations, + }); + if (dropSuccess) { + props.updateVisualization( + activeVisualization.setDimension({ + layerId, + groupId: group.groupId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + } + }} + > + {wrapInPopover( + newId, + group.groupId, +
+ { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: newId, + addingToGroupId: group.groupId, + }); + } + }} + size="xs" + > + + +
, + { + props.updateAll( + datasourceId, + newState, + activeVisualization.setDimension({ + layerId, + groupId: group.groupId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + setPopoverState({ + isOpen: true, + openId: newId, + addingToGroupId: null, // clear now that dimension exists + }); + }, + }} + /> + )} +
+ ) : null} + + + ); + })} + + + + + + { + // If we don't blur the remove / clear button, it remains focused + // which is a strange UX in this case. e.target.blur doesn't work + // due to who knows what, but probably event re-writing. Additionally, + // activeElement does not have blur so, we need to do some casting + safeguards. + const el = (document.activeElement as unknown) as { blur: () => void }; + + if (el?.blur) { + el.blur(); + } + + onRemoveLayer(); + }} + > + {isOnlyLayer + ? i18n.translate('xpack.lens.resetLayer', { + defaultMessage: 'Reset layer', + }) + : i18n.translate('xpack.lens.deleteLayer', { + defaultMessage: 'Delete layer', + })} + + + + + ); } @@ -263,7 +619,7 @@ function LayerSettings({ }: { layerId: string; activeVisualization: Visualization; - layerConfigProps: VisualizationLayerConfigProps; + layerConfigProps: VisualizationLayerWidgetProps; }) { const [isOpen, setIsOpen] = useState(false); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index dd591b3992fe..8d8d38944e18 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -87,14 +87,15 @@ describe('editor_frame', () => { mockVisualization.getLayerIds.mockReturnValue(['first']); mockVisualization2.getLayerIds.mockReturnValue(['second']); - mockDatasource = createMockDatasource(); - mockDatasource2 = createMockDatasource(); + mockDatasource = createMockDatasource('testDatasource'); + mockDatasource2 = createMockDatasource('testDatasource2'); expressionRendererMock = createExpressionRendererMock(); }); describe('initialization', () => { it('should initialize initial datasource', async () => { + mockVisualization.getLayerIds.mockReturnValue([]); await act(async () => { mount( { }); it('should initialize all datasources with state from doc', async () => { - const mockDatasource3 = createMockDatasource(); + const mockDatasource3 = createMockDatasource('testDatasource3'); const datasource1State = { datasource1: '' }; const datasource2State = { datasource2: '' }; @@ -198,9 +199,9 @@ describe('editor_frame', () => { ExpressionRenderer={expressionRendererMock} /> ); - expect(mockVisualization.renderLayerConfigPanel).not.toHaveBeenCalled(); expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled(); }); + expect(mockDatasource.renderDataPanel).toHaveBeenCalled(); }); it('should not initialize visualization before datasource is initialized', async () => { @@ -289,6 +290,7 @@ describe('editor_frame', () => { mockDatasource2.initialize.mockReturnValue(Promise.resolve(initialState)); mockDatasource2.getLayers.mockReturnValue(['abc', 'def']); mockDatasource2.removeLayer.mockReturnValue({ removed: true }); + mockVisualization.getLayerIds.mockReturnValue(['first', 'abc', 'def']); await act(async () => { mount( { ); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: initialState }) ); }); @@ -614,15 +615,14 @@ describe('editor_frame', () => { ); }); const updatedState = {}; - const setVisualizationState = (mockVisualization.renderLayerConfigPanel as jest.Mock).mock - .calls[0][1].setState; + const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1] + .setState; act(() => { - setVisualizationState(updatedState); + setDatasourceState(updatedState); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(2); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenLastCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2); + expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ state: updatedState, }) @@ -688,8 +688,7 @@ describe('editor_frame', () => { }); const updatedPublicAPI: DatasourcePublicAPI = { - renderLayerPanel: jest.fn(), - renderDimensionPanel: jest.fn(), + datasourceId: 'testDatasource', getOperationForColumnId: jest.fn(), getTableSpec: jest.fn(), }; @@ -701,9 +700,8 @@ describe('editor_frame', () => { setDatasourceState({}); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(2); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenLastCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2); + expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ frame: expect.objectContaining({ datasourceLayers: { @@ -719,6 +717,7 @@ describe('editor_frame', () => { it('should pass the datasource api for each layer to the visualization', async () => { mockDatasource.getLayers.mockReturnValue(['first']); mockDatasource2.getLayers.mockReturnValue(['second', 'third']); + mockVisualization.getLayerIds.mockReturnValue(['first', 'second', 'third']); await act(async () => { mount( @@ -755,10 +754,10 @@ describe('editor_frame', () => { ); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalled(); + expect(mockVisualization.getConfiguration).toHaveBeenCalled(); const datasourceLayers = - mockVisualization.renderLayerConfigPanel.mock.calls[0][1].frame.datasourceLayers; + mockVisualization.getConfiguration.mock.calls[0][0].frame.datasourceLayers; expect(datasourceLayers.first).toBe(mockDatasource.publicAPIMock); expect(datasourceLayers.second).toBe(mockDatasource2.publicAPIMock); expect(datasourceLayers.third).toBe(mockDatasource2.publicAPIMock); @@ -811,21 +810,18 @@ describe('editor_frame', () => { expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith( expect.objectContaining({ state: datasource1State, - setState: expect.anything(), layerId: 'first', }) ); expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith( expect.objectContaining({ state: datasource2State, - setState: expect.anything(), layerId: 'second', }) ); expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith( expect.objectContaining({ state: datasource2State, - setState: expect.anything(), layerId: 'third', }) ); @@ -858,45 +854,9 @@ describe('editor_frame', () => { expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({ dateRange, state: datasourceState, - setState: expect.any(Function), layerId: 'first', }); }); - - it('should re-create the public api after state has been set', async () => { - mockDatasource.getLayers.mockReturnValue(['first']); - - await act(async () => { - mount( - - ); - }); - - const updatedState = {}; - const setDatasourceState = mockDatasource.getPublicAPI.mock.calls[0][0].setState; - act(() => { - setDatasourceState(updatedState); - }); - - expect(mockDatasource.getPublicAPI).toHaveBeenLastCalledWith( - expect.objectContaining({ - state: updatedState, - setState: expect.any(Function), - layerId: 'first', - }) - ); - }); }); describe('switching', () => { @@ -1021,8 +981,7 @@ describe('editor_frame', () => { expect(mockVisualization2.getSuggestions).toHaveBeenCalled(); expect(mockVisualization2.initialize).toHaveBeenCalledWith(expect.anything(), initialState); - expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: { initial: true } }) ); }); @@ -1039,8 +998,7 @@ describe('editor_frame', () => { datasourceLayers: expect.objectContaining({ first: mockDatasource.publicAPIMock }), }) ); - expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: { initial: true } }) ); }); @@ -1239,9 +1197,8 @@ describe('editor_frame', () => { .simulate('click'); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(1); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(1); + expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) @@ -1306,8 +1263,7 @@ describe('editor_frame', () => { .simulate('drop'); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) @@ -1375,14 +1331,16 @@ describe('editor_frame', () => { instance.update(); act(() => { - instance.find(DragDrop).prop('onDrop')!({ + instance + .find(DragDrop) + .filter('[data-test-subj="mockVisA"]') + .prop('onDrop')!({ indexPatternId: '1', field: {}, }); }); - expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) @@ -1472,14 +1430,16 @@ describe('editor_frame', () => { instance.update(); act(() => { - instance.find(DragDrop).prop('onDrop')!({ + instance + .find(DragDrop) + .filter('[data-test-subj="lnsWorkspace"]') + .prop('onDrop')!({ indexPatternId: '1', field: {}, }); }); - expect(mockVisualization3.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization3.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index a456372c99c0..082519d9a8fe 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -21,6 +21,7 @@ import { FrameLayout } from './frame_layout'; import { SuggestionPanel } from './suggestion_panel'; import { WorkspacePanel } from './workspace_panel'; import { Document } from '../../persistence/saved_object_store'; +import { RootDragDropProvider } from '../../drag_drop'; import { getSavedObjectFormat } from './save'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { generateId } from '../../id_generator'; @@ -90,21 +91,11 @@ export function EditorFrame(props: EditorFrameProps) { const layers = datasource.getLayers(datasourceState); layers.forEach(layer => { - const publicAPI = props.datasourceMap[id].getPublicAPI({ + datasourceLayers[layer] = props.datasourceMap[id].getPublicAPI({ state: datasourceState, - setState: (newState: unknown) => { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - datasourceId: id, - updater: newState, - clearStagedPreview: true, - }); - }, layerId: layer, dateRange: props.dateRange, }); - - datasourceLayers[layer] = publicAPI; }); }); @@ -235,74 +226,79 @@ export function EditorFrame(props: EditorFrameProps) { ]); return ( - - } - configPanel={ - allLoaded && ( - + - ) - } - workspacePanel={ - allLoaded && ( - - + ) + } + workspacePanel={ + allLoaded && ( + + + + ) + } + suggestionsPanel={ + allLoaded && ( + - - ) - } - suggestionsPanel={ - allLoaded && ( - - ) - } - /> + ) + } + /> + ); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx index a69da8b49e23..56afe3ed69a7 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { EuiPage, EuiPageSideBar, EuiPageBody } from '@elastic/eui'; -import { RootDragDropProvider } from '../../drag_drop'; export interface FrameLayoutProps { dataPanel: React.ReactNode; @@ -17,19 +16,17 @@ export interface FrameLayoutProps { export function FrameLayout(props: FrameLayoutProps) { return ( - - -
- {props.dataPanel} - - {props.workspacePanel} - {props.suggestionsPanel} - - - {props.configPanel} - -
-
-
+ +
+ {props.dataPanel} + + {props.workspacePanel} + {props.suggestionsPanel} + + + {props.configPanel} + +
+
); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss index fee28c374ef7..6c6a63c8c7eb 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss @@ -1,4 +1,5 @@ @import './chart_switch'; +@import './config_panel_wrapper'; @import './data_panel_wrapper'; @import './expression_renderer'; @import './frame_layout'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts index 158a6cb8c979..60bfbc493f61 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts @@ -11,7 +11,7 @@ import { esFilters, IIndexPattern, IFieldType } from '../../../../../../../src/p describe('save editor frame state', () => { const mockVisualization = createMockVisualization(); mockVisualization.getPersistableState.mockImplementation(x => x); - const mockDatasource = createMockDatasource(); + const mockDatasource = createMockDatasource('a'); const mockIndexPattern = ({ id: 'indexpattern' } as unknown) as IIndexPattern; const mockField = ({ name: '@timestamp' } as unknown) as IFieldType; @@ -45,7 +45,7 @@ describe('save editor frame state', () => { }; it('transforms from internal state to persisted doc format', async () => { - const datasource = createMockDatasource(); + const datasource = createMockDatasource('a'); datasource.getPersistableState.mockImplementation(state => ({ stuff: `${state}_datasource_persisted`, })); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index 487a91c22b5d..63b8b1f04829 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -30,7 +30,7 @@ let datasourceStates: Record< beforeEach(() => { datasourceMap = { - mock: createMockDatasource(), + mock: createMockDatasource('a'), }; datasourceStates = { @@ -147,9 +147,9 @@ describe('suggestion helpers', () => { }, }; const multiDatasourceMap = { - mock: createMockDatasource(), - mock2: createMockDatasource(), - mock3: createMockDatasource(), + mock: createMockDatasource('a'), + mock2: createMockDatasource('a'), + mock3: createMockDatasource('a'), }; const droppedField = {}; getSuggestions({ diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index 9729d6259f84..b146f2467c46 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -39,7 +39,7 @@ describe('suggestion_panel', () => { beforeEach(() => { mockVisualization = createMockVisualization(); - mockDatasource = createMockDatasource(); + mockDatasource = createMockDatasource('a'); expressionRendererMock = createExpressionRendererMock(); dispatchMock = jest.fn(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 1115126792c8..93f6ea6ea67a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -373,7 +373,6 @@ function getPreviewExpression( layerId, dateRange: frame.dateRange, state: datasourceState, - setState: () => {}, }); } }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx index a51091d39f84..748e5b876da9 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx @@ -36,7 +36,7 @@ describe('workspace_panel', () => { mockVisualization = createMockVisualization(); mockVisualization2 = createMockVisualization(); - mockDatasource = createMockDatasource(); + mockDatasource = createMockDatasource('a'); expressionRendererMock = createExpressionRendererMock(); }); @@ -199,7 +199,7 @@ describe('workspace_panel', () => { }); it('should include data fetching for each layer in the expression', () => { - const mockDatasource2 = createMockDatasource(); + const mockDatasource2 = createMockDatasource('a'); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx index e606c69c8c38..5d2f68a5567e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx @@ -33,9 +33,24 @@ export function createMockVisualization(): jest.Mocked { getPersistableState: jest.fn(_state => _state), getSuggestions: jest.fn(_options => []), initialize: jest.fn((_frame, _state?) => ({})), - renderLayerConfigPanel: jest.fn(), + getConfiguration: jest.fn(props => ({ + groups: [ + { + groupId: 'a', + groupLabel: 'a', + layerId: 'layer1', + supportsMoreColumns: true, + accessors: [], + filterOperations: jest.fn(() => true), + dataTestSubj: 'mockVisA', + }, + ], + })), toExpression: jest.fn((_state, _frame) => null), toPreviewExpression: jest.fn((_state, _frame) => null), + + setDimension: jest.fn(), + removeDimension: jest.fn(), }; } @@ -43,12 +58,11 @@ export type DatasourceMock = jest.Mocked & { publicAPIMock: jest.Mocked; }; -export function createMockDatasource(): DatasourceMock { +export function createMockDatasource(id: string): DatasourceMock { const publicAPIMock: jest.Mocked = { + datasourceId: id, getTableSpec: jest.fn(() => []), getOperationForColumnId: jest.fn(), - renderDimensionPanel: jest.fn(), - renderLayerPanel: jest.fn(), }; return { @@ -60,12 +74,19 @@ export function createMockDatasource(): DatasourceMock { getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), initialize: jest.fn((_state?) => Promise.resolve()), renderDataPanel: jest.fn(), + renderLayerPanel: jest.fn(), toExpression: jest.fn((_frame, _state) => null), insertLayer: jest.fn((_state, _newLayerId) => {}), removeLayer: jest.fn((_state, _layerId) => {}), + removeColumn: jest.fn(props => {}), getLayers: jest.fn(_state => []), getMetaData: jest.fn(_state => ({ filterableIndexPatterns: [] })), + renderDimensionTrigger: jest.fn(), + renderDimensionEditor: jest.fn(), + canHandleDrop: jest.fn(), + onDrop: jest.fn(), + // this is an additional property which doesn't exist on real datasources // but can be used to validate whether specific API mock functions are called publicAPIMock, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx index 6b9dc88e7ed1..47fd810bb4c5 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx @@ -47,7 +47,7 @@ describe('editor_frame service', () => { pluginSetupDependencies ); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = await publicAPI.createInstance({}); + const instance = await publicAPI.createInstance(); instance.mount(mountpoint, { onError: jest.fn(), onChange: jest.fn(), @@ -66,7 +66,7 @@ describe('editor_frame service', () => { pluginSetupDependencies ); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = await publicAPI.createInstance({}); + const instance = await publicAPI.createInstance(); instance.mount(mountpoint, { onError: jest.fn(), onChange: jest.fn(), diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index 496573f6a1c9..2f91d14c397c 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -4,8 +4,6 @@ @import './variables'; @import './mixins'; -@import './config_panel'; - @import './app_plugin/index'; @import 'datatable_visualization/index'; @import './drag_drop/index'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss deleted file mode 100644 index ddb37505f998..000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss +++ /dev/null @@ -1,9 +0,0 @@ -.lnsIndexPatternDimensionPanel { - @include euiFontSizeS; - background-color: $euiColorEmptyShade; - border-radius: $euiBorderRadius; - display: flex; - align-items: center; - margin-top: $euiSizeXS; - overflow: hidden; -} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss index 2ce3e11171fc..26f805fe735f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss @@ -1,3 +1,2 @@ -@import './dimension_panel'; @import './field_select'; @import './popover'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss index 8f26ab91e0f1..07a72ee1f66f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss @@ -1,37 +1,24 @@ -.lnsPopoverEditor { +.lnsIndexPatternDimensionEditor { flex-grow: 1; line-height: 0; overflow: hidden; } -.lnsPopoverEditor__anchor { - max-width: 100%; - display: block; -} - -.lnsPopoverEditor__link { - width: 100%; - display: flex; - align-items: center; - padding: $euiSizeS; - min-height: $euiSizeXXL; -} - -.lnsPopoverEditor__left, -.lnsPopoverEditor__right { +.lnsIndexPatternDimensionEditor__left, +.lnsIndexPatternDimensionEditor__right { padding: $euiSizeS; } -.lnsPopoverEditor__left { +.lnsIndexPatternDimensionEditor__left { padding-top: 0; background-color: $euiPageBackgroundColor; } -.lnsPopoverEditor__right { +.lnsIndexPatternDimensionEditor__right { width: $euiSize * 20; } -.lnsPopoverEditor__operation { +.lnsIndexPatternDimensionEditor__operation { @include euiFontSizeS; color: $euiColorPrimary; @@ -41,11 +28,11 @@ } } -.lnsPopoverEditor__operation--selected { +.lnsIndexPatternDimensionEditor__operation--selected { font-weight: bold; color: $euiTextColor; } -.lnsPopoverEditor__operation--incompatible { +.lnsIndexPatternDimensionEditor__operation--incompatible { color: $euiColorMediumShade; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 56f75ae4b17b..41c317ccab29 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -7,27 +7,28 @@ import { ReactWrapper, ShallowWrapper } from 'enzyme'; import React from 'react'; import { act } from 'react-dom/test-utils'; -import { - EuiComboBox, - EuiSideNav, - EuiSideNavItemType, - EuiPopover, - EuiFieldNumber, -} from '@elastic/eui'; +import { EuiComboBox, EuiSideNav, EuiSideNavItemType, EuiFieldNumber } from '@elastic/eui'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { changeColumn } from '../state_helpers'; import { - IndexPatternDimensionPanel, - IndexPatternDimensionPanelComponent, - IndexPatternDimensionPanelProps, + IndexPatternDimensionEditorComponent, + IndexPatternDimensionEditorProps, + onDrop, + canHandleDrop, } from './dimension_panel'; -import { DropHandler, DragContextState } from '../../drag_drop'; +import { DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; +import { + IUiSettingsClient, + SavedObjectsClientContract, + HttpSetup, + CoreSetup, +} from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; +import { OperationMetadata } from '../../types'; jest.mock('ui/new_platform'); jest.mock('../loader'); @@ -79,20 +80,12 @@ const expectedIndexPatterns = { }, }; -describe('IndexPatternDimensionPanel', () => { - let wrapper: ReactWrapper | ShallowWrapper; +describe('IndexPatternDimensionEditorPanel', () => { let state: IndexPatternPrivateState; let setState: jest.Mock; - let defaultProps: IndexPatternDimensionPanelProps; + let defaultProps: IndexPatternDimensionEditorProps; let dragDropContext: DragContextState; - function openPopover() { - wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() - .simulate('click'); - } - beforeEach(() => { state = { indexPatternRefs: [], @@ -134,7 +127,6 @@ describe('IndexPatternDimensionPanel', () => { dragDropContext = createMockedDragDropContext(); defaultProps = { - dragDropContext, state, setState, dateRange: { fromDate: 'now-1d', toDate: 'now' }, @@ -158,475 +150,582 @@ describe('IndexPatternDimensionPanel', () => { }), } as unknown) as DataPublicPluginStart['fieldFormats'], } as unknown) as DataPublicPluginStart, + core: {} as CoreSetup, }; jest.clearAllMocks(); }); - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - } - }); + describe('Editor component', () => { + let wrapper: ReactWrapper | ShallowWrapper; - it('should display a configure button if dimension has no column yet', () => { - wrapper = mount(); - expect( - wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() - .prop('iconType') - ).toEqual('plusInCircleFilled'); - }); + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + }); - it('should call the filterOperations function', () => { - const filterOperations = jest.fn().mockReturnValue(true); + it('should call the filterOperations function', () => { + const filterOperations = jest.fn().mockReturnValue(true); - wrapper = shallow( - - ); + wrapper = shallow( + + ); - expect(filterOperations).toBeCalled(); - }); + expect(filterOperations).toBeCalled(); + }); - it('should show field select combo box on click', () => { - wrapper = mount(); + it('should show field select combo box on click', () => { + wrapper = mount(); - openPopover(); + expect( + wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') + ).toHaveLength(1); + }); - expect( - wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') - ).toHaveLength(1); - }); + it('should not show any choices if the filter returns false', () => { + wrapper = mount( + false} + /> + ); - it('should not show any choices if the filter returns false', () => { - wrapper = mount( - false} - /> - ); + expect( + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')! + .prop('options')! + ).toHaveLength(0); + }); - openPopover(); + it('should list all field names and document as a whole in prioritized order', () => { + wrapper = mount(); - expect( - wrapper + const options = wrapper .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')! - .prop('options')! - ).toHaveLength(0); - }); - - it('should list all field names and document as a whole in prioritized order', () => { - wrapper = mount(); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - expect(options).toHaveLength(2); + expect(options).toHaveLength(2); - expect(options![0].label).toEqual('Records'); + expect(options![0].label).toEqual('Records'); - expect(options![1].options!.map(({ label }) => label)).toEqual([ - 'timestamp', - 'bytes', - 'memory', - 'source', - ]); - }); + expect(options![1].options!.map(({ label }) => label)).toEqual([ + 'timestamp', + 'bytes', + 'memory', + 'source', + ]); + }); - it('should hide fields that have no data', () => { - const props = { - ...defaultProps, - state: { - ...defaultProps.state, - existingFields: { - 'my-fake-index-pattern': { - timestamp: true, - source: true, + it('should hide fields that have no data', () => { + const props = { + ...defaultProps, + state: { + ...defaultProps.state, + existingFields: { + 'my-fake-index-pattern': { + timestamp: true, + source: true, + }, }, }, - }, - }; - wrapper = mount(); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + }; + wrapper = mount(); - expect(options![1].options!.map(({ label }) => label)).toEqual(['timestamp', 'source']); - }); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - it('should indicate fields which are incompatible for the operation of the current column', () => { - wrapper = mount( - label)).toEqual(['timestamp', 'source']); + }); - // Private - operationType: 'max', - sourceField: 'bytes', + it('should indicate fields which are incompatible for the operation of the current column', () => { + wrapper = mount( + - ); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + }} + /> + ); - expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records'); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); + expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records'); - it('should indicate operations which are incompatible for the field of the current column', () => { - wrapper = mount( - label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); - // Private - operationType: 'max', - sourceField: 'bytes', + it('should indicate operations which are incompatible for the field of the current column', () => { + wrapper = mount( + - ); - - openPopover(); + }} + /> + ); - interface ItemType { - name: string; - 'data-test-subj': string; - } - const items: Array> = wrapper.find(EuiSideNav).prop('items'); - const options = (items[0].items as unknown) as ItemType[]; + interface ItemType { + name: string; + 'data-test-subj': string; + } + const items: Array> = wrapper.find(EuiSideNav).prop('items'); + const options = (items[0].items as unknown) as ItemType[]; - expect(options.find(({ name }) => name === 'Minimum')!['data-test-subj']).not.toContain( - 'Incompatible' - ); + expect(options.find(({ name }) => name === 'Minimum')!['data-test-subj']).not.toContain( + 'Incompatible' + ); - expect(options.find(({ name }) => name === 'Date histogram')!['data-test-subj']).toContain( - 'Incompatible' - ); - }); + expect(options.find(({ name }) => name === 'Date histogram')!['data-test-subj']).toContain( + 'Incompatible' + ); + }); - it('should keep the operation when switching to another field compatible with this operation', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, + it('should keep the operation when switching to another field compatible with this operation', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, - // Private - operationType: 'max', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, + // Private + operationType: 'max', + sourceField: 'bytes', + params: { format: { id: 'bytes' } }, + }, }, }, }, - }, - }; - - wrapper = mount(); + }; - openPopover(); + wrapper = mount( + + ); - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'memory')!; + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'memory')!; - act(() => { - comboBox.prop('onChange')!([option]); - }); + act(() => { + comboBox.prop('onChange')!([option]); + }); - expect(setState).toHaveBeenCalledWith({ - ...initialState, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'max', - sourceField: 'memory', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...initialState, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'max', + sourceField: 'memory', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - - it('should switch operations when selecting a field that requires another operation', () => { - wrapper = mount(); - openPopover(); + it('should switch operations when selecting a field that requires another operation', () => { + wrapper = mount(); - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; - act(() => { - comboBox.prop('onChange')!([option]); - }); + act(() => { + comboBox.prop('onChange')!([option]); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'terms', - sourceField: 'source', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'terms', + sourceField: 'source', + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - - it('should keep the field when switching to another operation compatible for this field', () => { - wrapper = mount( - { + wrapper = mount( + - ); - - openPopover(); + }} + /> + ); - act(() => { - wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); - }); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'min', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'min', + sourceField: 'bytes', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - it('should not set the state if selecting the currently active operation', () => { - wrapper = mount(); + it('should not set the state if selecting the currently active operation', () => { + wrapper = mount(); - openPopover(); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); + }); - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); + expect(setState).not.toHaveBeenCalled(); }); - expect(setState).not.toHaveBeenCalled(); - }); - - it('should update label on label input changes', () => { - wrapper = mount(); + it('should update label on label input changes', () => { + wrapper = mount(); - openPopover(); - - act(() => { - wrapper - .find('input[data-test-subj="indexPattern-label-edit"]') - .simulate('change', { target: { value: 'New Label' } }); - }); + act(() => { + wrapper + .find('input[data-test-subj="indexPattern-label-edit"]') + .simulate('change', { target: { value: 'New Label' } }); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - label: 'New Label', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'New Label', + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - describe('transient invalid state', () => { - it('should not set the state if selecting an operation incompatible with the current field', () => { - wrapper = mount(); + describe('transient invalid state', () => { + it('should not set the state if selecting an operation incompatible with the current field', () => { + wrapper = mount(); - openPopover(); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); + }); + + expect(setState).not.toHaveBeenCalled(); + }); + + it('should show error message in invalid state', () => { + wrapper = mount(); - act(() => { wrapper .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).not.toHaveLength( + 0 + ); + + expect(setState).not.toHaveBeenCalled(); }); - expect(setState).not.toHaveBeenCalled(); - }); + it('should leave error state if a compatible operation is selected', () => { + wrapper = mount(); - it('should show error message in invalid state', () => { - wrapper = mount(); + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - openPopover(); + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).not.toHaveLength(0); + it('should indicate fields compatible with selected operation', () => { + wrapper = mount(); - expect(setState).not.toHaveBeenCalled(); - }); + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - it('should leave error state if a compatible operation is selected', () => { - wrapper = mount(); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - openPopover(); + expect(options![0]['data-test-subj']).toContain('Incompatible'); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); + it('should select compatible operation if field not compatible with selected operation', () => { + wrapper = mount( + + ); - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); - }); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - it('should leave error state if the popover gets closed', () => { - wrapper = mount(); + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]'); + const options = comboBox.prop('options'); - openPopover(); + // options[1][2] is a `source` field of type `string` which doesn't support `avg` operation + act(() => { + comboBox.prop('onChange')!([options![1].options![2]]); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], + }, + }, + }); + }); - act(() => { - wrapper.find(EuiPopover).prop('closePopover')!(); + it('should select the Records field when count is selected', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'avg', + sourceField: 'bytes', + }, + }, + }, + }, + }; + wrapper = mount( + + ); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-count"]') + .simulate('click'); + + const newColumnState = setState.mock.calls[0][0].layers.first.columns.col2; + expect(newColumnState.operationType).toEqual('count'); + expect(newColumnState.sourceField).toEqual('Records'); }); - openPopover(); + it('should indicate document and field compatibility with selected document operation', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'count', + sourceField: 'Records', + }, + }, + }, + }, + }; + wrapper = mount( + + ); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); - }); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - it('should indicate fields compatible with selected operation', () => { - wrapper = mount(); + expect(options![0]['data-test-subj']).toContain('Incompatible'); - openPopover(); + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + it('should set datasource state if compatible field is selected for operation', () => { + wrapper = mount(); - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); + }); - expect(options![0]['data-test-subj']).toContain('Incompatible'); + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox + .prop('options')![1] + .options!.find(({ label }) => label === 'source')!; - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); + act(() => { + comboBox.prop('onChange')!([option]); + }); - it('should select compatible operation if field not compatible with selected operation', () => { - wrapper = mount(); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + }), + }, + }, + }, + }); + }); + }); - openPopover(); + it('should support selecting the operation before the field', () => { + wrapper = mount(); wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); @@ -635,9 +734,8 @@ describe('IndexPatternDimensionPanel', () => { .filter('[data-test-subj="indexPattern-dimension-field"]'); const options = comboBox.prop('options'); - // options[1][2] is a `source` field of type `string` which doesn't support `avg` operation act(() => { - comboBox.prop('onChange')!([options![1].options![2]]); + comboBox.prop('onChange')!([options![1].options![0]]); }); expect(setState).toHaveBeenCalledWith({ @@ -648,8 +746,8 @@ describe('IndexPatternDimensionPanel', () => { columns: { ...state.layers.first.columns, col2: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', + sourceField: 'bytes', + operationType: 'avg', // Other parts of this don't matter for this test }), }, @@ -659,41 +757,93 @@ describe('IndexPatternDimensionPanel', () => { }); }); - it('should select the Records field when count is selected', () => { - const initialState: IndexPatternPrivateState = { + it('should select operation directly if only one field is possible', () => { + const initialState = { + ...state, + indexPatterns: { + 1: { + ...state.indexPatterns['1'], + fields: state.indexPatterns['1'].fields.filter(field => field.name !== 'memory'), + }, + }, + }; + + wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + + expect(setState).toHaveBeenCalledWith({ + ...initialState, + layers: { + first: { + ...initialState.layers.first, + columns: { + ...initialState.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'avg', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], + }, + }, + }); + }); + + it('should select operation directly if only document is possible', () => { + wrapper = mount(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + + expect(setState).toHaveBeenCalledWith({ ...state, layers: { first: { ...state.layers.first, columns: { ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'avg', - sourceField: 'bytes', - }, + col2: expect.objectContaining({ + operationType: 'count', + // Other parts of this don't matter for this test + }), }, + columnOrder: ['col1', 'col2'], }, }, - }; - wrapper = mount( - - ); + }); + }); - openPopover(); + it('should indicate compatible fields when selecting the operation first', () => { + wrapper = mount(); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-count"]') - .simulate('click'); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - const newColumnState = setState.mock.calls[0][0].layers.first.columns.col2; - expect(newColumnState.operationType).toEqual('count'); - expect(newColumnState.sourceField).toEqual('Records'); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); + + expect(options![0]['data-test-subj']).toContain('Incompatible'); + + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'bytes')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] + ).not.toContain('Incompatible'); }); - it('should indicate document and field compatibility with selected document operation', () => { + it('should indicate document compatibility when document operation is selected', () => { const initialState: IndexPatternPrivateState = { ...state, layers: { @@ -713,45 +863,56 @@ describe('IndexPatternDimensionPanel', () => { }, }; wrapper = mount( - + ); - openPopover(); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); - const options = wrapper .find(EuiComboBox) .filter('[data-test-subj="indexPattern-dimension-field"]') .prop('options'); - expect(options![0]['data-test-subj']).toContain('Incompatible'); + expect(options![0]['data-test-subj']).not.toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] - ).not.toContain('Incompatible'); + options![1].options!.map(operation => + expect(operation['data-test-subj']).toContain('Incompatible') + ); }); - it('should set datasource state if compatible field is selected for operation', () => { - wrapper = mount(); + it('should show all operations that are not filtered out', () => { + wrapper = mount( + !op.isBucketed && op.dataType === 'number'} + /> + ); - openPopover(); + interface ItemType { + name: React.ReactNode; + } + const items: Array> = wrapper.find(EuiSideNav).prop('items'); + const options = (items[0].items as unknown) as ItemType[]; + + expect(options.map(({ name }: { name: React.ReactNode }) => name)).toEqual([ + 'Unique count', + 'Average', + 'Count', + 'Maximum', + 'Minimum', + 'Sum', + ]); + }); - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); - }); + it('should add a column on selection of a field', () => { + wrapper = mount(); const comboBox = wrapper .find(EuiComboBox) .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; + const option = comboBox.prop('options')![1].options![0]; act(() => { comboBox.prop('onChange')!([option]); @@ -764,479 +925,237 @@ describe('IndexPatternDimensionPanel', () => { ...state.layers.first, columns: { ...state.layers.first.columns, - col1: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', + col2: expect.objectContaining({ + sourceField: 'bytes', + // Other parts of this don't matter for this test }), }, + columnOrder: ['col1', 'col2'], }, }, }); }); - }); - - it('should support selecting the operation before the field', () => { - wrapper = mount(); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]'); - const options = comboBox.prop('options'); - - act(() => { - comboBox.prop('onChange')!([options![1].options![0]]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should select operation directly if only one field is possible', () => { - const initialState = { - ...state, - indexPatterns: { - 1: { - ...state.indexPatterns['1'], - fields: state.indexPatterns['1'].fields.filter(field => field.name !== 'memory'), - }, - }, - }; - - wrapper = mount( - - ); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - expect(setState).toHaveBeenCalledWith({ - ...initialState, - layers: { - first: { - ...initialState.layers.first, - columns: { - ...initialState.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should select operation directly if only document is possible', () => { - wrapper = mount(); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - operationType: 'count', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - it('should indicate compatible fields when selecting the operation first', () => { - wrapper = mount(); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); - - expect(options![0]['data-test-subj']).toContain('Incompatible'); - - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'bytes')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); - - it('should indicate document compatibility when document operation is selected', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'count', - sourceField: 'Records', - }, - }, - }, - }, - }; - wrapper = mount( - - ); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); - - expect(options![0]['data-test-subj']).not.toContain('Incompatible'); - - options![1].options!.map(operation => - expect(operation['data-test-subj']).toContain('Incompatible') - ); - }); - - it('should show all operations that are not filtered out', () => { - wrapper = mount( - !op.isBucketed && op.dataType === 'number'} - /> - ); - - openPopover(); - - interface ItemType { - name: React.ReactNode; - } - const items: Array> = wrapper.find(EuiSideNav).prop('items'); - const options = (items[0].items as unknown) as ItemType[]; - - expect(options.map(({ name }: { name: React.ReactNode }) => name)).toEqual([ - 'Unique count', - 'Average', - 'Count', - 'Maximum', - 'Minimum', - 'Sum', - ]); - }); - - it('should add a column on selection of a field', () => { - wrapper = mount(); - - openPopover(); - - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options![0]; - - act(() => { - comboBox.prop('onChange')!([option]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should use helper function when changing the function', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, + it('should use helper function when changing the function', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, - // Private - operationType: 'max', - sourceField: 'bytes', + // Private + operationType: 'max', + sourceField: 'bytes', + }, }, }, }, - }, - }; - wrapper = mount(); - - openPopover(); - - act(() => { - wrapper - .find('[data-test-subj="lns-indexPatternDimension-min"]') - .first() - .prop('onClick')!({} as React.MouseEvent<{}, MouseEvent>); - }); - - expect(changeColumn).toHaveBeenCalledWith({ - state: initialState, - columnId: 'col1', - layerId: 'first', - newColumn: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'min', - }), - }); - }); - - it('should clear the dimension with the clear button', () => { - wrapper = mount(); - - const clearButton = wrapper.find( - 'EuiButtonIcon[data-test-subj="indexPattern-dimensionPopover-remove"]' - ); + }; + wrapper = mount( + + ); - act(() => { - clearButton.simulate('click'); - }); + act(() => { + wrapper + .find('[data-test-subj="lns-indexPatternDimension-min"]') + .first() + .prop('onClick')!({} as React.MouseEvent<{}, MouseEvent>); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - indexPatternId: '1', - columns: {}, - columnOrder: [], - }, - }, + expect(changeColumn).toHaveBeenCalledWith({ + state: initialState, + columnId: 'col1', + layerId: 'first', + newColumn: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'min', + }), + }); }); - }); - - it('should clear the dimension when removing the selection in field combobox', () => { - wrapper = mount(); - openPopover(); + it('should clear the dimension when removing the selection in field combobox', () => { + wrapper = mount(); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('onChange')!([]); - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('onChange')!([]); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - indexPatternId: '1', - columns: {}, - columnOrder: [], + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + indexPatternId: '1', + columns: {}, + columnOrder: [], + }, }, - }, + }); }); - }); - it('allows custom format', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of bar', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'bar', + it('allows custom format', () => { + const stateWithNumberCol: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average of bar', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'bar', + }, }, }, }, - }, - }; - - wrapper = mount(); + }; - openPopover(); + wrapper = mount( + + ); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: 'bytes', label: 'Bytes' }]); - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: 'bytes', label: 'Bytes' }]); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - params: { - format: { id: 'bytes', params: { decimals: 2 } }, - }, - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + params: { + format: { id: 'bytes', params: { decimals: 2 } }, + }, + }), + }, }, }, - }, + }); }); - }); - it('keeps decimal places while switching', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of bar', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'bar', - params: { - format: { id: 'bytes', params: { decimals: 0 } }, + it('keeps decimal places while switching', () => { + const stateWithNumberCol: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average of bar', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'bar', + params: { + format: { id: 'bytes', params: { decimals: 0 } }, + }, }, }, }, }, - }, - }; + }; - wrapper = mount(); + wrapper = mount( + + ); - openPopover(); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: '', label: 'Default' }]); + }); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: '', label: 'Default' }]); - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: 'number', label: 'Number' }]); + }); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: 'number', label: 'Number' }]); + expect( + wrapper + .find(EuiFieldNumber) + .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') + .prop('value') + ).toEqual(0); }); - expect( - wrapper - .find(EuiFieldNumber) - .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .prop('value') - ).toEqual(0); - }); - - it('allows custom format with number of decimal places', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of bar', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'bar', - params: { - format: { id: 'bytes', params: { decimals: 2 } }, + it('allows custom format with number of decimal places', () => { + const stateWithNumberCol: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average of bar', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'bar', + params: { + format: { id: 'bytes', params: { decimals: 2 } }, + }, }, }, }, }, - }, - }; - - wrapper = mount(); + }; - openPopover(); + wrapper = mount( + + ); - act(() => { - wrapper - .find(EuiFieldNumber) - .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .prop('onChange')!({ target: { value: '0' } }); - }); + act(() => { + wrapper + .find(EuiFieldNumber) + .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') + .prop('onChange')!({ target: { value: '0' } }); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - params: { - format: { id: 'bytes', params: { decimals: 0 } }, - }, - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + params: { + format: { id: 'bytes', params: { decimals: 0 } }, + }, + }), + }, }, }, - }, + }); }); }); - describe('drag and drop', () => { + describe('Drag and drop', () => { function dragDropState(): IndexPatternPrivateState { return { indexPatternRefs: [], @@ -1287,112 +1206,80 @@ describe('IndexPatternDimensionPanel', () => { } it('is not droppable if no drag is happening', () => { - wrapper = mount( - - ); - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + canHandleDrop({ + ...defaultProps, + dragDropContext, + state: dragDropState(), + layerId: 'myLayer', + }) + ).toBe(false); }); it('is not droppable if the dragged item has no field', () => { - wrapper = shallow( - - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + }, + }) + ).toBe(false); }); it('is not droppable if field is not supported by filterOperations', () => { - wrapper = shallow( - false} - layerId="myLayer" - /> - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + }, + state: dragDropState(), + filterOperations: () => false, + layerId: 'myLayer', + }) + ).toBe(false); }); it('is droppable if the field is supported by filterOperations', () => { - wrapper = shallow( - op.dataType === 'number'} - layerId="myLayer" - /> - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeTruthy(); + }, + state: dragDropState(), + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(true); }); - it('is notdroppable if the field belongs to another index pattern', () => { - wrapper = shallow( - { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { ...dragDropContext, dragging: { field: { type: 'number', name: 'bar', aggregatable: true }, indexPatternId: 'foo2', }, - }} - state={dragDropState()} - filterOperations={op => op.dataType === 'number'} - layerId="myLayer" - /> - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + }, + state: dragDropState(), + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(false); }); it('appends the dropped column when a field is dropped', () => { @@ -1401,27 +1288,18 @@ describe('IndexPatternDimensionPanel', () => { indexPatternId: 'foo', }; const testState = dragDropState(); - wrapper = shallow( - op.dataType === 'number'} - layerId="myLayer" - /> - ); - - act(() => { - const onDrop = wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('onDrop') as DropHandler; - onDrop(dragging); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', }); expect(setState).toBeCalledTimes(1); @@ -1449,27 +1327,17 @@ describe('IndexPatternDimensionPanel', () => { indexPatternId: 'foo', }; const testState = dragDropState(); - wrapper = shallow( - op.isBucketed} - layerId="myLayer" - /> - ); - - act(() => { - const onDrop = wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('onDrop') as DropHandler; - - onDrop(dragging); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed, + layerId: 'myLayer', }); expect(setState).toBeCalledTimes(1); @@ -1497,26 +1365,16 @@ describe('IndexPatternDimensionPanel', () => { indexPatternId: 'foo', }; const testState = dragDropState(); - wrapper = shallow( - op.dataType === 'number'} - layerId="myLayer" - /> - ); - - act(() => { - const onDrop = wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('onDrop') as DropHandler; - - onDrop(dragging); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', }); expect(setState).toBeCalledTimes(1); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 59350ff215c2..5d87137db3d3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -5,27 +5,36 @@ */ import _ from 'lodash'; -import React, { memo, useMemo } from 'react'; -import { EuiButtonIcon } from '@elastic/eui'; +import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; +import { EuiLink } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { + DatasourceDimensionTriggerProps, + DatasourceDimensionEditorProps, + DatasourceDimensionDropProps, + DatasourceDimensionDropHandlerProps, +} from '../../types'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; -import { DatasourceDimensionPanelProps, StateSetter } from '../../types'; import { IndexPatternColumn, OperationType } from '../indexpattern'; import { getAvailableOperationsByMetadata, buildColumn, changeField } from '../operations'; import { PopoverEditor } from './popover_editor'; -import { DragContextState, ChildDragDropProvider, DragDrop } from '../../drag_drop'; -import { changeColumn, deleteColumn } from '../state_helpers'; +import { changeColumn } from '../state_helpers'; import { isDraggedField, hasField } from '../utils'; import { IndexPatternPrivateState, IndexPatternField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { DateRange } from '../../../../../../plugins/lens/common'; -export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { - state: IndexPatternPrivateState; - setState: StateSetter; - dragDropContext: DragContextState; +export type IndexPatternDimensionTriggerProps = DatasourceDimensionTriggerProps< + IndexPatternPrivateState +> & { + uniqueLabel: string; +}; + +export type IndexPatternDimensionEditorProps = DatasourceDimensionEditorProps< + IndexPatternPrivateState +> & { uiSettings: IUiSettingsClient; storage: IStorageWrapper; savedObjectsClient: SavedObjectsClientContract; @@ -41,152 +50,181 @@ export interface OperationFieldSupportMatrix { fieldByOperation: Partial>; } -export const IndexPatternDimensionPanelComponent = function IndexPatternDimensionPanel( - props: IndexPatternDimensionPanelProps -) { +type Props = Pick< + DatasourceDimensionDropProps, + 'layerId' | 'columnId' | 'state' | 'filterOperations' +>; + +// TODO: This code has historically been memoized, as a potentially performance +// sensitive task. If we can add memoization without breaking the behavior, we should. +const getOperationFieldSupportMatrix = (props: Props): OperationFieldSupportMatrix => { const layerId = props.layerId; const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; - const operationFieldSupportMatrix = useMemo(() => { - const filteredOperationsByMetadata = getAvailableOperationsByMetadata( - currentIndexPattern - ).filter(operation => props.filterOperations(operation.operationMetaData)); - - const supportedOperationsByField: Partial> = {}; - const supportedFieldsByOperation: Partial> = {}; - - filteredOperationsByMetadata.forEach(({ operations }) => { - operations.forEach(operation => { - if (supportedOperationsByField[operation.field]) { - supportedOperationsByField[operation.field]!.push(operation.operationType); - } else { - supportedOperationsByField[operation.field] = [operation.operationType]; - } - - if (supportedFieldsByOperation[operation.operationType]) { - supportedFieldsByOperation[operation.operationType]!.push(operation.field); - } else { - supportedFieldsByOperation[operation.operationType] = [operation.field]; - } - }); + const filteredOperationsByMetadata = getAvailableOperationsByMetadata( + currentIndexPattern + ).filter(operation => props.filterOperations(operation.operationMetaData)); + + const supportedOperationsByField: Partial> = {}; + const supportedFieldsByOperation: Partial> = {}; + + filteredOperationsByMetadata.forEach(({ operations }) => { + operations.forEach(operation => { + if (supportedOperationsByField[operation.field]) { + supportedOperationsByField[operation.field]!.push(operation.operationType); + } else { + supportedOperationsByField[operation.field] = [operation.operationType]; + } + + if (supportedFieldsByOperation[operation.operationType]) { + supportedFieldsByOperation[operation.operationType]!.push(operation.field); + } else { + supportedFieldsByOperation[operation.operationType] = [operation.field]; + } }); - return { - operationByField: _.mapValues(supportedOperationsByField, _.uniq), - fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), - }; - }, [currentIndexPattern, props.filterOperations]); + }); + return { + operationByField: _.mapValues(supportedOperationsByField, _.uniq), + fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), + }; +}; - const selectedColumn: IndexPatternColumn | null = - props.state.layers[layerId].columns[props.columnId] || null; +export function canHandleDrop(props: DatasourceDimensionDropProps) { + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + + const { dragging } = props.dragDropContext; + const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; function hasOperationForField(field: IndexPatternField) { return Boolean(operationFieldSupportMatrix.operationByField[field.name]); } - function canHandleDrop() { - const { dragging } = props.dragDropContext; - const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; + return ( + isDraggedField(dragging) && + layerIndexPatternId === dragging.indexPatternId && + Boolean(hasOperationForField(dragging.field)) + ); +} + +export function onDrop( + props: DatasourceDimensionDropHandlerProps +): boolean { + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + const droppedItem = props.droppedItem; + + function hasOperationForField(field: IndexPatternField) { + return Boolean(operationFieldSupportMatrix.operationByField[field.name]); + } - return ( - isDraggedField(dragging) && - layerIndexPatternId === dragging.indexPatternId && - Boolean(hasOperationForField(dragging.field)) - ); + if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { + // TODO: What do we do if we couldn't find a column? + return false; } + const operationsForNewField = + operationFieldSupportMatrix.operationByField[droppedItem.field.name]; + + const layerId = props.layerId; + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + const currentIndexPattern = + props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId]; + + // We need to check if dragging in a new field, was just a field change on the same + // index pattern and on the same operations (therefore checking if the new field supports + // our previous operation) + const hasFieldChanged = + selectedColumn && + hasField(selectedColumn) && + selectedColumn.sourceField !== droppedItem.field.name && + operationsForNewField && + operationsForNewField.includes(selectedColumn.operationType); + + // If only the field has changed use the onFieldChange method on the operation to get the + // new column, otherwise use the regular buildColumn to get a new column. + const newColumn = hasFieldChanged + ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) + : buildColumn({ + op: operationsForNewField ? operationsForNewField[0] : undefined, + columns: props.state.layers[props.layerId].columns, + indexPattern: currentIndexPattern, + layerId, + suggestedPriority: props.suggestedPriority, + field: droppedItem.field, + previousColumn: selectedColumn, + }); + + trackUiEvent('drop_onto_dimension'); + const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length); + trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); + + props.setState( + changeColumn({ + state: props.state, + layerId, + columnId: props.columnId, + newColumn, + // If the field has changed, the onFieldChange method needs to take care of everything including moving + // over params. If we create a new column above we want changeColumn to move over params. + keepParams: !hasFieldChanged, + }) + ); + + return true; +} + +export const IndexPatternDimensionTriggerComponent = function IndexPatternDimensionTrigger( + props: IndexPatternDimensionTriggerProps +) { + const layerId = props.layerId; + + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + + const { columnId, uniqueLabel } = props; + if (!selectedColumn) { + return null; + } + return ( + { + props.togglePopover(); + }} + data-test-subj="lns-dimensionTrigger" + aria-label={i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit configuration', + })} + title={i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit configuration', + })} + > + {uniqueLabel} + + ); +}; + +export const IndexPatternDimensionEditorComponent = function IndexPatternDimensionPanel( + props: IndexPatternDimensionEditorProps +) { + const layerId = props.layerId; + const currentIndexPattern = + props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId]; + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + return ( - - { - if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { - // TODO: What do we do if we couldn't find a column? - return; - } - - const operationsForNewField = - operationFieldSupportMatrix.operationByField[droppedItem.field.name]; - - // We need to check if dragging in a new field, was just a field change on the same - // index pattern and on the same operations (therefore checking if the new field supports - // our previous operation) - const hasFieldChanged = - selectedColumn && - hasField(selectedColumn) && - selectedColumn.sourceField !== droppedItem.field.name && - operationsForNewField && - operationsForNewField.includes(selectedColumn.operationType); - - // If only the field has changed use the onFieldChange method on the operation to get the - // new column, otherwise use the regular buildColumn to get a new column. - const newColumn = hasFieldChanged - ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) - : buildColumn({ - op: operationsForNewField ? operationsForNewField[0] : undefined, - columns: props.state.layers[props.layerId].columns, - indexPattern: currentIndexPattern, - layerId, - suggestedPriority: props.suggestedPriority, - field: droppedItem.field, - previousColumn: selectedColumn, - }); - - trackUiEvent('drop_onto_dimension'); - const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length); - trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); - - props.setState( - changeColumn({ - state: props.state, - layerId, - columnId: props.columnId, - newColumn, - // If the field has changed, the onFieldChange method needs to take care of everything including moving - // over params. If we create a new column above we want changeColumn to move over params. - keepParams: !hasFieldChanged, - }) - ); - }} - > - - {selectedColumn && ( - { - trackUiEvent('indexpattern_dimension_removed'); - props.setState( - deleteColumn({ - state: props.state, - layerId, - columnId: props.columnId, - }) - ); - if (props.onRemove) { - props.onRemove(props.columnId); - } - }} - /> - )} - - + ); }; -export const IndexPatternDimensionPanel = memo(IndexPatternDimensionPanelComponent); +export const IndexPatternDimensionTrigger = memo(IndexPatternDimensionTriggerComponent); +export const IndexPatternDimensionEditor = memo(IndexPatternDimensionEditorComponent); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index 056a8d177dfe..e26c338b6e24 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -7,22 +7,18 @@ import _ from 'lodash'; import React, { useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiPopover, EuiFlexItem, EuiFlexGroup, EuiSideNav, EuiCallOut, EuiFormRow, EuiFieldText, - EuiLink, - EuiButtonEmpty, EuiSpacer, } from '@elastic/eui'; import classNames from 'classnames'; import { IndexPatternColumn, OperationType } from '../indexpattern'; -import { IndexPatternDimensionPanelProps, OperationFieldSupportMatrix } from './dimension_panel'; +import { IndexPatternDimensionEditorProps, OperationFieldSupportMatrix } from './dimension_panel'; import { operationDefinitionMap, getOperationDisplay, @@ -39,7 +35,7 @@ import { FormatSelector } from './format_selector'; const operationPanels = getOperationDisplay(); -export interface PopoverEditorProps extends IndexPatternDimensionPanelProps { +export interface PopoverEditorProps extends IndexPatternDimensionEditorProps { selectedColumn?: IndexPatternColumn; operationFieldSupportMatrix: OperationFieldSupportMatrix; currentIndexPattern: IndexPattern; @@ -67,11 +63,9 @@ export function PopoverEditor(props: PopoverEditorProps) { setState, layerId, currentIndexPattern, - uniqueLabel, hideGrouping, } = props; const { operationByField, fieldByOperation } = operationFieldSupportMatrix; - const [isPopoverOpen, setPopoverOpen] = useState(false); const [ incompatibleSelectedOperationType, setInvalidOperationType, @@ -115,14 +109,14 @@ export function PopoverEditor(props: PopoverEditorProps) { items: getOperationTypes().map(({ operationType, compatibleWithCurrentField }) => ({ name: operationPanels[operationType].displayName, id: operationType as string, - className: classNames('lnsPopoverEditor__operation', { - 'lnsPopoverEditor__operation--selected': Boolean( + className: classNames('lnsIndexPatternDimensionEditor__operation', { + 'lnsIndexPatternDimensionEditor__operation--selected': Boolean( incompatibleSelectedOperationType === operationType || (!incompatibleSelectedOperationType && selectedColumn && selectedColumn.operationType === operationType) ), - 'lnsPopoverEditor__operation--incompatible': !compatibleWithCurrentField, + 'lnsIndexPatternDimensionEditor__operation--incompatible': !compatibleWithCurrentField, }), 'data-test-subj': `lns-indexPatternDimension${ compatibleWithCurrentField ? '' : 'Incompatible' @@ -188,246 +182,193 @@ export function PopoverEditor(props: PopoverEditorProps) { } return ( - { - setPopoverOpen(!isPopoverOpen); +
+ + + { + setState( + deleteColumn({ + state, + layerId, + columnId, + }) + ); }} - data-test-subj="indexPattern-configure-dimension" - aria-label={i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Edit configuration', - })} - title={i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Edit configuration', - })} - > - {uniqueLabel} - - ) : ( - <> - setPopoverOpen(!isPopoverOpen)} - size="xs" - > - - - - ) - } - isOpen={isPopoverOpen} - closePopover={() => { - setPopoverOpen(false); - setInvalidOperationType(null); - }} - anchorPosition="leftUp" - withTitle - panelPaddingSize="s" - > - {isPopoverOpen && ( - - - { - setState( - deleteColumn({ - state, - layerId, - columnId, - }) - ); - }} - onChoose={choice => { - let column: IndexPatternColumn; - if ( - !incompatibleSelectedOperationType && - selectedColumn && - 'field' in choice && - choice.operationType === selectedColumn.operationType - ) { - // If we just changed the field are not in an error state and the operation didn't change, - // we use the operations onFieldChange method to calculate the new column. - column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); - } else { - // Otherwise we'll use the buildColumn method to calculate a new column - const compatibleOperations = - ('field' in choice && - operationFieldSupportMatrix.operationByField[choice.field]) || - []; - let operation; - if (compatibleOperations.length > 0) { - operation = - incompatibleSelectedOperationType && - compatibleOperations.includes(incompatibleSelectedOperationType) - ? incompatibleSelectedOperationType - : compatibleOperations[0]; - } else if ('field' in choice) { - operation = choice.operationType; - } - column = buildColumn({ - columns: props.state.layers[props.layerId].columns, - field: fieldMap[choice.field], - indexPattern: currentIndexPattern, - layerId: props.layerId, - suggestedPriority: props.suggestedPriority, - op: operation as OperationType, - previousColumn: selectedColumn, - }); + onChoose={choice => { + let column: IndexPatternColumn; + if ( + !incompatibleSelectedOperationType && + selectedColumn && + 'field' in choice && + choice.operationType === selectedColumn.operationType + ) { + // If we just changed the field are not in an error state and the operation didn't change, + // we use the operations onFieldChange method to calculate the new column. + column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); + } else { + // Otherwise we'll use the buildColumn method to calculate a new column + const compatibleOperations = + ('field' in choice && + operationFieldSupportMatrix.operationByField[choice.field]) || + []; + let operation; + if (compatibleOperations.length > 0) { + operation = + incompatibleSelectedOperationType && + compatibleOperations.includes(incompatibleSelectedOperationType) + ? incompatibleSelectedOperationType + : compatibleOperations[0]; + } else if ('field' in choice) { + operation = choice.operationType; } + column = buildColumn({ + columns: props.state.layers[props.layerId].columns, + field: fieldMap[choice.field], + indexPattern: currentIndexPattern, + layerId: props.layerId, + suggestedPriority: props.suggestedPriority, + op: operation as OperationType, + previousColumn: selectedColumn, + }); + } - setState( - changeColumn({ - state, - layerId, - columnId, - newColumn: column, - keepParams: false, - }) - ); - setInvalidOperationType(null); - }} - /> - - - - - - - - {incompatibleSelectedOperationType && selectedColumn && ( - - )} - {incompatibleSelectedOperationType && !selectedColumn && ( - - )} - {!incompatibleSelectedOperationType && ParamEditor && ( - <> - - - - )} - {!incompatibleSelectedOperationType && selectedColumn && ( - - { - setState( - changeColumn({ - state, - layerId, - columnId, - newColumn: { - ...selectedColumn, - label: e.target.value, - }, - }) - ); - }} - /> - - )} - - {!hideGrouping && ( - { - setState({ - ...state, - layers: { - ...state.layers, - [props.layerId]: { - ...state.layers[props.layerId], - columnOrder, - }, - }, - }); - }} + setState( + changeColumn({ + state, + layerId, + columnId, + newColumn: column, + keepParams: false, + }) + ); + setInvalidOperationType(null); + }} + /> + + + + + + + + {incompatibleSelectedOperationType && selectedColumn && ( + + )} + {incompatibleSelectedOperationType && !selectedColumn && ( + + )} + {!incompatibleSelectedOperationType && ParamEditor && ( + <> + - )} - - {selectedColumn && selectedColumn.dataType === 'number' ? ( - { + + + )} + {!incompatibleSelectedOperationType && selectedColumn && ( + + { setState( - updateColumnParam({ + changeColumn({ state, layerId, - currentColumn: selectedColumn, - paramName: 'format', - value: newFormat, + columnId, + newColumn: { + ...selectedColumn, + label: e.target.value, + }, }) ); }} /> - ) : null} - - - - - )} - + + )} + + {!hideGrouping && ( + { + setState({ + ...state, + layers: { + ...state.layers, + [props.layerId]: { + ...state.layers[props.layerId], + columnOrder, + }, + }, + }); + }} + /> + )} + + {selectedColumn && selectedColumn.dataType === 'number' ? ( + { + setState( + updateColumnParam({ + state, + layerId, + currentColumn: selectedColumn, + paramName: 'format', + value: newFormat, + }) + ); + }} + /> + ) : null} + + + + +
); } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 25121eec30f2..76e59a170a9e 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -408,7 +408,6 @@ describe('IndexPattern Data Source', () => { const initialState = stateFromPersistedState(persistedState); publicAPI = indexPatternDatasource.getPublicAPI({ state: initialState, - setState: () => {}, layerId: 'first', dateRange: { fromDate: 'now-30d', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 00f52d6a1747..9c2a9c9bf4a0 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -12,7 +12,8 @@ import { CoreStart } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { - DatasourceDimensionPanelProps, + DatasourceDimensionEditorProps, + DatasourceDimensionTriggerProps, DatasourceDataPanelProps, Operation, DatasourceLayerPanelProps, @@ -20,7 +21,12 @@ import { } from '../types'; import { loadInitialState, changeIndexPattern, changeLayerIndexPattern } from './loader'; import { toExpression } from './to_expression'; -import { IndexPatternDimensionPanel } from './dimension_panel'; +import { + IndexPatternDimensionTrigger, + IndexPatternDimensionEditor, + canHandleDrop, + onDrop, +} from './dimension_panel'; import { IndexPatternDataPanel } from './datapanel'; import { getDatasourceSuggestionsForField, @@ -38,6 +44,7 @@ import { } from './types'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Plugin as DataPlugin } from '../../../../../../src/plugins/data/public'; +import { deleteColumn } from './state_helpers'; import { Datasource, StateSetter } from '..'; export { OperationType, IndexPatternColumn } from './operations'; @@ -80,6 +87,9 @@ export function uniqueLabels(layers: Record) { }; Object.values(layers).forEach(layer => { + if (!layer.columns) { + return; + } Object.entries(layer.columns).forEach(([columnId, column]) => { columnLabelMap[columnId] = makeUnique(column.label); }); @@ -156,6 +166,14 @@ export function getIndexPatternDatasource({ return Object.keys(state.layers); }, + removeColumn({ prevState, layerId, columnId }) { + return deleteColumn({ + state: prevState, + layerId, + columnId, + }); + }, + toExpression, getMetaData(state: IndexPatternPrivateState) { @@ -198,15 +216,97 @@ export function getIndexPatternDatasource({ ); }, - getPublicAPI({ - state, - setState, - layerId, - dateRange, - }: PublicAPIProps) { + renderDimensionTrigger: ( + domElement: Element, + props: DatasourceDimensionTriggerProps + ) => { + const columnLabelMap = uniqueLabels(props.state.layers); + + render( + + + + + , + domElement + ); + }, + + renderDimensionEditor: ( + domElement: Element, + props: DatasourceDimensionEditorProps + ) => { + const columnLabelMap = uniqueLabels(props.state.layers); + + render( + + + + + , + domElement + ); + }, + + renderLayerPanel: ( + domElement: Element, + props: DatasourceLayerPanelProps + ) => { + render( + { + changeLayerIndexPattern({ + savedObjectsClient, + indexPatternId, + setState: props.setState, + state: props.state, + layerId: props.layerId, + onError: onIndexPatternLoadError, + replaceIfPossible: true, + }); + }} + {...props} + />, + domElement + ); + }, + + canHandleDrop, + onDrop, + + getPublicAPI({ state, layerId }: PublicAPIProps) { const columnLabelMap = uniqueLabels(state.layers); return { + datasourceId: 'indexpattern', + getTableSpec: () => { return state.layers[layerId].columnOrder.map(colId => ({ columnId: colId })); }, @@ -218,58 +318,6 @@ export function getIndexPatternDatasource({ } return null; }, - renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => { - render( - - - - - , - domElement - ); - }, - - renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => { - render( - { - changeLayerIndexPattern({ - savedObjectsClient, - indexPatternId, - setState, - state, - layerId: props.layerId, - onError: onIndexPatternLoadError, - replaceIfPossible: true, - }); - }} - {...props} - />, - domElement - ); - }, }; }, getDatasourceSuggestionsForField(state, draggedField) { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index af7afb9cf934..219a6d935e43 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -178,6 +178,7 @@ describe('Layer Data Panel', () => { defaultProps = { layerId: 'first', state: initialState, + setState: jest.fn(), onChangeIndexPattern: jest.fn(async () => {}), }; }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx index ae346ecc72cb..eea00d52a77f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx @@ -11,7 +11,8 @@ import { DatasourceLayerPanelProps } from '../types'; import { IndexPatternPrivateState } from './types'; import { ChangeIndexPattern } from './change_indexpattern'; -export interface IndexPatternLayerPanelProps extends DatasourceLayerPanelProps { +export interface IndexPatternLayerPanelProps + extends DatasourceLayerPanelProps { state: IndexPatternPrivateState; onChangeIndexPattern: (newId: string) => void; } diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx deleted file mode 100644 index eac35f82a50f..000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx +++ /dev/null @@ -1,69 +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 { ReactWrapper } from 'enzyme'; -import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; -import { MetricConfigPanel } from './metric_config_panel'; -import { DatasourceDimensionPanelProps, Operation, DatasourcePublicAPI } from '../types'; -import { State } from './types'; -import { NativeRendererProps } from '../native_renderer'; -import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; - -describe('MetricConfigPanel', () => { - const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; - - function mockDatasource(): DatasourcePublicAPI { - return createMockDatasource().publicAPIMock; - } - - function testState(): State { - return { - accessor: 'foo', - layerId: 'bar', - }; - } - - function testSubj(component: ReactWrapper, subj: string) { - return component - .find(`[data-test-subj="${subj}"]`) - .first() - .props(); - } - - test('the value dimension panel only accepts singular numeric operations', () => { - const state = testState(); - const component = mount( - - ); - - const panel = testSubj(component, 'lns_metric_valueDimensionPanel'); - const nativeProps = (panel as NativeRendererProps).nativeProps; - const { columnId, filterOperations } = nativeProps; - const exampleOperation: Operation = { - dataType: 'number', - isBucketed: false, - label: 'bar', - }; - const ops: Operation[] = [ - { ...exampleOperation, dataType: 'number' }, - { ...exampleOperation, dataType: 'string' }, - { ...exampleOperation, dataType: 'boolean' }, - { ...exampleOperation, dataType: 'date' }, - ]; - expect(columnId).toEqual('shazm'); - expect(ops.filter(filterOperations)).toEqual([{ ...exampleOperation, dataType: 'number' }]); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx deleted file mode 100644 index 16e24f247fb6..000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx +++ /dev/null @@ -1,39 +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 { EuiFormRow } from '@elastic/eui'; -import { State } from './types'; -import { VisualizationLayerConfigProps, OperationMetadata } from '../types'; -import { NativeRenderer } from '../native_renderer'; - -const isMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; - -export function MetricConfigPanel(props: VisualizationLayerConfigProps) { - const { state, frame, layerId } = props; - const datasource = frame.datasourceLayers[layerId]; - - return ( - - - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx index 66ed963002f5..4d979a766cd2 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx @@ -91,6 +91,11 @@ export function MetricChart({ const { title, accessor, mode } = args; let value = '-'; const firstTable = Object.values(data.tables)[0]; + if (!accessor) { + return ( + + ); + } if (firstTable) { const column = firstTable.columns[0]; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts index 88964b95c2ac..276f24433c67 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts @@ -24,8 +24,8 @@ function mockFrame(): FramePublicAPI { ...createMockFramePublicAPI(), addNewLayer: () => 'l42', datasourceLayers: { - l1: createMockDatasource().publicAPIMock, - l42: createMockDatasource().publicAPIMock, + l1: createMockDatasource('l1').publicAPIMock, + l42: createMockDatasource('l42').publicAPIMock, }, }; } @@ -36,10 +36,10 @@ describe('metric_visualization', () => { (generateId as jest.Mock).mockReturnValueOnce('test-id1'); const initialState = metricVisualization.initialize(mockFrame()); - expect(initialState.accessor).toBeDefined(); + expect(initialState.accessor).not.toBeDefined(); expect(initialState).toMatchInlineSnapshot(` Object { - "accessor": "test-id1", + "accessor": undefined, "layerId": "l42", } `); @@ -60,7 +60,7 @@ describe('metric_visualization', () => { it('returns a clean layer', () => { (generateId as jest.Mock).mockReturnValueOnce('test-id1'); expect(metricVisualization.clearLayer(exampleState(), 'l1')).toEqual({ - accessor: 'test-id1', + accessor: undefined, layerId: 'l1', }); }); @@ -72,10 +72,47 @@ describe('metric_visualization', () => { }); }); + describe('#setDimension', () => { + it('sets the accessor', () => { + expect( + metricVisualization.setDimension({ + prevState: { + accessor: undefined, + layerId: 'l1', + }, + layerId: 'l1', + groupId: '', + columnId: 'newDimension', + }) + ).toEqual({ + accessor: 'newDimension', + layerId: 'l1', + }); + }); + }); + + describe('#removeDimension', () => { + it('removes the accessor', () => { + expect( + metricVisualization.removeDimension({ + prevState: { + accessor: 'a', + layerId: 'l1', + }, + layerId: 'l1', + columnId: 'a', + }) + ).toEqual({ + accessor: undefined, + layerId: 'l1', + }); + }); + }); + describe('#toExpression', () => { it('should map to a valid AST', () => { const datasource: DatasourcePublicAPI = { - ...createMockDatasource().publicAPIMock, + ...createMockDatasource('l1').publicAPIMock, getOperationForColumnId(_: string) { return { id: 'a', diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx index 6714c0578783..44256df5aed6 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -4,23 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { render } from 'react-dom'; -import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Ast } from '@kbn/interpreter/target/common'; import { getSuggestions } from './metric_suggestions'; -import { MetricConfigPanel } from './metric_config_panel'; -import { Visualization, FramePublicAPI } from '../types'; +import { Visualization, FramePublicAPI, OperationMetadata } from '../types'; import { State, PersistableState } from './types'; -import { generateId } from '../id_generator'; import chartMetricSVG from '../assets/chart_metric.svg'; const toExpression = ( state: State, frame: FramePublicAPI, mode: 'reduced' | 'full' = 'full' -): Ast => { +): Ast | null => { + if (!state.accessor) { + return null; + } + const [datasource] = Object.values(frame.datasourceLayers); const operation = datasource && datasource.getOperationForColumnId(state.accessor); @@ -57,7 +56,7 @@ export const metricVisualization: Visualization = { clearLayer(state) { return { ...state, - accessor: generateId(), + accessor: undefined, }; }, @@ -80,22 +79,37 @@ export const metricVisualization: Visualization = { return ( state || { layerId: frame.addNewLayer(), - accessor: generateId(), + accessor: undefined, } ); }, getPersistableState: state => state, - renderLayerConfigPanel: (domElement, props) => - render( - - - , - domElement - ), + getConfiguration(props) { + return { + groups: [ + { + groupId: 'metric', + groupLabel: i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric' }), + layerId: props.state.layerId, + accessors: props.state.accessor ? [props.state.accessor] : [], + supportsMoreColumns: false, + filterOperations: (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number', + }, + ], + }; + }, toExpression, toPreviewExpression: (state: State, frame: FramePublicAPI) => toExpression(state, frame, 'reduced'), + + setDimension({ prevState, columnId }) { + return { ...prevState, accessor: columnId }; + }, + + removeDimension({ prevState }) { + return { ...prevState, accessor: undefined }; + }, }; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts index 6348d80b15e2..53fc10393425 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts @@ -6,7 +6,7 @@ export interface State { layerId: string; - accessor: string; + accessor?: string; } export interface MetricConfig extends State { diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/index.ts b/x-pack/legacy/plugins/lens/public/multi_column_editor/index.ts deleted file mode 100644 index 92bad0dc9076..000000000000 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export * from './multi_column_editor'; diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx deleted file mode 100644 index 38f48c9cdaf7..000000000000 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx +++ /dev/null @@ -1,71 +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 { createMockDatasource } from '../editor_frame_service/mocks'; -import { MultiColumnEditor } from './multi_column_editor'; -import { mount } from 'enzyme'; - -jest.useFakeTimers(); - -describe('MultiColumnEditor', () => { - it('should add a trailing accessor if the accessor list is empty', () => { - const onAdd = jest.fn(); - mount( - true} - layerId="foo" - onAdd={onAdd} - onRemove={jest.fn()} - testSubj="bar" - /> - ); - - expect(onAdd).toHaveBeenCalledTimes(0); - - jest.runAllTimers(); - - expect(onAdd).toHaveBeenCalledTimes(1); - }); - - it('should add a trailing accessor if the last accessor is configured', () => { - const onAdd = jest.fn(); - mount( - true} - layerId="foo" - onAdd={onAdd} - onRemove={jest.fn()} - testSubj="bar" - /> - ); - - expect(onAdd).toHaveBeenCalledTimes(0); - - jest.runAllTimers(); - - expect(onAdd).toHaveBeenCalledTimes(1); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx deleted file mode 100644 index 422f1dcf60f3..000000000000 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx +++ /dev/null @@ -1,63 +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, { useEffect } from 'react'; -import { NativeRenderer } from '../native_renderer'; -import { DatasourcePublicAPI, OperationMetadata } from '../types'; -import { DragContextState } from '../drag_drop'; - -interface Props { - accessors: string[]; - datasource: DatasourcePublicAPI; - dragDropContext: DragContextState; - onRemove: (accessor: string) => void; - onAdd: () => void; - filterOperations: (op: OperationMetadata) => boolean; - suggestedPriority?: 0 | 1 | 2 | undefined; - testSubj: string; - layerId: string; -} - -export function MultiColumnEditor({ - accessors, - datasource, - dragDropContext, - onRemove, - onAdd, - filterOperations, - suggestedPriority, - testSubj, - layerId, -}: Props) { - const lastOperation = datasource.getOperationForColumnId(accessors[accessors.length - 1]); - - useEffect(() => { - if (accessors.length === 0 || lastOperation !== null) { - setTimeout(onAdd); - } - }, [lastOperation]); - - return ( - <> - {accessors.map(accessor => ( -
- -
- ))} - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx index cc029fee49d1..c74653c70703 100644 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -114,7 +114,7 @@ export class LensPlugin { const savedObjectsClient = coreStart.savedObjects.client; addHelpMenuToAppChrome(coreStart.chrome); - const instance = await this.createEditorFrame!({}); + const instance = await this.createEditorFrame!(); setReportManager( new LensReportManager({ diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index b7983eeb8dbb..c897979b06cf 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -13,14 +13,10 @@ import { Document } from './persistence'; import { DateRange } from '../../../../plugins/lens/common'; import { Query, Filter, SavedQuery } from '../../../../../src/plugins/data/public'; -// eslint-disable-next-line -export interface EditorFrameOptions {} - export type ErrorCallback = (e: { message: string }) => void; export interface PublicAPIProps { state: T; - setState: StateSetter; layerId: string; dateRange: DateRange; } @@ -34,6 +30,7 @@ export interface EditorFrameProps { savedQuery?: SavedQuery; // Frame loader (app or embeddable) is expected to call this when it loads and updates + // This should be replaced with a top-down state onChange: (newState: { filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; doc: Document; @@ -53,7 +50,7 @@ export interface EditorFrameSetup { } export interface EditorFrameStart { - createInstance: (options: EditorFrameOptions) => Promise; + createInstance: () => Promise; } // Hints the default nesting to the data source. 0 is the highest priority @@ -138,8 +135,14 @@ export interface Datasource { removeLayer: (state: T, layerId: string) => T; clearLayer: (state: T, layerId: string) => T; getLayers: (state: T) => string[]; + removeColumn: (props: { prevState: T; layerId: string; columnId: string }) => T; renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; + renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps) => void; + renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; + renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; + canHandleDrop: (props: DatasourceDimensionDropProps) => boolean; + onDrop: (props: DatasourceDimensionDropHandlerProps) => boolean; toExpression: (state: T, layerId: string) => Ast | string | null; @@ -155,22 +158,11 @@ export interface Datasource { * This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource */ export interface DatasourcePublicAPI { - getTableSpec: () => TableSpec; + datasourceId: string; + getTableSpec: () => Array<{ columnId: string }>; getOperationForColumnId: (columnId: string) => Operation | null; - - // Render can be called many times - renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => void; - renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; } -export interface TableSpecColumn { - // Column IDs are the keys for internal state in data sources and visualizations - columnId: string; -} - -// TableSpec is managed by visualizations -export type TableSpec = TableSpecColumn[]; - export interface DatasourceDataPanelProps { state: T; dragDropContext: DragContextState; @@ -181,31 +173,61 @@ export interface DatasourceDataPanelProps { filters: Filter[]; } -// The only way a visualization has to restrict the query building -export interface DatasourceDimensionPanelProps { - layerId: string; - columnId: string; - - dragDropContext: DragContextState; - - // Visualizations can restrict operations based on their own rules +interface SharedDimensionProps { + /** Visualizations can restrict operations based on their own rules. + * For example, limiting to only bucketed or only numeric operations. + */ filterOperations: (operation: OperationMetadata) => boolean; - // Visualizations can hint at the role this dimension would play, which - // affects the default ordering of the query + /** Visualizations can hint at the role this dimension would play, which + * affects the default ordering of the query + */ suggestedPriority?: DimensionPriority; - onRemove?: (accessor: string) => void; - // Some dimension editors will allow users to change the operation grouping - // from the panel, and this lets the visualization hint that it doesn't want - // users to have that level of control + /** Some dimension editors will allow users to change the operation grouping + * from the panel, and this lets the visualization hint that it doesn't want + * users to have that level of control + */ hideGrouping?: boolean; } -export interface DatasourceLayerPanelProps { +export type DatasourceDimensionProps = SharedDimensionProps & { layerId: string; + columnId: string; + onRemove?: (accessor: string) => void; + state: T; +}; + +// The only way a visualization has to restrict the query building +export type DatasourceDimensionEditorProps = DatasourceDimensionProps & { + setState: StateSetter; + core: Pick; + dateRange: DateRange; +}; + +export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & { + dragDropContext: DragContextState; + togglePopover: () => void; +}; + +export interface DatasourceLayerPanelProps { + layerId: string; + state: T; + setState: StateSetter; } +export type DatasourceDimensionDropProps = SharedDimensionProps & { + layerId: string; + columnId: string; + state: T; + setState: StateSetter; + dragDropContext: DragContextState; +}; + +export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { + droppedItem: unknown; +}; + export type DataType = 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip'; // An operation represents a column in a table, not any information @@ -239,12 +261,32 @@ export interface LensMultiTable { }; } -export interface VisualizationLayerConfigProps { +export interface VisualizationConfigProps { layerId: string; - dragDropContext: DragContextState; frame: FramePublicAPI; state: T; +} + +export type VisualizationLayerWidgetProps = VisualizationConfigProps & { setState: (newState: T) => void; +}; + +type VisualizationDimensionGroupConfig = SharedDimensionProps & { + groupLabel: string; + + /** ID is passed back to visualization. For example, `x` */ + groupId: string; + accessors: string[]; + supportsMoreColumns: boolean; + /** If required, a warning will appear if accessors are empty */ + required?: boolean; + dataTestSubj?: string; +}; + +interface VisualizationDimensionChangeProps { + layerId: string; + columnId: string; + prevState: T; } /** @@ -329,16 +371,18 @@ export interface Visualization { visualizationTypes: VisualizationType[]; getLayerIds: (state: T) => string[]; - clearLayer: (state: T, layerId: string) => T; - removeLayer?: (state: T, layerId: string) => T; - appendLayer?: (state: T, layerId: string) => T; + // Layer context menu is used by visualizations for styling the entire layer + // For example, the XY visualization uses this to have multiple chart types getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined; + renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; - renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerConfigProps) => void; + getConfiguration: ( + props: VisualizationConfigProps + ) => { groups: VisualizationDimensionGroupConfig[] }; getDescription: ( state: T @@ -354,7 +398,13 @@ export interface Visualization { getPersistableState: (state: T) => P; - renderLayerConfigPanel: (domElement: Element, props: VisualizationLayerConfigProps) => void; + // Actions triggered by the frame which tell the datasource that a dimension is being changed + setDimension: ( + props: VisualizationDimensionChangeProps & { + groupId: string; + } + ) => T; + removeDimension: (props: VisualizationDimensionChangeProps) => T; toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap similarity index 96% rename from x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap rename to x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index 76af8328673a..6b68679bfd4e 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`xy_visualization #toExpression should map to a valid AST 1`] = ` +exports[`#toExpression should map to a valid AST 1`] = ` Object { "chain": Array [ Object { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts new file mode 100644 index 000000000000..6bc379ea33bc --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { Ast } from '@kbn/interpreter/target/common'; +import { Position } from '@elastic/charts'; +import { xyVisualization } from './xy_visualization'; +import { Operation } from '../types'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; + +describe('#toExpression', () => { + let mockDatasource: ReturnType; + let frame: ReturnType; + + beforeEach(() => { + frame = createMockFramePublicAPI(); + mockDatasource = createMockDatasource('testDatasource'); + + mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ + { columnId: 'd' }, + { columnId: 'a' }, + { columnId: 'b' }, + { columnId: 'c' }, + ]); + + mockDatasource.publicAPIMock.getOperationForColumnId.mockImplementation(col => { + return { label: `col_${col}`, dataType: 'number' } as Operation; + }); + + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + }); + + it('should map to a valid AST', () => { + expect( + xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b', 'c'], + }, + ], + }, + frame + ) + ).toMatchSnapshot(); + }); + + it('should not generate an expression when missing x', () => { + expect( + xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: undefined, + xAccessor: undefined, + accessors: ['a'], + }, + ], + }, + frame + ) + ).toBeNull(); + }); + + it('should not generate an expression when missing y', () => { + expect( + xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: undefined, + xAccessor: 'a', + accessors: [], + }, + ], + }, + frame + ) + ).toBeNull(); + }); + + it('should default to labeling all columns with their column label', () => { + const expression = xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b', 'c'], + }, + ], + }, + frame + )! as Ast; + + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('d'); + expect(expression.chain[0].arguments.xTitle).toEqual(['col_a']); + expect(expression.chain[0].arguments.yTitle).toEqual(['col_b']); + expect( + (expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.columnToLabel + ).toEqual([ + JSON.stringify({ + b: 'col_b', + c: 'col_c', + d: 'col_d', + }), + ]); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts index f0e932d14f28..9b068b0ca5ef 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts @@ -9,6 +9,10 @@ import { ScaleType } from '@elastic/charts'; import { State, LayerConfig } from './types'; import { FramePublicAPI, OperationMetadata } from '../types'; +interface ValidLayer extends LayerConfig { + xAccessor: NonNullable; +} + function xyTitles(layer: LayerConfig, frame: FramePublicAPI) { const defaults = { xTitle: 'x', @@ -22,8 +26,8 @@ function xyTitles(layer: LayerConfig, frame: FramePublicAPI) { if (!datasource) { return defaults; } - const x = datasource.getOperationForColumnId(layer.xAccessor); - const y = datasource.getOperationForColumnId(layer.accessors[0]); + const x = layer.xAccessor ? datasource.getOperationForColumnId(layer.xAccessor) : null; + const y = layer.accessors[0] ? datasource.getOperationForColumnId(layer.accessors[0]) : null; return { xTitle: x ? x.label : defaults.xTitle, @@ -36,26 +40,6 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => return null; } - const stateWithValidAccessors = { - ...state, - layers: state.layers.map(layer => { - const datasource = frame.datasourceLayers[layer.layerId]; - - const newLayer = { ...layer }; - - if (!datasource.getOperationForColumnId(layer.splitAccessor)) { - delete newLayer.splitAccessor; - } - - return { - ...newLayer, - accessors: layer.accessors.filter(accessor => - Boolean(datasource.getOperationForColumnId(accessor)) - ), - }; - }), - }; - const metadata: Record> = {}; state.layers.forEach(layer => { metadata[layer.layerId] = {}; @@ -68,12 +52,7 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => }); }); - return buildExpression( - stateWithValidAccessors, - metadata, - frame, - xyTitles(state.layers[0], frame) - ); + return buildExpression(state, metadata, frame, xyTitles(state.layers[0], frame)); }; export function toPreviewExpression(state: State, frame: FramePublicAPI) { @@ -122,82 +101,94 @@ export const buildExpression = ( metadata: Record>, frame?: FramePublicAPI, { xTitle, yTitle }: { xTitle: string; yTitle: string } = { xTitle: '', yTitle: '' } -): Ast => ({ - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_chart', - arguments: { - xTitle: [xTitle], - yTitle: [yTitle], - legend: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_legendConfig', - arguments: { - isVisible: [state.legend.isVisible], - position: [state.legend.position], +): Ast | null => { + const validLayers = state.layers.filter((layer): layer is ValidLayer => + Boolean(layer.xAccessor && layer.accessors.length) + ); + if (!validLayers.length) { + return null; + } + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_chart', + arguments: { + xTitle: [xTitle], + yTitle: [yTitle], + legend: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_legendConfig', + arguments: { + isVisible: [state.legend.isVisible], + position: [state.legend.position], + }, }, - }, - ], - }, - ], - layers: state.layers.map(layer => { - const columnToLabel: Record = {}; - - if (frame) { - const datasource = frame.datasourceLayers[layer.layerId]; - layer.accessors.concat([layer.splitAccessor]).forEach(accessor => { - const operation = datasource.getOperationForColumnId(accessor); - if (operation && operation.label) { - columnToLabel[accessor] = operation.label; - } - }); - } - - const xAxisOperation = - frame && frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor); - - const isHistogramDimension = Boolean( - xAxisOperation && - xAxisOperation.isBucketed && - xAxisOperation.scale && - xAxisOperation.scale !== 'ordinal' - ); - - return { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_layer', - arguments: { - layerId: [layer.layerId], - - hide: [Boolean(layer.hide)], - - xAccessor: [layer.xAccessor], - yScaleType: [ - getScaleType(metadata[layer.layerId][layer.accessors[0]], ScaleType.Ordinal), - ], - xScaleType: [ - getScaleType(metadata[layer.layerId][layer.xAccessor], ScaleType.Linear), - ], - isHistogram: [isHistogramDimension], - splitAccessor: [layer.splitAccessor], - seriesType: [layer.seriesType], - accessors: layer.accessors, - columnToLabel: [JSON.stringify(columnToLabel)], + ], + }, + ], + layers: validLayers.map(layer => { + const columnToLabel: Record = {}; + + if (frame) { + const datasource = frame.datasourceLayers[layer.layerId]; + layer.accessors + .concat(layer.splitAccessor ? [layer.splitAccessor] : []) + .forEach(accessor => { + const operation = datasource.getOperationForColumnId(accessor); + if (operation && operation.label) { + columnToLabel[accessor] = operation.label; + } + }); + } + + const xAxisOperation = + frame && + frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor); + + const isHistogramDimension = Boolean( + xAxisOperation && + xAxisOperation.isBucketed && + xAxisOperation.scale && + xAxisOperation.scale !== 'ordinal' + ); + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_layer', + arguments: { + layerId: [layer.layerId], + + hide: [Boolean(layer.hide)], + + xAccessor: [layer.xAccessor], + yScaleType: [ + getScaleType(metadata[layer.layerId][layer.accessors[0]], ScaleType.Ordinal), + ], + xScaleType: [ + getScaleType(metadata[layer.layerId][layer.xAccessor], ScaleType.Linear), + ], + isHistogram: [isHistogramDimension], + splitAccessor: layer.splitAccessor ? [layer.splitAccessor] : [], + seriesType: [layer.seriesType], + accessors: layer.accessors, + columnToLabel: [JSON.stringify(columnToLabel)], + }, }, - }, - ], - }; - }), + ], + }; + }), + }, }, - }, - ], -}); + ], + }; +}; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts index b49e6fa6b4b6..f7b4afc76ec4 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts @@ -191,10 +191,10 @@ export type SeriesType = export interface LayerConfig { hide?: boolean; layerId: string; - xAccessor: string; + xAccessor?: string; accessors: string[]; seriesType: SeriesType; - splitAccessor: string; + splitAccessor?: string; } export type LayerArgs = LayerConfig & { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 301c4a58a0ff..7544ed0f87b7 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -5,22 +5,15 @@ */ import React from 'react'; -import { ReactWrapper } from 'enzyme'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { EuiButtonGroupProps } from '@elastic/eui'; -import { XYConfigPanel, LayerContextMenu } from './xy_config_panel'; -import { DatasourceDimensionPanelProps, Operation, FramePublicAPI } from '../types'; +import { LayerContextMenu } from './xy_config_panel'; +import { FramePublicAPI } from '../types'; import { State } from './types'; import { Position } from '@elastic/charts'; -import { NativeRendererProps } from '../native_renderer'; -import { generateId } from '../id_generator'; import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; -jest.mock('../id_generator'); - -describe('XYConfigPanel', () => { - const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; - +describe('LayerContextMenu', () => { let frame: FramePublicAPI; function testState(): State { @@ -39,17 +32,10 @@ describe('XYConfigPanel', () => { }; } - function testSubj(component: ReactWrapper, subj: string) { - return component - .find(`[data-test-subj="${subj}"]`) - .first() - .props(); - } - beforeEach(() => { frame = createMockFramePublicAPI(); frame.datasourceLayers = { - first: createMockDatasource().publicAPIMock, + first: createMockDatasource('test').publicAPIMock, }; }); @@ -64,7 +50,6 @@ describe('XYConfigPanel', () => { const component = mount( { const component = mount( { expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]); }); }); - - test('the x dimension panel accepts only bucketed operations', () => { - // TODO: this should eventually also accept raw operation - const state = testState(); - const component = mount( - - ); - - const panel = testSubj(component, 'lnsXY_xDimensionPanel'); - const nativeProps = (panel as NativeRendererProps).nativeProps; - const { columnId, filterOperations } = nativeProps; - const exampleOperation: Operation = { - dataType: 'number', - isBucketed: false, - label: 'bar', - }; - const bucketedOps: Operation[] = [ - { ...exampleOperation, isBucketed: true, dataType: 'number' }, - { ...exampleOperation, isBucketed: true, dataType: 'string' }, - { ...exampleOperation, isBucketed: true, dataType: 'boolean' }, - { ...exampleOperation, isBucketed: true, dataType: 'date' }, - ]; - const ops: Operation[] = [ - ...bucketedOps, - { ...exampleOperation, dataType: 'number' }, - { ...exampleOperation, dataType: 'string' }, - { ...exampleOperation, dataType: 'boolean' }, - { ...exampleOperation, dataType: 'date' }, - ]; - expect(columnId).toEqual('shazm'); - expect(ops.filter(filterOperations)).toEqual(bucketedOps); - }); - - test('the y dimension panel accepts numeric operations', () => { - const state = testState(); - const component = mount( - - ); - - const filterOperations = component - .find('[data-test-subj="lensXY_yDimensionPanel"]') - .first() - .prop('filterOperations') as (op: Operation) => boolean; - - const exampleOperation: Operation = { - dataType: 'number', - isBucketed: false, - label: 'bar', - }; - const ops: Operation[] = [ - { ...exampleOperation, dataType: 'number' }, - { ...exampleOperation, dataType: 'string' }, - { ...exampleOperation, dataType: 'boolean' }, - { ...exampleOperation, dataType: 'date' }, - ]; - expect(ops.filter(filterOperations).map(x => x.dataType)).toEqual(['number']); - }); - - test('allows removal of y dimensions', () => { - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - const onRemove = component - .find('[data-test-subj="lensXY_yDimensionPanel"]') - .first() - .prop('onRemove') as (accessor: string) => {}; - - onRemove('b'); - - expect(setState).toHaveBeenCalledTimes(1); - expect(setState.mock.calls[0][0]).toMatchObject({ - layers: [ - { - ...state.layers[0], - accessors: ['a', 'c'], - }, - ], - }); - }); - - test('allows adding a y axis dimension', () => { - (generateId as jest.Mock).mockReturnValueOnce('zed'); - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - const onAdd = component - .find('[data-test-subj="lensXY_yDimensionPanel"]') - .first() - .prop('onAdd') as () => {}; - - onAdd(); - - expect(setState).toHaveBeenCalledTimes(1); - expect(setState.mock.calls[0][0]).toMatchObject({ - layers: [ - { - ...state.layers[0], - accessors: ['a', 'b', 'c', 'zed'], - }, - ], - }); - }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx index dbcfa2439500..5e85680cc2b2 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -9,16 +9,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; import { State, SeriesType, visualizationTypes } from './types'; -import { VisualizationLayerConfigProps, OperationMetadata } from '../types'; -import { NativeRenderer } from '../native_renderer'; -import { MultiColumnEditor } from '../multi_column_editor'; -import { generateId } from '../id_generator'; +import { VisualizationLayerWidgetProps } from '../types'; import { isHorizontalChart, isHorizontalSeries } from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; -const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; -const isBucketed = (op: OperationMetadata) => op.isBucketed; - type UnwrapArray = T extends Array ? P : T; function updateLayer(state: State, layer: UnwrapArray, index: number): State { @@ -31,7 +25,7 @@ function updateLayer(state: State, layer: UnwrapArray, index: n }; } -export function LayerContextMenu(props: VisualizationLayerConfigProps) { +export function LayerContextMenu(props: VisualizationLayerWidgetProps) { const { state, layerId } = props; const horizontalOnly = isHorizontalChart(state.layers); const index = state.layers.findIndex(l => l.layerId === layerId); @@ -74,97 +68,3 @@ export function LayerContextMenu(props: VisualizationLayerConfigProps) { ); } - -export function XYConfigPanel(props: VisualizationLayerConfigProps) { - const { state, setState, frame, layerId } = props; - const index = props.state.layers.findIndex(l => l.layerId === layerId); - - if (index < 0) { - return null; - } - - const layer = props.state.layers[index]; - - return ( - <> - - - - - - setState( - updateLayer( - state, - { - ...layer, - accessors: [...layer.accessors, generateId()], - }, - index - ) - ) - } - onRemove={accessor => - setState( - updateLayer( - state, - { - ...layer, - accessors: layer.accessors.filter(col => col !== accessor), - }, - index - ) - ) - } - filterOperations={isNumericMetric} - data-test-subj="lensXY_yDimensionPanel" - testSubj="lensXY_yDimensionPanel" - layerId={layer.layerId} - /> - - - - - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx index 27fd6e706404..15aaf289eebf 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -238,6 +238,8 @@ export function XYChart({ data, args, formatFactory, timeZone, chartTheme }: XYC index ) => { if ( + !xAccessor || + !accessors.length || !data.tables[layerId] || data.tables[layerId].rows.length === 0 || data.tables[layerId].rows.every(row => typeof row[xAccessor] === 'undefined') @@ -246,7 +248,7 @@ export function XYChart({ data, args, formatFactory, timeZone, chartTheme }: XYC } const columnToLabelMap = columnToLabel ? JSON.parse(columnToLabel) : {}; - const splitAccessorLabel = columnToLabelMap[splitAccessor]; + const splitAccessorLabel = splitAccessor ? columnToLabelMap[splitAccessor] : ''; const yAccessors = accessors.map(accessor => columnToLabelMap[accessor] || accessor); const idForLegend = splitAccessorLabel || yAccessors; const sanitized = sanitizeRows({ diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index 04ff720309d6..ddbd9d11b5fa 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -123,7 +123,7 @@ describe('xy_suggestions', () => { Array [ Object { "seriesType": "bar_stacked", - "splitAccessor": "aaa", + "splitAccessor": undefined, "x": "date", "y": Array [ "bytes", @@ -240,7 +240,6 @@ describe('xy_suggestions', () => { }); test('only makes a seriesType suggestion for unchanged table without split', () => { - (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', @@ -249,7 +248,7 @@ describe('xy_suggestions', () => { accessors: ['price'], layerId: 'first', seriesType: 'bar', - splitAccessor: 'dummyCol', + splitAccessor: undefined, xAccessor: 'date', }, ], @@ -472,17 +471,17 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "ddd", - "x": "quantity", - "y": Array [ - "price", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "quantity", + "y": Array [ + "price", + ], + }, + ] + `); }); test('handles ip', () => { @@ -509,17 +508,17 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "ddd", - "x": "myip", - "y": Array [ - "quantity", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "myip", + "y": Array [ + "quantity", + ], + }, + ] + `); }); test('handles unbucketed suggestions', () => { @@ -545,16 +544,16 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "eee", - "x": "mybool", - "y": Array [ - "num votes", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "mybool", + "y": Array [ + "num votes", + ], + }, + ] + `); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts index 33181b7f3a46..5e9311bb1e92 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -15,7 +15,6 @@ import { TableChangeType, } from '../types'; import { State, SeriesType, XYState } from './types'; -import { generateId } from '../id_generator'; import { getIconForSeries } from './state_helpers'; const columnSortOrder = { @@ -356,7 +355,7 @@ function buildSuggestion({ layerId, seriesType, xAccessor: xValue.columnId, - splitAccessor: splitBy ? splitBy.columnId : generateId(), + splitAccessor: splitBy?.columnId, accessors: yValues.map(col => col.columnId), }; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts index a27a8e7754b8..beccf0dc46eb 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -9,10 +9,6 @@ import { Position } from '@elastic/charts'; import { Operation } from '../types'; import { State, SeriesType } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; -import { generateId } from '../id_generator'; -import { Ast } from '@kbn/interpreter/target/common'; - -jest.mock('../id_generator'); function exampleState(): State { return { @@ -87,31 +83,22 @@ describe('xy_visualization', () => { describe('#initialize', () => { it('loads default state', () => { - (generateId as jest.Mock) - .mockReturnValueOnce('test-id1') - .mockReturnValueOnce('test-id2') - .mockReturnValue('test-id3'); const mockFrame = createMockFramePublicAPI(); const initialState = xyVisualization.initialize(mockFrame); expect(initialState.layers).toHaveLength(1); - expect(initialState.layers[0].xAccessor).toBeDefined(); - expect(initialState.layers[0].accessors[0]).toBeDefined(); - expect(initialState.layers[0].xAccessor).not.toEqual(initialState.layers[0].accessors[0]); + expect(initialState.layers[0].xAccessor).not.toBeDefined(); + expect(initialState.layers[0].accessors).toHaveLength(0); expect(initialState).toMatchInlineSnapshot(` Object { "layers": Array [ Object { - "accessors": Array [ - "test-id1", - ], + "accessors": Array [], "layerId": "", "position": "top", "seriesType": "bar_stacked", "showGridlines": false, - "splitAccessor": "test-id2", - "xAccessor": "test-id3", }, ], "legend": Object { @@ -167,14 +154,11 @@ describe('xy_visualization', () => { describe('#clearLayer', () => { it('clears the specified layer', () => { - (generateId as jest.Mock).mockReturnValue('test_empty_id'); const layer = xyVisualization.clearLayer(exampleState(), 'first').layers[0]; expect(layer).toMatchObject({ - accessors: ['test_empty_id'], + accessors: [], layerId: 'first', seriesType: 'bar', - splitAccessor: 'test_empty_id', - xAccessor: 'test_empty_id', }); }); }); @@ -185,13 +169,94 @@ describe('xy_visualization', () => { }); }); - describe('#toExpression', () => { + describe('#setDimension', () => { + it('sets the x axis', () => { + expect( + xyVisualization.setDimension({ + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + }, + ], + }, + layerId: 'first', + groupId: 'x', + columnId: 'newCol', + }).layers[0] + ).toEqual({ + layerId: 'first', + seriesType: 'area', + xAccessor: 'newCol', + accessors: [], + }); + }); + + it('replaces the x axis', () => { + expect( + xyVisualization.setDimension({ + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + ], + }, + layerId: 'first', + groupId: 'x', + columnId: 'newCol', + }).layers[0] + ).toEqual({ + layerId: 'first', + seriesType: 'area', + xAccessor: 'newCol', + accessors: [], + }); + }); + }); + + describe('#removeDimension', () => { + it('removes the x axis', () => { + expect( + xyVisualization.removeDimension({ + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + ], + }, + layerId: 'first', + columnId: 'a', + }).layers[0] + ).toEqual({ + layerId: 'first', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + }); + }); + }); + + describe('#getConfiguration', () => { let mockDatasource: ReturnType; let frame: ReturnType; beforeEach(() => { frame = createMockFramePublicAPI(); - mockDatasource = createMockDatasource(); + mockDatasource = createMockDatasource('testDatasource'); mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ { columnId: 'd' }, @@ -200,36 +265,78 @@ describe('xy_visualization', () => { { columnId: 'c' }, ]); - mockDatasource.publicAPIMock.getOperationForColumnId.mockImplementation(col => { - return { label: `col_${col}`, dataType: 'number' } as Operation; - }); - frame.datasourceLayers = { first: mockDatasource.publicAPIMock, }; }); - it('should map to a valid AST', () => { - expect(xyVisualization.toExpression(exampleState(), frame)).toMatchSnapshot(); + it('should return options for 3 dimensions', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + expect(options).toHaveLength(3); + expect(options.map(o => o.groupId)).toEqual(['x', 'y', 'breakdown']); }); - it('should default to labeling all columns with their column label', () => { - const expression = xyVisualization.toExpression(exampleState(), frame)! as Ast; + it('should only accept bucketed operations for x', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + const filterOperations = options.find(o => o.groupId === 'x')!.filterOperations; - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('d'); - expect(expression.chain[0].arguments.xTitle).toEqual(['col_a']); - expect(expression.chain[0].arguments.yTitle).toEqual(['col_b']); - expect( - (expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.columnToLabel - ).toEqual([ - JSON.stringify({ - b: 'col_b', - c: 'col_c', - d: 'col_d', - }), - ]); + const exampleOperation: Operation = { + dataType: 'number', + isBucketed: false, + label: 'bar', + }; + const bucketedOps: Operation[] = [ + { ...exampleOperation, isBucketed: true, dataType: 'number' }, + { ...exampleOperation, isBucketed: true, dataType: 'string' }, + { ...exampleOperation, isBucketed: true, dataType: 'boolean' }, + { ...exampleOperation, isBucketed: true, dataType: 'date' }, + ]; + const ops: Operation[] = [ + ...bucketedOps, + { ...exampleOperation, dataType: 'number' }, + { ...exampleOperation, dataType: 'string' }, + { ...exampleOperation, dataType: 'boolean' }, + { ...exampleOperation, dataType: 'date' }, + ]; + expect(ops.filter(filterOperations)).toEqual(bucketedOps); + }); + + it('should not allow anything to be added to x', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + expect(options.find(o => o.groupId === 'x')?.supportsMoreColumns).toBe(false); + }); + + it('should allow number operations on y', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + const filterOperations = options.find(o => o.groupId === 'y')!.filterOperations; + const exampleOperation: Operation = { + dataType: 'number', + isBucketed: false, + label: 'bar', + }; + const ops: Operation[] = [ + { ...exampleOperation, dataType: 'number' }, + { ...exampleOperation, dataType: 'string' }, + { ...exampleOperation, dataType: 'boolean' }, + { ...exampleOperation, dataType: 'date' }, + ]; + expect(ops.filter(filterOperations).map(x => x.dataType)).toEqual(['number']); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx index 75d6fcc7d160..c72fa0fec24d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -11,17 +11,18 @@ import { Position } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { getSuggestions } from './xy_suggestions'; -import { XYConfigPanel, LayerContextMenu } from './xy_config_panel'; -import { Visualization } from '../types'; +import { LayerContextMenu } from './xy_config_panel'; +import { Visualization, OperationMetadata } from '../types'; import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types'; import { toExpression, toPreviewExpression } from './to_expression'; -import { generateId } from '../id_generator'; import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; import chartMixedSVG from '../assets/chart_mixed_xy.svg'; import { isHorizontalChart } from './state_helpers'; const defaultIcon = chartBarStackedSVG; const defaultSeriesType = 'bar_stacked'; +const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; +const isBucketed = (op: OperationMetadata) => op.isBucketed; function getDescription(state?: State) { if (!state) { @@ -133,12 +134,10 @@ export const xyVisualization: Visualization = { layers: [ { layerId: frame.addNewLayer(), - accessors: [generateId()], + accessors: [], position: Position.Top, seriesType: defaultSeriesType, showGridlines: false, - splitAccessor: generateId(), - xAccessor: generateId(), }, ], } @@ -147,13 +146,89 @@ export const xyVisualization: Visualization = { getPersistableState: state => state, - renderLayerConfigPanel: (domElement, props) => - render( - - - , - domElement - ), + getConfiguration(props) { + const layer = props.state.layers.find(l => l.layerId === props.layerId)!; + return { + groups: [ + { + groupId: 'x', + groupLabel: i18n.translate('xpack.lens.xyChart.xAxisLabel', { + defaultMessage: 'X-axis', + }), + accessors: layer.xAccessor ? [layer.xAccessor] : [], + filterOperations: isBucketed, + suggestedPriority: 1, + supportsMoreColumns: !layer.xAccessor, + required: true, + dataTestSubj: 'lnsXY_xDimensionPanel', + }, + { + groupId: 'y', + groupLabel: i18n.translate('xpack.lens.xyChart.yAxisLabel', { + defaultMessage: 'Y-axis', + }), + accessors: layer.accessors, + filterOperations: isNumericMetric, + supportsMoreColumns: true, + required: true, + dataTestSubj: 'lnsXY_yDimensionPanel', + }, + { + groupId: 'breakdown', + groupLabel: i18n.translate('xpack.lens.xyChart.splitSeries', { + defaultMessage: 'Break down by', + }), + accessors: layer.splitAccessor ? [layer.splitAccessor] : [], + filterOperations: isBucketed, + suggestedPriority: 0, + supportsMoreColumns: !layer.splitAccessor, + dataTestSubj: 'lnsXY_splitDimensionPanel', + }, + ], + }; + }, + + setDimension({ prevState, layerId, columnId, groupId }) { + const newLayer = prevState.layers.find(l => l.layerId === layerId); + if (!newLayer) { + return prevState; + } + + if (groupId === 'x') { + newLayer.xAccessor = columnId; + } + if (groupId === 'y') { + newLayer.accessors = [...newLayer.accessors.filter(a => a !== columnId), columnId]; + } + if (groupId === 'breakdown') { + newLayer.splitAccessor = columnId; + } + + return { + ...prevState, + layers: prevState.layers.map(l => (l.layerId === layerId ? newLayer : l)), + }; + }, + + removeDimension({ prevState, layerId, columnId }) { + const newLayer = prevState.layers.find(l => l.layerId === layerId); + if (!newLayer) { + return prevState; + } + + if (newLayer.xAccessor === columnId) { + delete newLayer.xAccessor; + } else if (newLayer.splitAccessor === columnId) { + delete newLayer.splitAccessor; + } else if (newLayer.accessors.includes(columnId)) { + newLayer.accessors = newLayer.accessors.filter(a => a !== columnId); + } + + return { + ...prevState, + layers: prevState.layers.map(l => (l.layerId === layerId ? newLayer : l)), + }; + }, getLayerContextMenuIcon({ state, layerId }) { const layer = state.layers.find(l => l.layerId === layerId); @@ -177,8 +252,6 @@ function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig { return { layerId, seriesType, - xAccessor: generateId(), - accessors: [generateId()], - splitAccessor: generateId(), + accessors: [], }; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c155344c7534..eb208e67ccfe 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6831,7 +6831,6 @@ "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "レイヤー{layerNumber}のみを表示", "xpack.lens.lensSavedObjectLabel": "レンズビジュアライゼーション", "xpack.lens.metric.label": "メトリック", - "xpack.lens.metric.valueLabel": "値", "xpack.lens.sugegstion.refreshSuggestionLabel": "更新", "xpack.lens.suggestion.refreshSuggestionTooltip": "選択したビジュアライゼーションに基づいて、候補を更新します。", "xpack.lens.suggestions.currentVisLabel": "現在", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ed0ac8f6f7fe..f85714a5913a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6831,7 +6831,6 @@ "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "仅显示图层 {layerNumber}", "xpack.lens.lensSavedObjectLabel": "Lens 可视化", "xpack.lens.metric.label": "指标", - "xpack.lens.metric.valueLabel": "值", "xpack.lens.sugegstion.refreshSuggestionLabel": "刷新", "xpack.lens.suggestion.refreshSuggestionTooltip": "基于选定可视化刷新建议。", "xpack.lens.suggestions.currentVisLabel": "当前", diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js index c90a0ae6d19f..19eebb3ba501 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js @@ -34,21 +34,21 @@ export default function({ getPageObjects, getService }) { await PageObjects.lens.goToTimeRange(); await PageObjects.lens.configureDimension({ dimension: - '[data-test-subj="lnsXY_xDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + '[data-test-subj="lnsXY_xDimensionPanel"] [data-test-subj="lns-empty-dimension"]', operation: 'date_histogram', field: '@timestamp', }); await PageObjects.lens.configureDimension({ dimension: - '[data-test-subj="lnsXY_yDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + '[data-test-subj="lnsXY_yDimensionPanel"] [data-test-subj="lns-empty-dimension"]', operation: 'avg', field: 'bytes', }); await PageObjects.lens.configureDimension({ dimension: - '[data-test-subj="lnsXY_splitDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + '[data-test-subj="lnsXY_splitDimensionPanel"] [data-test-subj="lns-empty-dimension"]', operation: 'terms', field: 'ip', }); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 3346f2ff7703..317bb0b27e97 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -77,21 +77,21 @@ export default function({ getService, getPageObjects, ...rest }: FtrProviderCont await PageObjects.lens.configureDimension({ dimension: - '[data-test-subj="lnsXY_xDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + '[data-test-subj="lnsXY_xDimensionPanel"] [data-test-subj="lns-empty-dimension"]', operation: 'date_histogram', field: '@timestamp', }); await PageObjects.lens.configureDimension({ dimension: - '[data-test-subj="lnsXY_yDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + '[data-test-subj="lnsXY_yDimensionPanel"] [data-test-subj="lns-empty-dimension"]', operation: 'avg', field: 'bytes', }); await PageObjects.lens.configureDimension({ dimension: - '[data-test-subj="lnsXY_splitDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + '[data-test-subj="lnsXY_splitDimensionPanel"] [data-test-subj="lns-empty-dimension"]', operation: 'terms', field: 'ip', }); From 53d23fcb3be3102d2b0c746766b6500d7c6db4fc Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Tue, 17 Mar 2020 08:34:39 -0600 Subject: [PATCH 042/115] [Endpoint] Adds take action dropdown and tests to alert details flyout (#59242) * adds dropdown * changes i18n fields * switches to buttons * adds tests for alert details flyout * updates es archiver data * finishes functional and react tests * cleanup tests for alerts * updates alert esarchive data * replaces es archives and fixes tests * rebase * fixes functional tests * suggested changes to take action button * addresses comments Co-authored-by: oatkiller --- .../view/alerts/alert_details.test.tsx | 81 + .../details/metadata/file_accordion.tsx | 1 + .../details/metadata/general_accordion.tsx | 1 + .../details/metadata/hash_accordion.tsx | 1 + .../details/metadata/host_accordion.tsx | 1 + .../metadata/source_process_accordion.tsx | 1 + .../source_process_token_accordion.tsx | 1 + .../view/alerts/details/overview/index.tsx | 15 +- .../details/overview/take_action_dropdown.tsx | 71 + .../endpoint/view/alerts/index.test.tsx | 57 +- .../alerts/test_helpers/render_alert_page.tsx | 59 + .../api_integration/apis/endpoint/alerts.ts | 155 +- .../api_integration/apis/endpoint/metadata.ts | 38 +- .../endpoint/{alert_list.ts => alerts.ts} | 33 +- x-pack/test/functional/apps/endpoint/index.ts | 2 +- .../functional/apps/endpoint/management.ts | 16 +- .../endpoint/alerts/api_feature/data.json.gz | Bin 2063322 -> 15777 bytes .../endpoint/alerts/api_feature/mappings.json | 5460 ++++------------- .../endpoint/metadata/api_feature/data.json | 382 -- .../metadata/api_feature/data.json.gz | Bin 0 -> 732 bytes .../metadata/api_feature/mappings.json | 33 +- .../page_objects/endpoint_alerts_page.ts | 11 +- 22 files changed, 1700 insertions(+), 4719 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/alert_details.test.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/take_action_dropdown.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/test_helpers/render_alert_page.tsx rename x-pack/test/functional/apps/endpoint/{alert_list.ts => alerts.ts} (55%) delete mode 100644 x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json create mode 100644 x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json.gz diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/alert_details.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/alert_details.test.tsx new file mode 100644 index 000000000000..0f5a9dd7fed1 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/alert_details.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as reactTestingLibrary from '@testing-library/react'; +import { appStoreFactory } from '../../store'; +import { fireEvent } from '@testing-library/react'; +import { MemoryHistory } from 'history'; +import { AppAction } from '../../types'; +import { mockAlertResultList } from '../../store/alerts/mock_alert_result_list'; +import { alertPageTestRender } from './test_helpers/render_alert_page'; + +describe('when the alert details flyout is open', () => { + let render: () => reactTestingLibrary.RenderResult; + let history: MemoryHistory; + let store: ReturnType; + + beforeEach(async () => { + // Creates the render elements for the tests to use + ({ render, history, store } = alertPageTestRender); + }); + describe('when the alerts details flyout is open', () => { + beforeEach(() => { + reactTestingLibrary.act(() => { + history.push({ + search: '?selected_alert=1', + }); + }); + }); + describe('when the data loads', () => { + beforeEach(() => { + reactTestingLibrary.act(() => { + const action: AppAction = { + type: 'serverReturnedAlertDetailsData', + payload: mockAlertResultList().alerts[0], + }; + store.dispatch(action); + }); + }); + it('should display take action button', async () => { + await render().findByTestId('alertDetailTakeActionDropdownButton'); + }); + describe('when the user clicks the take action button on the flyout', () => { + let renderResult: reactTestingLibrary.RenderResult; + beforeEach(async () => { + renderResult = render(); + const takeActionButton = await renderResult.findByTestId( + 'alertDetailTakeActionDropdownButton' + ); + if (takeActionButton) { + fireEvent.click(takeActionButton); + } + }); + it('should display the correct fields in the dropdown', async () => { + await renderResult.findByTestId('alertDetailTakeActionCloseAlertButton'); + await renderResult.findByTestId('alertDetailTakeActionWhitelistButton'); + }); + }); + describe('when the user navigates to the overview tab', () => { + let renderResult: reactTestingLibrary.RenderResult; + beforeEach(async () => { + renderResult = render(); + const overviewTab = await renderResult.findByTestId('overviewMetadata'); + if (overviewTab) { + fireEvent.click(overviewTab); + } + }); + it('should render all accordion panels', async () => { + await renderResult.findAllByTestId('alertDetailsAlertAccordion'); + await renderResult.findAllByTestId('alertDetailsHostAccordion'); + await renderResult.findAllByTestId('alertDetailsFileAccordion'); + await renderResult.findAllByTestId('alertDetailsHashAccordion'); + await renderResult.findAllByTestId('alertDetailsSourceProcessAccordion'); + await renderResult.findAllByTestId('alertDetailsSourceProcessTokenAccordion'); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/file_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/file_accordion.tsx index ac67e54f3877..26f198536846 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/file_accordion.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/file_accordion.tsx @@ -73,6 +73,7 @@ export const FileAccordion = memo(({ alertData }: { alertData: Immutable diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx index 070c78c96858..0183e9663bb4 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx @@ -61,6 +61,7 @@ export const GeneralAccordion = memo(({ alertData }: { alertData: Immutable diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/hash_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/hash_accordion.tsx index b2be083ce8f5..4a2f7378a36e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/hash_accordion.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/hash_accordion.tsx @@ -42,6 +42,7 @@ export const HashAccordion = memo(({ alertData }: { alertData: Immutable diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx index 4108781f0a79..edaba3725e02 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx @@ -48,6 +48,7 @@ export const HostAccordion = memo(({ alertData }: { alertData: Immutable diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx index 4d921ee39d95..4134bc35747d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx @@ -90,6 +90,7 @@ export const SourceProcessAccordion = memo(({ alertData }: { alertData: Immutabl } )} paddingSize="l" + data-test-subj="alertDetailsSourceProcessAccordion" > diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_token_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_token_accordion.tsx index 7d75d4478afb..00755673d3f8 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_token_accordion.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_token_accordion.tsx @@ -37,6 +37,7 @@ export const SourceProcessTokenAccordion = memo( } )} paddingSize="l" + data-test-subj="alertDetailsSourceProcessTokenAccordion" > 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 080c70ca43ba..82a4bc00a439 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 @@ -6,12 +6,20 @@ import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiTitle, EuiText, EuiHealth, EuiTabbedContent } from '@elastic/eui'; +import { + EuiSpacer, + EuiTitle, + EuiText, + EuiHealth, + EuiTabbedContent, + EuiTabbedContentTab, +} from '@elastic/eui'; import { useAlertListSelector } from '../../hooks/use_alerts_selector'; import * as selectors from '../../../../store/alerts/selectors'; import { MetadataPanel } from './metadata_panel'; import { FormattedDate } from '../../formatted_date'; import { AlertDetailResolver } from '../../resolver'; +import { TakeActionDropdown } from './take_action_dropdown'; export const AlertDetailsOverview = memo(() => { const alertDetailsData = useAlertListSelector(selectors.selectedAlertDetailsData); @@ -22,10 +30,11 @@ export const AlertDetailsOverview = memo(() => { selectors.selectedAlertIsLegacyEndpointEvent ); - const tabs = useMemo(() => { + const tabs: EuiTabbedContentTab[] = useMemo(() => { return [ { id: 'overviewMetadata', + 'data-test-subj': 'overviewMetadata', name: i18n.translate( 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.overview', { @@ -87,6 +96,8 @@ export const AlertDetailsOverview = memo(() => { Alert Status: Open + +

diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/take_action_dropdown.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/take_action_dropdown.tsx new file mode 100644 index 000000000000..8d8468b4df4a --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/take_action_dropdown.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useState, useCallback } from 'react'; +import { EuiPopover, EuiFormRow, EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +const TakeActionButton = memo(({ onClick }: { onClick: () => void }) => ( + + + +)); + +export const TakeActionDropdown = memo(() => { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const onClick = useCallback(() => { + setIsDropdownOpen(!isDropdownOpen); + }, [isDropdownOpen]); + + const closePopover = useCallback(() => { + setIsDropdownOpen(false); + }, []); + + return ( + } + isOpen={isDropdownOpen} + anchorPosition="downRight" + closePopover={closePopover} + data-test-subj="alertListTakeActionDropdownContent" + > + + + + + + + + + + + + + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx index 7fc5e18a6ba8..336c16b2c933 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx @@ -4,21 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { I18nProvider } from '@kbn/i18n/react'; -import { AlertIndex } from './index'; import { IIndexPattern } from 'src/plugins/data/public'; import { appStoreFactory } from '../../store'; -import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { fireEvent, act } from '@testing-library/react'; -import { RouteCapture } from '../route_capture'; -import { createMemoryHistory, MemoryHistory } from 'history'; -import { Router } from 'react-router-dom'; +import { MemoryHistory } from 'history'; import { AppAction } from '../../types'; import { mockAlertResultList } from '../../store/alerts/mock_alert_result_list'; -import { DepsStartMock, depsStartMock } from '../../mocks'; +import { DepsStartMock } from '../../mocks'; +import { alertPageTestRender } from './test_helpers/render_alert_page'; describe('when on the alerting page', () => { let render: () => reactTestingLibrary.RenderResult; @@ -27,42 +21,8 @@ describe('when on the alerting page', () => { let depsStart: DepsStartMock; beforeEach(async () => { - /** - * Create a 'history' instance that is only in-memory and causes no side effects to the testing environment. - */ - history = createMemoryHistory(); - /** - * Create a store, with the middleware disabled. We don't want side effects being created by our code in this test. - */ - store = appStoreFactory(); - - depsStart = depsStartMock(); - depsStart.data.ui.SearchBar.mockImplementation(() =>
); - - /** - * Render the test component, use this after setting up anything in `beforeEach`. - */ - render = () => { - /** - * Provide the store via `Provider`, and i18n APIs via `I18nProvider`. - * Use react-router via `Router`, passing our in-memory `history` instance. - * Use `RouteCapture` to emit url-change actions when the URL is changed. - * Finally, render the `AlertIndex` component which we are testing. - */ - return reactTestingLibrary.render( - - - - - - - - - - - - ); - }; + // Creates the render elements for the tests to use + ({ render, history, store, depsStart } = alertPageTestRender); }); it('should show a data grid', async () => { await render().findByTestId('alertListGrid'); @@ -80,7 +40,7 @@ describe('when on the alerting page', () => { reactTestingLibrary.act(() => { const action: AppAction = { type: 'serverReturnedAlertsData', - payload: mockAlertResultList(), + payload: mockAlertResultList({ total: 11 }), }; store.dispatch(action); }); @@ -93,16 +53,17 @@ describe('when on the alerting page', () => { * There should be a 'row' which is the header, and * row which is the alert item. */ - expect(rows).toHaveLength(2); + expect(rows).toHaveLength(11); }); describe('when the user has clicked the alert type in the grid', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { renderResult = render(); + const alertLinks = await renderResult.findAllByTestId('alertTypeCellLink'); /** * This is the cell with the alert type, it has a link. */ - fireEvent.click(await renderResult.findByTestId('alertTypeCellLink')); + fireEvent.click(alertLinks[0]); }); it('should show the flyout', async () => { await renderResult.findByTestId('alertDetailFlyout'); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/test_helpers/render_alert_page.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/test_helpers/render_alert_page.tsx new file mode 100644 index 000000000000..631167140761 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/test_helpers/render_alert_page.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import * as reactTestingLibrary from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { I18nProvider } from '@kbn/i18n/react'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; +import { AlertIndex } from '../index'; +import { appStoreFactory } from '../../../store'; +import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { RouteCapture } from '../../route_capture'; +import { depsStartMock } from '../../../mocks'; + +/** + * Create a 'history' instance that is only in-memory and causes no side effects to the testing environment. + */ +const history = createMemoryHistory(); +/** + * Create a store, with the middleware disabled. We don't want side effects being created by our code in this test. + */ +const store = appStoreFactory(); + +const depsStart = depsStartMock(); +depsStart.data.ui.SearchBar.mockImplementation(() =>
); + +export const alertPageTestRender = { + store, + history, + depsStart, + + /** + * Render the test component, use this after setting up anything in `beforeEach`. + */ + render: () => { + /** + * Provide the store via `Provider`, and i18n APIs via `I18nProvider`. + * Use react-router via `Router`, passing our in-memory `history` instance. + * Use `RouteCapture` to emit url-change actions when the URL is changed. + * Finally, render the `AlertIndex` component which we are testing. + */ + return reactTestingLibrary.render( + + + + + + + + + + + + ); + }, +}; diff --git a/x-pack/test/api_integration/apis/endpoint/alerts.ts b/x-pack/test/api_integration/apis/endpoint/alerts.ts index 06134a093f7a..140d8ca81369 100644 --- a/x-pack/test/api_integration/apis/endpoint/alerts.ts +++ b/x-pack/test/api_integration/apis/endpoint/alerts.ts @@ -6,6 +6,16 @@ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; +/** + * The number of alert documents in the es archive. + */ +const numberOfAlertsInFixture = 12; + +/** + * The default number of entries returned when no page_size is specified. + */ +const defaultPageSize = 10; + export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); @@ -17,12 +27,12 @@ export default function({ getService }: FtrProviderContext) { const nextPrevPrefixPageSize = 'page_size=10'; const nextPrevPrefix = `${nextPrevPrefixQuery}&${nextPrevPrefixDateRange}&${nextPrevPrefixSort}&${nextPrevPrefixOrder}&${nextPrevPrefixPageSize}`; - describe('test alerts api', () => { - describe('Tests for alerts API', () => { + describe('Endpoint alert API', () => { + describe('when data is in elasticsearch', () => { before(() => esArchiver.load('endpoint/alerts/api_feature')); after(() => esArchiver.unload('endpoint/alerts/api_feature')); - it('alerts api should not support post', async () => { + it('should not support POST requests', async () => { await supertest .post('/api/endpoint/alerts') .send({}) @@ -30,43 +40,66 @@ export default function({ getService }: FtrProviderContext) { .expect(404); }); - it('alerts api should return one entry for each alert with default paging', async () => { + it('should return one entry for each alert with default paging', async () => { const { body } = await supertest .get('/api/endpoint/alerts') .set('kbn-xsrf', 'xxx') .expect(200); - expect(body.total).to.eql(132); - expect(body.alerts.length).to.eql(10); - expect(body.request_page_size).to.eql(10); + expect(body.total).to.eql(numberOfAlertsInFixture); + expect(body.alerts.length).to.eql(defaultPageSize); + expect(body.request_page_size).to.eql(defaultPageSize); + /** + * No page_index was specified. It should return page 0. + */ expect(body.request_page_index).to.eql(0); + /** + * The total offset: page_index * page_size + */ expect(body.result_from_index).to.eql(0); }); - it('alerts api should return page based on paging properties passed.', async () => { + it('should return the page_size and page_index specified in the query params', async () => { + const pageSize = 1; + const pageIndex = 1; const { body } = await supertest - .get('/api/endpoint/alerts?page_size=1&page_index=1') + .get(`/api/endpoint/alerts?page_size=${pageSize}&page_index=${pageIndex}`) .set('kbn-xsrf', 'xxx') .expect(200); - expect(body.total).to.eql(132); - expect(body.alerts.length).to.eql(1); - expect(body.request_page_size).to.eql(1); - expect(body.request_page_index).to.eql(1); - expect(body.result_from_index).to.eql(1); - }); - - it('alerts api should return accurate total alerts if page index produces no result', async () => { - const { body } = await supertest - .get('/api/endpoint/alerts?page_size=100&page_index=3') - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.total).to.eql(132); - expect(body.alerts.length).to.eql(0); - expect(body.request_page_size).to.eql(100); - expect(body.request_page_index).to.eql(3); - expect(body.result_from_index).to.eql(300); - }); - - it('alerts api should return 400 when paging properties are below boundaries.', async () => { + expect(body.total).to.eql(numberOfAlertsInFixture); + /** + * Skipping the first page (with a size of 1). + */ + const expectedToBeSkipped = 1; + expect(body.alerts.length).to.eql(pageSize); + expect(body.request_page_size).to.eql(pageSize); + expect(body.request_page_index).to.eql(pageIndex); + expect(body.result_from_index).to.eql(expectedToBeSkipped); + }); + + describe('when the query params specify a page_index and page_size that return no results', () => { + let body: any; + const requestPageSize = 100; + const requestPageIndex = 3; + beforeEach(async () => { + const response = await supertest + .get(`/api/endpoint/alerts?page_size=${requestPageSize}&page_index=${requestPageIndex}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + body = response.body; + }); + it('should return accurate total counts', async () => { + expect(body.total).to.eql(numberOfAlertsInFixture); + /** + * Nothing was returned due to pagination. + */ + expect(body.alerts.length).to.eql(0); + expect(body.request_page_size).to.eql(requestPageSize); + expect(body.request_page_index).to.eql(requestPageIndex); + expect(body.result_from_index).to.eql(requestPageIndex * requestPageSize); + }); + }); + + it('should return 400 when paging properties are less than 1', async () => { const { body } = await supertest .get('/api/endpoint/alerts?page_size=0') .set('kbn-xsrf', 'xxx') @@ -74,12 +107,12 @@ export default function({ getService }: FtrProviderContext) { expect(body.message).to.contain('Value must be equal to or greater than [1]'); }); - it('alerts api should return links to the next and previous pages using cursor-based pagination', async () => { + it('should return links to the next and previous pages using cursor-based pagination', async () => { const { body } = await supertest .get('/api/endpoint/alerts?page_index=0') .set('kbn-xsrf', 'xxx') .expect(200); - expect(body.alerts.length).to.eql(10); + expect(body.alerts.length).to.eql(defaultPageSize); const lastTimestampFirstPage = body.alerts[9]['@timestamp']; const lastEventIdFirstPage = body.alerts[9].event.id; expect(body.next).to.eql( @@ -87,14 +120,14 @@ export default function({ getService }: FtrProviderContext) { ); }); - it('alerts api should return data using `next` link', async () => { + it('should return data using `next` link', async () => { const { body } = await supertest .get( - `/api/endpoint/alerts?${nextPrevPrefix}&after=1542789412000&after=c710bf2d-8686-4038-a2a1-43bdecc06b2a` + `/api/endpoint/alerts?${nextPrevPrefix}&after=1584044338719&after=66008e21-2493-4b15-a937-939ea228064a` ) .set('kbn-xsrf', 'xxx') .expect(200); - expect(body.alerts.length).to.eql(10); + expect(body.alerts.length).to.eql(defaultPageSize); const firstTimestampNextPage = body.alerts[0]['@timestamp']; const firstEventIdNextPage = body.alerts[0].event.id; expect(body.prev).to.eql( @@ -102,7 +135,7 @@ export default function({ getService }: FtrProviderContext) { ); }); - it('alerts api should return data using `prev` link', async () => { + it('should return data using `prev` link', async () => { const { body } = await supertest .get( `/api/endpoint/alerts?${nextPrevPrefix}&before=1542789412000&before=823d814d-fa0c-4e53-a94c-f6b296bb965b` @@ -112,85 +145,89 @@ export default function({ getService }: FtrProviderContext) { expect(body.alerts.length).to.eql(10); }); - it('alerts api should return no results when `before` is requested past beginning of first page', async () => { + it('should return no results when `before` is requested past beginning of first page', async () => { const { body } = await supertest .get( - `/api/endpoint/alerts?${nextPrevPrefix}&before=1542789473000&before=ffae628e-6236-45ce-ba24-7351e0af219e` + `/api/endpoint/alerts?${nextPrevPrefix}&before=1584044338726&before=5ff1a4ec-758e-49e7-89aa-2c6821fe6b54` ) .set('kbn-xsrf', 'xxx') .expect(200); expect(body.alerts.length).to.eql(0); }); - it('alerts api should return no results when `after` is requested past end of last page', async () => { + it('should return no results when `after` is requested past end of last page', async () => { const { body } = await supertest .get( - `/api/endpoint/alerts?${nextPrevPrefix}&after=1542341895000&after=01911945-48aa-478e-9712-f49c92a15f20` + `/api/endpoint/alerts?${nextPrevPrefix}&after=1584044338612&after=6d75d498-3cca-45ad-a304-525b95ae0412` ) .set('kbn-xsrf', 'xxx') .expect(200); expect(body.alerts.length).to.eql(0); }); - it('alerts api should return 400 when using `before` by custom sort parameter', async () => { + it('should return 400 when using `before` by custom sort parameter', async () => { await supertest .get( - `/api/endpoint/alerts?${nextPrevPrefixDateRange}&${nextPrevPrefixPageSize}&${nextPrevPrefixOrder}&sort=thread.id&before=2180&before=8362fcde-0b10-476f-97a8-8d6a43865226` + `/api/endpoint/alerts?${nextPrevPrefixDateRange}&${nextPrevPrefixPageSize}&${nextPrevPrefixOrder}&sort=process.pid&before=1&before=66008e21-2493-4b15-a937-939ea228064a` ) .set('kbn-xsrf', 'xxx') .expect(400); }); - it('alerts api should return data using `after` by custom sort parameter', async () => { + it('should return data using `after` by custom sort parameter', async () => { const { body } = await supertest .get( - `/api/endpoint/alerts?${nextPrevPrefixDateRange}&${nextPrevPrefixPageSize}&${nextPrevPrefixOrder}&sort=thread.id&after=2180&after=8362fcde-0b10-476f-97a8-8d6a43865226` + `/api/endpoint/alerts?${nextPrevPrefixDateRange}&${nextPrevPrefixPageSize}&${nextPrevPrefixOrder}&sort=process.pid&after=3&after=66008e21-2493-4b15-a937-939ea228064a` ) .set('kbn-xsrf', 'xxx') .expect(200); expect(body.alerts.length).to.eql(10); - expect(body.alerts[0].thread.id).to.eql(1912); + expect(body.alerts[0].process.pid).to.eql(2); }); - it('alerts api should filter results of alert data using rison-encoded filters', async () => { + it('should filter results of alert data using rison-encoded filters', async () => { + const hostname = 'Host-abmfhmc5ku'; const { body } = await supertest .get( - `/api/endpoint/alerts?filters=!((%27%24state%27%3A(store%3AappState)%2Cmeta%3A(alias%3A!n%2Cdisabled%3A!f%2Ckey%3Ahost.hostname%2Cnegate%3A!f%2Cparams%3A(query%3AHD-m3z-4c803698)%2Ctype%3Aphrase)%2Cquery%3A(match_phrase%3A(host.hostname%3AHD-m3z-4c803698))))` + `/api/endpoint/alerts?filters=!((%27%24state%27%3A(store%3AappState)%2Cmeta%3A(alias%3A!n%2Cdisabled%3A!f%2Ckey%3Ahost.hostname%2Cnegate%3A!f%2Cparams%3A(query%3A${hostname})%2Ctype%3Aphrase)%2Cquery%3A(match_phrase%3A(host.hostname%3A${hostname}))))` ) .set('kbn-xsrf', 'xxx') .expect(200); - expect(body.total).to.eql(72); - expect(body.alerts.length).to.eql(10); - expect(body.request_page_size).to.eql(10); + expect(body.total).to.eql(4); + expect(body.alerts.length).to.eql(4); + expect(body.request_page_size).to.eql(defaultPageSize); expect(body.request_page_index).to.eql(0); expect(body.result_from_index).to.eql(0); }); - it('alerts api should filter results of alert data using KQL', async () => { + it('should filter results of alert data using KQL', async () => { + const agentID = '7cf9f7a3-28a6-4d1e-bb45-005aa28f18d0'; const { body } = await supertest .get( - `/api/endpoint/alerts?query=(language%3Akuery%2Cquery%3A%27agent.id%20%3A%20"c89dc040-2350-4d59-baea-9ff2e369136f"%27)` + `/api/endpoint/alerts?query=(language%3Akuery%2Cquery%3A%27agent.id%20%3A%20"${agentID}"%27)` ) .set('kbn-xsrf', 'xxx') .expect(200); - expect(body.total).to.eql(72); - expect(body.alerts.length).to.eql(10); - expect(body.request_page_size).to.eql(10); + expect(body.total).to.eql(4); + expect(body.alerts.length).to.eql(4); + expect(body.request_page_size).to.eql(defaultPageSize); expect(body.request_page_index).to.eql(0); expect(body.result_from_index).to.eql(0); }); - it('alerts api should return alert details by id', async () => { + it('should return alert details by id', async () => { + const documentID = 'zbNm0HABdD75WLjLYgcB'; + const prevDocumentID = '2rNm0HABdD75WLjLYgcU'; const { body } = await supertest - .get('/api/endpoint/alerts/YjUYMHABAJk0XnHd6bqU') + .get(`/api/endpoint/alerts/${documentID}`) .set('kbn-xsrf', 'xxx') .expect(200); - expect(body.id).to.eql('YjUYMHABAJk0XnHd6bqU'); + expect(body.id).to.eql(documentID); + expect(body.prev).to.eql(`/api/endpoint/alerts/${prevDocumentID}`); expect(body.next).to.eql(null); // last alert, no more beyond this - expect(body.prev).to.eql('/api/endpoint/alerts/XjUYMHABAJk0XnHd6boX'); }); - it('alerts api should return 404 when alert is not found', async () => { + it('should return 404 when alert is not found', async () => { await supertest .get('/api/endpoint/alerts/does-not-exist') .set('kbn-xsrf', 'xxx') diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index 516891d84dc9..5f18bdd9bea0 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -6,6 +6,11 @@ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; +/** + * The number of alert documents in the es archive. + */ +const numberOfEndpointsInFixture = 3; + export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); @@ -34,8 +39,8 @@ export default function({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send() .expect(200); - expect(body.total).to.eql(3); - expect(body.endpoints.length).to.eql(3); + expect(body.total).to.eql(numberOfEndpointsInFixture); + expect(body.endpoints.length).to.eql(numberOfEndpointsInFixture); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); @@ -55,7 +60,7 @@ export default function({ getService }: FtrProviderContext) { ], }) .expect(200); - expect(body.total).to.eql(3); + expect(body.total).to.eql(numberOfEndpointsInFixture); expect(body.endpoints.length).to.eql(1); expect(body.request_page_size).to.eql(1); expect(body.request_page_index).to.eql(1); @@ -79,7 +84,7 @@ export default function({ getService }: FtrProviderContext) { ], }) .expect(200); - expect(body.total).to.eql(3); + expect(body.total).to.eql(numberOfEndpointsInFixture); expect(body.endpoints.length).to.eql(0); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(30); @@ -107,7 +112,7 @@ export default function({ getService }: FtrProviderContext) { const { body } = await supertest .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') - .send({ filter: 'not host.ip:10.101.149.26' }) + .send({ filter: 'not host.ip:10.100.170.247' }) .expect(200); expect(body.total).to.eql(2); expect(body.endpoints.length).to.eql(2); @@ -116,7 +121,7 @@ export default function({ getService }: FtrProviderContext) { }); it('metadata api should return page based on filters and paging passed.', async () => { - const notIncludedIp = '10.101.149.26'; + const notIncludedIp = '10.100.170.247'; const { body } = await supertest .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') @@ -136,7 +141,14 @@ export default function({ getService }: FtrProviderContext) { const resultIps: string[] = [].concat( ...body.endpoints.map((metadata: Record) => metadata.host.ip) ); - expect(resultIps).to.eql(['10.192.213.130', '10.70.28.129', '10.46.229.234']); + expect(resultIps).to.eql([ + '10.48.181.222', + '10.116.62.62', + '10.102.83.30', + '10.198.70.21', + '10.252.10.66', + '10.128.235.38', + ]); expect(resultIps).not.include.eql(notIncludedIp); expect(body.endpoints.length).to.eql(2); expect(body.request_page_size).to.eql(10); @@ -152,18 +164,18 @@ export default function({ getService }: FtrProviderContext) { filter: `host.os.variant.keyword:${variantValue}`, }) .expect(200); - expect(body.total).to.eql(2); + expect(body.total).to.eql(1); const resultOsVariantValue: Set = new Set( body.endpoints.map((metadata: Record) => metadata.host.os.variant) ); expect(Array.from(resultOsVariantValue)).to.eql([variantValue]); - expect(body.endpoints.length).to.eql(2); + expect(body.endpoints.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); it('metadata api should return the latest event for all the events for an endpoint', async () => { - const targetEndpointIp = '10.192.213.130'; + const targetEndpointIp = '10.100.170.247'; const { body } = await supertest .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') @@ -176,7 +188,7 @@ export default function({ getService }: FtrProviderContext) { (ip: string) => ip === targetEndpointIp ); expect(resultIp).to.eql([targetEndpointIp]); - expect(body.endpoints[0].event.created).to.eql('2020-01-24T16:06:09.541Z'); + expect(body.endpoints[0].event.created).to.eql(1584044335459); expect(body.endpoints.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); @@ -190,8 +202,8 @@ export default function({ getService }: FtrProviderContext) { filter: '', }) .expect(200); - expect(body.total).to.eql(3); - expect(body.endpoints.length).to.eql(3); + expect(body.total).to.eql(numberOfEndpointsInFixture); + expect(body.endpoints.length).to.eql(numberOfEndpointsInFixture); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); diff --git a/x-pack/test/functional/apps/endpoint/alert_list.ts b/x-pack/test/functional/apps/endpoint/alerts.ts similarity index 55% rename from x-pack/test/functional/apps/endpoint/alert_list.ts rename to x-pack/test/functional/apps/endpoint/alerts.ts index ac00258ff9c0..1ce7eb41e669 100644 --- a/x-pack/test/functional/apps/endpoint/alert_list.ts +++ b/x-pack/test/functional/apps/endpoint/alerts.ts @@ -12,7 +12,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - describe('Endpoint Alert List page', function() { + describe('Endpoint Alert Page: when es has data and user has navigated to the page', function() { this.tags(['ciGroup7']); before(async () => { await esArchiver.load('endpoint/alerts/api_feature'); @@ -32,12 +32,31 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('includes Alert list data grid', async () => { await testSubjects.existOrFail('alertListGrid'); }); - it('updates the url upon submitting a new search bar query', async () => { - await pageObjects.endpointAlerts.enterSearchBarQuery(); - await pageObjects.endpointAlerts.submitSearchBarFilter(); - const currentUrl = await browser.getCurrentUrl(); - expect(currentUrl).to.contain('query='); - expect(currentUrl).to.contain('date_range='); + describe('when submitting a new bar query', () => { + before(async () => { + await pageObjects.endpointAlerts.enterSearchBarQuery('test query'); + await pageObjects.endpointAlerts.submitSearchBarFilter(); + }); + it('should update the url correctly', async () => { + const currentUrl = await browser.getCurrentUrl(); + expect(currentUrl).to.contain('query='); + expect(currentUrl).to.contain('date_range='); + }); + after(async () => { + await pageObjects.endpointAlerts.enterSearchBarQuery(''); + await pageObjects.endpointAlerts.submitSearchBarFilter(); + }); + }); + + describe('and user has clicked details view link', () => { + before(async () => { + await pageObjects.endpointAlerts.setSearchBarDate('Mar 10, 2020 @ 19:33:40.767'); // A timestamp that encompases our es-archive data + await testSubjects.click('alertTypeCellLink'); + }); + + it('loads the Alert List Flyout correctly', async () => { + await testSubjects.existOrFail('alertDetailFlyout'); + }); }); after(async () => { diff --git a/x-pack/test/functional/apps/endpoint/index.ts b/x-pack/test/functional/apps/endpoint/index.ts index c6a7f723bfa2..15ce522ce56b 100644 --- a/x-pack/test/functional/apps/endpoint/index.ts +++ b/x-pack/test/functional/apps/endpoint/index.ts @@ -15,6 +15,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./management')); loadTestFile(require.resolve('./policy_list')); loadTestFile(require.resolve('./policy_details')); - loadTestFile(require.resolve('./alert_list')); + loadTestFile(require.resolve('./alerts')); }); } diff --git a/x-pack/test/functional/apps/endpoint/management.ts b/x-pack/test/functional/apps/endpoint/management.ts index 4925fa7678ab..640f6264c3a0 100644 --- a/x-pack/test/functional/apps/endpoint/management.ts +++ b/x-pack/test/functional/apps/endpoint/management.ts @@ -37,32 +37,32 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Last Active', ], [ - 'cadmann-4.example.com', + 'Host-cxz5glsoup', 'Policy Name', 'Policy Status', '0', - 'windows 10.0', - '10.192.213.130, 10.70.28.129', + 'windows 6.2', + '10.48.181.222, 10.116.62.62, 10.102.83.30', 'version', 'xxxx', ], [ - 'thurlow-9.example.com', + 'Host-frl2otafoa', 'Policy Name', 'Policy Status', '0', 'windows 10.0', - '10.46.229.234', + '10.198.70.21, 10.252.10.66, 10.128.235.38', 'version', 'xxxx', ], [ - 'rezzani-7.example.com', + 'Host-abmfhmc5ku', 'Policy Name', 'Policy Status', '0', - 'windows 10.0', - '10.101.149.26, 2606:a000:ffc0:39:11ef:37b9:3371:578c', + 'windows 6.2', + '10.100.170.247, 10.113.203.29, 10.83.81.146', 'version', 'xxxx', ], diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/data.json.gz b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/data.json.gz index 0788e40326bb3a6d22a80023d60a44d23743e051..05fc7d79faf46b6389d3a8c6f9eb2df2b1b0db30 100644 GIT binary patch literal 15777 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 A$^ZZW literal 2063322 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

(8myW43Yz1d%^ z2m1$Fe=VQh><;+h8~yTygHL*E0DA`y^xo_6`H?!f`}L^4+W)$LvetP?6?)Aba zx9RE8Qhns#Z-14WdFVFF-7mq^#f8;}n|~cXhV=FkJ-5(*yZBhhZuxrjDjjfq@Oks~ z`YZluuzuWpq~E%^E!sJF=;j~1{c1j~-Ys9(Ha6_b!RPG(u511F*`vE;zR;r&kM1V_ zVcCVx8-C}ZUVC`7*`{Lf?(zM%TmJFl-IdoLw&u-W2Rc1}|9SoH2P@$F*DAu7@)hsj z*E{;@i!)QW__FQ}-o;1es~(hs;fjbL8?uQP62*xp$u3-jP$b*jbSK&vOMpzd3h4 zq&le^I%m(Fv4>oU^Z`Z(Q=>{t&c(mHvo{P?{u~oFfsf9q#P_U%bk9o6caMUu@&}`Y zi5QJKTh}|}aDS%lm_M8pN}a?vm!hf7{@?G^(I(}r= z#m`4omR@b`oBDF{9L28-v~iMyqJYdFdu!#8y0cFA7>_b zPN?}&+Xqej#~*V$Ye%`&$^CKet^b}o@_fBFCn3a()ur`!XTNd2YHpG_RLh*3d|ssb4fMmZhL===2pdY?fR11gcu z9xTgf&LlHAedxL23rjw!=Ew>~jkQ2IgP|4rHf2u*a_noH@>*B1`BL1b`zcSbQ`0U0YgE(i6)u&WaD6s&t)foYb$=H;G1(6&S8mn3#G=mr|M{6Be z{w|e@iBXs0d_e7tSYK-uwA5^~SAsdq|A`H-dwwi>uEUm*?l!7CS)b0HFhf+Xo22gr5fl zlM^Zp9=T}aQ5<~5T$z2}5g$f{xBA2r(F4#PimWeN3ZR#VH`LgsY(h zYfWTM1yYiqjLkqXnks0LTWnv)l7s47r`+OEfloP5b=d`NA+;=Q*Tm>SS4zl6YgVzg zJpG0;ZrGzM&ekfCF^HGtRBnU@uhte{yY+(4i;;vA&R?nky9wH zMqQ0gPU7pUkw7c5vGtVA8H{cHKeE?G*XGlWF=O@%k_N89Sg)u?B9sUpG#5dYR++^? z#ga?Kv7HPZ2XBN~7+ZZ-g-JIZAe$h8GCoA5GYVJGpjD{Br@%see2qDPvl1!K4iX&e zZH5paOjm{e(SxN93%X`izZe$GV6!S4>;&yef^OY9hE2;QLf36?R&YkZLfmxHWrTw} zT{CJ8p$tguiJHXM7jKHAAV+zIk-c%hZ6s?KAu}OW<%J6Pw)P*h(upw_>uhUUPBfyE z;+c))l0s`(>s(PNHm4+3$T%U9&sdWDtl8(Fys#fkfkI@@p(ZCFs(HrRpL_H|jp&kE z=8b@1dF5#b3mrxHrC?!3C_;b+Z z>npUszU5Lr1s%!Nzgc(BunP!>eOKR6QTrXPDs>6bg zBK%TVFoP5U6LBD{7s=AEY+dWM&0v%G)sx35;V;dMs zu2zGSp#qK$@Q1)4U=oqV_s5z{3c+JC;4)@(h^4w}+qOa+z4~;zcBBehh{-r#i`6+$ zlaLMxUCoFm&4c>2^Hd864&PgAN%wWayCL zb|ZszVS@c-*371O9gvFAtXQW?sB=e z8EgThoTr% zKnO-)3o*&}tRq(0c|U8&;K3Wb)Wz#Y$T0Wd_1sFgvCv^dhYhzL8&HP{SVNNFT_Sx) zA(u>rlEl~&U)F?=xzz8fK8Y8XJrNN_4F{O=9^f zM<-i~7RF$c%Hq^L4|fd4ZY&h%g44FBWUUi+W2}uOgqq~o7)<6AlI4P&i352y;GJdP zU?Y3wXKlyX2PY62rm&p!y-01qxd{<<(9mrz{8G>`Bby6;5;Tz5**g|jnk1l(PP1Su7=uvHr33iNz!pp@ zL~&x6&83I>yH7^PF3Qm=SBDCW!`f1JsvJ)gn8rlt&QpY}#0yt47}|bsDiWMa44qp3 z4|xfx{n!XyqHPv8!-h05i#unq;%;3iIp%0UCIqS?WCX8V4lZCYF$CpHHF4%rf0SS) zDx5q*zplX0%5{D8zfoPNbI_fGKAF4G4jLzrf2-P9Cy$OzNgO;DA=~1y(S_=bucNej zP8~EUq2?6iz0VRpTjP?0>^&AvS(`ECTPlL39D!2GwRN%}Ku^j0NXCJ8m`X?z0grRg zIu&1h^2r6(C?UVAxvGRlMX!}-`N_A&IsuS%MI~V~i@PoktyRg+kha6IgSM^T@(tu2 zuIZrVuY-O%teu%8)rMVoW(_`0jV0NOts%=~-$U!pZbB1lMCZ532jfCQ3=XZzzu_#RvVh?n)tJ|*9$A7 zAL$>gil2329d>ls(P78A<3?e}{@;xqb^`36s)>zhODL5Rfa3-v>>bp^Ib6M;8ZtSZ zl@FzmM6)Kjj8YjR?VYxTBebm8v_cEr*e zC##fnVZ;ozW!h)*6eypr{JZspY4U){)w6SBg!;bTp z)}`27<088hn}00D1}50ftuYo;ut~hB99v!Jvnd4#6ae`=JUM5SSSU2{r2yw6TOPdR41w zc{Od_sW_LINr4F{80n17Y#`a9j27|&v7$NG#(LEJ{_7xtw9<&vRsZa7bC-$VVL^ul zw-XB{P=fZ}RxT8rE9_JeUujAS%Bv>0#_sCEENiO58c0>`EGx1_V*X?VLkd|?BMv;p z0&+G86UmrTZXtdstfE#`ScBDs?9qxXWh@pb(%@wn>yx7D(0g%tMN`ZD%+Zx9)(ZF(mmKWz=f&{HFObheK1x%n!GRg?O zxik9++KG+Gj#c#vybPjT2-cZw9HLS#`XbaR2Va}on{pXL+wiaYA}u!Oiq*~2-k|O z*O+Xni9KQVb!IlVk%jq122q3bJE6DpR$QUN3_#Xb2V2qqdZ+PBE?Zt2quW#oWL<)+szOV$keu{ zJ(ZTSYP{Ei8FEoE``DUK%DHLQc!BZckZw>L{0$A{hqcdZfBX@5inD)ye-%!+@^2eu zYj68-@TbFo$yse}&VtAFPyL(z%}dbno3IpZ|J?2G@}DLdi{P@?0_x!7AAg)2-a$+U zF&)I*I*1`XiLA6rSps#SkX%CaK3B*&8f|k#1M%tdJC4Lz=n8j@g zGs%WE`=;hxKve_QwQll3X9_G-!YNZmR7RiDj~t9vEv{#Urn+c3k;M89EwZs=a#@S5 z5mhX$RkBbIC)QJu$kbJBylT9m=xFWOc9pFof)7EZ5=-(0NvCaPrRTB6}vvdt_} ztqi1v4^<&3)Dp8?O_XMiei(2P0ZcWQ_07`rwlmQoLx&6_XBaBC zrA)TY&^Cp<1E?X`X{<_phlQ23jxTiLN7uQw>)3i-ul~!3ABp?%1e0tOK%^`oTxxpG zF~Bgxl(H_-Xajmw>&DY(=@mnzm(Bz&U1i z8c;bwg397gUA#)=?zGY%x8h}C3&DtEhHMCH6t2Xp-`?=O_2HVJNLd2*;*$i;6my8N zvi8N~>-$^t;$zqxY&GQtInfKxOa6IhC)%Ls9Mla0hE(9o3csvIN-0*&b$mU3+ptWbSOAhEuoEa;$f)&dG8xZs(bAKwH4 zS*T4y6hu*`XdBsM(!m&4*yvnLQ_CpPWyclCn_7etc}>*{UU1Pf1`I|AWz=+4QtPL^ z!@RxQHInSIMt?2RFe6!`bV;h<8gP)aZ5r7TRBdVyP49pul=fsKY}al?uzuQB7^&^5 z2Jy(4Y>Jsxg|>wd1X`>fv){nZ1TLP~v2JTf4k)PLZKDdYP;3OGR5!X^!zoV)%0aAD zFnfqirMno{gidWMw7?vwB%5n#8Kez*A{hdYmW{wx4zsr3U|S;$f%<8}!5Zho{Z7lG zLxT<7^+10+K17FZl{{3j$A?X$;aH3EVyQ$lO@n? zGFsJh5#C`qu+to@S2B8Q@FOSGDe$c2iOM~u;HTSGSf1DI?U_e|>7@igY0Uyc?1Af{ zZv6r(wv?cdd#rjs^or>b^aA$0>%sNp~U!<*e~yd1%f0hG`HMNkcXH|CegQX*wFSwBIGwn~AOIOtH&uu0%c;|so`(FiJE zt%q85bW8!o0Y?eojdG=ok}|LBTM7!WBp6ge&^iaEIGmlR1WyDN(Yrcht#(@RUh69Q zx3@Z;fADIr!~;Hg2^ac1*M*Ub9_?QNA|0jZD8;R$6c{F;6cXQ4Z5@{5FfCL;%o2^; zL@g~{rK>ZPyt6@3WnE)%H9#x;(#iSQ8XT8VsZHO?lBp0!smB6Vrf@{(g zTq!zR#&>0NO&lYpR?Vy+Ba?EGa)#tlMH{rTp`E}O6l;o@mg%MN;Srs=14qWCOZFaHM zb%kWuDG#xh7+FS8v51vH3c}8XcDDtYf@%xqcv^B?Yp8&0+Jt{ zwmET9)@KdiHE|6^_=j^TiglD2k#p)UL@MaDU2uAEj-3ShL`QY$iYr7LI>foS* zgIfp(!CZWyufq-scyYxx2{*8=g3~!_lnCE*s#LC4NrmQTQ>;#_q)M^Dd2#T{sszcI zIEEd7Q9fp0Dc~@t)fp%uShQ4HH!wz<7}$^9erDnzJj9x$nY~a0E=`6)QH;u1K>}|y z7UgmT?T7p$l~%GGg&{yOFk^6FNmE-No$7|e{!!lE>cF4_gANRC9T)_;zMlsJ7sU}r zt1z{0#>GWVg~i7gsK}VF)}nwMGz5VWOKs=5putF(JfbR;#({xg1;^@(xNvdQ8e9}A zBBKcoHKioWLRX^1v*WrAs2+^wn<-Jn ztBV$bWn~mGBp6YeC=s~U_N@fD^;u>`C)b3%y*@Al ze!aN5w)Eopj6*}0mo$TUNlM?>)ToBJ+F^yy0|Vs_K+0S&k8Gtf|zd&s2@6Sr<1>KASZPv_P@W0}0D$ zdmgq4`9UnvMNkBlk{{c2r|1kyu*cWzAlr&2Ddh}h5a6lkbxCMyHN##Hlybu8ps6j% zhZM-q7;;chj_?LuKCg)!seC+)`yFs}z|jH6_<6luz%fA{tYz`OZ3n`}91yj_QfiDD z$QPj=S5pr1wzSo;N}-j{8APEj0ytc3(y5NggQbAt_G4<*ZBb>=#weG9@Esu!-iJ() zEowGe(&Sl+9R8sFAD}P7(y2ijT^AZM4;aQfSR6#x6=J z6;;Tw2Dx@7TbW~^8Wm&?N}C*Na5}m6fkvSo#u15ys*;I0!>lPYp+yKvO|?>D;o+la zk5*^CaPGm@#@2!y)0>mu?{n!bJvh%M7Uu3}z{uut$E^VgSL$~rYqi_MoD^CV~*YL~0 zg+k*=>j`qCvI#!0WAqAwI8wr7B@`#Es*wjUrTJSC4L{> zAMUtikjhEE%-#SZau8-9#DbVi+mcP_GMixiXuvrY05>HmAs;GY)q)eHM3Olu7)%wb z0J6@tWeKDFYl=#Y(GwWq7`hZcV}cy66<`Z>{M%QaQrXQj4IAB}Q@7}JD;J$Y2tN-F z%3^CDTACbdGEJW;F-tg}R5E17>}Nv_)x;V#U{%gRON3R*l_G(Cp)h7!f}^wO2t!gf zIclQbxriA+EqS*60-{elNeGXy7i89W@Tj1akgGS?~s@Aly`fg=?=GUA$H0aRaHlhK+Br>mtQKgI}RQBy3l_|Pp zB*?C=$!TA`Nln&jOvM@(vT7yP#Kvhzl@OI0lNZ|(S8Y||z_m(1*@Z$?x1=a zVu_%}Zc=kIS9=vWhh~AT#V8ju6%{RU@d7Y_3S3oQJ|`g~oJ-5X4P-LU*e10{YdzQ- zG@~BbL~d^Fx$eNAE6Dz(z+ff|vM*)CUjrFp)kZ-}RV}1dlkzix(0NkiTKV0T$iTu9 z*g}#J-^J9nEiy1BlXQUA%V!KSAg~f(D9V-)0CZ;1+Q!yJS{%Pb@kSZY801idQ61CPKy93NmJ_i=5m}c}Aw(Ui8h<^JmXG zVCY!FF9i%U!V-d+pavp=F{nyZ1yuE(Ov2Vku85n?sKeDQZo$Grtu1^eN%}N-`zS`r;1Ac9*YH)3pd#ft0AizF*ztm1wyQ~ zsOmB#7UrTu^2tbvvUE(i3?F^9iKshbbv8v z&`Rz+1Ys0x6k1&+$dAq%U2<#gqg{?vjTw$rVES5@*;%p6Ft1-&tap@!gDctVn2nF`dZ>p*!T zQ>ry>AAZK50fKQtkETEr*1qez4mJ<+-qv1+4IMUg*l@eC!G5pENsFtkP}8zysm+zChL~9l%Vt+N~uXt*vy>&_TmbT%_&g*mL31Mbdu-8vO5eqSAw#L&HC@m^5Sz z+UW_l7APAPm1bLlE!Ameqbs)L9SSvEt>=n`$lFL+>mvV%t7k3Ix?7e>Bihm=|1_Jd?V8fNj;K|kqr2-Heri!&;NpUr1@k4R6Y~BzVTmd8Tdk#9wkKPgrV^Cgx zr9(*uL4KjgjV^UKL53!>6jiQ;QxIwpTViaIgN##Nu@YOHt+rD10di$PFf2SmDIZAX zi^^F4dSpQ3l~vP4h9O^=pIck(z@giA{-wZSHnyF8P!ot?H2DaIS{qxy+%iFvw_X?n zC}9ou>TTzUCb^uARSt3{Xfy3sF42Wx)+!bPYyJ1Yeakz(plxbjpET z3T3PiYf~IACK*7-UPeibrbg>S4#HZr&$e|d3Y3x!x!4$dHK2ksRLC&QSXHyPR^bS} z#T((lgXe2=D@z?1bWGuw0)ts(3gQBO9vLtsXb4cLM)nCwh17(v<$ol&9#u(pf($mN zoWYa0=4cdzY`AE$Rw(NXoe)sRtfp|#*C ziUc*ZI7dxUsE9yMh(VE#BwRx07$dm+Ll66}1Bcct;|4O^W5A)qh7KD#Y`EpvKoj-$ zDbacFobuHWH1#9G6oRYB))h5k5BU@uBm!5aB1B_M!Rmsw#R?DckXsJy7;I=838r=P z;S4qk-4F(moEX7kON?4rOtNw876XT>OKUy~AdJ8Ov_?Z>uBFP#CuUz-0=ss&I|$Ips0PU;xcPGQS)!)l|K3j?5g3 z$|2>5O=&OHq4u}4FuukOY%(gjq>4A*w&J?v2?Q++SWHJx&PLitO+*Bi)IuVw@I#G<` zPd$mV&*^IY3klI{$fmG1;1U+=TE_!jglc%jSEyGU3uII7ASzx zwfKtO$mePt8c?*u9Rp!2=pnH<|5(wcgqq2qb-6XlroLHvzP|cuZT%l-3g3SI-|qw5IepvacGiyaZ~XbJbC4_9m)msV{@sx;<->!R zw)xUkelOk|oapmk81l>+&wbjVRYv{y9&`zquDE@du;-sM%D^Qjd1}-@{-C#j)04R@ z2X$i6rRn?cTl8Ofnwn~syz*B(P4*^_xSu>OzY>Ry`;kR>wMBpvdY=d)z=p#Om{uAAH`{hhcqT+evE z-kW>1{`ke}()zpeNnvcYJM^mTP#otthm~t#Nz6mp=d2 z50K{ujbg9tZ|%hWFY9fzl~XInuJQcH;N^b)(A$f0t-Y_ zhJ#GUyt(q%;o#uJuHMJP{y`k{YJdOx1m9#*mv(bo&fu4A+$kBH4|-zap1c>me(fD> za<9L~`|+UlZyfQk{oM4f=QJHNpT%9d=e>AXwuY9)-O?LyK5TDDZ{#NA^=EbMWSr?( zx*k8s6n@&}{lVteUYpa}`tJ^G!Dig&avkS% zJ!?+~o{qWmQBePI@5GfhH&wRw#Zb9pd=q2axQA2npsPF(dwA+Lo{T#kBiA+$4|+x5 z>5OB#kx!EG+Hljc?g6L6_F@nBm~S#?!-1w_(<9z(;ShfhH-X)th zMV`6+owjS%Jwd!>)!_Tl&^?|O){_o&|cd#Y~Q=V+`igvgG~v^kAtAz zQGfezhkK2I9*89p6ZM3De7Mmwa2$ER9*#UEFy9f_8T5CL0WxoIwTCsKUIOoZHm+TPdx z?oGm^mO}MjhM4O89(>tJ{q3#XYq?7|zu#*o!!65bSJ`>y|Ni&vfMb#$+G5m~z2TV- zPL^8Qvdj5z91f0?BG%rmtuH8#yY=yq|2)p#FlNLj#~;zzPtK$uoc;C4(|tSH zN%q9K<+D*+XQx>s-xk0|XN#OYU|i0|@WYFR2A6k&&VTREoxX66Z;3Q3$MH$;VGH#B z_~T5H$=v^E@6DR4IF>!$_wy-E_?57!tgM{Q(-j~ySb&U}!x4@e0b2}4VlWZsyWcE< zY%oh}d&|1_z+TPXMnYQMUENE{U;ZnndgwyezhC-KKRtRk_77h@JxVD3?Cw82osaJQ z(aYzrK22IY{z-WMasK_k|MkEB^`LSE^4f<-_05xKt-qZw>aX469(>!MwWDp-Z;um|zd3mR>Sg^ntMr@y=i}_k z`Fu&|j{Qe%puTZd{3RLb!&<6Zt09GN{Y?qqrbhN{$vuB-W5kYZfc$YkmOt+BcwHZV z@1(4;Kc8=}2cItDqeI|3Sb50n&sLu6u{n7D;*;&rZyZql(cF6cpFa}nKK^#g&fR`^ z()!;&k!+TMV*WM=wQp0h{H;=uZw-kda|;nZ&iDPsjqm&8_WR3EPWR`J=c?G=fce>85$wPVRSQ?M44z<(g9L-F6i zj*-fz7VD1;dl@A{9n^}^7&Rv>5mO$+A7SaWrrA{JFD1brn{ob~<2Uw2|F-PAXq|bO-yZt!;$O1?KcqYQuVn&W`ChL} z{Pdse7c(oa{rusl;xFFeH%|_J{xfy%IAQMdm;J3A)1yq^mq$NoKOY}3`q8NmzWHl9 z>d~*%&*jw^@1JYmx^npJ4m`hd<<6ZqR}OxZ>z6M*xq9`^rF3vb?_Id?_S)g|7f*Jc z-+2&TJa`@--Y(a!h2MV!d792WJve{k*1LZ9-iudv?p@qT2lw{3AH4qlNB!a2k9_0e z?d>h?w=Q8gxcVG-+uqA3J5NMWc7J`2JLSs5c%7b}yMMD>d3wH`zklBUDBhG`EwMVy}zCNgrUYy^#`R>WpUw_^E{r0)H zx9&c>|M+3L^uuF%^!(zl@1C?l){Qf!Zzl^u@ z!&2|Oq3`>(-}Ug(?Y{Hk;_vav<-W(Vb>aTI{YPc{3Ea6z-@mO7pKYbz&tKg6LF12a zpJ-NsU#XE0rLB6;B=H?67dV2GhOSc|8c=zbdtAic8H-5YD}Y-gF6~`; zS)bJ#SDtQPIo$gG*Wd8m_SJJ+DDMtlzP$bV3f{Z@>+5Sf&u`xT{lW_fZ{Egh_0ipn zxANnybmQgKi`yxmd;IeH{evz1ZR^U_!+UN7wzdCqQ|{lz{`mY47azQNyPuzKzq$Bm zNB3{UH@BZ1j+c)goWBzuou?nR+Uq+nZlBvacz5r&3+=Zna(AcqhcCC^{$Lkh!<|Q0 zFa5BkkIvV(^5fR_?zyeUTjN%~{=s}==PvAhzxUe@zwG^fc;oVg{mQ#9U%Y+& zgX~|td+BZ2yNnNI>+!|wZ{G5wzW?UY&es0n{R{W6-MM`w+Artsz|NKSkD{szz?px`LSQPzq7r2ama5G5BKfW zwO3C(8dL9cJTyYnlQ+JC(J?#_$y2A|w~b^F|%@1NcREmvN?z(>D7 zzI2gz|6+sRmEOd2SN3k)d2{#i<97L?l>MD?uG~F8emi{q`1&Kbap{+f_0^*b@Zt5*+x-#vF_y!pOefAQ|sUuy?i|A>?y03V%5e{H$dpVjB> zLs`(f{U@KaApAuB{-v_xPv_#Vc?Cy4v6K7fPpUY+WB%0B`cVAw$$Gw7d&plrc*NB{ z)^D%!%j_$7)_?Ysz1sh*%;Crv{@JhpvhN@H7(c)FtwYHo#PY2a#BbB+^sSZv-x@Rv zv!if~pH}~Te5sE;v+w$!-}vdL+UsmnpX*GS9~M$Wn=SBZj+7c^Fsn63IH6QxRf)nU zo_c@!;>hv(d1ZBq%jn&oesV_yd(P$CfC}Fx_B$*7_qVP0kh^P!Rz6V;Q)&MEhtK$@ zxAvKD`1tw3U*ymH%CYRea_QTr`0m?OHMm&&r1Ij)iz8JPz60U!V)T#tak=a_f8!L- z|LXAlanP!Anr~kCdp$Qi=L=R@a&1ZFKQ5>IhozMN zUK!=5HsN2CPoA=L@*kE>{*#i)f4f}r)8}!@GRc3lL~?C;go4atwy||By(btuV_2cvYMP(itsWm!3*Hm3c zmzZ)kDk4JywWJ9#3*h=obX>El|sU#$vS3mewxA{?xDW@1aCa5eW&xGV2W-pj3 zl`+-$m&N`EKA$HCKmW1qblRPOzgKCxy2|wA3ez>!rJro%9BrTdi#oog7JsS5|K#|M zt;J7DpXFQsjavMF%TB(3Hm9(a@88;gVlR)+aOO61)${#Ns>M&yz$p4dUH!^`rx5=H zf9Cz|E!|ia0-Q8Du;70W`s^m#{{;a8{ZG{YNCJ?uo}%4*OE8V2=?J038DrBB%(T~` zDawl6kK-ZZMBFHA8L`&cN4Mod(0WB}j*m5iK+N9>8d6Ft)^bDY4j@8ou~JbQP#a^@ z)eYm12SY)}*@2}YfOY@ahPIe9w8mo2!sx3XV+hP?h*g5q3Z-k3lE$D7>gXFofH^Sh zEZX_LPaBy{N}e6I{r#ma*8+k?3VeAWKsZH{Mgyi2Vl{;3F0vJ7_oG{i5ETH*i6D^7 z=q|T84KO4fjsSAEdqlV4LV0ylts@Xn4KWYb09>Qlpi*2tXqe zkOU=mZn&%8tPSaL6rIT=rKL8o3Q$O;2xdyE?&d>9rZC)_K6zSfo1-CkrCvoJ4H6W$(ffL{fk5hgQum$RpO2HG*2H(sCTtDc3BA?zq%! z!3ZrT>qNz0msoqKEE_iHLo~3;=PkSb5goQJ-QC)~x`>5^4GSB-bT&}HQ#5yX58DdV zFxcIAEkO`j2;A<60c}llcR{c@3vgsN^xWP2Vyh#~0yK8_ea)y3>^)QOTJk8dm@wE^YOCQwRb87)Db(@~MF&@6#7w~QEiOFbIZ6ltV1>P#~o)t5GE zjvK}X4s%TW*$#nvdhoiwJY2}IkYORi7fuGS(~SLa1MSwF)oQESw74C0BWfWIx3F=A z^Z$ViipgzlE!CkAsc@z&QVAqXHKD7gg1;|Ob*Yhg2ASPn4?nu3*1)NdN~rGc%bY#J zCkz<;K!(O_45@{Z5<9_SO1YVh;>Tu-wHGU?H|kZpsq~1#sml};Rf25X*bfUF%+5eH z{26OL=yeu7ENECF3||!*HYCCzr=dY&75DOUD;85`;MR(}|86-NLtN$Vf1rW;osb0U z+{EwEEiu$)Nn>D5AYxe_vMi?w)_xWRX2be;y z6Cpy~Ek3zUh<&(xiQ4D<+uBt}bq1Y6rnKcsvg20U@!-q%thrs^qdNfAyFJ-JxY|7Nq6g zOT#kb1x^suoa4wn)tN=QYE1c`EEv+FpF}92lxjM-vO&TULN4q`$p}LW^$5^iL=dDg z^O;zIV$7RWj}R2n=VN{T5eV+=ZQoi*utX2OG7@ZH^dQ;C9chnV_qVDO4BXo%tl(A| zX62Vt#$|&$>;>8i^LCt(8iOkVBozX6ufIubtma2a$02K~6AU=dZ5+}O#Ld6V%7R=2 zluorJPBwfUx;A=Xt(Y7I8CoD;^PCp0ejY0-m0I!7p-Z(OPCP26mb-Ktg&m>Lb#t)r zKIce;Nj{ro_K(Q0v-`t>g+(BIRan>*fqDp=u$Os1yYx55N$$6PDDq==$u_jNREW zuw+*%O&Sxc&IZRr#^|;7ltK*+2PPH`N!W5pWxyDc%{Gk$E|AjaL&^W4M%ab0uwY@q z!h$c41xlybEJ_8XItKzo={eAh$ztfCK&5E9qTK8Q3wjHSwsx;BG+1b`(BMm?!C%9K ze`k-Vx}PpGb3&_|X!7$^Jj&L|q6R1L5k>9-+?!FWNnKiW$~7bou27is8XCmXjEiPJ zpkr>3DsYS0G#g~gHX=6=TV+uH3>~m?Ba$N7k5=uYI@FRP%!e^?(y*1-tuu5>68!8^ zU5#LdVtvz;0S9xXceXi2dsnu1c9!tLLW6|{Um^|Uvz9e~g9an#78}j*aj4lCtp~}; zx>jl{ONEcClq-kg0LYS3?4yXKps}`GM-2h$$u!8hyRlD6G|uXV1VajDAtsC^S;UcY zS6CY{7!{hq?C0v#3S-2XR*He7F_+#oU^LPs98>KPUA2S=;xEgC;wNLVRz2VUI2sV6 zr0`jflYc~m=lgwO!NP)t1z#o$PSMXuB#qwGm|N%2+*ZccEJ&Cso}EwbSdrogG)%() zPCaO9g~@$!txXe#HOfFanHH>6N{uNLYdvHLBf4u3N?j=VTH0C`1O>NlHk+0P9ZVu_ z@*3zYeW;n+@YN@%aY&S0sJldP-WiA38--dnj0GZbPCY%-n8D4R?X5ev7aS}&Sa9$q z;vncLiqCpAZaCPI``JL?CQ}*FeaQwPzIR~|wG=zF0AhcHZ^C9I_+t5T)G+?gXQBkw3N>2JC+mxh7>mk~3 ziBfcogkuQC#8}ZHh9Vl{>TVauaplI0eg+pPMGC~xN(iI&6tF2qhU9mXLdRmYt1r>z z)GVfM4zV8Vru9F?6e7p7n2hr$ATG_J@e3FhFf3sB;=yp5Aqhwb32Q7O#Zs*rb0MlU zdL3pfVgtw1a%$l$QRl$5DzRdk<8o4_HU>CoP6h)NLp9EvOF{N4JA}jpJGz8W4iy3! zu5V&wM~zy?7zE%nm`(Yzvn08Om{kUm$d?mfq~fH0mA}@FC7dN z;52PdDWW^xprMu+6H~1N3}#ojZWd22SE~u3=Rq}RaG6naog4o-_FP=NFSkSPb;Y&wDIK^fX)C4x9j$C^gwQ0A2olB1vQjn8N7X;XS z(Ga1u?60MeYEPJ=;NUrf^JFwQLBf~-v8+1sEH-0}rheh52nAd0C7&>C;ka}mgSr-I znrS3N>Vd$Qj0i~@R_KC3hqRQq$J`5(aczCfe~qdemY*cxxScar&k_y0H}Bory>)Yu z4NJP}*TsenNmo_+>o4@*z(E$buw4tJSvD|QFA+lIhH4ZLPoAkpPL4S|4P0w97-D&5 zxc1yS59@7Bx@w(b1fWSmC8AKLF?RK52XWN3{fmRu>6ngkRgqgv%SCo*KkMm06ne>IBPC4$Zw5Hv810))M4z|mXgdHz`a zv?0$3i0G&_fT+XBB3o*k)+jHs^EZ9A-}sNnaIjw&EG&_QuL=tr5?M%git6UkG;@w2 zakFVaW#~xJ?Qo9-l}}DdP3E3>Z!Hfr3bVDIRV<+rk-EKJ-TwN0b#o&st-|c%83#cx z&VnJO#;kb&Lx%3cWo=}^H1rH=7R}e)3vx%AJ^RK8Rc8}c(bjDW5P{LU-&C%WT`)Mj znr>PH?OFGo&ZJhsKiPY`_tQ`LY2)Iew zOPUpN*@-vn?AqY9t@pA8#ynh}RKYAUmgXv=xka1Eq-xqqs>8`QI68i^Mn$32V|0CD zn-;uI>ZH}P?X0xd>#LVb?0TWWLW3`n1{O~dlqeXZ>xCSe^FZd2u{Fifimlb58FHvOMJz)T$e>PEXx3bpLBT(+qk+2C>Ff|m zfHjQl&Mh|Q6Ivdlg~2X#8D<~mc$TIy@-97<2vD@SrEV`J7jSTf4sD#6`< zS2@DHR}@Z8g3c+BI4^iaX+Xjo7l??#l+U&zdiU1N-Mt5Ui#Awd)L#`1HZVq=%uW%b z9-;DRtOypU8EFZ`$^C8(vw6nJ1AIi&$k+y=_SkjM2&mS0914zw?2@I9j)c)kQbQ2a zP;ukBD8YEvF z9FjshIcG^m*3oLLF-K95qwSiQ`s|74Dq~HDjFNKJ4vfYHROg5Va=p?<>DAoh=ioSL zZAEjAKY1`6|G~56YvYz$ke;#5;HW#qv-hh%j9GfY?h!=EbGURdq>h1HTtMG69-uo$ z&e$^uBmApKwf|^7eg5p*`uy49%jf$mBG&Bp@Yjg7I~Ma@7yr_1`dfec$xl-c_m?2W z!kUFOUm$DL}w4W{viwRgF}o zHk*?>SEN(@QEzS7oApv$G=wUy4yX-op|_cx@Jne!mbjpMAf2u_+3 z96B->N=cYUYDGY0Che*yp#v^k)VOKwH;^1RaJDsdAHHQ>eOS^rJ5u6PuReGtbx0c{NTCnRV zL!^eXdT$5LCj$c3*B4Vxhsgy<9*uI_Mi)5KY3W+=u{ zi>bBHH1`5>atjU%$!xrLz=!LFIVWOPW6<0rYU_$9hKUP`)M_e#QHVz!66UZ^YMjW= z=%-RzOM_@7^@cOSw!6L&nROK6r!z>kN2p+_x*toeSgRIswAu4Xsk_k7xN$8wg7U05 zIZGP&v5))yi-iV@Q2457uz^A$iJSrrD0!6Jah5KSRTR+f7B@1CqiH=S3xy+r(AkQ6 z;VA~RTy@sE3DjnFb?L%~A#rTbv4k>Q9a!g7T9M}7J^N~urMK8>STWK1qv8ji9iXNn z@&9oUoD+aFqiV2OvS$Ka@dzHJ(wt>*Rx!^z0KQ8I6 z3mX27n@s;*^MA8_c9XiG;s49_Oz9N4ueG#{t`DkJmkx3E&gSTrRcLT1>Es|ohzTi< z-mB|}82!bw5Ur$HY7hgjY0o5cdT8y$JU5+Ga_j1fA4z_7SfTF)g63(_7;@RQBng$7?B z4J@8w22gA$Beqnq#>4{|s+;sMW}tXzJGp%;W~)}_%+F9lYsnl9$|1RHUO478TSpj! zgOX*=4gv@;`|SCr zTq}iEFcYVf;|Nejs6`oa($U>BM~zX1{S0AXUDwOIRFN1{h1nN&Ht&wq&4w9`IFRO^ z545;;KIlwqQpj2NC-lv8z; z5>W@CbI8F^1H0UD%`tQH&DW(g6ODYC3XsF5~OU{e|A)x@Zq;{!NL+< z{;IIBiP2@VQzRaVsDxo!-Q-5f8nH@hl7f5ksjL|b*Fue>I9W^*a0%{}ra4PNa*rj# zn$Aky^-XCbBE(!VOYtJrYL?SCkR5ChDBXZ@@_rIONKexTCV8 zW{BU0z>GQgzuizqR~OYbOfX1FbD}JsZQ{{vpRu>QpkYD7SDSbw|C8|l1`YpzDF*^R zMTEI%jlo^%o|?PXgTZ}xa0i@bi=<&ED+h9~T~gLMCw8{xrry;+>%k=eIHA^bU(Hap zW9QyeYPGNmxtE`2EMlbY!bPp3OM8sKU8xm8H({xnIVA@*?LogS2 z(pxFyPWsT83yivAWB&sT=A@olAPV(blv+%j%}u#G;S$~0pA3VPKw=Pe=u{n4C5|y0 zP|PA^#hUwwCaV7;-Du_}wVH#Ag3{tld(vTztfwA1x_0md1#^Mm5GWL>I1_RMDXlj6 zmlpLu3GQ*72WcU~LWD1n2zr`naA|ZaJ!dN|x%=$OU^d|v zq==EKuGqOb-ZYW{eV{7@3lgj#kby=lA;jUItw}jT&ei3B_O5dj-N1o8kHk>4v|(w? z$)00%*VYe22sGLpzEiCPY>-Ns+fG*9=!;CKge1+dRXUo~5OXafsA|_C1!?1^!6i}R zvzP|=(Qt-m?e6pay}g@Dz1p&E^wn+~ZAwHz% zZCK`jos+RplL?gU4t`Ig4VQ9U#lcWGa>nL#lTzBMHm!eR0qPhrWG%A{y83x?ntiuR z?@eME1v0o8nmbxFW7a*o7829O(gtzbw2(qXOsH}O&5!o}z{2(Gm-_3iB{y~1Li)dV!n(^N?xv8VN(Ve!J>Kx%CNK+p4f!Y(vn%3EU zSV}GfLhE8>1R9}Z7?cM3wPVS#YpiK?74$IcbhZ)Z`NP-pVgIhaSa7gNg|7z(n)c7PQj|3#-Ad?IbD);E7Q&FM zrkW8)56LBhBhIquSeujDN(q22IR?!wr${x%SYu0#LXOr7w}Cam?CUoZj%tN$otsjc z6Qeh+qB(N3c*cEFw{Gt&J4g!_mcYW7jRlJQ*HrYsGfW{vX9*!uU>Y+rn^UgLX;`k< zPHuZ-!=3v&0|_FWQz=qw=oKqwKbF_D&z{8s(yLON^EYQ0qo^-mT{c8<^Iht2i(gh3 z#2rh9S-0FkIk-RUiU~%*GG_*b2GlY2QgcC~QXB)ZT8>IE70w_TetVmS1tT^&TP)nW zdgH>ats9q?9i(Lk>8p|qo3Vo==`=e?gdTqgEfp{rv|6y^NQ3+q z#)#nZ1O3eYjSeZ+QG*S?6Ti7i#bQLm647t2Y!(hUvP#mk81(tkG=+Nj_QlJ$zb`mg zq{3H)gH4nQz^6!1&7C?8acuxOw>->7sB;e{cBpP))u)?L0&y%`F#Of7*s(m z3~rsG`TYoH4Q4u5q;#g`3ioy{ZS7)QXt2;=p}`kN164XjeL|!l?AjuVsaA=-#LQ8% zxz`@TnvO>`jWp}Ra&2OqwF9$23*B-Y2CG0%t>K-Lynh;4rID&OjUd znTUGuxIM40-z+p(wvCnu^OwDClq{YC4eqIDmB>Vuq(QO35MXvsP031Gkw5eS4cXmp zY8W_Eve29<+NE`7HMhyDm~xB;)sQR2;EKd;c`wE7y9CLRJ2yXLXbv4dp~~R{8fvGO zQ_zvR3zV)C`FPG+261=2Rhez6 zb5+0yImOxnCR(Qy6q_5Um}iOEQCHP4+m#MC-yEfzw8SYolh)xtwW$Oe*@Ei|991hg zgZv-VlR2nDEF+9+DF^0~X^w3k4KQcdCTY`#GNbE0HaSxq+}Yc{wSD96LW6|{3k|+J z8t7>Xn6*%4ik3>B_EgQyF-FQU6|JM5oNtu6)z}JzA)RwKC_@+kt)(J~){NHSu6_hL zYpmv31fex1;;LbmP!l(8z1aGms5%&jsIPkv%|uqS43P2+bhlbll3Gjj#e@-xiZW|Gth77YywbMXzcPq;SLmH`- z-0B!3C<)p3x7(U2tic^Ijtfjnm? zfLs40Syr><0~wqJ>S_W5A;7Rwt%z7PGWA8JJ~NAaftfUgREDp|?~WBU`&zYbT(*&` z1WNI2+u?0p-?@5WA;TgQzA7?oh)fVWMGQekuOupZr`qRvAIHMITkH05=w}xyQ%)s@VI(Y=z zhqZul1`m97S~^jm&6uQ)YA$Cs3g)t)(>ADKG{E5aIoND!NHe@g9N8(jjh>= z_S92!lNm$l%&{b9jB1ij-Y9~eD`c&CR>Kx|>4AE5f84TVDY{|=&mVIWLLCEKONine zb(UhA>7o_=BiEFjL?V^f_CW1^wU!)X75#0k5s)n%6`hS%T@SFZewh5AP3Dv!*CIW( zI{Pmr-mrnp967j-gEQ@5as6N%Znq~(5Mc=-EV-v&dJw^l-oMWVNTq3WgO0^LYCn;6 z7+f1kkt(((*{BxGwYU>HJLUn*UUJkbQ*bi4>R2Nc)S$a#U);_U0m*O@K=ey8b~Cr< z?C;z9b{2_RYOW3D-gpYm7V1W3i3$$M4uc+IkGWu=j^Jm<41v?AV1ZKDu(AY3j0k7a zANx->JU!SyKv>AI2!%x`d~rfy^5EZxK^Zc`tfPkPrZqtC8F?V}R7-8~WTD`aqK6PG zYmZtxx9m2z%`SDqO;+{DJBC3c7X>r5DOU9h0z{BpD_fKfoqGx}dJJo4=s-(;`C_%l z*Z?gZ)h#z7gEG@tLzEhkYRP_2!zdfm+%z%;n%Phr27{Oav3{1B{LgwDxm|s=1Q8Y( zEHL=;VBpqJP7zV&X^@kDz=E6;0JYLXVgc#?*@-YHSVn0=N`ZSV6soTv<(%EWhm)FF zzgGsXJS;}P1SZMslA;%|N?oI0*$PG@5nY>JgsBIy%(=vtpn-z{Ri$utAyAXk!G!=6 zzX={=L_HeYmYAvo$j`2u27_oRN<7nMMi*~A*u8z{>OzBs1`7?oI2wqVog#*i2vgIR zJGo^=_qN^Tw*n=UsL@VtUxC9$%p(KR2+VGOIfuZl(HKo_jWp<)lRK(pC8WZdhql^$ z8K}@2%r6iQhr>D=MAsist%_EP#&bAAAJ{~k5$K?a%E~>1I_Ja$EI@|V4OwAEoi~dH zt{PR5)0qy=IikVR%xIy(LW3`m28yQ{wj4W!QmNVKGXI3CnM*vHeq3T-Gi+H?0xB)d z+OiP48}%L;Trnt72zhMR0H?u#R6hte3c^jH)+SFLMG z4=Uy9)|XP_Af||tA*Et6D4wN(GU=-YlDz! z#JQp5QXn`wa#p{wJ~9>>8wLYK5=dv8ZTjlv{?q5r78oorSYYr4!r(K{@9!vsq$Nr| z%F<-x8avL`4!Jk04uq3oFvk?5D*_lv0UNndPLP8;=P?oW)u#M?9oihKmYZ}7gXYY$ zSyJpm!)ZXwZs14B(LLF<%3xMRr^FHZsEi6F=N|mbE-fAXfItIkXh;}E8XC=tW^Q63XTvok5E^leeeO-g7!j2v&8(mf35~7C$fa05Vcgnr z^CIVmrCz6skdWqj-T!y5za=W5sf=6{{c{l;mR+)R=3=9YpMJ=OWtWO5n9yeul`h^%#sL4f>MqZpjr;YHZ7uzb3`ffnZ^+AZr|Fwdh^DD zhD9lSRcP1@rQr7O6f|f#lsYF+(y&=YhVFFtUK@=9vohf1(t~EgskyYEJ{n6b;vTw^ zql98VAv@^(SY>PC#ydDQR4QBH>`X~951kV{OAZNKc&x5+{y+oMC?#RbrSyuHOYAeF zG6iim_p?$TQ8H_?fOT@Rau8QGA})%O^$nu|MVMJuXIgr2@9K@c+gl3^7M<`_!C)hG z!f7H3c|dgI35}aVm<^4hDY0l!x7o0wO!xyC-1O$2u+)~=?K9x0*r~(Yq9(0r3*03z2J;W4sufbJ z{#s0;7-GgjLonf{b+3>VNgSJJOor>%7ceX;;j03}W~c-;{A={^@1z=ach%FdUPlCj zVlXVHHZsbHC7-MkAPEYqxoLH2(J3?sfYxY4qo%ed?^IkM@a%o(HavBwiL{{Xgv!c&1Bge)9j5Wk#nsuW?Z9{8DyZw&@qSX)KQ$fTA`FV0?z!)6yt_)AH1% zL>OaK?FF+2o;7Ae=UC%9N0_bYaVNyFxS_@pTH&5dxtCUylg*I`J!;C(^NN(Gk7j7r zD5VCA$d=U{1W5`ov5xM`XLWs$8ckL;R;~y`M(#2822yZ?T33K^#N>a@6bru|yz<}t zMI$U&Sg`PAV+Zf4x{T%FsGaNUu@M{3(R;UG|Q?E-q}w-jNWT zA{UhtGq~ZXt*aWlVQ#fbb-SKZZ~0_}&}xh!cOHHWSTGynem>9Ub{%71)4_ry$N(yC zzN<9X3DQRG)^cU09DAaQ2J82ZiYo!YqGTg0&Kl`v#ONx3Tgp@vE!LQku_tK3RSyFUkB?Y-|V?d&W#SY*QAyvd~t_Wy-UFgt}znEfXj zjuvo^n2{nHp^a2v&aUZex*I`}iWvntRw+>}0BfaD8LBp0Q^G7-vNrcxYsfWK)Lct4 zO~A7Yow?f|vTKI5)6)V<6vNP|l$keWP+SZI7}fGDyc44~Te9ZfImcREM7fwCu8Fc$ zN}I(7i3|*Ow!QA29_0FTb~joA3kw>)BBijIXgEb+!5E;)n1g7rlE5fq4k&8^(}2en z+c?K)aJx?K?GI5`5DFPeaUy7~q*7M3#W+So3_=>GJukh6kPYMTYbiIEKHZHyQ;03C z9grX~sybR+>Hs%%zC>+){WSkB!J-Z~hk+4nXlGZ9v2mTbyW|prH!S2V5sbz0aF%Ge zzPEMp`qKDrLBpaNzH~HDz*CIbq^!Nik+h5T!S2QvL}YHvDHb|;c20HA-fev7O);>0 z`zG%7&(29DxD;4DB=da*Ta-4;LTg>HODrWs2sAVgR?Cnp#?)zb4cu|Bi`l#()Mk=1 zx)fl_rNl-p15$u;*Ouq_grP7xadL4nz(t1erlBE83~;vDNKc=%Cw_tj4NH9C z-$Fyd{~q+&O}77w_(G8Xf$*OfUwB`q@L6Y@zttf-Q=ff80aFqwV32yLO$sOEb}|hb z55`boxXkcZB5*LwokH+`_-Rh20hqHxilmVMLJB229TH?J?(su#w#?M0up-L+kx;;F zsU$;97|^)q6#Yz)*&X7NI#;mS*L60~J{tYWR#KU<@T|UU*)$q}1WIQ+N$vjA{T;vE zg$4@^zN$T>&4@Ou>M7il`6#;Wh8|oLL`q_0yWl$_U|l8*o&&GDLNiGvII@OnR7Y7~|*!dHfaO$;N%&l;WmoiIYvF~*#M z69f0z4k%TO6C#3>dMvc zFP5=k4K5tn6jb?4o21^my|=Y{bqO&qQsFCO!)8bY3!n9~`5QPWvDb!{X9pr`)}d(5 zRH~@W_7NvfQ0N7R)!4fG=v>IrQDQV`8cjk^Yx*69S(MP~Ob2q~o(surt-+{<@hB`o z68-zD0djf5Dl}m3n2Xv-V2)af}Tw|`)pGwRAQf(R_0`>yQukDN7FdPsu zA)IY4>byrSsLEA;Cg|FOLMO@f2yN&<4-2Fa%@w z<=q*tpqNr1)6~<+k|4I2xmWFZ4mYcqYmcMxC{VCNTwPuEKJB!NSW*;Ck~$E%7-+Mh zl9`&NAu+_jWvnHE^o&Atrdrogg;OoTO= zFq^?Lx(+lnX2a3Xj&n3^ROt@pyDE%!JxNXKkiS4z+kSOb)kYnHJF!4m4MEDPDCh9P!I zfrWWa)LfHmG>Zj@X2&}ui~tG4pb>;>G8Yfp6Aza!>-gZ#ua=TobP!>8<K?vvA;7hoIw!)9zA zshp-}xl(28(qgC?HMTa((=172f~>0=UH{SQZYWUc7BX|rt`w>OHYklFhgB5}e+*0b zf7v^;=BAQm-@l(vq2QHZQ(0A+m9?L)ZOmxEVCLI5I%>$W-FRw`K*ag(H%rD~u&f57 z-Wyu0&)KxwNTpg!QvUK^If;b^!6i~r(E411o)p9}2S>)5l+;TP$r{V*goh?E zCTLTt(x6#RC8(At>bKkMu?ndF$|iwI1LG@HQ~n~=RAImk2zt?U4@fXaQ*;+q1xvGq zZO=CX?A<<{0xH$mtX^r=*A1b}StKEVemZQJ#pImNFnIF(aC2*5!N7un1wS7P;3|zy zje6FG9mh-=QO#s?r6dc!B(!c)aQH08C^LjWH7O1t^eN3^sSv3VRngb}zfx<01I zWJ#(IjETKjYKV=qFOeL@U>r`3Dknm6=ydNQhP~AdV>PDcSelxBs!oXd;aTo&P<>W& zC^jJ_R&@DV=8Of3PL)VJ(+cH#t9Msc7gp~LqjQE3!Y_q`c?=<_dHjAhpqgZ~4OJ|- zl&CeMp0;o={;j69331-Dj3e)xcxs&Fl&&nxAeR|Zb zr<$P|?DO2B!lSoRoyrCv-6IN#X$q2{ix`U0lP#7IjiD4O2UmK_zLdS8f=VDzb&{!> zIXg58$T6b7&=&WH*zjoW-k=f&EDTuq$+2Mh3dcthz*P0HO|#CEgg&phaFo&$Qf<9_ z+GbBV0wdhK8088jFPTGtE+UGZRL}6K|EX^YO{!ACN(R#1a}8vufep$Pt`(B^lV`Gh ziYzM*Vl@3=D2Ixy*H9v6qk6!ZWTn;Oi%9jl5OA{1k#LPBP|KX>4GY#ph-S78?_S<} z)X6XihJg$N8GdRq_$vh#FlGnhZR3_CWMmU_FJ4YPzf;VV9u_5+;sPT{vc|A1ma|}tOlYC? zDGG=Rolsq6f*i2T84vn^JL_iL?{3ZhJ~Tfal)^6tgZWenS14v~5X`s&j3|?Dxi*9n zFngy~-p0$T*BTPHFs@4GN>O|(YIgORIfDr@B_Fk@9>At7Q>)$-3!%?d^BF*FHNLj$ zEN4qo$uMqrjnd21IL4TWOOak$jfRz2pq8qq`i9_+dj4e4T^EP@J0y}0u z)A^cbN?{NRgHZUT*f1YLA;frvPDhe^d0Mp~(5I+z7Kce)cpxmo_41x~Xw~gjFCUpA zbM-Zf^GVHZaSjYl2`SiOjrCM*@z^p6M}0jPQYHN@Hf&x=(d^WaLN8z-lT##{EoOGk zHxt#|J9H?iZv;v)`VdYKNnE6&5(uD(H2u|_;lLZmZYIUdpK(z3r5+p%eeVXPF!aU# z$&~_HdxazQG|SXs4$CFM{vcuFdR zs+UATtQXxT=c1%=JZET7DW?2}nI<8vz28wftPf}ygu);cembGR>au@74PwaZtBXVd z>A{46U_!`*n99@;1!0>(P!J>PL2Ks9qazFyP>)#DV*M90V~?lUH|~*-*97`{Y`)VgvLJ5SNRC z+8i~&tSSL8C;<|Bn_N-L6QZ3GU?$ZC-YY~*W#x~wQ1y?TORp;^E~;vX2HNy6LIiFq zLsSV+tSSnew~$&dwE$N|lI~2JX-c+~f^b2=)-pwo)N)Akg@a(DDx!-6_kIiqTWRQV zG#ncZ$3{Q@u@PLQ{T+xgl5#Z0TR{_}bFpN$>>+zMDgWmzz#L334I^7rBlOs9ALm%B zfQeOnoLrUssav*{2&T1&J{Xf->Ip*B3zU0NGe;~YO`W0>7*uUA*CJg9_{4^blc11; zgalx-Yu2SKu}|PsO8APU#!?NCDv0?a0|l@831=}y=NuXKw)OM=?)|`qK`0DD;pYZflPIS^J`ctC_(5i1Dp`=DmJep>My2E0EB%5qA(K3^bt@fs< z{b-B>m<&|8=8E%%17n=N#j_oyv#HPC95PV{7<`{l$Zpi|!bR5q77XYg3Ih|b5>mFD zp;XFNH9;t5T;~eWg_uQs{L4eiAtX+qmIgekoh_jcvLQ)qb2EV^ccnOmfw$m*YsgfI zdL6V6)|TL?VRqG96H;kmYX7_FnR6sDgf_-%gKM#t!ZxKo1St1$(Z;b7L;uS9njlKd ztwxI_g|GdAe>w~>M%7LTSJwusryuIkZT;9O67KbR2|IE)T7I{;rI)p@Y>+`I3~(5f z!cPi^5W@Gvp?YIQ&ACZfT1&k?f!GE%#gbLCe<>U&tF;k*Y;xrL=z6$`~=43+RrWWeStfC*lyQ1sub zCK$=i7YtM>cu+|-gXGiCFxXfxdqb|#z=Gk_=x1a>yh5&#Qwvst25PDizSV zZl{a6yzO0sp6eN7j?I_IE*Fp1W#_~tG&H3~ja+YATKc5t-XFV1uT!)D>fY;mMv{XX z%q1f`KaJ2yP&qqMITLJX(KesF>(|n_r&)sS&F~VVsWz&FP(Y;W0eS#B);S9ysAx1% z-=3M~8f{9`kGTvq7?O>CDP=H!$woHdRr(y|?rtXv8OOePAQL4uRJXbhH^9rI%awzG zU@XBEXpt*cshFJ_F<&$+c9QN*>25Mocd3+Z>@mzXL9iv$G8?udWT_69y^_ zRQTDc&{I&aKn3=Jsd`(~xsH@$iUG1KQ8G8lliFQ>q=K5;n0x}GR$T>0&*Y(~i@Dgc z&paUnaYhBT+vIIiLte?&g6w?BHAFPlnVi(SXMG4yow-S*#=VxNdM7s0oma%(a@&Cl z17n#ZB$FbiKGMPaDo~Ykl}vGkYnZpgBc@>V<1y1n!sC_Yg{7rIFAQWD$nbNM!N;o% z*0BvrA~sf?|Cpn5HLLZ+%FdP?;PRs+*4GoT)Jz5gY?`wwJQ@=effgngot;)Od+v?u z`pivfQK*3_f)qlqxuEnTrHT$FD~?ZkLFGkrruV<@xl=3zkc(Gf7*NGxqM`mvl_M#) z;K;UM$kg2Q>iVqcc_V{&5oR%f^Rr}lxU%r*&H#oXtnf>LVNOEKd?BCYTSCkQ4XPRS zqMm+I1DiYrqLA1(HvaN_qgX^8dMhC&vTA;tMD^}52(lQ%Q}T_vCV-G5Srapgt;!bV3Al$$c}Zns zh%pa97=Z9I0)ekkiXc=ByWEP@YMiavs&OvW_!fMjNlBz<<8y?;yQr^hLM+a6CaCIp z8+E_cMV}Ht@Qx`VvdJD}NLBVOn2U|dHI*qPM-dZ?N*`bZ-_o2lyF=?-bm(T9eY&QmdTe*=6SWsZf!aWYfeJq% z6_Bsc*~qI0-NUmDxRF}Z7Hn#0eV@@*p`hPiSB_YE=7n-V-A z?m4SD6%ch`eF+>BWe3h@5!DCiReK0d9kQwJskf>L)Yf~KB@#xPDVQ1zTTtv6@Jcva z*}AA2s3{nnF6vvx;`9pUiwXufhA@jf)Xy^EksO}v?!P^&UmmD1P+_3L&qswwS7>lV zMAbHHr6uYC+Qy(rfY5wa!z!1D5zzU{-s_*SW~gf6g$WyZL7Ng!t}Qs#3fadPB?YzL z0a#zMgp{L&-aW6TKC;s#>nC@`IO}&O3NKT@m1v6X|ENsyMVIkn1)yc%+(R3HsnzN(Gf;Fxyd^hbQuGs9GL!jeac_%w?|8g;V2i0E2;Sax0ucQgQ5U z?IB(@>{%*?Nn;DnN|=kZz(~m{Qwl~OOe1T!nWoBR!aMajs$f!czO;nJa%nZ$1Sw=g zt`AgGE{OW}O(qvXpG6f~NNj3WwzMjQY$SSyOtp8j@V*5IwJL$wWh2T1(v`gnmH=JE zIUB9khu=BpXVD`2T&~gT(*X;EMEIq!Fdq^jnyd6crBq`~-e9hRSSbNl$*EP>P_SHH zrKZj~xM-^y-bR8It?~nNKs1%}B@L~|B*JeHswFFnQ1cWHIkavi_*c2K!wfZe43S7MPkTd%13MnzFO}R8TZ^IIRZ0|Tb)A;hq z+k>(-3`-bbFu>sFgF%S?3f(M{3u=kgX*4|)kfCSkL{Q6IELvs5r7$oqWt*&cs}D`p z8wOAUQVC6}zBGm&#K4kANFadn3*4D@|C{j2V@U;gNf)%c8zk(4>l7aYjj;AY(u`(#Kn8f3yRhK2c%2_eQSG{h!BTPWAus;jz)A@A5)_PP3o z@$#NV;Po+4azj!pL&&xY2FOO#@T5y(!KcZmnSr5|YusKrjL2Wrt zoud|+R83fwC*o{PS%Uihsfb|qDhQfM(dAlP>E}x(Q7>U8R&7xOsd|{V=?H+md}OvI z3F8*1_5IH7!2pCoCk#WEe|nv8g}z3lCce+eDIt)pmK;(Nz7piwIN5gutpwN|?9O1SV(L%ywKtKSbKwIyl;s*9imQ0&^=eu9qjCs?k4;~q%A5rj$Oo{!)_ayr*gkl-y+s2I1{nOxFu;Ev z493L>j8`~5vME~=Tc$*6R|U|sbJi#Ct)gH`OKkNwlZ5J1QCpsKX=I`2tQwyiwK7E@ z6eHFeTh)YT3MuvixkABO@?cVRC1k>W#4`m3Ca08H_pJt=xY^K0zOH{b2E)HZxsEcAL|X+AI^^kBK*=s_@~MPFjojI z=j1VE7Yn!Sjd&BN+E5ycY3f^F9$LeI^ea85XLFu;Ptr$-Cfu>?3oqNmH^wGFG)tav9yY z11kC4wS-CpfH|vM4tm*re8f!amN&Q4x@>Ii4LBIW3BME$<}jS#8LprfOw5f9WJiv? zT5L`>ajUJF5Tmn~tA$b_W2znhx0q@a` zX)tg?n8X=dAcUHG6OZgTQ&cTrvFO&Mc|)vPrWU7c)0a^^P(ytX8=fy1khAQVa0apE z&oD@3Z%?JdK!ZUi3~5I{w@yHaR~WQeJyb3=ax-e(jhD!J(_$ZZ#%kjy6p)?i1ga_( z)T&D*RumX4#)ROClVH$IHteTSg%Vu@NKqtK3uIDAu6fCZBoL(Bj9_(Nf2+SdLrT_V zu>>U+q+VQ6s6n*VG!Y+DC@pxYu4I^}q*RFHRUx?9cC|QYWnjX< zgn2?8rI_fH>3s7K(-6cMpciYQx#laCY?5ogJ}vSAhYy zo_=bx59+WBVpRJH%7v!Zp8e%#N0fka5m$^pg9uF)W+;6^CjjB(*4ASfL{yjF7;Al^ z%IFKkm`d<5XN>3*W~LIuRDs}yTq%XY>#Oy15m!ZDx`gPl_q^!6eQE`7d)Pb_7aMw8 z5p%VU^?K(E1{k6e+RnBI>h{5ZzU|$opa0+I1+JZa(d2A+gSC>zJAUB%%(!` z4uThk6n}j0E~v2o%%;fbo|rEAh=;Eu1^@j!rFQQ}KIK>1)bvkzZ|nF2I|_ftjxN}& z|MHHky)aeddv-*B$BusJUAxkb{@M)KA9~~dhT8;ip%V3bc7%V^ZTcb7{XLt4AG4M) z`}Zzf=uG;zZ|JYzI(LCJ|NTw%)5jm!)Rnx;j|XgeJ1)*08E?w{kneQ$Jx#w+7ZS<7 zV^ee`7xS+jI=+z5@I9Nlx;D6+E&L;99It+xroMMS^ntQhatco0JI7J)*F+Z+{!$^t z%Qxly-|*JKU%3TSHsrpzNuPc`f6#Ea`&Pa_1-e#ux6|hMbmrx~M>n3hTaQ=a`GW=i za&W^n-M_5wZyvt?eM^pIYkd2@BH`DM`E-J~vAMqSK{i3hof!T-ravc`txY_FddBA?*1r^_V#y6KQy{<1SLEDoTT64mbMP$ z!rz~5bWQH1ro%K=LsiOlaNBu^PCdSsj zeYupQX1l+CABk{ez9oz0qzPHC^;zq&Ii>a6|OZ@!9qd-QfNjkI!CWHrrm= zO?7-%k9M~Dk5&5N`l#yS`Z7PV*}n3v@iMd7>FWM&zoVnmL&(vd?CS@2JMB!Gxb+<$ zg7In}*PhKrmp0OlerEMeKilir%KY#q`zkZOmHL&^f6d^l|8d2&@q(+T>&<4bXGUdo zP&T9*RBN;j!$y82!Dk!_wXw)%0U*=%qzmA9j# zy^jW}fBEa?zS<_e2l7Log1+$4j11FLInWKs>^J#w;o0nRZR6;0RMqUC`?~+oH(l53 z%gttYwg$b;)7H_Cy}PHT za{2R`Gy2NB-%AGvqr?5wu_z<8MdfXeFi+X);r`LoZ}u1yk9H6BZp>zrw|Dmy-3K2( zLZi07yRBAeU!A?fS$sC1ju)EkC$z75thaM_d++E_ouu)e>+bm8j(QZUKkf(i{CuY` zHX~}DdK{zU-L0c-8FkP9P6j8~o!H@DS8I@sMgMN!$>?BB1{ z`c3|n6!Vw2_b)!e=U!u`=v?30&1vgfJm$apj*Zv5GhS}S?#L&_G&A*PY#u0B)G3*qa&WY> z`R9@Rewt(8!i2v${j2j~{PQ97^S_?|vkZyKC$FyGLK9jGf)S(|`9r|M!3X z_kaGUKj72ltL+_M^|dkAKRx~Qz~}2KI!S0dL?%bBH{h#x>ZKr2G`W6}U`Pk>0+Ssp>K|b!KaID36 zbHh3`H+;o-BPU5WVry0$MR0V!Anwx@p6%=W-(8lo^ZEWtIrwrD=bi%pf{9N#zOeID z&b~z5aC~LTuc>jD^wmtDi@U1>S^C}eLKAKZ~||BL0ecU8Gvdc)3c=BLZJaP?yq_0`39+dH-hAK&WmMIOs| zt;_#s#5i_X31V%vxjqyr6va;z7geO6U36$(45xtaciz#el= zIZ%Kq%@)8w-UWh;)*?u+1Ii=x4lYZf)MX_xzSWxXUjW7vJ4K+}+z;*?s=n?7iN#Z=U7*_s#n!5VrmG?Sq?-o_>%Q zFZYg~zg$}J2QRmlUmxFo!Vm60p+`&4mKRu_-g4&P-mY7%Yx|okn}Je^^djs{_s5ofBO()eH7r< zUXI5o4>ow^;qu)-4wi1)lY8yO*3$ElgD^{{k1(_TzmbxzI=20^_`RD z$2ZHpXLld|`S#uF>z&2L*Vj#0KU_L`64%%6Y_8wOt<}}ncV3m{>whfx4{o8to5O|Y zo4207-(Ox{ef{Cjr#tr^tiP)_pRSj%{(gOR7f*H~uI?T^3Xf&^^?i8$YV-d3)9vGf zvc7k7(=2? zde8S(mKHwj-+z2R-8p)5^8Dy{O-?qC@4vXWdhmAZPrjd5{GSi+A3R%ID$DQo?;Nmu z`QW*Y_~!O5ZtdG==}j)rPw=+ff6s^O&tzq9>Al^&BWn~E7GHhXTF=Xy@O%kxzbkKc z7X160ODhl9K6$svKHR=@=gre+D+f!@-#vx&a{1)(9xQA>{^Qou*RMaUpBx>m#Fy>Q z#mzhS-|f6uNIRSFp8Xka?OD6BShrq2y#HbA@jE`*UVLDmzQdQ>i!1l{@f>s~&4dwhRo_wlp$i+f;B-r4(Q z{l(JLw6WkH?cZBk_UZb@{=-)X3+~T_yY~)Xsu5Ub`QveT^}@-<%?C@bPu^{%?d6lD z^%dTFWKW*$9Jc+9*EgS=^_%!$p&mcqdv<-{;KR#57weyQ!;2M>H~Y))9>k?%c)ot` z)`JCJzggaeCkxA~*B3Sx+SBy#!J@tU`2NQA#kJdOe?ItQ?fv1SJBwR|R`>Vb9X|+L zOD}G{%WHSso3OC4^zh^zt;^QQ`pUxA;j6`0_n$wzYvUi+pTo-Cy1Do6=99hs_MqL_ zxor*>_v+&7ciYQP-aq^B(rnO8^LC>gZEW0s{q(54-@d--Up#oYTkbzRxcT6??Y?w3 zpWn}ZRo*;ad-LM)qh}j$KKS~_>dk=97G9;BOE22v<5%wK`iqr&xAtXa^%3l?!tTM# zuytqq?!ofLAB&GawCl^Oi_89aryjrjfba6nvb6E;PC15^g?#;R>u6!+!Oj~WHV+y+ ze0BT*-aOnoSbCLT9j+Gnb7Q~k!QKJAzI*aS7GJF_uP(Llr*(&0@#y~1rpb2}*Iz$f zDCIaDz1qLIjtlL&-F@_Ie{cO+x_P{__s8P#-pazsL%;rXarOA#oBsIA{`Y9%af!S7 zX8ZnMw-?K!#hsHAeDdMetM#Xct83{EZmr+kJUn=s4z||cxV-andHKc8#zQ&0w|KI- z`C?})FXfl}%k6Og5a2^SdAV~Sw_n1O$K+06^T6#L-h#as@!cJBc>Tk1d%pi+|9Hc( z`Saw}%Y_$9C$tgx&b@XgyxIDoZ{`tg)a&n~ZOcnfZ*Tl@bl2rqcleQayRVk-s@Sa` zj-Tg4Nw?l@ymj0ktyn1-& zI@|Yr=k1;AIlNt=_-;LH--fl@#~b}t9K61J^VMd!yL^51#oNtA`^O>VM{nLeyZ+WA z@*hvpy{{ke=1tg!2anI)!sRDz=iqg^V?V$_UA?~+;;ScXM-ddb`SHTtrSu4oAANZK z7IKj%FFriq%O`I0@zJyE&u?!(1FdH~*(tmAWAVm^t<5i{jeQ|5zskQq^DOtiRJYgkCA;se zF0!ed$ttOws>hKi8%{PHKkB&H-uD& z_R&B$l6rZnNJHw4Kcbi~eDSW~e$QK}y!}{{@X^1y;7x7p9(*M%`fttz_1#-Hw%vyt zzOYdl#9wHHy|G>~|AIjOvR427#n`!P->}5)*DlhhCrzpqxVE>I4*L;Yf54=o%rA7* z7u6E~d*QOrf?i*$ip$Hzrk09*T{qQ>!q)VmqowXTj@)QT7aaB9^n;>tG17%1s*43w zU`H*FiW?D*svCi*Lp%~c@@^DlBz4rnD1;G8ucy*SURdy}a;l~Gy0D~!-JQ|BopSm2 z>Msh#zP{MMypF%x*VlhPsf_FL-Ciu$`uh9-Wl7doe>ilQ>d2luawB+R-{r2yAK&9$~V8Es4>3S4?V@dc%8F zp9#qjV^O&&t*SOH=28L6sd`hQNd!3{m)E^b zsd@Y2gkpT8^cRIn|Ah+StC;@N+cabIf4b;5BjBt1d;aRbuV(n0uI*d)`mGhdSE=az z>&o5P7wV$(-s<=N>ny}?88rMo=IZp4uVAuH--RDBTW2fG+;mO4zyG8{U37>x=*Q#g zt)PFvGdR8S2`c3)b8-1Ih87P0*cJ|cl-;P|g^R5JjTR0D|84my2LBgm;XwSK?D3`s zhbkt{%7lW3d&~%-N^x96p($ig;|$nB&NauHszdQm(79?0X2zyOB3F6x5c^Lf=*8)# z_1OS@{8_Dyt4#@Nr4SLw>h4=>N=p!5f>GwUAnZf-%8EIf6VR(3nCwh?f3Ce=d$#MJ(|Ko)>hu}{2YUs_cpSSWMU*bQQgq!RK%qo*Hg55*Chrrz{~*8U$^Q2YEz8zFmXRzVO8 z#!d%P(=Hw-cV#C36kE&DAq_Q6+POFtd6_SQ-_CHNXdr~n_6}WWFas@ zu)3v)owgtnC8tQS=kC4hz2ZMwB~O1vP0lNPNHhFfAr z_3#}?RjLHFS`tmFr8b<*l227GG1xxc-nURpmEO(AJKqP985?3UP*Al3I54rrkfW&d z2`!cA$OjcJb3=ktX~5{{Vn^N|g2A0z_XZXWgJFI#7R=#b7$5x=2F^$5^VijG6i^Qx zdTdrXqp(mjouOVn7^YgI_TM1~1Xf#LgRR6-pB2QlFeQEvfs!l2$jtM%r(z%r zmg>pJru*Q@qh-zl$u-9SnTe`F-E+>m3tcB>4h3u0pFu*2CaC(s962Tl5?dyPD9D`6 zkqjIHVW63+gq76?0~Q8}@JnG~P9#EfS0Dj|+&rnXof{SI2mh=FIR_-bdQ&}DRY#D8UkQ%6a&w?&{;mi}%$+_r3 za-!^LE~h4;hDu1xz4X0>)}$2i-l%qP&3OuJe4v=J&kb4&DbCqcm>yc`vE&(_7p;OB zSTIP0Uy22DC=t-Oue&;b0~oldTMfmu4hPQaQae(oUtwVy)Jd`3vlJr-F=sISpovL* z6jw|0y{QhBIBj;M!X%-n?N2S+9Am(29ThA{h_zAkJ*pFW+P9hoZLrEDp*>Y+rn z{f=VB-qt6oS#(CK7aOs7i_QC1j8{w)-C8U2RE2OhSjN!BkqAFHF6_AM9}Z*~$S{!M z=O%-vc!l_~fQYRDB677LMj$ulP}LEqr0(~nWT2|_s5aN&l?e&5$JVD(nhL=NH+d%T zDH(iJn6#LJE5>9%?nWL}FX)pgB;O21PSYhrEj=1S%@&{70>@lJO}3SsLygIZYA8<8 zM0C+Awh*&E&J&9%6vCX5fmNafzA&5ShsdyS>&3$A5Lg)4FtFjLX9GgKf@;X>ysM{A z97`=+IjfJY4t6xQsrPLsnE11a^UkXuUUE^vU@=p|6hc7=rZa;lOeg--Q&EAEs8^?{ zFW-D0a9k^>V({4%HR?ge5re6Lg-R3&d^Op7Qgvg!Pc_Dt^|>k9A~|L+6{WC54q{y( zFQH?j1QX^i;v5Z`cwrXi4;85$Na?MJ1~d%8gEP&Y=q6 zO46T8vn@<5O^7D7QfwmQ&`9YB38>=6V+3cVnU*X|v)tp^+=^76Sp^KH=%my%XEX?? z2vq3LQYk!LU3&V=&p8Z+?n2*kZQrujZ#Cw>R(BzPL7jhtP3|e2EwdMA3^hm2SIj0R zHDXxog%9;-n~Wrs^x38~LgOr9>!~|UpU)hHr+DZTIJsif3OBYGd%QPSbB7B{ZNU;SQ+N54R zX9k*$I%6EsU{UwkTgv2$x@T2#0XwC9%OvNyB*!_I)@NC;!@xO&)Q74B!dzok4|tk^ zhFn7pIb@0gs4hRX(o9BcQUDV~e+qrHPIM^H(5TxiYOX7x_a^%}>g{l9p!=8&^8@W2 zzR^JIzYcfvQ5($kV5X;E+dP_S?^va)iC0LRbRH@|KM3uPR`8sHVeec;^=ylqaA19Q zwvlWWCl$M0WGmRBjnqPP)+M!%QxYdr$$-5a3NrTKXADL6U!5V!pvyJioBK>|+;G;V zlf9Z@CkATw`fJLRpcdztjjO5$A_ZqqO~0u%>4UjfR2j6<=q0A<^WXk~W*QV=j%Qk1 zwY8b|2CIDT(B=zs{H>?I&V4@a&He0klv+RB<~*uqNaL zw~s$?IR2dn<2?y&5A37_2zmzFTWA)mrdu83%iVhw z1!RiDMlo21T+|_VE+i2btj$wua;y5?z8J6OEHs5i3NGawB{6zXhM|)f%cPe+pKza> zNhxD;1(WF2`%GS9?R_7rLMN%npupg3kZ5}pH{=*1w|-FW=kEN)gcv=49W(hMZaiFj zv9vn*BmEbr;^&Rnz>a|(13SKW-XwPH|K05HS6~M>V6nCzp)}6Uk#vwqAlI^#czHZE z7A7calWJuMWHS}?$_38*M|(}kdTsqKH3MiJ-yRI3)!WBgwAI5`(0BppqQD4gjS$Nu%3}p zFs<31SJTg(Dp3|;<7`A_Bw1g?y6mg5pyI`)hW3j=)|2kX=g(Fd zQrSB*;fZG~VDeFwNWq*-ckyEtlfX1p4IUUJpx3?BsVp!IYTaTV_hPsahM*>|0{7g{ zlBhP_y{Z7)PshN3e`lfC} zL_dpBlAp=&XzkvBg+U|yQdpQ1jlg(?UOqmn8O@DMudPZ3d%deOVlos|n|ygSn55=x z$z0S42O~ZJW?x&Dz*s_?a%AMP>7C``I0vbhfKhaP@^4jF-^6Bo4d9q3$E45tHDu+F z39U-?);lMpUa;viBwb6bQPn?;$AFa@^z^4DPBKZ6;%mh^XC&y&x_c7VEJ?6-cje{6 z>Og~m1_KR#J{qXYdj(z4Dkn!4%d(a^k zwetlLS#9%I4eM@#E z4tq31AyBx7rvulXvT8#BWrki*zJ7L639=tC_dE9=7f5E?kIvxK~l>+|vo!uSz-Dd02Q3;?>cOm`u z+sCI5h#3$wAm*n-48v90N}F8NQFk0OWyBClbEPB;z9h6RU7o^fGxx*pnvCAqkX&eD zsSsJV!3Q(t=!pxXz94y%rI$V!6DvET)}OO4ksQTf98Q)jXGc$@fI_ExmmVLtx?!xw z)ErBzQ3XerApJltyW-JjP%CB=l0qy+U*{}^HCO=F|DS2u+`ZMiE2~{`+*(-~a4>`= zeiZ{nT-leIi1L(d3$H4i#Cip~T))$?LTw zE3Fn^M7>&F2viR>6)4;jv#4dxb0xoRUs5oJAE!Ad% zIJO2=C9!XF6sFMcfa@LWW}DW!x;E&Aq509bT-&$o^;@m_FVp-;%}-!=l|X?@T-e#% z`#GoR#u=tupe75}!KBfRXJKO~`sjBT4BKR! zlvISkp&=zf6Sb$5F6(l4R%uXr@v^FgXw@*gVi{Xft);Kp3~$qf-xAx+i~2vv=VddoiB39fs`!JbhUo|??nCn zsnOtC8|xKYu7v^$sj*iblw8$if)M*=path5>DH|6z3_7MXdL<|e*VUVL9{bTI6B|Z zXzAJQAv0(I!*^VyAC)wsc;O;>0K?y?7&v_W#s3B{kcH$RTU2+tK~+mDsW&}!YT$cj z>g5$`#+%ZsgV`q*R5bL?cTJz9PjozGa#${{w_tTG#O&19w_KDCPzop%YNQy1DNG-N zfTGeSBPvmht>BXc%xe5BRk2X2?cFkJqC(P;gKbqMQ2=gUc_9_%3=Dc_0Kv=@4DDp^ zsBG;FgCvKn(O*k6%t_Yh#WJLCz(Fa#_sCApwB8Lujl9}+9|K!;+kqxH>t|zyk$Y4% zszt`^b1Gt*>mh`wXz?&5-XLV7)WAXl{S1j5M-zkZQXy4NU4Ya^lgBlj>4a#=rN$LY zaH)6cu9kYEQ{M|MutcuemsWcQX-7|D%Pyc7tJo@W&f*Qh^SH&xOhdqHQjYeAUKRrl z1{(a7G{7r}gILIoYXO)_%z(O1apQr#DR6`dR_&~YfFuA(jaXG9ltzN77*C1=N0g=% zEvw~@q`ta7LCv&U@B&nAik$k1e~L{#dlvx@`%zX@(A(>(g?rp9{kn* z*2Zsk-{VCOb_!5p2Z~b7{oC4KCQFHpSEn#sVMJTwC`Ju*tZ2n%7i!lFp`lf~V#?+H zP#Yu?j;IDWsRM7-Qnw1?Aou=9mjsPZ$In-nTnxw(Z0misO$f!L8fy*4poz&iThoN; zaOWmziqLrEnn@wC&&68fD2uoHJ7-*02Gtk=N)%4xyOWIaM5%~@+MJErp@`JW@&5Kv z$BVa~@6~iDAHRf4yW8K2k*_V_7D*XW%Z-~Kq}l|#3sDKR7q?oEHJ5D1wwb7kVpzRRyPI< z>U3S|$VN;3na0t)n(QjY`wwOv4fchT#csLu|CoFjOO2O9CIhYOt4Yl0GnUR_5h?#l z^7{=wYd2{YqizM;$QCkWSnpU?)=Dq3dh5BIbD+xFH}j8itxU8%rN!m)TcK2aDpci0 z)3;x}>5MB+%Drt6j7*vfBtQ60E_sm(Q-pWgXvFFM9-_k0FHG1G7GI9;%RBtGQ z&IsUAPYPvSBx!p{Iw(f$(>Oash7;yp;y|(>a9S2&*314sLVN;1zklf_flUFU(>Ss| zS`VjUI$?vZ7r3w((;o|+s6t|*F{XuX^k+>>CLm|kr{)0#Rf%9RjjuHk1VmJ+^Bd}u!Yh*d;!NH zJJB@Ospim)&rNJdSn=8ZTG&uNMqN>^$}J^WCskdSG4_!5m-W4tF6?raoVJ|<38dMW z`oTWR!4xpKrS}*S`i?r}Gt%@XPUs zkys?ll@r-p#x*0|Y@6CCoxs>^w}{&OH|?R(Un|aiFOdVdDy1z!9%()%Vh8i zl#(>d%P0bR3YOg!k3rqE)QmZ+H*!RKS01~?Z)4SVGm;Q@dOyJu;tI_EiJ zhtA^AB#%S>uhzZ8L?%V~$}0&cRsS3ZdXTxr&Ku$WVbAGcfSWMe&aXb5mvw!p1L4ei z93#UoKbzgXIJfw!p!g0Gi42@(r22wCJl@mE;3|u*s?fHS;$^XI>u0%7=D8F&%P+l^ zT&eaZC^wuuSQkjshYeL-ghBdxSJU7sdl-5J+kB>=XdEpen2`#0pNWA-$pGesA7l6) zajqHsZe?Q5k}F?z$+Y~$v*XDA4AFUI5sYQcHd~A|67#0VM(JNQ7YC008p!z-dY=n= zhg)Uzr{HsWyQRHA+s5l<3o?E2e{1V?4j0sBvGMnX?k~f{=mS(UmF)7WrxK^jp`vy_ z%-0Xsr%&trNM%MO0t$;esNDeg3bWo)eBJffhk`*7F6v!HyJTV7y!G#FAm>W9<$McQ zWNNKZpo@b!bR?DL4It~$n{xDx^i7-)Le4GR5d8L6kJ7U ztF)wY=LXM+-bZ{Lho#pEJ4cdez_c32$3j#G+{puiu#hGEzTCf3u?b*P(VG;O0lzz5 z(y72Rn-0X5)AtchYlaz2Yzvt*b0?MaLelb4OXr&ZDuiS3fpLXFR3-N<`*=~Ib7uHf zx)mVjJJOub^JaMK*z^4ZXtb(d`EzIATG4q&Q7NzLm8aDR78lkY5X7)AjZG+yg^4^Y z!Brf)Qpqp{$kM>|m#usKX!Nw$P}z|0=?N5(s9kTkA=Ln?tJcK-Zze=?V*_VO?B@+J zK=;Aiw z6jP}D*o%p=D0x!(xM;XUB18l}`!hNEHzn998Azw{mGURns4NXda8^iOs4Pa-6k`4I zoKFhtNuaUNGaB;an7E~xbc!uHM~LBVV{0|Oy%OxE*dxen{zAiQbYA&s z2<}kNg^eHGsqm5kX zZu|j}_!b6^T0NC}FTh1E!Kg+!^OdXYfBsVcAb{?a&D26?Z&Qv_X#HTPqRjuEdw%H4 zcM-z_40TO&ao;v&6B^=&_ljwQAvY}@9D3;0Sc)ZZvSp9hHAR_xD4_L_dGosgQgf>D z085mX8lqH9@`G)$W0k1Xll2qF5%DNPyYUx~LZdP0KvFxy5E6%W$-35nm;O2V`OC{% zrYfO2DCCH+^L#{D*UUu!%c&k{eAPxk-Oq|efC9?_99!ZMAmr-A6F2F*I#uLA6?3;z zo1-2U)_9`*;N~AoM11%mTD%HpF3BHy(ADlh&tIAYwT6Otq5}TY-(^hv#NRfMzVNH! zl&`WGE_TZ|^<%Q74x!s6nMEfPHY0M^caQSN8C_OAn`FEak;U2Hh0C?*CN-z<8 zDhL=giBP{Vg5(uS3#}(0+3zJ?G$!512w5D+;OL$V9h5*~6&1LVglbIt3KPHfuyF1M zJLkk2#!~hR&JZozw4zkkHUzYfBqcxc#zUwzd{$5_>od}5eXFqOhrzmBleiOw<3qy} z_`1rrcub}1^dneI6Szn|&w8*q_A8|z(1L&*gXg=esrjl!@BiL`yh9SSdWCaQ9Qikb zl|MN1Xp4T82Q)d^ON=?yv)85C73?F%JpKYlRw^HV%U8*D8>Hhrrt^$d!43~1v&`my z9uueo-5m-qrehI20VV!bRfJgikuBMoo2%5$f_*gel7srGxTU7($a$M&CxBw+P^6`0 zBa@lzfF)mwown$nEs4s&)ZM5i&|SVj_k` zomJCATld-fS!hE?y+EBYV4ydGBpZc!m|>f6?s3x8C1t3q}5bCwSuVECGjJNeu*=vRPN*Rkdg>>%zmxvuo>$dM&8BJh$QE0AW?KeD&& z)A`9g;gRj_C^GhN28jrT1>5y^;4GZMj)POpuTr=mu4{@X+)ld75wuilVLlLUw!N*} zzlt6j6ed~SSp1R}9hyj;K1rC|rv-h#CD$YoXof3|9ct`9I-C`(Ov2iyr0C8|EzZ)A zB5$qXqHdk&1F&1<3FfgIJl5~DtqC8*ko!srt@6znc?2-NGOqpM!yenXYus#xcNRVr z4myBD9caE)g8BKp>~UySqYPb?o`7ywT?s=kgvr@xs)20vS+O3|lKb`hLrD+^u^hs( z>!`fr>tTg7o7w|TlCB@kI7&^kMBQm0&mKfcmR;{5X6SUiBGce1L%h~#(0;gk3L`WV zUp!TXO})PUl_r+>6m#0C>OQPh;ZJ2K_$eJt*XhN&#mVN{yEr&oq)N}voFCbpvXDMO zdKfOqEk)R9d;v&ZD;ZujR<_6fr57~!8XEJYZ^?bB`iL&d^Etf$#$E*7ht0*TSb4ex z_2?z7Oy$}8ejz|oHFvvVV}Ep%EUgw^VZJQv+f!Ntfv>bmi9!u1l}LY*_VY;Cz@L%- zz{UWr6qI5xkgJ$i6(E{VjRN>yyt-kYJ%9b~X6`Wmu)h`$I)(TNi7FyURFgQe$VhnH z<=lD2B{PD3f~e-PoLIKdVYM&5H|IJkc3@?@Azql~)5xnqM>d?{M=SA>%8^+BuCpPsm!_A%5@M71YKK@s zWhX=To5cuJ_rcE#jcnX}&#bu3`hQ0+2W3b0)1=%?Y!W&YJcwJP(~cYyM%^yRFN+Hs zhx%;85*U98lAV+~s!;pZukC|;j>UZ!2YtNZS6m=R&!hg`tTq`vS58(_L*B#s6eo1h z!q!nZx;z>J-M5G=w+2Z8*%G5hCWZUoA_vkgu|J5_*weU~LiEr@p1BgYBFfS#Medo8Zv&j4#Xe zH+4FKDhyqh&5v5eA6(IsWK^IsBJgs$pYt1%`>Yt4-N0=xZ?Q*0?1 zP|Toi3>Wc2b6Hg>e7qjBFZCD(ta#`SRdeYeXQkoJR0xzqpBlTbraBz}*HEjGSwU z=g@M$>YAeVN<)l}V091d<5&VyMZF&>Y^hoxAc(Wp(~^Jk z=ntzyY?Sd!hzITFS2c0&LjpJpjFl1pdy~ZJj_m`(FA>LvNkLh|87>E^3o9PvUH9hA z)$);W{H4L|yT8qls`z!-M`N8X71%>fR6(>Pz&xk)h5BRTM!b*$v<(y2uR1=lAt1p+k zNzzzZt{ASSWdYuZvyY;rQ#LMfzjHH;2iT>O8HsNQal&}Vj?#?5d{X$ zDO65I?3SCe&|CXSDlVKAfy?DGq zB1sxyMq}30hL-TD!JZ?-Lg&ySjpu>i-oE1_XOD~Eq8vJ@Ws>TX74b#0Lok)Z3bB?^ zzeUl~;$z}-j0xvM6JayAfsPwb#!R(meb0pmqCJ6Wbf5%Xll0EiAHvRyXr654C^T+0 ziS~3BO8hhGoSY;j{7HL74I!Ov5r3iKL*3+iqBAb+=PiM}^na zYHy=e>#fAMi#P+3L}Stc(Fl?pWEH_3d?JE$2QOV>;rh+(e_lK;y;$ z6IiZ22ycBrxmfb~$RA~!V-{*C@`&+r7L;xv`)@nEnX)fdVbEE(!>|HE>*X!SK>Z?y z=0U(R^aOs_;5j1B*vj1T`TRZ`d9!$QrXgRw!3M|qJ{@T;x| zI!No+5y+wxz6@*UQq~vX%r5OxL0f+P$!)|7fGZu@m~un0?&(xqTWjD8%SToH?}tcG z7UmwdCl^H+IARp14F_3C{ySoHzaD=t6%Ch#JUC3Of}_|8o-3;qYC*b!>kITD9gLa= zULpb_R(S^PkQqJQnol9V_nEycRi;^~W=nahs_bH~%EE_rg0$Z11FoMoJ4Y@jM|A&5q zCC?_8)iFzr!!Oh5VwPjBs&C8CvbgNrO99_3(Y+WDsM54Bz!ybq-Pk=o!K#Rz- zg5X9p0-uHKH=+0AhNLU$K-puUIl|bCBl>%;ob>rLc5kP=ks|VDTg5V#OtUogm3=cX zquimjgV((JYQ=>W!QXK;N6k74n~ZM6g5xG7QC%*myC6n~Q8&JnUhYNMy;kpsO&{6X z`uTowOL7XA`*WoNtb)VB{`yk1F2jQB&Def{wyFVu)CB6d>bT^dC`+C2PPq6M7OH3* z!7{tR2|9G6E3P^&G_IvV1Yo?Z^Q2S%*Kt!P@<=nDB^gLc=Ivm^6$5~gRt>YqWR}VMmlLEE>GdAU>y$&{&dgP-B4l# z2E|v=hB%x+TO*bp1qP|&%*c)<-X`g^s z#4;wl%=S-0I*A~m6)PjjThm3>k@++240c+A9kWc%%OW#3XSlAqG_5V`_s?i4G0s8E zS*NIdoqLJ9nL3{nwi>#@9BLX$68FQVS*)F^#U5oh;kEJD?Q*fdV~7$~W&`S+>SyQ+ z$Eg|O#9G=K>$;!W*7g1` z&maE8+v&-#_Qtjs_Rk5OBL3GW#^)!VZijzfC5+Tj(Ns56Tz@|CL3nIEwHmof#=e1u3KZM?N!wGW2|awGdu93YS@A-#51|f?fq+yxva4M z^N%4se^#%DB;hQLg4zri5qnLm8sW-K|BYO|Hr~92CFXlzW5|=|fnt=7JXT>lPn_hAHmEsjcwDmQT%82Lnyx3tX*7Va)kLo9ol#bVWzO zRG+ujt(WXk^Y+Iz-B4});nm9}*0_+o|85n;l)X`2=E8GqhEgM<7Sy&@rl7xM5bpG7 z-_|x&@3VBzpJ{l6+Ls|MbIj;4=Z2B+yG4IP=$v2#|Qj%B0o z?^geui;7GIT1qL`{11xqH`2Ht57uvK8ugRI-@38t+FR;VAA|)@imLM_Tu#NnOVphW zYMZeh$;4+?roK&7dIG*ArIWXSE2mY?SEt93&+rTc|f&dS7{j89S7-;L(;d&YkOirJ{`B6GF? zsp*<`zD>|m%~?U<@IZ1x#)5z9=~ET4gX059BK8TcszExcvJah ziRye_Yijp8$ae2qGa({-Qe6po%c`d{&8KxmAxPD+Y^!;TZ710n_x|zp+KJ=k!F@fH zO>@ESKKbTbqacCpy;nizl+qP(mpd3paM7datIA@U!_2dm9Bj$4F;ol|Sr_6i*^pm| z6Ur?QFuf54-z{`N)eY3Nk>m^v=%ck_Hppk5C6hn zrwKe;?kGgwo72M-)9{CS(Vt1<7ZjptlW_s{rHe{8We`=hd7HJr-fk@?urkiF1^Dcz zkds@62R_?fU#bqaF3!Ih)VQ!hv79{5TG_Hwa5#;NUn*W4?@c{Cw#o<{pQWO*&0qO` z3}|kytkV_|1Mc6p9#^lO82DEuZUygaKRkVU>An;Avz-oHJ|&~Zr|Z5n{7J9nT^B*z z)b?$AFnfOAFGwD^i0eT;NZ8l|Eh9`531KUWxbmy--tB!AxtcHg_S4(1K7iZW@`|$a zU>jEa`(@z!)yKcjmr`I~*_9p{yYPe2 zaMQ=#LlMXGpGuMZ?#@78W9OYq!dsJ9k9+Sc%7>TqxC{EL*O>k4to1h|^Nx62%|Ic9 zf>-mvBd6LN)UdaMj~rBZAw!9Kv&kd1e=+^L@?h|_zHB52&oTgp^0u-PD{xX~uC|^F z{cg5U#31Wt9wTq!?9LT;sM%k*NA;m6XAs!3{IMM6=C4vLAMC}&;ae7pn)zg|fY-;fDE{t?;9)p6(#Gpl2ILwYbU(7UiZJT*riOS?JU4}7Z zST`UD%QQ_j2Xex)0y`Ix;9k`p;n>!pv?n;UNMFrjx>+{V#CP>uLrW&aXtXnCBW^vl ztZ|l|ZHwZ>VR15#p{Ss)^5*LD=CYQNj>NV#gaT@#x?IQ0>lK_{N9o7h?b8WK(9_U` zJ5lFKi&J&J7neMe>4I%w%g^Tpk@(td4u^df!q-5*co@Mb2*HmAIf;q*y|l;SrCkyl zM=N$>pO5s$KG5FUWrE-G>MTM^_!``r(#6!pno@puN7qN>)mD4G^U>5ip~HuVS9j~v z`(@wMA-@+t>!AygE&YJ!hr=SL zt4oRxz{%K+SmFrJ%=y+z-$HwHOMOEhr1$+KYR&#y?Y6MJf$ufz)@Hk5ZBJ`W&lll+ zk#xhZw~XW6UsIlu7RpD%pQ*2ZT0Yi|+7YfU zn=G>vt}eC${{1~Tt>#G}o&(*d|MhUbIQaAOZ(%aMWs^yy)9_NT&Htm8=Ss`SfrYud z)57ZwaPn70->3srACS(irDe3~qmowp*0wFYD+a1X#mRG6_j)mQ^!C0#80jH)+-hri z?=}qVH2k!NFQ=$oYw=C(u1`TE<2Qdg9!_H7I?fVWHZRlfH_jN)L>$(=Z!Incl5MX@ zMAibXUs5}quehqO7`vWZRX)kJ>)CGszmB^d1N}DVJpi3Qyyt(i_;SPFi)Vx1@BZ>+ zXN}9d<@({WQK0hGk^2MD^EUeR?0w^n##YY!`E9fMEok~mC|tVM;d9fOSWu?c#rq}R zVSEE^2k;hEbf~>IU>GEI#S^lZ-Qv*Y z^UusY@Z{umgj>ic;L&M`*-q%+?eJbYi%3n6z`9Y?^qNr&VasLrRzP!NGtZ{TbByEp zmnz1^$)m@sfOWqo;m$SPPPl}v`P=HD)DBPL9s%v;r`w&P16u(BIeeUxx7~sC1T^k7 zDDV1?hK^nVk~@lz_P6UayPlPHS5NoX``z4EUy}SzJ-xpz6wIIX7`8*g+L86o0#}+2 zKOgQ4-)_ei<2Go#{o^J;_j=RK>FLes8>gQogjFOpm&FU=gtje@;~w5lzP8T~ef^U> zfjgiBfhC+ok;|(KA#Y#8d=c;4hs*J)0U8n<&uZ^S;>)yqOSy#kGnhxf)zn;r+>+td zMqqtUP$1K%wEn9f*XxofLUI#hN&8tPsps>2`DU^DnX%f`jmb^Y+d;DiXMc77=S*qx z#l5{nc}sZ1-^JeeWAC;H=gg?(c_n(Zvg_dSad%Gd)$MI6&KJq}&BuE>0VhqXmZ{Uw zDq;T3H^%98XJcszXUU=F&HZW`ga(J^z~q3_)86v>ej&7}@8hxOZ`cuD>$P(~_vU%^ z)^69WXQZ<%=vDu;d-*wnk-d9g01esHuzx&!&wZX?8n1%;qqMMXi`tFtpvq>h<*f@O>`k-!1?& zYE=6;UbC_B$l}=JOButWnWmLa@B!mhhmUqe?2zGS>-=`G7p}T_vlG*GcGfgg(>E1L z5sJ$NW7*Fiq zd2RA`-FK?%BawZ!#Dn@kQua1^ua(h~&%*L~ApMO{FB!Ynmc5$0GShEvyAC|+JOLLY zFPF~`-Rpi1BtmyjQx4~QmwhN5uZ-P2=BPFPertJ`{_DQ41fkDo8!<;n(`lQ}OGFME z)-6v9hf;6sXm4je7vf>fop(=%XCi2hrUA|jf=;csCeO_k4=>jj^M0f!!>>!zd4j1u zL90f!jw`qU#uRqn*6(6?4E@496KWR{Y|qXvXWw5Z{e4U>VqRNnV4lz&y4u{CZnLpT zwrXz|6*@d_FU|t!AB;A;-%UatS&a1jA1q8U-UaKP`zjxmYPy_%>`ae@k(^4k2W&X3 z_@XK0_8&;u`+k_W!;oAA7<6?EUk??52CT4KpLhB%O74O-g`b}8;wB}CgFlom5#S13MGM~S`vmc5dELGbFYaBSL2uU*gFn+XEuZO1 z-Q7J7BhMB?1Q!9^#xgY==*>@iX^PNKF>ZKP4eSzu(~X7C&E#HBIO<1(R` zk>tALCZ1Lw@k-%XdUMF@d9>Waccjp@xgtWr_(DMw^6(I6)be+)w&o6E{Ym0xX!ugQ zHseG4;RAA}XW*pWx<}t6GPm)Q81pEMmp_|(a(uf34ZerSJlhx@m$NiPzdx=U=bzIC z0O^^h>T|M;yt!BEELCexS zTfTV9s}Gi2+-X4pD!J=Bp{izE2K%M=ifh`Qz&|E`%CKs-dRnh~2;#ZOexK=yT;Exl ze&vvPCPva;6LqgVmCaXRC|<>2rEKT}PsE>_LF%cJI%&(>2z!rcE?AN~`V|zqsOf6h z>rRA!;VwRvkD~UrW$+pMEityO%U)C8%~d~?#RjnbF$Rz=HHL@+bP8=G@7+D#V+R|} zsEa0tGQ}#oHEoh<7DUyTZcz}vilvAU&0DMzVTtH?cDPl4^`8n8$6&(u*Q)3Q9v-Xh~gtn|FrQc z<4-%ytqRPa&y5ss$2UdGea5soIS>)YqORD(eQnURsL>Io8K9g zAZ_uk8taR`6*xGc!qmZ{BOjm#3ZfU3IKq8z%4-P5oYj2jBCL4iQisC{0X^(m5abAk z)T&qq4TqP)lfr?5SDiG={+XaFvn6J!7igQ3aCPnWLjF{Y7l1i+!_^KX!#7)^q9RkA|j-9UC6MCz3}O>@uX_XBO3>taWDWimx+3yIqn01FVTF4 zPQoP*@*ZyHr>e$s=6V*fiste$BNX|x-n>wu6~a{N-7_=Mx?aFPfVa7`vAs7$|9@&& zgczM%|2G{^iu^G7V?0)`4Udo70(Jl7gQ5&>Ehd+-(C^4u>MP`7tUgUpClijMcTxo} zWXRnM4HrM045ikDPT&_U)?#P{Yt?qgta>d+iHVfsWx~^G~+2%x< zUR^feFO6YZb3f$)cDEYceKfaPH<`iM+&|qjNgn%EI<-u^Kvq)3L`jb1n;#mE3uC+V?Z|TiF^@w2WYROFbIA)?_XU zR-2DsUy@O5&KSh+yPPE~`nZ8B!)0-1XJ2s;u(FI(zpO94tFdt-rEbd6apy{9tj2ac zE?Z=WJ}x9q@mc1E$A5L*7UTMy4_bc=R0PnDpeNT2rge#d$2;#Q;3opayyvgJ42++4 z{ZEoFu#S}mlWbR`oyOT=splKRB+@%rQM#>91LN#K{m@tAXD!D5bDDN|b?> zF_aOv#WfrG?KIx!-@e3lbCsLslcCogfUDXczr!~sCl2ht%h7beAxNyuem*kMZ}Yh! zy8}NCSIf}t=BQi*q2wL`h+Z=q{!BIx(@p91EQmH=QB8YwSuJD*!+a(S1GQOyVg@6B zKDY6tJ=I(@(fuhtda7#bSY1@dI=Lw0+d)GnX=OUKco$x}JtWbmn{_7P?iaLjii-xv{J|352?7sIxqdMufz&T^bB zM6-lEtg&El?eN*;A0{UVra%1Qr9F@f-K9WJKX+cXf zc>38VKclKE@`3(1OQgzy;r`x;U=$HS!qH)d@`$Rb4xbm{K~=eOvj646GvL<<1nTz; z6v*6{9V%RGWYHBG*@>(f6x{+=EnT@7UB>_W+0^G;J%chlcK zBwg+cW=EYBgig#0czTPErSK9iES+jHvWa-w5Gw*Z@p1hTM-C%=1dh9Hk}g}R#my`w z>Er}6tai-*)U7g-Ct+yicN6d8YCyVF7O)K!kr93&HgpA{hOq<4J+&b+KFbOoCd(Hq#YPb_S# z>bys*3vFFW1wTi6_jW!Lt)3o=h$o9t@*frks6wy0epBv|qZ2eV@cB15`N@S(#nSnj z|4iIB4F5Z-VBVhs-pVcS6ZfyUIE4&9MGza5*qNwONWx4Rfb;Fyy{~1q>}7Aw&;>!n z?elkUWIqtp1l^{|jM;3$^&q1Cd;UKcey6Ex2MaEUYdPvw5tzVpN0#&9y4g7RS5c)x zFT0v&BT$Cf`9%qDpYO#>f1R&ypJ$eY+|-_dX{n*P9C z8~qYJHN`Fax_F0xHbeK#w2zZNAZRF(+Yq2w8CjV{3(-Q z9uW%M+(3yv|7L!D*RIy-{eBc04gOFyj#v}B3&TK#6&gJx6L{Fl)8J!#6yOODh}z%z z7+)iTlt5^ZvnzF@XGUHR1}`G>P}AOFf1{5+U$^PGpUBAL9{2jz#qrX%wg~*P>oHnn zBbgypA7}Her#Ikw+))qDaCK#EW979_ZDsKG`5EgUu2!#7KGY~hEJfHaB!6+RI>n&V)4DoVVu455I zv3*Km7#B@^Nm{lv84`c~ZDF2d)zYx=q~qC+^W4GJGhy{`Mb%$BeA-$%2D z{C}@cZoI_o;2H3CSl$?{xAr{PS-am*y`^eg&r8|?M*g$HMxBnB0uS#F9<2;@6r6<6 z1uVat5;3~m65)OAbYwfdM7Ywn?NzeXZR^g-Ud0sKzW?HA%*FIR`E8~|kHL#reo#Ax zH6>J9w!H05-F>vSI2P42axRSxnKQPcNk+fyQ~VjIZ&DQWDTSTYl3J|{$dF+$Y2Ikv zgexcLZ--DxOqC5gKBe&WrxZrBhp%rN{EkY-{Q*MHcCGA7Uty94ij^4dPNrZ9NwVeW zrzn>z#zu-D46|&O^B7?Yz|FBsS%%2|QU|vZ`g;7@!NOF6Coz|<-#TyKryqpx7(S+QyP3xG~eM1hFygeiDwTFNd;`s@MfBzp8uClHm$U&i$S3FWJYv_fpTx5Fi zV4>zzny|U4KoNRqq%_K&GSq;$d%H@Jp^Uy^_JL9zOA-9k#u0$KB}SdUPUhY_C&`d- zP^hp({~~!KwN7v>N(%ODFlw;9dC$-RsH>D4pWCIG)qRj^dBb>^%3u(dgm`{NrIhu4NAxRZDzHUTkqF4X7r5u~XuZN@lT2`WpPHqt#lz9No(2 zgv87iTN9nw{%S2`N!bz%W7B3{bsmo>Uh+)`2LXu?s` zFoaHhC6f=f9UUhbsFro#Mjxugwevxfu>*1$-J|VA0$(hApVYploR>&DgH&@lFz-w@ zgy3z{D78DfZv2eh|J#iTh7}=30UvaQFk5j^=J)a%QLvTkkz_&Lx511m+I6xp3NYiqURlsW%O0=I6WO5Yz*tmH%!JB@t2qh-^S>P`;09?~Wa%^|fk_rO zYv0~|S1UH(uAen|7so$zY6&~Cf4&BKMW=K%PNfzKIs|@Rzkk$yQel-;lMRwJfwdbr zGpDfbI*oFQvaLXofY+c5NH!>w0Nz1p-_(XVYFu|P+u%6|hO4!=9bWBS22Ihx)UeqDQ_w&sK7gg{u98bf8g)wAJbds96@7ie@ht|!tqb(YEaGbKPy=R}}-(G<>_UJpe! z#8^R!`k)@=UMjQ#IPb+xCAA}2K6EZQU4!X|@<1%i7f`WoQOoZ!GS%ar1knSu;L1|=!k96p(aO;&w&Fki?lL48G&bNng zo!rAjS$Sa80b+?$Y885L46cW2h>6%0!4*QILTN}V@yZyJ&Vw3`M=YowViBE`o_Ub# zB#s!^G?U#&C|iTun<3aQQ!O}@<#Wcb3Y%PPF|bH4RKdiA6TF(@|2xY9h0tG#8_WLG zot;%OcgJrhB=phj&8{f+a=j;k^@#W%LxGnXFdz6y%8im-3q2C2!b{TQ+oVZEGtKn6*iO??@AzD(GV%4T(dN)Xr)^V`&-h`z9WW6bi?C}hWCudKAi5( zV4)l*v*8ag(t5cCy|8d-h3*w)mrVA6)X>gI{06Tu>bzZDnyZ*u)aEvMjVtFk%u28n zhaP(vT05t*q}{ETrzi0+KCmCSmj;05Z-; zTA7i8jUv7Xu_&lWn&N??Q~x7uC@z#u-{^ zV}GH4v$aKfM{?-${BV+zC2?7V#(1f$Htgf}VVpN}S~)g&&z#Z5X1AlI7zUR_e)a*j zdn_H(*PUjtGbaUV9^m-(!^i3OGxYR;F=5DkfF&^@@z7ccD7vgZ-55#glY*+1}elbv&-4 z;70X{XuM;8NpAHYll>}*S1nPqM6A%_Co(FSAqHv0`z!IYZ)x+qj9>u$X

pvJU6- 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

# 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 MP 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 z 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

yo$bQ3}kLh|SdW7WRtY$S+?D+jZa-r9W=|L2X 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%0AaP+x~edTq-LNqLv%OvDOeQDf~``}3UiF^ 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≈qsi%PEak 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 z%0A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

l&Yy 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%0A-haHku>6SZvkx2O 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%0AbBIfDUgkm8O@0HuJVT=WLFWRluyrqj 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

@*Mn6-d-}v1Ti*M#m%q$q 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 z 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%0Dva%kPSZkEfq4L<%hkU&d?tyvRjvJtu0+A889G zSlT63On*K&X)QCuOLsADvn^rw5NOmj0fh-NrRyk28$+#Y_) zNmb#T>25;`Hw~+N=@fTWl1~1a7))qnLyJOP9~psoA?ukhP8;pT;vP7B_Z}xfoU<~i zSVP%PSA1lgtaK^1Q7Xn#I#Nu>W`w`0s5Fh$Q-&SK=E#MaHO0{u0gPWKug=#m0sM4q|# za+`@61xQ&lJFEdrIDPFGolX|ydy6#sJMmPMn$|N@kBC692Jdn5~5`noTSX(sIg z)cPu$3uQ61qopb-nha3alW25;fx&*n0{|>i6`pGTriH)8i_9Lx{7DO2Ccd!!NeeFw z$r)%>!{mO|vuTg%ll$$8Pr-RyY!e*!RAs3W8MU;p(#LwB4W~G@b(;7B15YnSo=nnI zO$xnX{^@}pQ)zb=TEga=V|$2lCYr4OY>OeO1W?M&B^R6fhHvV<^iH~uMWcAE>ou7x zQjlJTVR@g&iru6GWXbsiRiC9p|OneG_!r-**KJ__5xjZPEsXMH1r5xb4a0Ay!M}IiF z5$ler6eLjpPg@wK{(sAaEnhxjaiD^kaA}=@mbUIImjr*R+6s+!8Hbil}m;Z}3rVpcfy{Eo*RX)v;TqX3W< zYEj>mmn3vAPHt1BF>tE(a~>|baCmw^fd52_*gAh~iJxbv6x|rlWNyg3VVOQjcqa2j zHjdG@)fAolww2S2#P~wa4;H45xn#iHCj6&`w@bWY7A0n!FrZzH=f``)`$26&hBc2) z^_koZZ}=3~=IV&JE38xfcX2{WNby_ki5NK5q*SwA`|~`s#SR)J-&JPu@6wR97#}X- z$PxJQYz?&|a>Wk9_1R&ifISYG7|Yk>bZO;^E94rLwFpRXf6~Iy8=e;bq=nn-g7v^@ z;jA(1oSYi?aOv+d=J7G}_lxV@T(e6mnMwyYG1d&=wD6erp*65UO<*+*%!J(*u+@mh zf&XU0!NMHs|35Qfj=xOU3@@eObEtSIKwD%g@9%4$2tgkr;)uE;ET)9)KV@MwYe}rR z-Y>(^$GfUZxhqQdvV+ETrip z9(YKJnDKb3Y@3!PbU_75qTOh==&vDOWh zpfErkW4Jtj!ou`7LyI2LrcNjUn}~#(c2I$+;GcklB#9`1hqT9N@ouidmj{vx z?2tUr6}QI?0RWZQZHE5~CJgw)gtHvp#<1Pg7l`1DT}3}m_IP86i7H!>wAM8pyLRdVtIgg3V#$&PbW z!e!s}t+i~y_l*ksZ?^k}^ZKnusP>NN zd;!i(th|T~iUi%Lg*ypBE@S3m*1NYL*{T>(o=7%2u28B+$%!pi*5+tLn{Y(-cs!7R zs>vD?`}>T$NSnrYOXc!1EjZo=cS*e8@az!?XrRwwcKwAKHMef$mtoLKH563;Gb~*6 zudwi9jb}fdj-T>l#Ny)p>ff+%hzrReL3}>q5BpD?=K>pkIF2>kj~l!`JY1F~8j+Dz zlAtP!JRz3}uT(2W1B%0tncz}WCyNtu@^dh02FFhKbZDAFEOzVr^YWm}P@yWQ^Fbc> zdPAs6vZUX?nP*s7&SY95;+UW|qUYR)(Bpq3z*abCCDUeUn_q2NfZUH~fTBs{Ae1FElzX1q0G5Ef018Q-+%P~;O6;jEoxKlQXu zM-3J@iuAQnC8c%W2aNS6FD6*VmpGRt%2YF30;4mG?7lL&D2}8W8%oNK{VOXh7-wEV zQ`>LgbRe&xa=7`veteSEwG<<@c|?Y~98EXFqNr~g_!I-G9{1T=%^Zs!Bj+=1$mv>7 z;r4Xjw_>YpIKS%e?hLne#y25&V2=1Udqi1c>Hq3~NjXSQSw^VJQpQ=A+kA{$(B~&F z)wkl>;nnlm=xzH~%qQ~MN)(g>VXFOd);nj_Oi%M+aGL>zrXho@gkHgo=y%ZK+)N_3 zcv8A5@Ev+xEgNUUQVLq&M+D{h90lvfsH{LXc1oOxd*eA!N*@xnluDl{agnX36Kc3H zA}+qEs4D!LyXz+;CT-A%dgWe9YPkwJE(!qmsA1R|_Y_Mijl$m|@R~cg2+S?h^^$zM z*50{x?KRf*!tOgOZ&NkbTXV@;+~U8T^ab{<_PlrFXU^xQTRbMEez6F2s9`}Bk&#qj z+m^Mg(@~!M(uo?Yg4=H=2MPdstcH(Ic_&cy&BxKNWjk4@$k?Yg7E&~^WJE9+8j(k2 zhdJaEy=OX7#?1STD%1R}UNUPnE=4OtAPi={6*rB)q4crh&yu%R%Q}NkYM?fEZJIGn zp&#A+3fS0h&&mCAX;#)qk#gWDuv_Z^hS9StCr%8|$NY3xqyG3NtuCx>d&zjvcEp#x zVNu6&_&xi__t%0NM#7axi|Rj=o5;zaW4<$)ZaZ!0&9>II>COL{#v<#>7W{!@N zZd&!7@cxW&@hW)N>oU^8EtG&bzZmpLtjrmFGCORWn=;esosbS}PAzvM#hOvB(A24k z#s7@QLQdZVTlHu<>#v>gT{dy^Mtvvx>qa zSqcG15Hc5cdSA0YEO9yC{&V{$rzT0C3@anM$tX|5kFCLtUtIlrVq;%KdJO14);-`A zax2x33!{9R~_rV77BS{_NPbhd&84OKtNE)3db2}Xi5#6X(6 z$NTJ0_kR9ywq2rpQ=MTA7uNMLD=@w<2QnxJuax5nMwq49>o z-QBHmhi=@R!rk4qad&r$#O{8zBVXiUB%I~MrP*#vJ*@1py&vAGtcL%vAvP6)kMix;sM4AYGSS{ zqXDDaL>U=!&7xGlz~HE3a?Z;{+276?dng*c}(X4rHmcI8??#ocB6VV zy*!`!x~hc1gIj`g-B^K801xfgm<*Kcl2Sd@_AM4GWr zY|h!=Z_O&GKVw5bV}^?AP3rm#Olhy9f7r_3TNCdR5gg zKjgc#?w4cQ3?fWG%VG^7(3IhxhO#JCHFo#it=KjO0Q6YsG@3Bh>}8kaw0K6PpM8H8 z@mwVgea5~s)Zo3(s?P!w;kp3%N*RdpiQoxZWz*}+ftHI=g6pwpudp)i|DyoB{XdAnEC0WVz#oC){VCp!yy|pB zrg8zYbH5LwLz4+KVhRtwo)hLURXOc2D^u#IilOq~szcsa`oM?VcXIvz(*uSi>~`V4 zYW99zT(m8N7N26fJNXgKH$1E4TV~Nud_zaL2+31JMtP(+gj4XiREa`HD+J??K=>jR zc)HSb9C_Xd*0douS}f-dtQhwP{}?f>)#k;mzF`^8ZMx`;ZD(9_3ivA9yXd+8$^L1mpAIxTz3*~saFGf!0!CZ3aQ??V z?&ajR`CgO`3GlVfvyX@#TG17m8?1OU8g`JgSIeIfQOUKBo4#1T`}u4bK~(IlW#jSu z$e@R_AfAaUWlY9;#IBA^C&12G)eEiI;hSZ}$tG+zGHl`r7P6q*Z!)x{MXHDoT)8b3 z>K&*_YVHH^8B?S9iqb^H;WU*B(F~7qQpurCb)^{a*+Imv%%37xwCD% z3_m+Qg6`>SGb)PuxM=2GPEnlhO*WMLuFWHLzj=|0ku6O`gG2zNN@n$ zr{lR=I}G|Fr=^(6itL)Ev_ljLUwWd$k1w0^UoDQ_OTbISi61U~9H*Zg0Cz`_6fKw!TA0)g#I1mH<19WGE9 z@xBti+a0FhL!L3g7Go`r)zbh)W`(S!B?YVoWUV5Wdcz7NJ!q~Cf1<_WOMSe>az(0Q z2qR>Li!65{)5Y78C5w*T`U6sWYcUuA*Reyr@hyt-`*S%X95-x;6cV$*B+|C7L%k9h zIMsc`s*_jez{T+Ebu?P$TjP>iY-N1R*83yD4$tm}t3yzmlx@q@DLzG__} zE9Lmhp>PAp5$X~VlwyKX1}QHoj;LJmV@yTXkeFTS{WqjHd`7}95DmaDg(!b*ObI5z zV!*mefKcpJ85y<=ePl7`cvQ&`%JliliT=lFw8e+N*#FfNiIg3N-jXfiAD>dfo*{#m zfu9D!KSLW5aGwfTlv@1#Bgo9Erk_MKJfUQB%t8YR=14W0>TFdZ$4cmors8A-HN-^{ zZ7v}6x2*NJT&<9$MS|N5&-rU-^((}XH++tIGH~D7zI^3in-ogT7X8w!dlW4^f_!jo zSymplynBN{ZJ;jn&a~cGcsV}poU20j4ph-nz+r_*0bSmOm(xBEoutT4CbZo|n-f!y z;GIjDVia{=Zd!8(q&?~{s#{{2D^;Sv<`(sJu9C{4$Ax)Nx2gVym<32z)7nEq$)For z8q7fo4wVN_F?Q}#F#{F+yk z6gC;s-hdfRB4uj47Yx;@-b2Ba$Qp$UGR+!mwhC)<1USNoKY1epo-Z0c{<=*8J^?G5 z7y@~C`4W@!Fd76L22K=~un&!W1Ckc|z7Dt`7N}Z_u*p2JcuZ)pYN~xQYi|+Is0>3~3-PYEG zCApxVk!-0zcE4~ofGUoy*vw`G&QVDX*q0b#ek|K?8-~=cTcXe?5xPn`5Uno_XpQb} zoVSbL*?l8vi--DP+UQvbF^lPzH&i%?UQRboq7^9!9~KFY54PmcQ&iBd^l4d+$dj`N z5sB0Hu?GhqBTAT;1{Xm9%oFE@F=*=y%!U)&74=VX$ZQ?)$#C#;@7!}5hN*vtk^;l2 zqVOtf%YeWbWa{u^yp%LtIPTaeAU1=IwNSOhsN}M6(bxQf>(9oYwJ%y2Iw;7%r$@+& zM7bv*#KLk%F(l;o?Q|fCb*)>H9JF)aYQGLg!ng{IVtk)u%S}?E#m#ge^1!b+zO_l} z-_j^pwjNBX)k=}9#Wf+S_>6`!WJAVH#pDe-L8;AGXr$YD1hlC>O&}n#&{lE>w*1*SS zqH@%xa~@E^g)ozFPzKjW_;^io2anwTupp4}YHvKi|Fm^ds5ce12`I z>v02q6x62Tl59!E<<2#SdBmXt!h+Bqt@d$xV7DI>lNCNe@u@YQ2jNkWA3o1F=Pz2K zynfHAr9DfjF@K`!pQ1iJo^@nts&we?IC3xj2u03=Dn=YQkh{CC+|$5wX0v z1E+|dp!+abBI2xn(xum-4@1}y^nqY_AuH7vsu4zm6t5f@|JJ2lGk_<+)cIIk!1(FP z8-D&av)Iymkc^oA*3#P++(8pGJVnN(TA6ap7gDz=soyqt+R)b#ow z7Vy|ivZs~*SNgVRORQ}DG{}%<@!Rqu3)V@luIf*W;Wh!VE&Y()IU4wTROUOGjD-#G zr;E3zHz=Wl3ozeStO|mx_L|IVrLzlv?;dIL_qZzRl(Bpq3YK>zTI(NGdzE5ib7E(> zV)^gTyo~3g7#-AeWtn$k3_S- zTM@RooSP08*nc@we=f`QJ`ds`+31qzUqy`2$#^?*Z@gUL4+(tdrhKk0Y`|sn(nZsQb_vaIXEg zqBL5YU^*60h?Q87K-t;LX+sY#3mw_Jc7wh+{BM)GJFKl?i6II<@9DlR^AvHp=WZW1 zw!3jp+fi|53X+w?Rt)YANStvY&$}`x8mAUS8Bp2$~ zy=5!awddcUgh|{JKDT%Ye@ZFlj(kHhPF~W;1`0@f@h=ariEt#xZ4v)+TP?A<_YjY} zkq0hJ6$z1ea_=(3NWBjPaj5Gosr`J^Qu<}$ifeq@7RE83m$_)^Wp&7&6C*%K*6NN5 zTRnN@D#NxRLv!uv*0v`77`|)lS09!D?vdWOBhr{)9m2mvRB(AXJPY?H$`#SgAAe19 z<1aH!=b`tjTWO6h z5m3M?n+zm;iQ>s^anFD05vSF1@zJ;AKh-jSdU2b{fz_nW<9+Jnk+!ax&A%EQ|F#Hc z^UduM<*{5H|Ja|(IJ0c%ZSOXFxwrh_-JAC&y4N??xOD403&IR!yD~%o$li}KY9dj% zjEjGN<+4CX6Y$xVi?%5u%qxhx*6Hn{O4TUhg4O>Q(9<07rod^h4_b{x!Pf4 zT7R;L0PHa*EbZUzMefByC#cmy%Kkf_^ z_vVGD-HJZFN|Okm`7-zf4ZIsKu8eNvPE8m4Z2faXtqsk}sSRG}Ii30ZIO_R)RutOa zb>av`x_sY6J7ZD!o;~KXyHM#dqk)Tqo8yb)AW*qh>#;Ija$?Yt^v_l(JXUM(fkcT0 zb)y1x+7$lsz9X#5m5=&q^V5Tm`{8d`TQGKY-o)(6^ikx91KS<6m1xsVbipXsHSTJw|7AKPVEzTx(D&U^-!B9`TB- zO4~_3yTb!J-`r55yExNaQA-QFgZ%{aw4Sg_`YAZ^0V7yRH-$_B{ese>-m)Jx+&GvI=kPl_b4$6Fb2q^ zwJ>}{6H^1}orBzoe^iQ-V~fMl`X1gxi6K&k8wt{RRX2=0?ryb6z?U1v63fbnR?;S; z6hmE76c>f4UF+GBUKMi8l?4m5^gDB1xDdO8_wff~iT6(cu)w zc0JWQes|9~@_QYQ?<2p5m458MyO(ZQs;2X8o4?_cIE`nx?+t|YeFWfrDDUo)pw;fj z{S}BZ4bjmakJd+CAounlp8BvhUibh4RsLx2IoV`n{^iGh zOx(26QM(xtDlm6&DYQIt9|~r=kq~QZ{v3wNw?qV?<{ZQ?@V3a zUvTD6cKe3=Hg6|6ZuowK#%Y2uP4D->U*7)q7X}^2#phhK-&;;p=Dx_r9bD^pb+tY> z^|U|lZVZnfrLtUoObd85wcTIn|GNFS?nvG{z(rjEJ)AVMd{t4Mr;NG*A1}!`czNw@w6bJoH zbGLi`y>6Qm^8INzd(%O4bz=WIHhuV^{QaCrQ1AD3b=Sl5?ef^edu?W|fP=@@=?_H@ zr_zqQXI%k@nqLN9`KWOz3bF4mg+GFd9e0dSG*GrkdiVuF1$z&3Nh`L5 z?=wDzf0Np3dIWvuw>EVE6NWwvex;j&epEu;zyA((<@`mtH1&3ij{7$jL3n1j@X|LQ zVOKkwQjZwr&~tu1c);DM(?4Rk@H!nR?A6xvu{f4$xUR51!oYCJ)4K91Tig9p%(Kz= zZ5IsZ!O+*c>G@)#HG6O1qy4vPH)D%tj_-M=0bKUO$~nu!TmbMi%Ik8fk!I_`z}Ic$ zHA?Z|P}8@ur|rYc{?@Z@_Tz5%;O70O2i>^jTl>@Q#-UNIU}-DtDJ)`(Pk-uL!;5_g zf9~T-j-9JJ?n=&mw>^miQ<{2hioy2gJJbwrCyT|or{7Ytrs8es`TGypbtg;2q4(_M zmF$D!>4r_-ZQ0T6fta7GTAoeKAld9YU$@_0Zgp2rx42&)&OZVhwqCk6ALc5&8{eOg z$Gjhh9>#XT!}n@$WI$eBP5AD(cek6M`396n51*OM2i(Qfkfr86LJf$CSmCZZ2SWys z-;?XVjq8=Jx*p%orpAkhoQb{j>AXAlEg@ejcjDcJ5Z}8Kqt_3y&-F}}sOO0azb=Kl z?$)>aazp+=O#$1u9Oky{uz!_)f9g7&ej9t_+l5*x;vSqt(R4iGwh}w|dA(SlnqB#t zJF%q4Z1$}bmbyFqyDzMDvX$&=MeLBBiS5bw1`^oRHe4m_)c1YeUwWvx%f09cykGh& z{Pc2lAX+QP#E?Bw(Bm18yXCiG_{*!|{XAxs3w7vd!?msBb${c~m~X9TYq>U8__Ty( z<^?sEIQfF`{%R?yjOX}0s`|0kP;hJWD;;;FNQlbxlcf_GHPs*$_lLvM;q>W!^A91N zEl(6!!rAkldLM?@%J+i}#VD5U?mIv)@5NGXL&9);tc0^*Y6KvicNZ?xmIt(6j9d6 zYZx!SLaWmMMv1%6h5q)1I2*cug@RGsTLuOSr~68W3WM%L_AI?EU7Yr;&i#ab&413| z8c=Q>gjJvR5VMpVMP=`q`aYk}t!Gd54QqJD`3Rt{2Ypx^U!Her*56uqJ-b;x;v+BP z#I2vNWOub;ZQZ3zzY2rZ*iq z{HWda?=P(FsBJZz!;LtYxIpOSLbPxF?rDcZ()CUq$8&k)perEoV*Yt*K++1nDXjbF zaz8@%xzqVOXy$bI_eT9|obl=P+|TUDkB+49YEgFUZF+eBc7L8+e^9rzfdrHUXgcCjX3v<+d7V5NFKd4a{dRlv_TZ^yTx)AJ zxENVMok;DQyJ-C8`Ek6U!v9x0IMv~Gr2g{wwlwTVUEA&X^~o+s=hJNO!-nryR_-@n zfzBM<#D5qnIpCdU672@zx%}msGl@Oj=gqaQH!i5&zw46D*|rahS-TyT7i4OTJI>KCM_794`x{Y z^=dS#IcONm7d7`=znsbG5(d(>mQWg(CPtR2-J=c{W+_1n6)&~T#huN*^`Epb! z_mbM67%Ry|zVtBNK(W0FzgXAT>MH^r!?dKc3$A<5v$W1NX-hKJOElFDwMJi`tTti9 zP>F-4JHRy0RIQVu1JoUAsj??qTJ#lmHtV(hleWShOx>;aeU!cHSL652IFd29_g5#r zyrkl9Q}>-8H-=Mtoe$?gyQv8lR)(7Q*|Gbw9a@rK!wb#{;}1LIvY!J=RcsOV-+hKA z3X$(8ctJkH2Kmy|)pGRmSX5+~Kg#t)9LaxFq7OH&U=abT6XAxS3Yl2%k*tE9($Z{I z#^mIZ(nbyRM|CJ!0>MYem0X8Zf$dQFQ3t9)(8-0)k@rL}`C@32d@?!M#CjL<&d*-& z{u?wy?a~p)q}s(1KPt1GWpFU!Z<+xxpTp0_A3i%_f0)j@w7buq^w!3_8dROlYTIZ+ zKZ-~wDL)_&pnn#=D3)3epM1K`5{maWKX>^)e%Ex|mU?u@)OOGz-sV04`;~R^{dMpI zR4;S$`a3GJ?|X5Lg2OWo{plUuUrGDyI98V>uAjd?~;+-=jl>UFNI8Gde|F^LiR#suX44>gwnZc*fsFG@U zFl+x*9@eUL*+5tIzQ0}@yQnAou1EBc8VyLe!**&za4RgzR3ef z)r#ebo0Z}i3?c!eRo%A7VBo1a!OwZ$x>XF@_f;J~p*;84DPQUFJX$;XIBV`-27JPJ zBke>5CFJ;UzG8-ReMpG1yTI{lt4^|1q|*}iD$ottdq`53h>&zJlYGtJm%G z&C728vNI$cGAXD3)hl>lYO9q3v#JF6`0_N-JUPQtU^&_vM6>Cl8H+O61ZD*=%&198 z*b_dT%i($~ls8sA3lahzT-c}DoHSB#V;Lz5;>I!TTcs&8S5}f zyv|dv5?z(LQiL9oq=XkC$ATKhlfvHvD@M9KpkiN+Ex&R<3cN<HOPY%J9Z zPa@#3+9RAx?BAUCj0Z&MeM~7bBPZ5QTwn-aKAgcuNfa4&# z&r$Ang@ZCazI5Px1;fNbc&oR0Asw0oWRe3D(EA2M`A~%-N?IwCf z*_b3xH|o;!g{actsR?X7p^Wsv4jrdAxZ_UeHA8%fTSQ68c^Riqr0VD zn8p(04)>?9#|ly+2pU=Psj$ung$@G2Eh==Mxn3Jt28e*d+#pkpMEH(`Qy@az+Ui4W zQq(&FSHPcs}o zLP*3&wm}qd6SV>qL}AYHEvvfx zzR)dVLbNv~EpfBGY$m@w-|;bX+_hl~$aO^R)t3}gwKsfj-r_YBMK(6$cs%)DBYUyB z0+UdVqI+2;xgD(ocFV-%2%DKb&V;sjgRa{((xg6_@ZJ$E@f=|0L>`MkjPVq{Up%HE)9Q- z9D-o%1a!r#02G9aeg0#7!*H7ub|SGZ(AoI`tY8`T?*#8NdILVvYqm6!_Vv~iSR@$h z4;6LjEF1?Jq9x4~WxPOeHH0@lZ0)nfMJN_A85tM~ECc|y7$f_ixnofo`U0qbWzw3D zjruY`H&;0$R!>Q~FfVb)e_qeJDk8zt#sp51t~Y>pLnZnJOqOSF!M+w&5Q znDnwF3?F?=E%vhPkG1xGXPGB~6q;w(K-5N(lkLOk_53u&+8YG9s?wtLIcVy^;$r3@ zGMk=v?4B%k%#{~Ywx?B4jqdDstb>jeC#NFxY8eO%` zrt6?(T6jGK&>jBLfHh|o{F|o>dj}62Q>Y1&tA^y*0nicpA&PN* zql~=?JAUV9xDQ_uq+9z=8VC?`>O3bd97ghYmp^KjUtVl#`PEh{Rk*AVY2Ywn>vlIe z6BC1J#=h~5T&c8r%%1C8t8%*w_bB##&O-k3LNT+W+fr#L*55#jZAkWg*IRTuYU{*j zo*Lw1CPvWD-m9QG*aB_!@qHx4UK0engE{N@-yk^#sRypi{?=~8}5pfLl?u6a?R1y zCpqIxd|3L+V6J_a0SK?B==U~uPt_p7nN?k3Uo68>%s}jB?J$ZT;+Z2fk&L!J?T}@H!ENeKxe6G8zGsb#PKt)bxtP@!^_$%5+)e z??7T&hC}8E%}0*$k5K@C$s|}VTqc>oIycL$J5M#BnF`b|T_AKEJWhpgw=GL&Prgva z>9DH&$o$uif`!4|j(gSVemMgzlkl$@rpfJh#Cm-A{I;>5qHx+4RaBc22%EHu{V?Za zRFkF@yE8%8N$7S{rAEyuY9`1hE^?A$CI`&h?MXWrl))lgP)fQqww|S-CWQz%$n^MY z7n&hsU;~q!(zNjGnIoWlcnu)At)Be7fVnAo0w%!QESgh=CXxPxk&@L*IX|y+e#rap z+s7$s{<4A?7Dx5R+D0KYMo(WL4B{_E#x_YRD_ghxvre}3=+P<$^JxPuWyG3<^sKaE zb9AjvOz3Fp9aoFY?||X>vnC8v1;G&r%O*?eMAl(7dEZBmN`8GIp~2Qz~Mt*L%oEG^hmu3N*l#W#%0p z!!~E)%~nfS*=Jf2+V!r9(1D^nK%z*9OJD<+K(-VETU;o-j6#^yr!`_tyzdxi^CpbE z9}~q86=3RehoQovsp`-7pszebl8E|>{QA?Vdw*ATn9F2(t_KgImVHEp4Ei%Uztn0) z61t2Fgto6PK$D{*N7T4u#+odMW?1-5+)Cm=KYN_h*rd8wD?D&0VRn^RZogOQb&6l)imA)A?>=t z{bS1R1ko^f8VBgI_gFM@$|W&OVk}!yRjd&ieNAhFDx)q&M}3Q$KWD-XZS$l`COM~6 zP~r-cB?c`+$Qg`Z#ToQLlOcX%-{)v6|f%p^N3PZJGu z*SK0G0VDz&D1Ja?8#QWeBPC64-zP_RZ%8)eK~A*WbK^37S9Duy4zmJNTFH5|J3_AB@c zD7C?Om4hDv4WGP7nG?k=uMqNDwNQQF@urQ~!+T{S+cq?3xM&ne7t5bhUy|`D%27qf zdwmsIeDBf*fo1MO@W3G``MLUP!#Q#pA*vLvQw*hGJzXk}dsfD-3A^+Ri22BILUyR? z(w54qZ|GHzZ!s}%R`L0wp)mNrEY0F^9D5cs0&FQC{Z#|a~6@1Lt04(k{4$ctJ5#z3@X_y$R4FZN)&PtESoRHgD1~zo6^Z~ zRZR){$2JRw8{KR}QhbShHxXkI92LQO!Nu+7QIm;T%q~sx%45#eS~Qjf%kA|`S0N=& zbN^M*RDQAz#c923mHup0&I^>4yEd&pBI+cNN^6JWla5CiZvH8_#T{w}yjMw_U&H^w zW-bmLdQSr+xjg({tqcoqbLwFpk|atCAWHxb>1V~jGV!zNFQWH2N{1<@X5rAiO2lpi z%p1c`a@i1O91FI;j6j^a*+ytbi^EmjHa2I-vELpJp~D2Mk)2q2^^Fp;mBp@ zxf&Kyz?S8l&fvgui^ivAW*kM^mF(}r%iBOm21c{ZV8c)7)3yg}RrSr&6q0lh2egb} zg+D^oe96-hO>-v->N2o=6|j!U7WwhVeNr5!xBGd|ZQT;m`QLMeRGp`po?!3@jGoQ| zz@h(^=;Hy@0pK($Ko!EOYcgUR9&8f|Kj+8iB5XBOB?R!qq)BMXAk}!Rf_dg>MG8C! zNNc{o9tu>?$F>JiX$4%-j7pwOC!d;w?w^Yr+gVY20x8TirU^(4K8K>B=ETDo4E^MJ zTE(5lb469nR|PAyST>5@AE1ym(79zv@0Fdc^{W#m{vz0b;@_g5#k1_c_NnzDk$U3U zczrek93hQZF#-c=6Q=$_q;oNa=-jE*2i7K(CvhwW2gb)a%VM-zQ1khKR&6cu#AaT9*LL6Qg1~Q6psp}dqp-YDz*ED zA9y0pKJK+sU86>3hD#4VHWcl!;y8`j$x4rv%BGENlYGG_&*WNRTL4Xc>AYCJPavw3 zPs95UC@G(4%<%k`0^X)MVVb#Q+%bxpH~w19JNF-la1Ry1Hq&T;q7 z!Kq+5A6H`<3)!>l;(SVoV3L_q`T)e(8&@+sMXHY;r5Rkdt?&#WT(ZzYzW$J2!z6n7v1si z)PON~WQBFzCM$;NBqQi^4nU&vn%;9D6D@eMbkb=)4qT{Sbw3FN;Z-7++n5M~N%e0| zleN= zh!o{xAs0U}(LulHDj~F7>X&~@8DM2pD~n$xSeb)|qT1buqq1$ntw95B%U@Se{}Sco zUW6I((z9BS)M=Oqbi*}<0Y}0c*Gd%{0>Ra!l;rIO&Utxja;rv}25U8@hXAA%(zaDp z<_h3IL9^WkA^8NF+V85W*27dosK2IJ;>ZiNp0(6ZSd4!TuV|_r-LNPc~PtqHuw< zk7SRBAk}?JF>&&8PF7RDw?i884=1-gmp`XvX$q$&z%QwTqzXYE!#Q%oTd*k_-?Uw1 zUe|HNcB7PZKY8HbyO|VkiWV_TCs3*}rwv9Fm7bC|qB6pvhK6<@{cci2HxYmzIIc2< zPn72zd-_Lnni|kPPNtQ8@}^ zbZ(&k(E!IKGi4Q0X4`nM3!v2ZHG-4Yu?a_KSjA^gGcQZ1zfGB>E@tnKaW(70#3TBB z@pUznOmEgktYI>Uqc-qD&k)Mb67){BeIA}3OjS4I&KwaDWV99W zMayDbq3IP9v{1cm&A;(E$#7x&tKZG+NvKueol=aRHTLRFkZIE;lES0>O}i@K$O02X z&{+0ffVce(MGO-p#UQt|q6%WfLW*WqUT=quG{{tHt-!Z#DwK}rh_B9Yft zEn=~En5bXsUTLh+QRA`I@i&s4mZ1;ov2Ji9d7TZHl(wu`8)G`XbYkL&>3#KZvz~y5 zpf%Q-&yRGB5~4;W<^BE=!7C+a9S>sH%@>S-o&Gk~zwrtp^y0MX+EMB{*x$)wE|9n} zFwv^J$n_JEgpF%7n>8q?v5~@iw}Ysxf()>zu|^Zwy~vUJ$uqoykhk$+-CpJY5%v#H zw>I$bW?kYf!|)xV@zL+FS_VUYYP`gWqKt%gCxSi%oz0TiX#Sb!gY;EB1MG5EST!kt zcq4?<-&CPZ=oM7q0AUy|<@f24Kq^u0g>g$vQt_y9kk&z(`Jxc2KH_tEd=NZInnivge&x;nr`#`sIJYjsy)jJ3F1CnUr=>8ERR(G(^#^hYrgVa@;7#-?RPfU7zZF13+{lHP<=;Ry2 z^vWGy#OPuOcvwR$+60hS>nLIAS2$M(Q+SaGz_`G&dtkp$Cs)t$&D3T`gRCxxy3jnl zand=kS=p13jvd!|JirKq*@%^{M~yM|s=~OwIcceq)7bS>6`yWz_$rBy&p3u@e1TgLKIaGs6URo0x=q!{GFi(0~YK`O2aEYUF4*6cZE) zsd2U7;a=AuxP9Qb9N;)u@&zNSWl~14P_;#Lk-jjmujAO3BeGn9jx6ws4okx)CyQC4 zuLC0(un#@DAH{zIcA6&qTucC6Rkse`aOoRyr*yVFa2R`=)e*MI1lsv5n}Y)D#80Zio%Tx^Kl;p;^tUsxAttYMBSLS&E4 zE>A3&oq;e%N6N~cf2Oe{!bsnjbTe%qW<7Hbv6vUjk7O!(MBmvQ$&_?p9i#BdDZB2? z;Hs5!u*(O^*04NCBh4`V#w~cngQ`n>5e55Yd=?up=(9xCu&C_gr6?vK2O8b75ig>swH#nu%_9MtC|0Q@5iK&gX}eX+qf$FrC02qMagvbO;iZC2x<lNgh7XKT)S*q65{ z~mT6D!GTC-ftPoarWe`b@xOB ztX3EepR}xL6Tx;KQ7Yf^{jn-SD=c6OQf`tq^XnCORTnO)$syTfNvp{-pQs1h?Gvz! zf#{tTUyd*-Bs~TalJ@agmrA@e__0!lbmOiig8@Rh(Mt{_MFgwl^=C6_p42deg+PBi zwiKI@*08`4#j*%3EkL(47^%Xj93J0gS#p~9;5mJh%+v&ly;Ixg<4hY830d~3(BD`= z>uY2-8W0WuVDW`lNz^Zog*PYYS#V{ez3t;Tv-7G>oU>`dr9VS}OxS2R`+qpRV=-*j>9>1S$qI;Y#g|kM&*;1{f2t$Mu1lq>ENOL+t z5YZx-St&(}`y%S<_fEAfDZd@*Gmb|X9>_SgYO!s=%MxRjmms##nsBT3 zy2LO>GrG*UV3L_NuPfL8W%O0TePCgDjjRT0jLTJcIz)Jh?Hi62=uOqA_Vd%Y%r#sL z%;0lcombcK2EEkiZk={Kc)*|pdZZbb$jP#&8*7-dqqp`}r z0t9Q>H%%w<1WG1SNhNie%YJ7sz?-7>ZU~?}-M`Lw=e!EnR+yBek0+x;cdh=5-Z{q2 zXD`~Q;Q@Lsk0E7b_>}}?3KT^ev)U(|rGuvsGvPLt<{OAE(dtH?kQ-bD>8!)yH^~0l ze1Ra-qIg~?N{OV<=l@<-+|P9*4EcZ>dh`(pO0$B`Nt9}=%^ zXgN?;1q!^VapeylX`)gc%q6ek+rD>AT6QCU8&v!=YNa_j+TnO?h^VQsYWlyXbB;RG_f>D>DG9myO*D(aG@LS zr(SgSM^{JsKM{Om zF|a?ycsYcZc98IG5{gMx#;}A`gT5N?6uFc!b%9V5Ca!Zs(5LG3MM^Rz#ALzu3Pq>M z^&j`6{tCCI*-Y$6&;9>7!bX{Dxg~){xvf99>uQrkkJb?mRHI8~GO9?(o3ojS7KOJ! z_P%Lx5y6)Ss2~=qI9D9TM}QvWEej}1&X}N_T^`>YxpSQz9F}#l++`(~oZ# z#j2WjJyVqFInBI)(zLR(%QQ1p(0239x(Oa^Hn|Nbn>gjcJoDx>GE>?bjY~QBNrdoP z-I&CJ-rdc0^~?UE16UtlC)baXUi$5?64*}-8$6OBz)ph3w<@IenZ!;nZh}%k946ZY zFwUg|truB~KRzOx!69o&c->kW`>Xx816>!6^kPfC?Jk9N73zH&hZD z5A2>L+-ygQ1q*!i70})3;{Ffw91)|?S?1eg05Xfp2D4Ch%h6tUar;}I1^=SXOca$* z+H`7>X7Df=qF+9aZIZ;gcDJ`%Yr6cljTvZ>v)V%|$XR%WUX z&1hPpLYO*dDBQG2*1M+N57)#-DE6d7+&vquC+WFGZed3s!rVh-7ML z|9}Y`3ET`;vD);0)uJfQVB;1v*fKEGwfnEOp@W%-QY&q~PGoWcHR_^?*RT{UKfjrz ziBcZnO6R%Y-X9#w1^_hC1I`j{5tLz?Eu}1*yeNz{)*p;0^XY_eF`TgxtU3))l)2XH zx)qnui2OfVNls-mhU%Izw>kbHt3CuYQII*IBpw%?}Pu;tv za!QN#wce<%;2`(L^;tWiPvx&ztnQ3K^de82OngO=TBX$DiA0)^s4H2WaC2<3Uu4p$ zVO@lQZ(_O#Jtmv2ssOQwMTIf6e3^lhj*>va3|~Omxv5-uRRzU*!?+h!zM1YU;KM*p z91{1=am0TOK?ge``8REusRSvFa;6&L%~jQYZ&>>ol(Z6-=xww1DxWwc1s?v8-Q4yR z==OX{Q+FL%)hg|4W(?~xiq+KI+6!YYZa-WojTItHhZqaIpBJg8OCHOdaUMk*eX3Y@ z+xS&;FDsN70`w!u1-eHf8wtiolH-)X=A0}$HKkAa49xSI(#eLx@VNc%)5;VtrhLmq zcavR9L>%YaLEICcGIfk(-vY=T`@xYB((ACpu&GVzLaRGqA$*_j`1jMe;xwu=&yag| zmNn!%0>NPr-IeX{s0#$}{{y8!TED!boJFxJ#B5V$V_Or|)|U}V8WRt6h6ZekyA(lM zq!b-ysZH=yt@B_dBg$kn(7LAFTF0~?xq`{cQc%^>o&}srfeMoi=cX3GP)4W{a;gm- zMD6Vipj}}S;7OA-(iXu22_K9$d}An)$qa-HHs+LJ-gcYOgES~-x}&gpkTwUKwBH@v zxJg@s2$%1*L4*boesS$odkaKh)f^Q{BY@SofEcZ{Atg|3esqvW zN;IkiU3_At?1_+-bt$S;{ZF;R9Z4%2sNlu3l-i<`3~Hsigl&DUnSlWfjkLW4QnR7UA;k(FT#ZtV9q9%{2tc+2$M?*3?m z_-K#^nhFiYc*+_nQcl%9Bm~?4Lh%$7K}j+~Fw!{$RTw@58Kt}?O=uvSWW&5w#Y2!r z%2_nrxl%{FeP9}7Xpo^nhTj|+Si}B2G9(#00a=*c#F zogjH9tZ*tuWynXJR02&#J7y&*$e2s0bvc|Nt8C7Rw50h$24kJIUisP9OAY;yclR3B z(D;!4ENqw$AChx$i|Q7&wkl+l0wFoHhH@>2$EI|`paYFb4>&K12i|8wtz*GtAVEzX zlc}aeNomG~tzJZ*s_boY!jKNqu@bl0C8wOulyk1I;6h5{3Ts?~gCbIrIXG>FG6f5n zoPA;55NuLeU?|dqvnp!}1hOXa=48k@4Z*rHUvN;{lGeP#nfj3WX*YB_wAX~6HdNt{ zf`fUa3Swk0@dnsn0$co95NPRCW=P1+TQsT&VHh7KJCP*0gv>K2S?97Bg$K$WnQJ)7 zhUt`osZ0z|I0h7%Rlwkb$jI1M6V1?ofe=yJ2@CMM4>^31{r>DWY89F(OSV5 z6+*NWWhq24piR-Rm05QUiZYX{TSOl`@&UDmwU!VhqYDb~3QMTF6jNeS(FI$*{z9wb zNJX!!`R_wz0WpH|C86p;4=e*{T%g!>3VWz>rqS*9LP>Z9oIZVtpYLZqgq9m__Fs4!; zzPXZXYac93BIXSi4D&ZVTUdCtwY<6f{B?r{jr-`2ZPz?6?jwh{a34WTfi&CJ+7~4f z7SABTSfhf>ihn;58hr9f*yPbz<`>?`qOzeH;~JcgVSGaJX=hIGB9V=({S$g^B4lfo z)R?6+0SLV;2yQAgWY9{n(n~(NIIOVB$Y?2n%Sw?OmbwriBYKNcy1*1d)D}E^yGjV_ z<_;RFV{f)aE><@FiAHP}pa1Xs0Qb({w}t(!qx2Vjy}bDNHhTPwu06bad{8%&NN#Q7XvU%=C@KRn>@;7mt4<8NqRT#=|E2!K&| z-ReG-2u3`zKO>R5jYOwOsZMz*?RcT2ynfA}5en}jRNX(AB-Hg?&Hs&1PFs*LqWMmR z8r6tDAr#tQB-9AQ`x&9korFTIuaA8$)EyT3-Gs7rmukFFBf|c2p@#ZE8DXxk=2M9@ z+~wKn+fI`emQ|4+2`o7k%8n$o{)A9)>s4-97x>zbajZ~or1{@(A=Dl8!7mf)E^F%7 z3H8sdDK`mY++Be0jzaP1lZ5&Q7y2a4>Fxr2cYS<*f}V1B0lwP_RSiyE*Ufk+c6UL& zUn7zJM^+Qd#PB3WUizU>(vDciSA~+Jh1f5ILjMbJSlW>#c1@yDH|d2$5`KY1YP46u z%_RbTE2i*+IWlhgQj$1DB6-U>0wI*tBU{@;iGBohQw0)mYo&ysY9#D<^{qy(d2^xs z?J4Kv$2Qvh?B)WgD^Km>^JNdhp!=Dwd%EuB?tW+wJy6fLTMMtYS6^(dZNIzf5n!8<7)bF>3musEg+lYgWI@JwSC%1&XK8<{TF69<2^s8TFqu1UKy_4-a+Qzw* zQ@wIO5_r9vKDQ16-)pPgY3)xp}zJUCzGPH$MqkA|bnM$nC3w@&EU z+HCP!<=1OJbjECW-CW31Kla(svypRYH*|Q#))(qb)FHCmWt|R(siVwzCi#WpziyzP zelUw?18xn++c+I=Hlm&@zE(f&Qa;?PDu7Y1TVLBd2z);5BLBZ}Hr$M!K94&X!YZq+ zIw^OM@^q|?vw>zRAHO~P>ab1K>hFdg<(nv{n@M{<@NC3g42=4R2WP6Zs;S)Wv7r3K z=q|$6aSvytK{u(-^3cp>JR5g5LT>FI4qBPv>5^i)lTPCC>Tt7>?h(bq`eF}51MV_s z!+~Za(+YK}IYi&hjiC{87wH!205h_Sy189Wc+K_p>sDurI!EnZx3gbg>7%fBIDO^a z3~`pfk2#~a`Q#ul(-`zZwJoT{Iz4@^(I|J&IzzO5-W~94%<6UC?;f#i-Ti~Z0h?{5 z*WKqgXDDc^?)28?!9qv=BGSC4_!ZuNh`N!uwt$|a^`}J_- z8G-pe!%n~3IR(hH*RG2-lsd^5(P%%M-X9_R_iM})Fn9L4G3?C@CRc{T&Dciz&gGB0 zz0>7NgBe&!Z%#+sq8X`yOBQl9YFppBox7MxH3IHE4>8l{-9Oon-Mw~d)o8Q3Kku!x z;nsPyNpfEL*Z*A}a7x5Vbw-^W46igebG1~r%he+e`=^mCTkp2Em!FM~z&Y%+zZ}xX z(~uhpW4<~4%3c2EQryVpUyr@q#l_A-Uaq*$#%(R0hx%MNz{ZF8Ts~o1pv~~hkuDY^ z+jm!w`?%j_&T$d_v~g;mv>sPO@56^nkwFWoCWP?!kv@Fb`BwG@hx@~@uaUbyo1IJd zzSHX-UX3|DUkRsw*Ps9W&wu~(pL)Q><7NyQ-ut~F*k7D}I+OoDdtcVnM$)W(e}09+ zw}gkS! z@ZS%UV{i9Yam60QEItrcJjo!tlch9B5ZdOCMk)DGWey+9yswWTa}|WwErnyBz+)|!sFt8{PTMn>=A%_Qe3YC=<08*mJ#1e3ffVrIIN!wXqvd`)qQg#hznR}J zy&FBme=ZTGkO(N}4(*2`}XNvM68Olu`1&0WeV%NUI?5kqn%hTwx#`tjJd z`$SshBg){t2j&tzPn;FqSR_YWoD+d^>AS54*HauY#k(UxgX*3TY1aM>uUC@Y}IgAE|tJ<^saLlmH z8~6`z{mEyIR-;FzGI79%_%l4^lr-(;Uw`$_I&APqvhV*8%K1lH@-~r`m)25x=+iJN zrI()lPDy!8U+$ipCsyQ#M(@jyyvX}($mbt>Rrc^8hr2s?7Qfsdka4hd@59qX~@J&9vQZ ztTv35X7jWySHt3wSc9{vgN?9wHl3yqrsT6xy$a>;O z{rYZWZuh!7Kfif+^KGlOv~qlvrnioxIleyLY{O;C!Djn>-E83e;WFPnXe=LZon7?e zac6pUkQgbUOaNM3fy=kPa#nXiib$&5>ed$)`=K8b#+Agim z)^EDY8_Ry-{OEG`{9+p~8yCxaOPjsZ<~O?>R@Jw)<=)QrY@EO9F7&M2U)dE7jy|=a z*%dqfD8$`M_=L;Xwtu{XtDV_v(OAH3ZR#@zH_hWP-{8Bm@aZZZwd(46dUkciiqBUK ztIVf`g`=&V)!yvx)fV^r^OqYPUO(ITI=6LrcyoMt-dlD1`P)omVfm`HSNE;P)y_9F z*AZfMCN=lhmT#IHSN8I3W<_jW!T#CI>hjXoWxw9|y775_s@4vm4NyihRB9T-HmMtBW@8uB5L! zXLFsMaJ&s{>Ud$VH9y;$SDPI=g*ZOnZ2Qgn-1cHOw&MEY+5BR^{^|6aoSI*ns!Maz z>vnf87UllV>BaJDdt>K%ro-9gl~|6)d$U{qq^{PxOSALJPn~qv4tjO@t-iR_-zOz7 zZ}IDfIoOkUGQBc;czM9RQ}vU2-tucJGh%UL`DAKl`_uNfm9N{^ z{q=>JX4ISA&eg?=Y0mD=U4`uhd1UG*vul@E`WTy+$E)>b|6t}|d3R?~xUW;Ye04E3 zI#<)5JKemJ7h0cKZ>E!G4zJGUKVR?M?6VU+%}!6^`N_%h;nsP+KAUQ&y_L0gTwd!< zuUzEzzMS4&4r&vRHnxxUHr97ej&4-?x;brNr+(n4XZP~P#ev*9-dkOo>*DI>I`3@q zc5mM_7tR)Y^Cw?tHg59N{N~KOx@e_~{TsLn({c9XY9U_m)q0rfH_z*gU? z&JViN$578xqP@P;?Hup;>5Eq9>&!)GwSKv#j<;quFP4tV=U@7Ny}l7$n{Ku$|N1l& z*JoOnm+<*!?%;T2kl-!%zGC^M;m}ywQ`L{v7Y@xvK@%pSrooyWN}a#fh}++vUN2eQ)+spBTHa zlo!lV^M-Ebxjso#*G}a5*{x3}U(Xk1I9RajO12N?7m2i|n~UAhM?ZIUvbVE~-vs1u zCky;Q*Bkb%|4Glx9o&e^t*LageSEAw_pX{f*x~$OZDGoaYrAl|Fcr+{s&-e$=Ij&S z{&aCtwxV~qIDODCi}O>Pd#8;V@wLyx`q9?3F%*}#s&L)0s;ZjU3Zm;iN?wy>Zg;^7tt9dHyP3Ld@i<7luzCQPLHl82P@XrUm z%h0oU`gQt(&m7-;TAW#0>RoS6E#}KlX{~c}{T7BY8fIJmiU`FXeD!YFNd_ zBl{Q6Is;SX%Lub6GLxK8>SNFfiY<;GLsaP__XytR9ISd&_)$3Am(cX_^sbKS&c1nO zO`WuRPl!eN%{`%Aocq}8oqc41C87PJnvq6lz*pEGmN<)6e>`d~@!=H*L)vMd`u}`E;b!mX!^A3vXqo=(?@Hw#cr(0 z+AJ+tE59k;u0dh@vl7hb<(FL4a;QaF(@;yY#*wVy8mgL-HG?%wwQOp})BsCuK7I1p zf=`MupOgr__@(-rGSjCQ`?uHecXB;_e^?Rd%hEnC_I&!~e_Y7<^UU?QoT8UKZz;Xm4jzU@w z+_Dsm0zRC*MsGnV8JW-8Yh^f5PY;@D3z4`c=0wCavKQH_^+~blnAe;qsWI5>FbI`B zQ`yj=-f)?Mv@&M^&!qP(ODoU`ruoC+BoFV`->~~ANf${LERRMiq}9r(Tof4@kLgJ( zgMZ`x5dZz|hj}aQeOpJ3Y-TKo64j8ojZErBFz$B9_Ltx#)Bu#E5u7;1aVF$}pR~wLliWdurKZBi7Rl)naj=uheUl$_R&eL3=x-!L4pA_ zFj8^a-m!wg!rW4Y1=aA7-wX>Tad?PRZ<#&I9VZM@ zBf&`jORxsGC0(D4NV-h$2nJ;yHg)8CAen0~BU-ST8KMHF`2KxlJRrm}RLW(N!;YLv zUWSbu9<0`h0m&Q!NkQwlU`>P+=ruh8v$bnfDe!28A}*;e)8|qX%9hVMmt1 zXEezq4cgjL_nuA87$Yjhh&#wEl{S_sqn!^bWS+C}Zqf$NYDs+Q`G}Br`inL>tFWM= z2)`8;Odv%7Cf_iT+(t61d1SeOgAHU-OHD>Ufd$Jv4U6sGrx;1@Lg1`8nyg0UP^Op_ zmztJxFx?*fLi-bL*8D`cpUp+bgV z8yS>#Z-_4=M-<6$2Q~?g40*`bnnZ@U_GEs)hzvRrdL$8&QG^Me2L+kg%vqT@ZloB2 z49XFhWao{Hj0Fz1D0v{hKnH8%Rc7Gq3$ttQDMN}S8ltm^%3Hy$3(+Jmaty&F=NS@* zvfeQV(g}hsfIy#9+9HeEm`Oth1D5G$lPceZ4E4Fa`eqeasIZ~JhF=~VfV(%ahCs$U zS^C<#n4(QKkc~|Swqqib^(}^|_&aQ{Qjr}VLnOi=AZQQXmN8mi#JZv_n#5?nBEK?Aa#jYd+XfedP| zxlSpBz?~CGb1}>nbcY5^bFRqNFWLXZHJsxyIb{VaGd4{g;X@+ZUS=}lMZ#1n$dt0k z3l1gDW%TqqW|7Uv(geqRj!}5cBruExLJSbOQQ)`~m|6D1VivZ1Y03IN_aRCS~C# zF7<~?$B2df^9_bJU#p}4jopPR1zjoVyS*E(py3Ve-!d0U)1#w8AO#P}I87QGw97`A zJgisGy@Ixk8)F?Xh-5x1p@Wvu7)Tf*7bD8In3u{%FKq}hmq8X*g0aCEZ-tgdLr5-= z1w2YY^N>t3!36D)1DX6f$IJo<<_%{C>6KwzOD6$&21aHxlDbZ%sFejZLA@Prr~Ejp zqbvd z#xI-qL<%&E60&wiSRNu7;Ec1mILg(e`Cpi!9ddlE zKJm5fz1hv-kM!So#UCoM3Og$7sIcSFal^2q`%hzsdINUY%qDyeiJniVGvJ4)=r zjyLnNV8>R49gi=qO0jvxMOG;`|7R&S&r&E}6D{yQi3Fv^8;jm)>5v((9NGwJXh!^f zGJe#Itoi7TQUY@*Nqv^6h>xV)8Gq_zcmN6BV@Uy2#)^{OsdzR)dIK7y2xRxmOk{vy zW)vjk$P<~z%7XTWAju}=OuAzRpFiVNdAUn%4 z*9w_ciEREL2t$%pk_|cV9t*5ij+jV<5MnX$T|(wL%fuQKw}1u|$&yB50kd2hT1J_` zY}Q~5sl3E($^DF8r%cil3lJs#?E@e>B)oH)2~5CZm@q8RM7v5@W}H~Cu{vMh-KpT9 zf`bYUenmKNZIu4+bV~FaT|;aBi&LqWx|0r2>PhO7}MdgNdxtRc{y{2F7I& z8BFP-;C0R{F;eu_XGkm$Ytw(9yQGL=D9AbKGVK>-2t+JcFj?{7!|?tKx9vZbkTSBQ zxOS0=XbA~QVHhXo5t9_oMoX&<={NTvj=(j^2&j=|!=#}_n08KS5rqa|OnZ}vIz?x4 z>GsBS1Xltc$_1U-q&0mrV5Jq~&0SjGUaDZB;t0PLEKCSTu<(XnJ}QtJ4Oz2NTjh(7 znMeae7I-A9(!N{`hCWLXgN-F{+dx zO&Ah5#c4y|Q4KZ(3APtk_v@P#8dPXdp~0_*24wQSffwXtz4n4Bog5fJX1&dd^oB

prs*3!ZC26vk6gnq9a^$ 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 diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json index fa5d6447762b..47bb1868e706 100644 --- a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json +++ b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json @@ -9,7 +9,7 @@ "version": "1.5.0-dev" }, "date_detection": false, - "dynamic": "strict", + "dynamic": "false", "dynamic_templates": [ { "strings_as_keyword": { @@ -49,36 +49,14 @@ } } }, - "as": { + "dll": { "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "authenticode": { - "properties": { - "cert_signer": { + "code_signature": { "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" + "exists": { + "type": "boolean" }, - "serial_number": { + "status": { "ignore_above": 1024, "type": "keyword" }, @@ -86,234 +64,111 @@ "ignore_above": 1024, "type": "keyword" }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" } } }, - "cert_timestamp": { + "compile_time": { + "type": "date" + }, + "hash": { "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { + "md5": { "ignore_above": 1024, "type": "keyword" }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "more_info_link": { - "ignore_above": 1024, - "type": "keyword" - }, - "program_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "publisher_link": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "call_stack": { - "properties": { - "instruction_pointer": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_section": { - "properties": { - "memory_address": { + "sha1": { "ignore_above": 1024, "type": "keyword" }, - "memory_size": { + "sha256": { "ignore_above": 1024, "type": "keyword" }, - "protection": { + "sha512": { "ignore_above": 1024, "type": "keyword" } } }, - "module_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "rva": { - "ignore_above": 1024, - "type": "keyword" - }, - "symbol_info": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "client": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { + "malware_classifier": { "properties": { - "number": { - "type": "long" - }, - "organization": { + "features": { "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" } - }, - "ignore_above": 1024, - "type": "keyword" + } } } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" }, - "country_name": { + "identifier": { "ignore_above": 1024, "type": "keyword" }, - "location": { - "type": "geo_point" + "score": { + "type": "double" }, - "name": { - "ignore_above": 1024, - "type": "keyword" + "threshold": { + "type": "double" }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "upx_packed": { + "type": "boolean" }, - "region_name": { + "version": { "ignore_above": 1024, "type": "keyword" } } }, - "ip": { - "type": "ip" - }, - "mac": { + "mapped_address": { "ignore_above": 1024, "type": "keyword" }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { + "mapped_size": { "type": "long" }, - "registered_domain": { + "name": { "ignore_above": 1024, "type": "keyword" }, - "top_level_domain": { + "path": { "ignore_above": 1024, "type": "keyword" }, - "user": { + "pe": { "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { + "company": { "ignore_above": 1024, "type": "keyword" }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, + "description": { "ignore_above": 1024, "type": "keyword" }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { + "file_version": { "ignore_above": 1024, "type": "keyword" }, - "id": { + "original_file_name": { "ignore_above": 1024, "type": "keyword" }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, + "product": { "ignore_above": 1024, "type": "keyword" } @@ -321,2674 +176,341 @@ } } }, - "cloud": { + "ecs": { "properties": { - "account": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "availability_zone": { + "version": { "ignore_above": 1024, "type": "keyword" - }, - "instance": { + } + } + }, + "endpoint": { + "properties": { + "artifact": { "properties": { - "id": { + "hash": { "ignore_above": 1024, "type": "keyword" }, "name": { "ignore_above": 1024, "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" } } }, - "machine": { + "policy": { "properties": { - "type": { + "id": { "ignore_above": 1024, "type": "keyword" } } - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "region": { - "ignore_above": 1024, - "type": "keyword" } } }, - "container": { + "event": { "properties": { - "id": { + "action": { "ignore_above": 1024, "type": "keyword" }, - "image": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "tag": { - "ignore_above": 1024, - "type": "keyword" - } - } + "category": { + "ignore_above": 1024, + "type": "keyword" }, - "labels": { - "type": "object" + "created": { + "type": "date" }, - "name": { + "dataset": { "ignore_above": 1024, "type": "keyword" }, - "runtime": { + "hash": { "ignore_above": 1024, "type": "keyword" - } - } - }, - "destination": { - "properties": { - "address": { + }, + "id": { "ignore_above": 1024, "type": "keyword" }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" + "ingested": { + "type": "date" }, - "domain": { + "kind": { "ignore_above": 1024, "type": "keyword" }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { + "module": { "ignore_above": 1024, "type": "keyword" }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { + "outcome": { "ignore_above": 1024, "type": "keyword" }, - "top_level_domain": { + "sequence": { + "type": "long" + }, + "type": { "ignore_above": 1024, "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } } } }, - "dns": { + "file": { "properties": { - "answers": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "data": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "ttl": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "header_flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" + "accessed": { + "type": "date" }, - "op_code": { + "attributes": { "ignore_above": 1024, "type": "keyword" }, - "question": { + "code_signature": { "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" + "exists": { + "type": "boolean" }, - "registered_domain": { + "status": { "ignore_above": 1024, "type": "keyword" }, - "subdomain": { + "subject_name": { "ignore_above": 1024, "type": "keyword" }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" + "trusted": { + "type": "boolean" }, - "type": { - "ignore_above": 1024, - "type": "keyword" + "valid": { + "type": "boolean" } } }, - "resolved_ip": { - "type": "ip" - }, - "response_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ecs": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "error": { - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "norms": false, - "type": "text" - }, - "stack_trace": { - "doc_values": false, - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "index": false, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "code": { - "ignore_above": 1024, - "type": "keyword" - }, "created": { "type": "date" }, - "dataset": { - "ignore_above": 1024, - "type": "keyword" - }, - "duration": { - "type": "long" - }, - "end": { + "ctime": { "type": "date" }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { + "device": { "ignore_above": 1024, "type": "keyword" }, - "ingested": { - "type": "date" - }, - "kind": { + "directory": { "ignore_above": 1024, "type": "keyword" }, - "module": { - "ignore_above": 1024, + "drive_letter": { + "ignore_above": 1, "type": "keyword" }, - "original": { - "doc_values": false, - "ignore_above": 1024, - "index": false, - "type": "keyword" + "entry_modified": { + "type": "double" }, - "outcome": { + "extension": { "ignore_above": 1024, "type": "keyword" }, - "provider": { + "gid": { "ignore_above": 1024, "type": "keyword" }, - "risk_score": { - "type": "float" - }, - "risk_score_norm": { - "type": "float" - }, - "sequence": { - "type": "long" - }, - "severity": { - "type": "long" - }, - "start": { - "type": "date" - }, - "timezone": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "file": { - "properties": { - "accessed": { - "type": "date" - }, - "attributes": { - "ignore_above": 1024, - "type": "keyword" - }, - "created": { - "type": "date" - }, - "ctime": { - "type": "date" - }, - "device": { - "ignore_above": 1024, - "type": "keyword" - }, - "directory": { - "ignore_above": 1024, - "type": "keyword" - }, - "drive_letter": { - "ignore_above": 1, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "inode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mtime": { - "type": "date" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "owner": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" - }, - "target_path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "file_classification": { - "properties": { - "captured_file": { - "type": "boolean" - }, - "entry_modified": { - "type": "double" - }, - "is_signature_trusted": { - "type": "boolean" - }, - "macro_details": { - "properties": { - "code_page": { - "type": "long" - }, - "errors": { - "properties": { - "count": { - "type": "long" - }, - "error_type": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "file_extension": { - "type": "long" - }, - "macro_collection_hashes": { - "properties": { - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "project_file_hashes": { - "properties": { - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "stream_data": { - "properties": { - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "raw_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "raw_code_size": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - } - } - }, - "malware_classification": { - "properties": { - "compressed_malware_features": { - "properties": { - "data_buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "prevention_threshold": { - "type": "double" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "quarantine_result": { - "properties": { - "alert_correlation_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "quarantine_path": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "signature_signer": { - "ignore_above": 1024, - "type": "keyword" - }, - "temp_file_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "user_blacklisted": { - "type": "boolean" - }, - "yara_hits": { - "properties": { - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "matched_data": { - "ignore_above": 1024, - "type": "keyword" - }, - "rule_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "properties": { - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "http": { - "properties": { - "request": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "method": { - "ignore_above": 1024, - "type": "keyword" - }, - "referrer": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "response": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "status_code": { - "type": "long" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "type": "object" - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger": { - "ignore_above": 1024, - "type": "keyword" - }, - "origin": { - "properties": { - "file": { - "properties": { - "line": { - "type": "integer" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "original": { - "doc_values": false, - "ignore_above": 1024, - "index": false, - "type": "keyword" - }, - "syslog": { - "properties": { - "facility": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "priority": { - "type": "long" - }, - "severity": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "malware_classification": { - "properties": { - "compressed_malware_features": { - "properties": { - "data_buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "prevention_threshold": { - "type": "double" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "modules": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "authenticode": { - "properties": { - "cert_signer": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "cert_timestamp": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "more_info_link": { - "ignore_above": 1024, - "type": "keyword" - }, - "program_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "publisher_link": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "compile_time": { - "type": "date" - }, - "hash": { - "properties": { - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "malware_classification": { - "properties": { - "compressed_malware_features": { - "properties": { - "data_buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "prevention_threshold": { - "type": "double" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "mapped_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "mapped_size": { - "type": "long" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "pe_exports": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "ordinal": { - "type": "long" - } - }, - "type": "nested" - }, - "pe_imports": { - "properties": { - "dll_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "import_names": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "signature_signer": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_status": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "network": { - "properties": { - "application": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "community_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "direction": { - "ignore_above": 1024, - "type": "keyword" - }, - "forwarded_ip": { - "type": "ip" - }, - "iana_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "transport": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "observer": { - "properties": { - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vendor": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "organization": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "package": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "build_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "checksum": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "install_scope": { - "ignore_above": 1024, - "type": "keyword" - }, - "installed": { - "type": "date" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "process": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "argv_list": { - "ignore_above": 1024, - "type": "keyword" - }, - "authenticode": { - "properties": { - "cert_signer": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "cert_timestamp": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "more_info_link": { - "ignore_above": 1024, - "type": "keyword" - }, - "program_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "publisher_link": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "cpu_percent": { - "type": "double" - }, - "cwd": { - "ignore_above": 1024, - "type": "keyword" - }, - "defense_evasions": { - "properties": { - "call_stack": { - "properties": { - "instruction_pointer": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_section": { - "properties": { - "memory_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_size": { - "ignore_above": 1024, - "type": "keyword" - }, - "protection": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "module_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "rva": { - "ignore_above": 1024, - "type": "keyword" - }, - "symbol_info": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "delta_count": { - "ignore_above": 1024, - "type": "keyword" - }, - "evasion_subtype": { - "ignore_above": 1024, - "type": "keyword" - }, - "evasion_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "instruction_pointer": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_sections": { - "properties": { - "memory_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_size": { - "ignore_above": 1024, - "type": "keyword" - }, - "protection": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "module_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "thread": { - "properties": { - "thread_id": { - "type": "long" - }, - "thread_start_address": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "total_memory_size": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "env_variables": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "file_hash": { - "properties": { - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "gid": { - "type": "long" - }, - "group": { - "ignore_above": 1024, - "type": "keyword" - }, - "handle": { - "properties": { - "handle_id": { - "type": "long" - }, - "handle_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "handle_type": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "has_unbacked_execute_memory": { - "type": "boolean" - }, - "hash": { - "properties": { - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash_matched_module": { - "type": "boolean" - }, - "is_endpoint": { - "type": "boolean" - }, - "malware_classification": { - "properties": { - "compressed_malware_features": { - "properties": { - "data_buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "prevention_threshold": { - "type": "double" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "memory_percent": { - "type": "double" - }, - "memory_region": { - "properties": { - "allocation_base": { - "ignore_above": 1024, - "type": "keyword" - }, - "allocation_protection": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "histogram": { - "properties": { - "histogram_array": { - "ignore_above": 1024, - "type": "keyword" - }, - "histogram_flavor": { - "ignore_above": 1024, - "type": "keyword" - }, - "histogram_resolution": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "length": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "module_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "permission": { - "ignore_above": 1024, - "type": "keyword" - }, - "protection": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_base": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_size": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_tag": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "unbacked_on_disk": { - "type": "boolean" - } - }, - "type": "nested" - }, - "modules": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "authenticode": { - "properties": { - "cert_signer": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "cert_timestamp": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "more_info_link": { - "ignore_above": 1024, - "type": "keyword" - }, - "program_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "publisher_link": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "compile_time": { - "type": "date" - }, - "hash": { - "properties": { - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "malware_classification": { - "properties": { - "compressed_malware_features": { - "properties": { - "data_buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "prevention_threshold": { - "type": "double" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "mapped_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "mapped_size": { - "type": "long" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "pe_exports": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "ordinal": { - "type": "long" - } - }, - "type": "nested" - }, - "pe_imports": { - "properties": { - "dll_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "import_names": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "signature_signer": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_status": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "num_threads": { - "type": "long" - }, - "parent": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe_info": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "authenticode": { - "properties": { - "cert_signer": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "cert_timestamp": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "more_info_link": { - "ignore_above": 1024, - "type": "keyword" - }, - "program_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "publisher_link": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "compile_time": { - "type": "long" - }, - "entry_point_address": { - "type": "long" - }, - "is_dll": { - "type": "boolean" - }, - "malware_classification": { - "properties": { - "compressed_malware_features": { - "properties": { - "data_buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "prevention_threshold": { - "type": "double" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe_exports": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "ordinal": { - "type": "long" - } - }, - "type": "nested" - }, - "pe_imports": { - "properties": { - "dll_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "import_names": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "resources": { - "properties": { - "resource_data": { - "properties": { - "entropy": { - "type": "double" - }, - "size": { - "type": "long" - } - } - }, - "resource_id": { - "type": "long" - }, - "resource_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "resource_type": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "sections": { - "properties": { - "entropy": { - "type": "double" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "raw_offset": { - "ignore_above": 1024, - "type": "keyword" - }, - "raw_size": { - "ignore_above": 1024, - "type": "keyword" - }, - "virtual_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "virtual_size": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "signature_signer": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_status": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_info": { - "properties": { - "code_page": { - "type": "long" - }, - "key": { - "ignore_above": 1024, - "type": "keyword" - }, - "language": { - "type": "long" - }, - "value_string": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - } - } - }, - "pgid": { - "type": "long" - }, - "phys_memory_bytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "services": { - "ignore_above": 1024, - "type": "keyword" - }, - "session_id": { - "type": "long" - }, - "short_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "sid": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_signer": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_status": { - "ignore_above": 1024, - "type": "keyword" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "threads": { + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { "properties": { - "entrypoint": { + "md5": { "ignore_above": 1024, "type": "keyword" }, - "id": { - "type": "long" + "sha1": { + "ignore_above": 1024, + "type": "keyword" }, - "start": { - "type": "date" + "sha256": { + "ignore_above": 1024, + "type": "keyword" }, - "uptime": { - "type": "long" + "sha512": { + "ignore_above": 1024, + "type": "keyword" } - }, - "type": "nested" + } }, - "title": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, + "inode": { "ignore_above": 1024, "type": "keyword" }, - "token": { + "macro": { "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" + "code_page": { + "type": "long" }, - "impersonation_level": { - "ignore_above": 1024, - "type": "keyword" + "collection": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } }, - "integrity_level": { - "type": "long" + "errors": { + "properties": { + "count": { + "type": "long" + }, + "error_type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" }, - "integrity_level_name": { - "ignore_above": 1024, - "type": "keyword" + "file_extension": { + "type": "long" }, - "is_appcontainer": { - "type": "boolean" + "project_file": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } }, - "privileges": { + "stream": { "properties": { - "description": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { "ignore_above": 1024, "type": "keyword" }, - "enabled": { - "type": "boolean" + "raw_code": { + "ignore_above": 1024, + "type": "keyword" }, - "name": { + "raw_code_size": { "ignore_above": 1024, "type": "keyword" } }, "type": "nested" + } + } + }, + "malware_classifier": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } }, - "sid": { + "identifier": { "ignore_above": 1024, "type": "keyword" }, - "type": { - "ignore_above": 1024, - "type": "keyword" + "score": { + "type": "double" }, - "user": { + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { "ignore_above": 1024, "type": "keyword" } } }, - "tty_device_major_number": { - "type": "integer" - }, - "tty_device_minor_number": { - "type": "integer" - }, - "tty_device_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "type": "long" - }, - "unbacked_execute_byte_count": { - "ignore_above": 1024, - "type": "keyword" - }, - "unbacked_execute_region_count": { - "ignore_above": 1024, - "type": "keyword" - }, - "unique_pid": { - "ignore_above": 1024, - "type": "keyword" - }, - "unique_ppid": { + "mode": { "ignore_above": 1024, "type": "keyword" }, - "uptime": { - "type": "long" + "mtime": { + "type": "date" }, - "user": { + "name": { "ignore_above": 1024, "type": "keyword" }, - "virt_memory_bytes": { + "owner": { "ignore_above": 1024, "type": "keyword" }, - "working_directory": { + "path": { "fields": { "text": { "norms": false, @@ -2997,126 +519,64 @@ }, "ignore_above": 1024, "type": "keyword" - } - } - }, - "registry": { - "properties": { - "data": { + }, + "pe": { "properties": { - "bytes": { + "company": { "ignore_above": 1024, "type": "keyword" }, - "strings": { + "description": { "ignore_above": 1024, "type": "keyword" }, - "type": { + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { "ignore_above": 1024, "type": "keyword" } } }, - "hive": { - "ignore_above": 1024, - "type": "keyword" - }, - "key": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "value": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "related": { - "properties": { - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "rule": { - "properties": { - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" + "size": { + "type": "long" }, - "reference": { + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, "ignore_above": 1024, "type": "keyword" }, - "ruleset": { + "temp_file_path": { "ignore_above": 1024, "type": "keyword" }, - "uuid": { + "type": { "ignore_above": 1024, "type": "keyword" }, - "version": { + "uid": { "ignore_above": 1024, "type": "keyword" } } }, - "server": { + "host": { "properties": { - "address": { + "architecture": { "ignore_above": 1024, "type": "keyword" }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, "domain": { "ignore_above": 1024, "type": "keyword" @@ -3156,6 +616,14 @@ } } }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, "ip": { "type": "ip" }, @@ -3163,29 +631,56 @@ "ignore_above": 1024, "type": "keyword" }, - "nat": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { "properties": { - "ip": { - "type": "ip" + "family": { + "ignore_above": 1024, + "type": "keyword" }, - "port": { - "type": "long" + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" } } }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { + "type": { "ignore_above": 1024, "type": "keyword" }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" + "uptime": { + "type": "long" }, "user": { "properties": { @@ -3245,153 +740,296 @@ } } }, - "service": { + "process": { "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { + "args": { "ignore_above": 1024, "type": "keyword" }, - "name": { - "ignore_above": 1024, - "type": "keyword" + "args_count": { + "type": "long" }, - "node": { + "code_signature": { "properties": { - "name": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { "ignore_above": 1024, "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" } } }, - "state": { + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, "ignore_above": 1024, "type": "keyword" }, - "type": { + "cpu_percent": { + "type": "double" + }, + "cwd": { "ignore_above": 1024, "type": "keyword" }, - "version": { + "domain": { "ignore_above": 1024, "type": "keyword" - } - } - }, - "source": { - "properties": { - "address": { + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "env_variables": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "group": { "ignore_above": 1024, "type": "keyword" }, - "as": { + "handles": { "properties": { - "number": { + "id": { "type": "long" }, - "organization": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classifier": { + "properties": { + "features": { "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" } - }, - "ignore_above": 1024, - "type": "keyword" + } } } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" } } }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" + "memory_percent": { + "type": "double" }, - "geo": { + "memory_region": { "properties": { - "city_name": { + "allocation_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "allocation_protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram": { + "properties": { + "histogram_array": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_flavor": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_resolution": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "length": { "ignore_above": 1024, "type": "keyword" }, - "continent_name": { + "memory": { "ignore_above": 1024, "type": "keyword" }, - "country_iso_code": { + "memory_address": { "ignore_above": 1024, "type": "keyword" }, - "country_name": { + "module_path": { "ignore_above": 1024, "type": "keyword" }, - "location": { - "type": "geo_point" + "permission": { + "ignore_above": 1024, + "type": "keyword" }, - "name": { + "protection": { "ignore_above": 1024, "type": "keyword" }, - "region_iso_code": { + "region_base": { "ignore_above": 1024, "type": "keyword" }, - "region_name": { + "region_size": { "ignore_above": 1024, "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" }, - "port": { - "type": "long" + "region_tag": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "unbacked_on_disk": { + "type": "boolean" } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" + }, + "type": "nested" }, - "registered_domain": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, "ignore_above": 1024, "type": "keyword" }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" + "num_threads": { + "type": "long" }, - "user": { + "parent": { "properties": { - "domain": { + "args": { "ignore_above": 1024, "type": "keyword" }, - "email": { + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, "ignore_above": 1024, "type": "keyword" }, - "full_name": { + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { "fields": { "text": { "norms": false, @@ -3401,31 +1039,98 @@ "ignore_above": 1024, "type": "keyword" }, - "group": { + "exit_code": { + "type": "long" + }, + "hash": { "properties": { - "domain": { + "md5": { "ignore_above": 1024, "type": "keyword" }, - "id": { + "sha1": { "ignore_above": 1024, "type": "keyword" }, - "name": { + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { "ignore_above": 1024, "type": "keyword" } } }, - "hash": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, "ignore_above": 1024, "type": "keyword" }, - "id": { + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, "ignore_above": 1024, "type": "keyword" }, - "name": { + "uptime": { + "type": "long" + }, + "working_directory": { "fields": { "text": { "norms": false, @@ -3436,272 +1141,344 @@ "type": "keyword" } } - } - } - }, - "tags": { - "ignore_above": 1024, - "type": "keyword" - }, - "target": { - "properties": { - "process": { + }, + "pe": { "properties": { - "args": { + "company": { "ignore_above": 1024, "type": "keyword" }, - "args_count": { - "type": "long" + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" }, - "argv_list": { + "original_file_name": { "ignore_above": 1024, "type": "keyword" }, - "authenticode": { + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "phys_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "session_id": { + "type": "long" + }, + "short_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { "properties": { - "cert_signer": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" }, - "cert_timestamp": { + "memory_section": { "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { + "memory_address": { "ignore_above": 1024, "type": "keyword" }, - "subject_name": { + "memory_size": { "ignore_above": 1024, "type": "keyword" }, - "timestamp_string": { + "protection": { "ignore_above": 1024, "type": "keyword" } } }, - "more_info_link": { + "module_path": { "ignore_above": 1024, "type": "keyword" }, - "program_name": { + "rva": { "ignore_above": 1024, "type": "keyword" }, - "publisher_link": { + "symbol_info": { "ignore_above": 1024, "type": "keyword" } } }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, + "entrypoint": { "ignore_above": 1024, "type": "keyword" }, - "cpu_percent": { - "type": "double" + "id": { + "type": "long" }, - "cwd": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { "ignore_above": 1024, "type": "keyword" }, - "defense_evasions": { + "token": { "properties": { - "call_stack": { - "properties": { - "instruction_pointer": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_section": { - "properties": { - "memory_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_size": { - "ignore_above": 1024, - "type": "keyword" - }, - "protection": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "module_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "rva": { - "ignore_above": 1024, - "type": "keyword" - }, - "symbol_info": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "delta_count": { + "domain": { "ignore_above": 1024, "type": "keyword" }, - "evasion_subtype": { + "impersonation_level": { "ignore_above": 1024, "type": "keyword" }, - "evasion_type": { - "ignore_above": 1024, - "type": "keyword" + "integrity_level": { + "type": "long" }, - "instruction_pointer": { + "integrity_level_name": { "ignore_above": 1024, "type": "keyword" }, - "memory_sections": { + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { "properties": { - "memory_address": { + "description": { "ignore_above": 1024, "type": "keyword" }, - "memory_size": { - "ignore_above": 1024, - "type": "keyword" + "enabled": { + "type": "boolean" }, - "protection": { + "name": { "ignore_above": 1024, "type": "keyword" } }, "type": "nested" }, - "module_path": { + "sid": { "ignore_above": 1024, "type": "keyword" }, - "thread": { - "properties": { - "thread_id": { - "type": "long" - }, - "thread_start_address": { - "ignore_above": 1024, - "type": "keyword" - } - } + "type": { + "ignore_above": 1024, + "type": "keyword" }, - "total_memory_size": { + "user": { "ignore_above": 1024, "type": "keyword" } - }, - "type": "nested" + } }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { "domain": { "ignore_above": 1024, "type": "keyword" }, - "env_variables": { + "impersonation_level": { "ignore_above": 1024, "type": "keyword" }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { "ignore_above": 1024, "type": "keyword" }, - "exit_code": { - "type": "long" + "is_appcontainer": { + "type": "boolean" }, - "file_hash": { + "privileges": { "properties": { - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { + "description": { "ignore_above": 1024, "type": "keyword" }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" + "enabled": { + "type": "boolean" }, - "sha512": { + "name": { "ignore_above": 1024, "type": "keyword" } - } + }, + "type": "nested" }, - "gid": { - "type": "long" + "sid": { + "ignore_above": 1024, + "type": "keyword" }, - "group": { + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { "ignore_above": 1024, "type": "keyword" + } + } + }, + "tty_device": { + "properties": { + "major_number": { + "type": "integer" + }, + "minor_number": { + "type": "integer" }, - "handle": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "virt_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "target": { + "properties": { + "dll": { + "properties": { + "code_signature": { "properties": { - "handle_id": { - "type": "long" + "exists": { + "type": "boolean" }, - "handle_name": { + "status": { "ignore_above": 1024, "type": "keyword" }, - "handle_type": { + "subject_name": { "ignore_above": 1024, "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" } - }, - "type": "nested" + } }, - "has_unbacked_execute_memory": { - "type": "boolean" + "compile_time": { + "type": "date" }, "hash": { "properties": { - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, "md5": { "ignore_above": 1024, "type": "keyword" @@ -3720,26 +1497,24 @@ } } }, - "hash_matched_module": { - "type": "boolean" - }, - "is_endpoint": { - "type": "boolean" - }, - "malware_classification": { + "malware_classifier": { "properties": { - "compressed_malware_features": { + "features": { "properties": { - "data_buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } } } }, @@ -3747,9 +1522,6 @@ "ignore_above": 1024, "type": "keyword" }, - "prevention_threshold": { - "type": "double" - }, "score": { "type": "double" }, @@ -3765,176 +1537,166 @@ } } }, - "memory_percent": { - "type": "double" + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" }, - "memory_region": { + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { "properties": { - "allocation_base": { + "company": { "ignore_above": 1024, "type": "keyword" }, - "allocation_protection": { + "description": { "ignore_above": 1024, "type": "keyword" }, - "bytes": { + "file_version": { "ignore_above": 1024, "type": "keyword" }, - "histogram": { - "properties": { - "histogram_array": { - "ignore_above": 1024, - "type": "keyword" - }, - "histogram_flavor": { - "ignore_above": 1024, - "type": "keyword" - }, - "histogram_resolution": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "length": { + "original_file_name": { "ignore_above": 1024, "type": "keyword" }, - "memory": { + "product": { "ignore_above": 1024, "type": "keyword" + } + } + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" }, - "memory_address": { + "status": { "ignore_above": 1024, "type": "keyword" }, - "module_path": { + "subject_name": { "ignore_above": 1024, "type": "keyword" }, - "permission": { + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_percent": { + "type": "double" + }, + "cwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "env_variables": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "handles": { + "properties": { + "id": { + "type": "long" + }, + "name": { "ignore_above": 1024, "type": "keyword" }, - "protection": { + "type": { "ignore_above": 1024, "type": "keyword" - }, - "region_base": { + } + }, + "type": "nested" + }, + "hash": { + "properties": { + "md5": { "ignore_above": 1024, "type": "keyword" }, - "region_size": { + "sha1": { "ignore_above": 1024, "type": "keyword" }, - "region_tag": { + "sha256": { "ignore_above": 1024, "type": "keyword" }, - "type": { + "sha512": { "ignore_above": 1024, "type": "keyword" - }, - "unbacked_on_disk": { - "type": "boolean" } - }, - "type": "nested" + } }, - "modules": { + "malware_classifier": { "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "authenticode": { - "properties": { - "cert_signer": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "cert_timestamp": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "more_info_link": { - "ignore_above": 1024, - "type": "keyword" - }, - "program_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "publisher_link": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "compile_time": { - "type": "date" - }, - "hash": { - "properties": { - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "malware_classification": { + "features": { "properties": { - "compressed_malware_features": { + "data": { "properties": { - "data_buffer": { + "buffer": { "ignore_above": 1024, "type": "keyword" }, @@ -3946,72 +1708,104 @@ "type": "keyword" } } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "prevention_threshold": { - "type": "double" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" } } }, - "mapped_address": { + "identifier": { "ignore_above": 1024, "type": "keyword" }, - "mapped_size": { - "type": "long" + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "memory_percent": { + "type": "double" + }, + "memory_region": { + "properties": { + "allocation_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "allocation_protection": { + "ignore_above": 1024, + "type": "keyword" }, - "path": { + "bytes": { "ignore_above": 1024, "type": "keyword" }, - "pe_exports": { + "histogram": { "properties": { - "name": { + "histogram_array": { "ignore_above": 1024, "type": "keyword" }, - "ordinal": { - "type": "long" - } - }, - "type": "nested" - }, - "pe_imports": { - "properties": { - "dll_name": { + "histogram_flavor": { "ignore_above": 1024, "type": "keyword" }, - "import_names": { + "histogram_resolution": { "ignore_above": 1024, "type": "keyword" } }, "type": "nested" }, - "signature_signer": { + "length": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "permission": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_tag": { "ignore_above": 1024, "type": "keyword" }, - "signature_status": { + "type": { "ignore_above": 1024, "type": "keyword" + }, + "unbacked_on_disk": { + "type": "boolean" } }, "type": "nested" @@ -4038,6 +1832,27 @@ "args_count": { "type": "long" }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, "command_line": { "fields": { "text": { @@ -4048,20 +1863,11 @@ "ignore_above": 1024, "type": "keyword" }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, + "entity_id": { "ignore_above": 1024, "type": "keyword" }, - "exit_code": { - "type": "long" - }, - "name": { + "executable": { "fields": { "text": { "norms": false, @@ -4071,30 +1877,30 @@ "ignore_above": 1024, "type": "keyword" }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { + "exit_code": { "type": "long" }, - "start": { - "type": "date" - }, - "thread": { + "hash": { "properties": { - "id": { - "type": "long" + "md5": { + "ignore_above": 1024, + "type": "keyword" }, - "name": { + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { "ignore_above": 1024, "type": "keyword" } } }, - "title": { + "name": { "fields": { "text": { "norms": false, @@ -4104,236 +1910,97 @@ "ignore_above": 1024, "type": "keyword" }, - "uptime": { + "pgid": { "type": "long" }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe_info": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "authenticode": { - "properties": { - "cert_signer": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "cert_timestamp": { - "properties": { - "issuer_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timestamp_string": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "more_info_link": { - "ignore_above": 1024, - "type": "keyword" - }, - "program_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "publisher_link": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "compile_time": { + "pid": { "type": "long" }, - "entry_point_address": { + "ppid": { "type": "long" }, - "is_dll": { - "type": "boolean" - }, - "malware_classification": { - "properties": { - "compressed_malware_features": { - "properties": { - "data_buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "prevention_threshold": { - "type": "double" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe_exports": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "ordinal": { - "type": "long" - } - }, - "type": "nested" - }, - "pe_imports": { - "properties": { - "dll_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "import_names": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" + "start": { + "type": "date" }, - "resources": { - "properties": { - "resource_data": { - "properties": { - "entropy": { - "type": "double" - }, - "size": { - "type": "long" - } - } - }, - "resource_id": { - "type": "long" - }, - "resource_name": { + "thread": { + "properties": { + "entrypoint": { "ignore_above": 1024, "type": "keyword" }, - "resource_type": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "sections": { - "properties": { - "entropy": { - "type": "double" + "id": { + "type": "long" }, "name": { "ignore_above": 1024, "type": "keyword" }, - "raw_offset": { + "service": { "ignore_above": 1024, "type": "keyword" }, - "raw_size": { - "ignore_above": 1024, - "type": "keyword" + "start": { + "type": "date" }, - "virtual_address": { + "start_address": { "ignore_above": 1024, "type": "keyword" }, - "virtual_size": { + "start_address_module": { "ignore_above": 1024, "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" } }, - "type": "nested" + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" }, - "signature_signer": { + "description": { "ignore_above": 1024, "type": "keyword" }, - "signature_status": { + "file_version": { "ignore_above": 1024, "type": "keyword" }, - "version_info": { - "properties": { - "code_page": { - "type": "long" - }, - "key": { - "ignore_above": 1024, - "type": "keyword" - }, - "language": { - "type": "long" - }, - "value_string": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" } } }, @@ -4365,30 +2032,47 @@ "ignore_above": 1024, "type": "keyword" }, - "signature_signer": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_status": { - "ignore_above": 1024, - "type": "keyword" - }, "start": { "type": "date" }, "thread": { "properties": { - "id": { - "type": "long" + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "threads": { - "properties": { "entrypoint": { "ignore_above": 1024, "type": "keyword" @@ -4396,185 +2080,87 @@ "id": { "type": "long" }, - "start": { - "type": "date" - }, - "uptime": { - "type": "long" - } - }, - "type": "nested" - }, - "title": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "token": { - "properties": { - "domain": { + "name": { "ignore_above": 1024, "type": "keyword" }, - "impersonation_level": { + "service": { "ignore_above": 1024, "type": "keyword" }, - "integrity_level": { - "type": "long" + "start": { + "type": "date" }, - "integrity_level_name": { + "start_address": { "ignore_above": 1024, "type": "keyword" }, - "is_appcontainer": { - "type": "boolean" + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" }, - "privileges": { + "token": { "properties": { - "description": { + "domain": { "ignore_above": 1024, "type": "keyword" }, - "enabled": { - "type": "boolean" + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" }, - "name": { + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { "ignore_above": 1024, "type": "keyword" - } - }, - "type": "nested" - }, - "sid": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "tty_device_major_number": { - "type": "integer" - }, - "tty_device_minor_number": { - "type": "integer" - }, - "tty_device_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "type": "long" - }, - "unbacked_execute_byte_count": { - "ignore_above": 1024, - "type": "keyword" - }, - "unbacked_execute_region_count": { - "ignore_above": 1024, - "type": "keyword" - }, - "unique_pid": { - "ignore_above": 1024, - "type": "keyword" - }, - "unique_ppid": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - }, - "virt_memory_bytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "thread": { - "properties": { - "call_stack": { - "properties": { - "instruction_pointer": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_section": { - "properties": { - "memory_address": { + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { "ignore_above": 1024, "type": "keyword" }, - "memory_size": { + "type": { "ignore_above": 1024, "type": "keyword" }, - "protection": { + "user": { "ignore_above": 1024, "type": "keyword" } } }, - "module_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "rva": { - "ignore_above": 1024, - "type": "keyword" - }, - "symbol_info": { - "ignore_above": 1024, - "type": "keyword" + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" } }, - "type": "nested" - }, - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "service_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "start": { - "type": "date" - }, - "start_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "start_address_module": { "ignore_above": 1024, "type": "keyword" }, @@ -4627,430 +2213,89 @@ "type": "keyword" } } - } - } - } - } - }, - "thread": { - "properties": { - "call_stack": { - "properties": { - "instruction_pointer": { - "ignore_above": 1024, - "type": "keyword" }, - "memory_section": { + "tty_device": { "properties": { - "memory_address": { - "ignore_above": 1024, - "type": "keyword" + "major_number": { + "type": "integer" }, - "memory_size": { - "ignore_above": 1024, - "type": "keyword" + "minor_number": { + "type": "integer" }, - "protection": { + "name": { "ignore_above": 1024, "type": "keyword" } } }, - "module_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "rva": { - "ignore_above": 1024, - "type": "keyword" - }, - "symbol_info": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "service_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "start": { - "type": "date" - }, - "start_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "start_address_module": { - "ignore_above": 1024, - "type": "keyword" - }, - "token": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "impersonation_level": { - "ignore_above": 1024, - "type": "keyword" - }, - "integrity_level": { + "uptime": { "type": "long" }, - "integrity_level_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "is_appcontainer": { - "type": "boolean" - }, - "privileges": { - "properties": { - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "sid": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, "user": { "ignore_above": 1024, "type": "keyword" - } - } - } - } - }, - "threat": { - "properties": { - "framework": { - "ignore_above": 1024, - "type": "keyword" - }, - "tactic": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "technique": { - "properties": { - "id": { + "virt_memory_bytes": { "ignore_above": 1024, "type": "keyword" }, - "name": { + "working_directory": { "fields": { "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "tls": { - "properties": { - "cipher": { - "ignore_above": 1024, - "type": "keyword" - }, - "client": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "server_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - }, - "supported_ciphers": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "established": { - "type": "boolean" - }, - "next_protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "resumed": { - "type": "boolean" - }, - "server": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3s": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "subject": { + "norms": false, + "type": "text" + } + }, "ignore_above": 1024, "type": "keyword" } } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_protocol": { - "ignore_above": 1024, - "type": "keyword" } } }, - "token": { + "threat": { "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "impersonation_level": { - "ignore_above": 1024, - "type": "keyword" - }, - "integrity_level": { - "type": "long" - }, - "integrity_level_name": { + "framework": { "ignore_above": 1024, "type": "keyword" }, - "is_appcontainer": { - "type": "boolean" - }, - "privileges": { + "tactic": { "properties": { - "description": { + "id": { "ignore_above": 1024, "type": "keyword" }, - "enabled": { - "type": "boolean" - }, "name": { "ignore_above": 1024, "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" } - }, - "type": "nested" - }, - "sid": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "trace": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "transaction": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "url": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "fragment": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" + } }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "password": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "query": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "scheme": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "username": { - "ignore_above": 1024, - "type": "keyword" + } } } }, @@ -5109,143 +2354,6 @@ "type": "keyword" } } - }, - "user_agent": { - "properties": { - "device": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vulnerability": { - "properties": { - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "classification": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "enumeration": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "report_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "scanner": { - "properties": { - "vendor": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "score": { - "properties": { - "base": { - "type": "float" - }, - "environmental": { - "type": "float" - }, - "temporal": { - "type": "float" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "severity": { - "ignore_above": 1024, - "type": "keyword" - } - } } } }, @@ -5258,8 +2366,8 @@ }, "number_of_replicas": "1", "number_of_shards": "1", - "refresh_interval": "1s" + "refresh_interval": "5s" } } } -} +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json deleted file mode 100644 index 6a7911b5be61..000000000000 --- a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json +++ /dev/null @@ -1,382 +0,0 @@ -{ - "type": "doc", - "value": { - "id": "3KVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent", - "source": { - "@timestamp": 1579881969541, - "agent": { - "id": "963b081e-60d1-482c-befd-a5815fa8290f", - "version": "6.6.1", - "name" : "Elastic Endpoint" - }, - "endpoint": { - "policy": { - "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" - } - }, - "event": { - "created": "2020-01-24T16:06:09.541Z" - }, - "host": { - "architecture": "x86", - "hostname": "cadmann-4.example.com", - "id": "1fb3e58f-6ab0-4406-9d2a-91911207a712", - "ip": [ - "10.192.213.130", - "10.70.28.129" - ], - "mac": [ - "a9-71-6a-cc-93-85", - "f7-31-84-d3-21-68", - "2-95-12-39-ca-71" - ], - "os": { - "full": "Windows 10", - "name": "windows 10.0", - "version": "10.0", - "variant" : "Windows Pro" - } - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "3aVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent", - "source": { - "@timestamp": 1579881969541, - "agent": { - "id": "b3412d6f-b022-4448-8fee-21cc936ea86b", - "version": "6.0.0", - "name" : "Elastic Endpoint" - }, - "endpoint": { - "policy": { - "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" - } - }, - "event": { - "created": "2020-01-24T16:06:09.541Z" - }, - "host": { - "architecture": "x86_64", - "hostname": "thurlow-9.example.com", - "id": "2f735e3d-be14-483b-9822-bad06e9045ca", - "ip": [ - "10.46.229.234" - ], - "mac": [ - "30-8c-45-55-69-b8", - "e5-36-7e-8f-a3-84", - "39-a1-37-20-18-74" - ], - "os": { - "full": "Windows Server 2016", - "name": "windows 10.0", - "version": "10.0", - "variant" : "Windows Server" - } - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "3qVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent", - "source": { - "@timestamp": 1579881969541, - "agent": { - "id": "3838df35-a095-4af4-8fce-0b6d78793f2e", - "version": "6.8.0", - "name" : "Elastic Endpoint" - }, - "endpoint": { - "policy": { - "id": "00000000-0000-0000-0000-000000000000" - } - }, - "event": { - "created": "2020-01-24T16:06:09.541Z" - }, - "host": { - "hostname": "rezzani-7.example.com", - "id": "fc0ff548-feba-41b6-8367-65e8790d0eaf", - "ip": [ - "10.101.149.26", - "2606:a000:ffc0:39:11ef:37b9:3371:578c" - ], - "mac": [ - "e2-6d-f9-0-46-2e" - ], - "os": { - "full": "Windows 10", - "name": "windows 10.0", - "version": "10.0", - "variant" : "Windows Pro" - } - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "36VN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent", - "source": { - "@timestamp": 1579878369541, - "agent": { - "id": "963b081e-60d1-482c-befd-a5815fa8290f", - "version": "6.6.1", - "name" : "Elastic Endpoint" - }, - "endpoint": { - "policy": { - "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" - } - }, - "event": { - "created": "2020-01-24T15:06:09.541Z" - }, - "host": { - "architecture": "x86", - "hostname": "cadmann-4.example.com", - "id": "1fb3e58f-6ab0-4406-9d2a-91911207a712", - "ip": [ - "10.192.213.130", - "10.70.28.129" - ], - "mac": [ - "a9-71-6a-cc-93-85", - "f7-31-84-d3-21-68", - "2-95-12-39-ca-71" - ], - "os": { - "full": "Windows Server 2016", - "name": "windows 10.0", - "version": "10.0", - "variant" : "Windows Server 2016" - } - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "4KVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent", - "source": { - "@timestamp": 1579878369541, - "agent": { - "id": "b3412d6f-b022-4448-8fee-21cc936ea86b", - "version": "6.0.0", - "name" : "Elastic Endpoint" - }, - "endpoint": { - "policy": { - "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" - } - }, - "event": { - "created": "2020-01-24T15:06:09.541Z" - }, - "host": { - "hostname": "thurlow-9.example.com", - "id": "2f735e3d-be14-483b-9822-bad06e9045ca", - "ip": [ - "10.46.229.234" - ], - "mac": [ - "30-8c-45-55-69-b8", - "e5-36-7e-8f-a3-84", - "39-a1-37-20-18-74" - ], - "os": { - "full": "Windows Server 2012", - "name": "windows 6.2", - "version": "6.2", - "variant" : "Windows Server 2012" - } - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "4aVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent", - "source": { - "@timestamp": 1579878369541, - "agent": { - "id": "3838df35-a095-4af4-8fce-0b6d78793f2e", - "version": "6.8.0", - "name" : "Elastic Endpoint" - }, - "endpoint": { - "policy": { - "id": "00000000-0000-0000-0000-000000000000" - } - }, - "event": { - "created": "2020-01-24T15:06:09.541Z" - }, - "host": { - "architecture": "x86", - "hostname": "rezzani-7.example.com", - "id": "fc0ff548-feba-41b6-8367-65e8790d0eaf", - "ip": [ - "10.101.149.26", - "2606:a000:ffc0:39:11ef:37b9:3371:578c" - ], - "mac": [ - "e2-6d-f9-0-46-2e" - ], - "os": { - "full": "Windows Server 2012", - "name": "windows 6.2", - "version": "6.2", - "variant" : "Windows Server 2012" - } - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "4qVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent", - "source": { - "@timestamp": 1579874769541, - "agent": { - "id": "963b081e-60d1-482c-befd-a5815fa8290f", - "version": "6.6.1", - "name" : "Elastic Endpoint" - }, - "endpoint": { - "policy": { - "id": "00000000-0000-0000-0000-000000000000" - } - }, - "event": { - "created": "2020-01-24T14:06:09.541Z" - }, - "host": { - "hostname": "cadmann-4.example.com", - "id": "1fb3e58f-6ab0-4406-9d2a-91911207a712", - "ip": [ - "10.192.213.130", - "10.70.28.129" - ], - "mac": [ - "a9-71-6a-cc-93-85", - "f7-31-84-d3-21-68", - "2-95-12-39-ca-71" - ], - "os": { - "full": "Windows Server 2012R2", - "name": "windows 6.3", - "version": "6.3", - "variant" : "Windows Server 2012 R2" - } - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "46VN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent", - "source": { - "@timestamp": 1579874769541, - "agent": { - "id": "b3412d6f-b022-4448-8fee-21cc936ea86b", - "version": "6.0.0", - "name" : "Elastic Endpoint" - }, - "endpoint": { - "policy": { - "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" - } - }, - "event": { - "created": "2020-01-24T14:06:09.541Z" - }, - "host": { - "hostname": "thurlow-9.example.com", - "id": "2f735e3d-be14-483b-9822-bad06e9045ca", - "ip": [ - "10.46.229.234" - ], - "mac": [ - "30-8c-45-55-69-b8", - "e5-36-7e-8f-a3-84", - "39-a1-37-20-18-74" - ], - "os": { - "full": "Windows Server 2012R2", - "name": "windows 6.3", - "version": "6.3", - "variant" : "Windows Server 2012 R2" - } - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "5KVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent", - "source": { - "@timestamp": 1579874769541, - "agent": { - "id": "3838df35-a095-4af4-8fce-0b6d78793f2e", - "version": "6.8.0", - "name" : "Elastic Endpoint" - }, - "endpoint": { - "policy": { - "id": "00000000-0000-0000-0000-000000000000" - } - }, - "event": { - "created": "2020-01-24T14:06:09.541Z" - }, - "host": { - "architecture": "x86", - "hostname": "rezzani-7.example.com", - "id": "fc0ff548-feba-41b6-8367-65e8790d0eaf", - "ip": [ - "10.101.149.26", - "2606:a000:ffc0:39:11ef:37b9:3371:578c" - ], - "mac": [ - "e2-6d-f9-0-46-2e" - ], - "os": { - "full": "Windows Server 2012", - "name": "windows 6.2", - "version": "6.2", - "variant" : "Windows Server 2012" - } - } - } - } -} diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json.gz b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..94a96c54ee9cb66afbd74d89be6a774720c8c94e GIT binary patch literal 732 zcmV<20wet&iwFP!000021KpKNPuoBcfbaPgQO=CDJNxi%Pc@W^1FBjf6;)La`%K&> zag-NO#DDKPkdW8`B1A7pu@%qG?9S`kx$Shjz4^@~^geWZzH+^zTCg<3MJ>5aAL+C7 z_3F*}mk~KDj{T=xT%P|v|M4w;U#Zs`V;`<-r7-rBsvOV3^h0{nwZg1gOx@#_N%C=C zj>2qiMw29^LUF=5V}c8Pv{qT|v1PMyC757&upXju0l32)EGH6_betMPaz$+K=+SB! zrn9md*QPRKneII--EEFq`@MVGq#Bm)W=FR1;RFj!G&_Y;R2ujRDWE7g2X(@*;FNzZ z@?y(}WH&aWRhlo$`Jj8cOtF>Yes803*LG^zir-CxnFrrYS<7@#%^H@qWbMIO`VYpA z;$Y;&pT%~n?t^pOdg^Qhn(G|64-_;y1mr4bRx_a#*|DmN_;Wj0N-|2)lama2zMF`8 zjj{|$dUR_^u}sm7a`~8=Ut33wjBB1HQV(dL9D;^a3rqG}RtZlOBZ+qMjZzp^3*)kBjhFol8_Nv=_l!wnR;Jzrj)&g&Nv-nH8-&`Z8`%r5rt+D zgP_1oonWLXOqDJ^W*kLw0G(Q8HJr2sAPP+EW>U7PMU^2F8B+(d$yss`tb>Rk4VVV+ zf`bHM(`5wSL*TC^my+j!P0o`SiTygQp2T&v!5i}GTD%S}{sXUCz07OwqK>&?fXc{p z+0y{lash-0V^Bq^Jb8xKcQ#E^%3i~(yZ$Ts!&I2bo@r`>B7`uITGkgLiK*WWg{Zk> zXgLpByvQq88BKE$5%&z!R0mAI_6oszo-%SUuPW3yS`BDG3Gk}HD*;VXj{v4oTSNUm OM(sbt^Zzdg3jhEPfL<^F literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json index d6647e62b019..61ddf3c4e65d 100644 --- a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json +++ b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json @@ -3,7 +3,7 @@ "value": { "aliases": { }, - "index": "endpoint-agent", + "index": "endpoint-agent-1", "mappings": { "properties": { "@timestamp": { @@ -28,15 +28,6 @@ } }, "type": "text" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" } } }, @@ -52,6 +43,15 @@ } }, "type": "text" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } } @@ -60,21 +60,12 @@ "event": { "properties": { "created": { - "type": "date" + "type": "long" } } }, "host": { "properties": { - "architecture": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, "hostname": { "fields": { "keyword": { @@ -162,4 +153,4 @@ } } } -} +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/endpoint_alerts_page.ts b/x-pack/test/functional/page_objects/endpoint_alerts_page.ts index d04a2d5ac2f2..a5ad45536de8 100644 --- a/x-pack/test/functional/page_objects/endpoint_alerts_page.ts +++ b/x-pack/test/functional/page_objects/endpoint_alerts_page.ts @@ -10,11 +10,18 @@ export function EndpointAlertsPageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); return { - async enterSearchBarQuery() { - return await testSubjects.setValue('alertsSearchBar', 'test query'); + async enterSearchBarQuery(query: string) { + return await testSubjects.setValue('alertsSearchBar', query, { clearWithKeyboard: true }); }, async submitSearchBarFilter() { return await testSubjects.click('querySubmitButton'); }, + async setSearchBarDate(timestamp: string) { + await testSubjects.click('superDatePickerShowDatesButton'); + await testSubjects.click('superDatePickerstartDatePopoverButton'); + await testSubjects.click('superDatePickerAbsoluteTab'); + await testSubjects.setValue('superDatePickerAbsoluteDateInput', timestamp); + await this.submitSearchBarFilter(); + }, }; } From b9cc3e940cb8b21ca32783e249d47da2eeb37afa Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 17 Mar 2020 07:38:39 -0700 Subject: [PATCH 043/115] skip flaky test (#60369) --- .../siem/cypress/integration/timeline_flyout_button.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts index 736eee421a30..02da7cbc2846 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts @@ -29,7 +29,8 @@ describe('timeline flyout button', () => { cy.get(TIMELINE_FLYOUT_HEADER).should('have.css', 'visibility', 'visible'); }); - it('sets the flyout button background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the flyout button', () => { + // FLAKY: https://github.com/elastic/kibana/issues/60369 + it.skip('sets the flyout button background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the flyout button', () => { dragFirstHostToTimeline(); cy.get(TIMELINE_NOT_READY_TO_DROP_BUTTON).should( From 0f9f81c30affd6e79f00f9edca03a8abf0842ce5 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 17 Mar 2020 10:46:54 -0400 Subject: [PATCH 044/115] [SIEM] Fix link on overview page (#60348) * Fix link on overview page * no needs of useMemo * clean up * review I * review II * review III --- .../public/components/link_to/helpers.test.ts | 19 ++++++++++ .../siem/public/components/link_to/helpers.ts | 7 ++++ .../link_to/redirect_to_detection_engine.tsx | 8 ++-- .../components/link_to/redirect_to_hosts.tsx | 9 +++-- .../link_to/redirect_to_network.tsx | 6 ++- .../link_to/redirect_to_timelines.tsx | 8 +++- .../public/components/navigation/helpers.ts | 7 ++-- .../navigation/tab_navigation/index.tsx | 4 +- .../navigation/use_get_url_search.tsx | 20 ++++++++++ .../page/overview/overview_host/index.tsx | 21 ++++++---- .../page/overview/overview_network/index.tsx | 21 ++++++---- .../components/recent_timelines/index.tsx | 14 +++++-- .../public/components/url_state/helpers.ts | 38 +------------------ .../signals_histogram_panel/index.tsx | 33 ++++++++++------ .../overview/alerts_by_category/index.tsx | 7 +++- .../overview/events_by_dataset/index.tsx | 11 +++++- 16 files changed, 146 insertions(+), 87 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/link_to/helpers.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/link_to/helpers.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/navigation/use_get_url_search.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/link_to/helpers.test.ts new file mode 100644 index 000000000000..14b367de674a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/link_to/helpers.test.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { appendSearch } from './helpers'; + +describe('appendSearch', () => { + test('should return empty string if no parameter', () => { + expect(appendSearch()).toEqual(''); + }); + test('should return empty string if parameter is undefined', () => { + expect(appendSearch(undefined)).toEqual(''); + }); + test('should return parameter if parameter is defined', () => { + expect(appendSearch('helloWorld')).toEqual('helloWorld'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/helpers.ts b/x-pack/legacy/plugins/siem/public/components/link_to/helpers.ts new file mode 100644 index 000000000000..9d818ab3b647 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/link_to/helpers.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 const appendSearch = (search?: string) => (search != null ? `${search}` : ''); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx index 3701069389b7..18111aa93a27 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { DetectionEngineTab } from '../../pages/detection_engine/types'; +import { appendSearch } from './helpers'; import { RedirectWrapper } from './redirect_wrapper'; export type DetectionEngineComponentProps = RouteComponentProps<{ @@ -63,9 +64,10 @@ export const RedirectToEditRulePage = ({ const baseDetectionEngineUrl = `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`; -export const getDetectionEngineUrl = () => `${baseDetectionEngineUrl}`; -export const getDetectionEngineAlertUrl = () => - `${baseDetectionEngineUrl}/${DetectionEngineTab.alerts}`; +export const getDetectionEngineUrl = (search?: string) => + `${baseDetectionEngineUrl}${appendSearch(search)}`; +export const getDetectionEngineAlertUrl = (search?: string) => + `${baseDetectionEngineUrl}/${DetectionEngineTab.alerts}${appendSearch(search)}`; export const getDetectionEngineTabUrl = (tabPath: string) => `${baseDetectionEngineUrl}/${tabPath}`; export const getRulesUrl = () => `${baseDetectionEngineUrl}/rules`; export const getCreateRuleUrl = () => `${baseDetectionEngineUrl}/rules/create`; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx index 05139320b171..746a959cc996 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx @@ -7,10 +7,12 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { RedirectWrapper } from './redirect_wrapper'; import { HostsTableType } from '../../store/hosts/model'; import { SiemPageName } from '../../pages/home/types'; +import { appendSearch } from './helpers'; +import { RedirectWrapper } from './redirect_wrapper'; + export type HostComponentProps = RouteComponentProps<{ detailName: string; tabName: HostsTableType; @@ -44,9 +46,10 @@ export const RedirectToHostDetailsPage = ({ const baseHostsUrl = `#/link-to/${SiemPageName.hosts}`; -export const getHostsUrl = () => baseHostsUrl; +export const getHostsUrl = (search?: string) => `${baseHostsUrl}${appendSearch(search)}`; -export const getTabsOnHostsUrl = (tabName: HostsTableType) => `${baseHostsUrl}/${tabName}`; +export const getTabsOnHostsUrl = (tabName: HostsTableType, search?: string) => + `${baseHostsUrl}/${tabName}${appendSearch(search)}`; export const getHostDetailsUrl = (detailName: string) => `${baseHostsUrl}/${detailName}`; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx index f206e2f323a7..71925edd5c08 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx @@ -7,10 +7,12 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { RedirectWrapper } from './redirect_wrapper'; import { SiemPageName } from '../../pages/home/types'; import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; +import { appendSearch } from './helpers'; +import { RedirectWrapper } from './redirect_wrapper'; + export type NetworkComponentProps = RouteComponentProps<{ detailName?: string; flowTarget?: string; @@ -33,7 +35,7 @@ export const RedirectToNetworkPage = ({ ); const baseNetworkUrl = `#/link-to/${SiemPageName.network}`; -export const getNetworkUrl = () => baseNetworkUrl; +export const getNetworkUrl = (search?: string) => `${baseNetworkUrl}${appendSearch(search)}`; export const getIPDetailsUrl = ( detailName: string, flowTarget?: FlowTarget | FlowTargetSourceDest diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx index 1b71432b3f72..27765a4125af 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx @@ -6,9 +6,12 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { RedirectWrapper } from './redirect_wrapper'; + import { SiemPageName } from '../../pages/home/types'; +import { appendSearch } from './helpers'; +import { RedirectWrapper } from './redirect_wrapper'; + export type TimelineComponentProps = RouteComponentProps<{ search: string; }>; @@ -17,4 +20,5 @@ export const RedirectToTimelinesPage = ({ location: { search } }: TimelineCompon ); -export const getTimelinesUrl = () => `#/link-to/${SiemPageName.timelines}`; +export const getTimelinesUrl = (search?: string) => + `#/link-to/${SiemPageName.timelines}${appendSearch(search)}`; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts index 9a95d93a2df7..899d108fe246 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts @@ -10,7 +10,7 @@ import { Location } from 'history'; import { UrlInputsModel } from '../../store/inputs/model'; import { TimelineUrl } from '../../store/timeline/model'; import { CONSTANTS } from '../url_state/constants'; -import { URL_STATE_KEYS, KeyUrlState } from '../url_state/types'; +import { URL_STATE_KEYS, KeyUrlState, UrlState } from '../url_state/types'; import { replaceQueryStringInLocation, replaceStateKeyInQueryString, @@ -18,10 +18,9 @@ import { } from '../url_state/helpers'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; -import { TabNavigationProps } from './tab_navigation/types'; import { SearchNavTab } from './types'; -export const getSearch = (tab: SearchNavTab, urlState: TabNavigationProps): string => { +export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) { return URL_STATE_KEYS[tab.urlKey].reduce( (myLocation: Location, urlKey: KeyUrlState) => { @@ -58,7 +57,7 @@ export const getSearch = (tab: SearchNavTab, urlState: TabNavigationProps): stri ); }, { - pathname: urlState.pathName, + pathname: '', hash: '', search: '', state: '', diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx index cebf9b90656c..ab4d75a2b116 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx @@ -66,7 +66,9 @@ export const TabNavigationComponent = (props: TabNavigationProps) => { () => Object.values(navTabs).map(tab => { const isSelected = selectedTabId === tab.id; - const hrefWithSearch = tab.href + getSearch(tab, props); + const { query, filters, savedQuery, timerange, timeline } = props; + const hrefWithSearch = + tab.href + getSearch(tab, { query, filters, savedQuery, timerange, timeline }); return ( { + const mapState = makeMapStateToProps(); + const { urlState } = useSelector(mapState, isEqual); + const urlSearch = useMemo(() => getSearch(tab, urlState), [tab, urlState]); + return urlSearch; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx index 3868885fa29e..52c142ceff48 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx @@ -8,7 +8,7 @@ import { isEmpty } from 'lodash/fp'; import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; +import React, { useMemo } from 'react'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; import { ESQuery } from '../../../../../common/typed_json'; @@ -23,6 +23,8 @@ import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats' import { manageQuery } from '../../../page/manage_query'; import { inputsModel } from '../../../../store/inputs'; import { InspectButtonContainer } from '../../../inspect'; +import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; +import { navTabs } from '../../../../pages/home/home_navigations'; export interface OwnProps { startDate: number; @@ -51,7 +53,15 @@ const OverviewHostComponent: React.FC = ({ setQuery, }) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - + const urlSearch = useGetUrlSearch(navTabs.hosts); + const hostPageButton = useMemo( + () => ( + + + + ), + [urlSearch] + ); return ( @@ -95,12 +105,7 @@ const OverviewHostComponent: React.FC = ({ /> } > - - - + {hostPageButton} = ({ setQuery, }) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - + const urlSearch = useGetUrlSearch(navTabs.network); + const networkPageButton = useMemo( + () => ( + + + + ), + [urlSearch] + ); return ( @@ -96,12 +106,7 @@ const OverviewNetworkComponent: React.FC = ({ /> } > - - - + {networkPageButton} ; @@ -45,6 +48,11 @@ const StatefulRecentTimelinesComponent = React.memo( const noTimelinesMessage = filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; + const urlSearch = useGetUrlSearch(navTabs.timelines); + const linkAllTimelines = useMemo( + () => {i18n.VIEW_ALL_TIMELINES}, + [urlSearch] + ); return ( ( /> )} - - {i18n.VIEW_ALL_TIMELINES} - + {linkAllTimelines} )} diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index d085af91da1f..b30244e57d0f 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -19,12 +19,7 @@ import { TimelineUrl } from '../../store/timeline/model'; import { formatDate } from '../super_date_picker'; import { NavTab } from '../navigation/types'; import { CONSTANTS, UrlStateType } from './constants'; -import { - LocationTypes, - UrlStateContainerPropTypes, - ReplaceStateInLocation, - UpdateUrlStateString, -} from './types'; +import { ReplaceStateInLocation, UpdateUrlStateString } from './types'; export const decodeRisonUrlState = (value: string | undefined): T | null => { try { @@ -113,42 +108,13 @@ export const getTitle = ( return navTabs[pageName] != null ? navTabs[pageName].name : ''; }; -export const getCurrentLocation = ( - pageName: string, - detailName: string | undefined -): LocationTypes => { - if (pageName === SiemPageName.overview) { - return CONSTANTS.overviewPage; - } else if (pageName === SiemPageName.hosts) { - if (detailName != null) { - return CONSTANTS.hostsDetails; - } - return CONSTANTS.hostsPage; - } else if (pageName === SiemPageName.network) { - if (detailName != null) { - return CONSTANTS.networkDetails; - } - return CONSTANTS.networkPage; - } else if (pageName === SiemPageName.detections) { - return CONSTANTS.detectionsPage; - } else if (pageName === SiemPageName.timelines) { - return CONSTANTS.timelinePage; - } else if (pageName === SiemPageName.case) { - if (detailName != null) { - return CONSTANTS.caseDetails; - } - return CONSTANTS.casePage; - } - return CONSTANTS.unknown; -}; - export const makeMapStateToProps = () => { const getInputsSelector = inputsSelectors.inputsSelector(); const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); const getGlobalSavedQuerySelector = inputsSelectors.globalSavedQuerySelector(); const getTimelines = timelineSelectors.getTimelines(); - const mapStateToProps = (state: State, { pageName, detailName }: UrlStateContainerPropTypes) => { + const mapStateToProps = (state: State) => { const inputState = getInputsSelector(state); const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx index 079293bd4523..e25442b31da4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx @@ -11,19 +11,21 @@ import styled from 'styled-components'; import { isEmpty } from 'lodash/fp'; import { HeaderSection } from '../../../../components/header_section'; -import { SignalsHistogram } from './signals_histogram'; + import { Filter, esQuery, Query } from '../../../../../../../../../src/plugins/data/public'; -import { RegisterQuery, SignalsHistogramOption, SignalsAggregation, SignalsTotal } from './types'; -import { signalsHistogramOptions } from './config'; -import { getDetectionEngineUrl } from '../../../../components/link_to'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; -import { useKibana, useUiSetting$ } from '../../../../lib/kibana'; -import { InspectButtonContainer } from '../../../../components/inspect'; import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query'; +import { getDetectionEngineUrl } from '../../../../components/link_to'; +import { InspectButtonContainer } from '../../../../components/inspect'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; import { MatrixLoader } from '../../../../components/matrix_histogram/matrix_loader'; - +import { useKibana, useUiSetting$ } from '../../../../lib/kibana'; +import { navTabs } from '../../../home/home_navigations'; +import { signalsHistogramOptions } from './config'; import { formatSignalsData, getSignalsHistogramQuery, showInitialLoadingSpinner } from './helpers'; +import { SignalsHistogram } from './signals_histogram'; import * as i18n from './translations'; +import { RegisterQuery, SignalsHistogramOption, SignalsAggregation, SignalsTotal } from './types'; const DEFAULT_PANEL_HEIGHT = 300; @@ -101,6 +103,7 @@ export const SignalsHistogramPanel = memo( signalIndexName ); const kibana = useKibana(); + const urlSearch = useGetUrlSearch(navTabs.detections); const totalSignals = useMemo( () => @@ -184,6 +187,16 @@ export const SignalsHistogramPanel = memo( ); }, [selectedStackByOption.value, from, to, query, filters]); + const linkButton = useMemo(() => { + if (showLinkToSignals) { + return ( + + {i18n.VIEW_SIGNALS} + + ); + } + }, [showLinkToSignals, urlSearch]); + return ( @@ -210,11 +223,7 @@ export const SignalsHistogramPanel = memo( /> )} - {showLinkToSignals && ( - - {i18n.VIEW_SIGNALS} - - )} + {linkButton} diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx index f71d83558ae9..e0d383c59e2e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx @@ -30,6 +30,8 @@ import { histogramConfigs, } from '../../../components/alerts_viewer/histogram_configs'; import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; +import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; +import { navTabs } from '../../home/home_navigations'; const ID = 'alertsByCategoryOverview'; @@ -73,10 +75,11 @@ const AlertsByCategoryComponent: React.FC = ({ const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.detections); const alertsCountViewAlertsButton = useMemo( - () => {i18n.VIEW_ALERTS}, - [] + () => {i18n.VIEW_ALERTS}, + [urlSearch] ); const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx index 315aac5fcae9..cc1f9b1cc568 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx @@ -28,6 +28,8 @@ import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import * as i18n from '../translations'; import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; +import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; +import { navTabs } from '../../home/home_navigations'; const NO_FILTERS: Filter[] = []; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; @@ -69,10 +71,15 @@ const EventsByDatasetComponent: React.FC = ({ const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.hosts); const eventsCountViewEventsButton = useMemo( - () => {i18n.VIEW_EVENTS}, - [] + () => ( + + {i18n.VIEW_EVENTS} + + ), + [urlSearch] ); const filterQuery = useMemo( From ce64895b2e83a18555027ad8c451eb1135c66c10 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 17 Mar 2020 10:53:59 -0400 Subject: [PATCH 045/115] [FTR] Add support for --include and --exclude files via tags (#60123) --- .../src/functional_test_runner/cli.ts | 18 +++++-- .../lib/config/schema.ts | 13 +++-- .../lib/mocha/decorate_mocha_ui.js | 7 ++- .../lib/mocha/load_test_files.js | 27 +--------- .../lib/mocha/setup_mocha.js | 14 +++++- .../run_tests/__snapshots__/args.test.js.snap | 50 +++++++++++++++++++ .../run_tests/__snapshots__/cli.test.js.snap | 2 + .../functional_tests/cli/run_tests/args.js | 15 ++++++ .../src/functional_tests/lib/run_ftr.js | 6 ++- 9 files changed, 117 insertions(+), 35 deletions(-) diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index 11b9450f2af6..3aaaa47ead5b 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -48,12 +48,15 @@ export function runFtrCli() { kbnTestServer: { installDir: parseInstallDir(flags), }, + suiteFiles: { + include: toArray(flags.include as string | string[]).map(makeAbsolutePath), + exclude: toArray(flags.exclude as string | string[]).map(makeAbsolutePath), + }, suiteTags: { include: toArray(flags['include-tag'] as string | string[]), exclude: toArray(flags['exclude-tag'] as string | string[]), }, updateBaselines: flags.updateBaselines, - excludeTestFiles: flags.exclude || undefined, } ); @@ -104,7 +107,15 @@ export function runFtrCli() { }, { flags: { - string: ['config', 'grep', 'exclude', 'include-tag', 'exclude-tag', 'kibana-install-dir'], + string: [ + 'config', + 'grep', + 'include', + 'exclude', + 'include-tag', + 'exclude-tag', + 'kibana-install-dir', + ], boolean: ['bail', 'invert', 'test-stats', 'updateBaselines', 'throttle', 'headless'], default: { config: 'test/functional/config.js', @@ -115,7 +126,8 @@ export function runFtrCli() { --bail stop tests after the first failure --grep pattern used to select which tests to run --invert invert grep to exclude tests - --exclude=file path to a test file that should not be loaded + --include=file a test file to be included, pass multiple times for multiple files + --exclude=file a test file to be excluded, pass multiple times for multiple files --include-tag=tag a tag to be included, pass multiple times for multiple tags --exclude-tag=tag a tag to be excluded, pass multiple times for multiple tags --test-stats print the number of tests (included and excluded) to STDERR diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 75623d6c0889..28e8396d0beb 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -64,9 +64,16 @@ export const schema = Joi.object() testFiles: Joi.array().items(Joi.string()), testRunner: Joi.func(), - excludeTestFiles: Joi.array() - .items(Joi.string()) - .default([]), + suiteFiles: Joi.object() + .keys({ + include: Joi.array() + .items(Joi.string()) + .default([]), + exclude: Joi.array() + .items(Joi.string()) + .default([]), + }) + .default(), suiteTags: Joi.object() .keys({ diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js index 64fc51a04aac..1cac852a7e71 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ - +import { relative } from 'path'; +import { REPO_ROOT } from '@kbn/dev-utils'; import { createAssignmentProxy } from './assignment_proxy'; import { wrapFunction } from './wrap_function'; import { wrapRunnableArgs } from './wrap_runnable_args'; @@ -65,6 +66,10 @@ export function decorateMochaUi(lifecycle, context) { this._tags = [].concat(this._tags || [], tags); }; + const relativeFilePath = relative(REPO_ROOT, this.file); + this.tags(relativeFilePath); + this.suiteTag = relativeFilePath; // The tag that uniquely targets this suite/file + provider.call(this); after(async () => { diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js index 70b0c0874e5e..6ee65b1b7e39 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js @@ -31,28 +31,12 @@ import { decorateMochaUi } from './decorate_mocha_ui'; * @param {String} path * @return {undefined} - mutates mocha, no return value */ -export const loadTestFiles = ({ - mocha, - log, - lifecycle, - providers, - paths, - excludePaths, - updateBaselines, -}) => { - const pendingExcludes = new Set(excludePaths.slice(0)); - +export const loadTestFiles = ({ mocha, log, lifecycle, providers, paths, updateBaselines }) => { const innerLoadTestFile = path => { if (typeof path !== 'string' || !isAbsolute(path)) { throw new TypeError('loadTestFile() only accepts absolute paths'); } - if (pendingExcludes.has(path)) { - pendingExcludes.delete(path); - log.warning('Skipping test file %s', path); - return; - } - loadTracer(path, `testFile[${path}]`, () => { log.verbose('Loading test file %s', path); @@ -94,13 +78,4 @@ export const loadTestFiles = ({ }; paths.forEach(innerLoadTestFile); - - if (pendingExcludes.size) { - throw new Error( - `After loading all test files some exclude paths were not consumed:${[ - '', - ...pendingExcludes, - ].join('\n -')}` - ); - } }; diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js index 326877919d98..61851cece0e8 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js @@ -18,6 +18,8 @@ */ import Mocha from 'mocha'; +import { relative } from 'path'; +import { REPO_ROOT } from '@kbn/dev-utils'; import { loadTestFiles } from './load_test_files'; import { filterSuitesByTags } from './filter_suites_by_tags'; @@ -50,10 +52,20 @@ export async function setupMocha(lifecycle, log, config, providers) { lifecycle, providers, paths: config.get('testFiles'), - excludePaths: config.get('excludeTestFiles'), updateBaselines: config.get('updateBaselines'), }); + // Each suite has a tag that is the path relative to the root of the repo + // So we just need to take input paths, make them relative to the root, and use them as tags + // Also, this is a separate filterSuitesByTags() call so that the test suites will be filtered first by + // files, then by tags. This way, you can target tags (like smoke) in a specific file. + filterSuitesByTags({ + log, + mocha, + include: config.get('suiteFiles.include').map(file => relative(REPO_ROOT, file)), + exclude: config.get('suiteFiles.exclude').map(file => relative(REPO_ROOT, file)), + }); + filterSuitesByTags({ log, mocha, diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap index bbf8b38712ac..434c374d5d23 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap @@ -16,6 +16,8 @@ Options: --bail Stop the test run at the first failure. --grep Pattern to select which tests to run. --updateBaselines Replace baseline screenshots with whatever is generated from the test. + --include Files that must included to be run, can be included multiple times. + --exclude Files that must NOT be included to be run, can be included multiple times. --include-tag Tags that suites must include to be run, can be included multiple times. --exclude-tag Tags that suites must NOT include to be run, can be included multiple times. --assert-none-excluded Exit with 1/0 based on if any test is excluded with the current set of tags. @@ -34,6 +36,10 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -52,6 +58,10 @@ Object { "debug": true, "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -69,6 +79,10 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -90,6 +104,10 @@ Object { "extraKbnOpts": Object { "server.foo": "bar", }, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -107,6 +125,10 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "quiet": true, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -124,6 +146,10 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "silent": true, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -140,6 +166,10 @@ Object { "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -156,6 +186,10 @@ Object { "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -173,6 +207,10 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "installDir": "foo", + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -190,6 +228,10 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "grep": "management", + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -206,6 +248,10 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -223,6 +269,10 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap index b12739b3b5df..6ede71a6c394 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap @@ -16,6 +16,8 @@ Options: --bail Stop the test run at the first failure. --grep Pattern to select which tests to run. --updateBaselines Replace baseline screenshots with whatever is generated from the test. + --include Files that must included to be run, can be included multiple times. + --exclude Files that must NOT be included to be run, can be included multiple times. --include-tag Tags that suites must include to be run, can be included multiple times. --exclude-tag Tags that suites must NOT include to be run, can be included multiple times. --assert-none-excluded Exit with 1/0 based on if any test is excluded with the current set of tags. diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js index b34006a38a45..7d2414305de8 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js @@ -46,6 +46,14 @@ const options = { updateBaselines: { desc: 'Replace baseline screenshots with whatever is generated from the test.', }, + include: { + arg: '', + desc: 'Files that must included to be run, can be included multiple times.', + }, + exclude: { + arg: '', + desc: 'Files that must NOT be included to be run, can be included multiple times.', + }, 'include-tag': { arg: '', desc: 'Tags that suites must include to be run, can be included multiple times.', @@ -115,6 +123,13 @@ export function processOptions(userOptions, defaultConfigPaths) { delete userOptions['kibana-install-dir']; } + userOptions.suiteFiles = { + include: [].concat(userOptions.include || []), + exclude: [].concat(userOptions.exclude || []), + }; + delete userOptions.include; + delete userOptions.exclude; + userOptions.suiteTags = { include: [].concat(userOptions['include-tag'] || []), exclude: [].concat(userOptions['exclude-tag'] || []), diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.js b/packages/kbn-test/src/functional_tests/lib/run_ftr.js index 9b631e33f3b2..14883ac977c4 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.js +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.js @@ -22,7 +22,7 @@ import { CliError } from './run_cli'; async function createFtr({ configPath, - options: { installDir, log, bail, grep, updateBaselines, suiteTags }, + options: { installDir, log, bail, grep, updateBaselines, suiteFiles, suiteTags }, }) { const config = await readConfigFile(log, configPath); @@ -37,6 +37,10 @@ async function createFtr({ installDir, }, updateBaselines, + suiteFiles: { + include: [...suiteFiles.include, ...config.get('suiteFiles.include')], + exclude: [...suiteFiles.exclude, ...config.get('suiteFiles.exclude')], + }, suiteTags: { include: [...suiteTags.include, ...config.get('suiteTags.include')], exclude: [...suiteTags.exclude, ...config.get('suiteTags.exclude')], From b71099d620012e5df2850a69faac7efaf0f26d29 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 17 Mar 2020 08:13:32 -0700 Subject: [PATCH 046/115] skip flaky suite (#58643) (#58991) --- .../security_and_spaces/tests/alerting/alerts.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 70c885bb0a69..6766705f688a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -26,7 +26,9 @@ export default function alertTests({ getService }: FtrProviderContext) { const esTestIndexTool = new ESTestIndexTool(es, retry); const taskManagerUtils = new TaskManagerUtils(es, retry); - describe('alerts', () => { + // FLAKY: https://github.com/elastic/kibana/issues/58643 + // FLAKY: https://github.com/elastic/kibana/issues/58991 + describe.skip('alerts', () => { const authorizationIndex = '.kibana-test-authorization'; const objectRemover = new ObjectRemover(supertest); From 6a70d21ef31bcc93a9a0d512c753a78fb2b54515 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Tue, 17 Mar 2020 16:29:01 +0100 Subject: [PATCH 047/115] [ML] Functional tests - disable df analytics clone tests --- .../apps/machine_learning/data_frame_analytics/cloning.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts index 512de861e673..51155fccc358 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts @@ -12,7 +12,9 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('jobs cloning supported by UI form', function() { + + // failing test, see https://github.com/elastic/kibana/issues/60389 + describe.skip('jobs cloning supported by UI form', function() { this.tags(['smoke']); const testDataList: Array<{ From 9318862f1967d6fefdfca5e94417ff1617e5ba06 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 17 Mar 2020 12:30:17 -0400 Subject: [PATCH 048/115] Allow kbn-config-schema to ignore unknown keys (#59560) * allow kbn-config-schema to ignore unknown keys * Consolidate unknown key configuration * updates following merge Co-authored-by: Elastic Machine --- ...plugin-core-server.routeconfig.validate.md | 4 +- packages/kbn-config-schema/README.md | 4 +- .../src/types/object_type.test.ts | 43 ++++++++++++++++--- .../src/types/object_type.ts | 21 ++++++--- src/core/server/http/router/route.ts | 4 +- src/core/server/http/router/router.test.ts | 2 +- .../server/ui_settings/routes/set_many.ts | 2 +- .../server/ui_settings/ui_settings_config.ts | 2 +- .../autocomplete/value_suggestions_route.ts | 4 +- src/plugins/data/server/search/routes.ts | 6 +-- src/plugins/timelion/config.ts | 2 +- src/plugins/timelion/server/routes/run.ts | 12 ++---- .../vis_type_timeseries/server/routes/vis.ts | 2 +- .../plugins/rendering_plugin/server/plugin.ts | 2 +- .../api/license/register_license_route.ts | 2 +- .../plugins/rollup/server/routes/api/jobs.ts | 2 +- .../signals/signal_params_schema.ts | 6 +-- .../lib/framework/kibana_framework_adapter.ts | 2 +- .../apm/server/routes/create_api/index.ts | 2 +- .../canvas/server/routes/workpad/update.ts | 2 +- .../plugins/case/server/routes/api/utils.ts | 2 +- .../file_upload/server/routes/file_upload.js | 12 +++--- x-pack/plugins/graph/server/routes/explore.ts | 2 +- x-pack/plugins/graph/server/routes/search.ts | 2 +- .../routes/api/templates/validate_schemas.ts | 6 +-- .../framework/kibana_framework_adapter.ts | 2 +- .../server/routes/inventory_metadata/index.ts | 2 +- .../results/log_entry_categories.ts | 2 +- .../results/log_entry_category_datasets.ts | 2 +- .../results/log_entry_category_examples.ts | 2 +- .../log_analysis/results/log_entry_rate.ts | 2 +- .../routes/log_analysis/validation/indices.ts | 2 +- .../server/routes/log_entries/entries.ts | 2 +- .../server/routes/log_entries/highlights.ts | 2 +- .../infra/server/routes/log_entries/item.ts | 2 +- .../server/routes/log_entries/summary.ts | 2 +- .../routes/log_entries/summary_highlights.ts | 2 +- .../infra/server/routes/metadata/index.ts | 2 +- .../server/routes/metrics_explorer/index.ts | 2 +- .../infra/server/routes/node_details/index.ts | 2 +- .../infra/server/routes/snapshot/index.ts | 2 +- .../lens/server/routes/existing_fields.ts | 2 +- .../plugins/lens/server/routes/field_stats.ts | 6 +-- .../searchprofiler/server/routes/profile.ts | 2 +- .../server/routes/authentication/common.ts | 2 +- .../server/routes/authentication/oidc.ts | 6 +-- .../server/routes/role_mapping/post.ts | 4 +- .../security/server/routes/views/login.ts | 2 +- .../server/routes/api/validate_schemas.ts | 10 ++--- .../routes/api/indices/register_get_route.ts | 2 +- .../api/watch/register_execute_route.ts | 4 +- .../routes/api/watch/register_save_route.ts | 2 +- .../api/watch/register_visualize_route.ts | 4 +- 53 files changed, 132 insertions(+), 96 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md b/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md index 204d8a786fed..3bbabc04f250 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md +++ b/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md @@ -14,7 +14,7 @@ validate: RouteValidatorFullConfig | false; ## Remarks -You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `validate: false`. In this case request params, query, and body will be \*\*empty\*\* objects and have no access to raw values. In some cases you may want to use another validation library. To do this, you need to instruct the `@kbn/config-schema` library to output \*\*non-validated values\*\* with setting schema as `schema.object({}, { allowUnknowns: true })`; +You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `validate: false`. In this case request params, query, and body will be \*\*empty\*\* objects and have no access to raw values. In some cases you may want to use another validation library. To do this, you need to instruct the `@kbn/config-schema` library to output \*\*non-validated values\*\* with setting schema as `schema.object({}, { unknowns: 'allow' })`; ## Example @@ -49,7 +49,7 @@ router.get({ path: 'path/{id}', validate: { // handler has access to raw non-validated params in runtime - params: schema.object({}, { allowUnknowns: true }) + params: schema.object({}, { unknowns: 'allow' }) }, }, (context, req, res,) { diff --git a/packages/kbn-config-schema/README.md b/packages/kbn-config-schema/README.md index 8719a2ae558a..a4f2c1f6458c 100644 --- a/packages/kbn-config-schema/README.md +++ b/packages/kbn-config-schema/README.md @@ -239,7 +239,7 @@ __Output type:__ `{ [K in keyof TProps]: TypeOf } as TObject` __Options:__ * `defaultValue: TObject | Reference | (() => TObject)` - defines a default value, see [Default values](#default-values) section for more details. * `validate: (value: TObject) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. - * `allowUnknowns: boolean` - indicates whether unknown object properties should be allowed. It's `false` by default. + * `unknowns: 'allow' | 'ignore' | 'forbid'` - indicates whether unknown object properties should be allowed, ignored, or forbidden. It's `forbid` by default. __Usage:__ ```typescript @@ -250,7 +250,7 @@ const valueSchema = schema.object({ ``` __Notes:__ -* Using `allowUnknowns` is discouraged and should only be used in exceptional circumstances. Consider using `schema.recordOf()` instead. +* Using `unknowns: 'allow'` is discouraged and should only be used in exceptional circumstances. Consider using `schema.recordOf()` instead. * Currently `schema.object()` always has a default value of `{}`, but this may change in the near future. Try to not rely on this behaviour and specify default value explicitly or use `schema.maybe()` if the value is optional. * `schema.object()` also supports a json string as input if it can be safely parsed using `JSON.parse` and if the resulting value is a plain object. diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index 29e341983fde..47a0f5f7a549 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -276,10 +276,10 @@ test('individual keys can validated', () => { ); }); -test('allow unknown keys when allowUnknowns = true', () => { +test('allow unknown keys when unknowns = `allow`', () => { const type = schema.object( { foo: schema.string({ defaultValue: 'test' }) }, - { allowUnknowns: true } + { unknowns: 'allow' } ); expect( @@ -292,10 +292,10 @@ test('allow unknown keys when allowUnknowns = true', () => { }); }); -test('allowUnknowns = true affects only own keys', () => { +test('unknowns = `allow` affects only own keys', () => { const type = schema.object( { foo: schema.object({ bar: schema.string() }) }, - { allowUnknowns: true } + { unknowns: 'allow' } ); expect(() => @@ -308,10 +308,10 @@ test('allowUnknowns = true affects only own keys', () => { ).toThrowErrorMatchingInlineSnapshot(`"[foo.baz]: definition for this key is missing"`); }); -test('does not allow unknown keys when allowUnknowns = false', () => { +test('does not allow unknown keys when unknowns = `forbid`', () => { const type = schema.object( { foo: schema.string({ defaultValue: 'test' }) }, - { allowUnknowns: false } + { unknowns: 'forbid' } ); expect(() => type.validate({ @@ -319,3 +319,34 @@ test('does not allow unknown keys when allowUnknowns = false', () => { }) ).toThrowErrorMatchingInlineSnapshot(`"[bar]: definition for this key is missing"`); }); + +test('allow and remove unknown keys when unknowns = `ignore`', () => { + const type = schema.object( + { foo: schema.string({ defaultValue: 'test' }) }, + { unknowns: 'ignore' } + ); + + expect( + type.validate({ + bar: 'baz', + }) + ).toEqual({ + foo: 'test', + }); +}); + +test('unknowns = `ignore` affects only own keys', () => { + const type = schema.object( + { foo: schema.object({ bar: schema.string() }) }, + { unknowns: 'ignore' } + ); + + expect(() => + type.validate({ + foo: { + bar: 'bar', + baz: 'baz', + }, + }) + ).toThrowErrorMatchingInlineSnapshot(`"[foo.baz]: definition for this key is missing"`); +}); diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index f34acd0d2ce6..5a50e714a593 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -30,17 +30,25 @@ export type TypeOf> = RT['type']; // this might not have perfect _rendering_ output, but it will be typed. export type ObjectResultType

= Readonly<{ [K in keyof P]: TypeOf }>; +interface UnknownOptions { + /** + * Options for dealing with unknown keys: + * - allow: unknown keys will be permitted + * - ignore: unknown keys will not fail validation, but will be stripped out + * - forbid (default): unknown keys will fail validation + */ + unknowns?: 'allow' | 'ignore' | 'forbid'; +} + export type ObjectTypeOptions

= TypeOptions< { [K in keyof P]: TypeOf } -> & { - /** Should uknown keys not be defined in the schema be allowed. Defaults to `false` */ - allowUnknowns?: boolean; -}; +> & + UnknownOptions; export class ObjectType

extends Type> { private props: Record; - constructor(props: P, { allowUnknowns = false, ...typeOptions }: ObjectTypeOptions

= {}) { + constructor(props: P, { unknowns = 'forbid', ...typeOptions }: ObjectTypeOptions

= {}) { const schemaKeys = {} as Record; for (const [key, value] of Object.entries(props)) { schemaKeys[key] = value.getSchema(); @@ -50,7 +58,8 @@ export class ObjectType

extends Type> .keys(schemaKeys) .default() .optional() - .unknown(Boolean(allowUnknowns)); + .unknown(unknowns === 'allow') + .options({ stripUnknown: { objects: unknowns === 'ignore' } }); super(schema, typeOptions); this.props = schemaKeys; diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index bb0a8616e722..9789d266587a 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -179,7 +179,7 @@ export interface RouteConfig { * access to raw values. * In some cases you may want to use another validation library. To do this, you need to * instruct the `@kbn/config-schema` library to output **non-validated values** with - * setting schema as `schema.object({}, { allowUnknowns: true })`; + * setting schema as `schema.object({}, { unknowns: 'allow' })`; * * @example * ```ts @@ -212,7 +212,7 @@ export interface RouteConfig { * path: 'path/{id}', * validate: { * // handler has access to raw non-validated params in runtime - * params: schema.object({}, { allowUnknowns: true }) + * params: schema.object({}, { unknowns: 'allow' }) * }, * }, * (context, req, res,) { diff --git a/src/core/server/http/router/router.test.ts b/src/core/server/http/router/router.test.ts index a936da6a40a9..9655e2153b86 100644 --- a/src/core/server/http/router/router.test.ts +++ b/src/core/server/http/router/router.test.ts @@ -59,7 +59,7 @@ describe('Router', () => { { path: '/', options: { body: { output: 'file' } } as any, // We explicitly don't support 'file' - validate: { body: schema.object({}, { allowUnknowns: true }) }, + validate: { body: schema.object({}, { unknowns: 'allow' }) }, }, (context, req, res) => res.ok({}) ) diff --git a/src/core/server/ui_settings/routes/set_many.ts b/src/core/server/ui_settings/routes/set_many.ts index 5623c3fe11b8..d19a36a7ce76 100644 --- a/src/core/server/ui_settings/routes/set_many.ts +++ b/src/core/server/ui_settings/routes/set_many.ts @@ -24,7 +24,7 @@ import { CannotOverrideError } from '../ui_settings_errors'; const validate = { body: schema.object({ - changes: schema.object({}, { allowUnknowns: true }), + changes: schema.object({}, { unknowns: 'allow' }), }), }; diff --git a/src/core/server/ui_settings/ui_settings_config.ts b/src/core/server/ui_settings/ui_settings_config.ts index a54d482a0296..a0ac48e2dd08 100644 --- a/src/core/server/ui_settings/ui_settings_config.ts +++ b/src/core/server/ui_settings/ui_settings_config.ts @@ -39,7 +39,7 @@ const configSchema = schema.object({ }) ), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), }); diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index 03dbd4098441..b7569a22e9fc 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -39,7 +39,7 @@ export function registerValueSuggestionsRoute( { index: schema.string(), }, - { allowUnknowns: false } + { unknowns: 'allow' } ), body: schema.object( { @@ -47,7 +47,7 @@ export function registerValueSuggestionsRoute( query: schema.string(), boolFilter: schema.maybe(schema.any()), }, - { allowUnknowns: false } + { unknowns: 'allow' } ), }, }, diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index e618f99084ae..b90d7d4ff80c 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -28,9 +28,9 @@ export function registerSearchRoute(router: IRouter): void { validate: { params: schema.object({ strategy: schema.string() }), - query: schema.object({}, { allowUnknowns: true }), + query: schema.object({}, { unknowns: 'allow' }), - body: schema.object({}, { allowUnknowns: true }), + body: schema.object({}, { unknowns: 'allow' }), }, }, async (context, request, res) => { @@ -64,7 +64,7 @@ export function registerSearchRoute(router: IRouter): void { id: schema.string(), }), - query: schema.object({}, { allowUnknowns: true }), + query: schema.object({}, { unknowns: 'allow' }), }, }, async (context, request, res) => { diff --git a/src/plugins/timelion/config.ts b/src/plugins/timelion/config.ts index 561fb4de9f58..eaea1aaca1b7 100644 --- a/src/plugins/timelion/config.ts +++ b/src/plugins/timelion/config.ts @@ -25,7 +25,7 @@ export const configSchema = schema.object( graphiteUrls: schema.maybe(schema.arrayOf(schema.string())), }, // This option should be removed as soon as we entirely migrate config from legacy Timelion plugin. - { allowUnknowns: true } + { unknowns: 'allow' } ); export type ConfigSchema = TypeOf; diff --git a/src/plugins/timelion/server/routes/run.ts b/src/plugins/timelion/server/routes/run.ts index b7a4179da768..b773bba68ea8 100644 --- a/src/plugins/timelion/server/routes/run.ts +++ b/src/plugins/timelion/server/routes/run.ts @@ -78,15 +78,11 @@ export function runRoute( es: schema.object({ filter: schema.object({ bool: schema.object({ - filter: schema.maybe( - schema.arrayOf(schema.object({}, { allowUnknowns: true })) - ), - must: schema.maybe(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), - should: schema.maybe( - schema.arrayOf(schema.object({}, { allowUnknowns: true })) - ), + filter: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + must: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + should: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), must_not: schema.maybe( - schema.arrayOf(schema.object({}, { allowUnknowns: true })) + schema.arrayOf(schema.object({}, { unknowns: 'allow' })) ), }), }), diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index e2d1e4d114ad..9abbc4ad617d 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -23,7 +23,7 @@ import { getVisData, GetVisDataOptions } from '../lib/get_vis_data'; import { visPayloadSchema } from './post_vis_schema'; import { Framework, ValidationTelemetryServiceSetup } from '../index'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const visDataRoutes = ( router: IRouter, diff --git a/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts b/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts index fad19728b751..3f6a8e8773e0 100644 --- a/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts +++ b/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts @@ -33,7 +33,7 @@ export class RenderingPlugin implements Plugin { { includeUserSettings: schema.boolean({ defaultValue: true }), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), params: schema.object({ id: schema.maybe(schema.string()), diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts index cdc929a2f3bb..03ec583a3416 100644 --- a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts +++ b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts @@ -15,7 +15,7 @@ export function registerLicenseRoute(server: Server, legacy: Legacy, xpackInfo: validate: { query: schema.object({ acknowledge: schema.string() }), body: schema.object({ - license: schema.object({}, { allowUnknowns: true }), + license: schema.object({}, { unknowns: 'allow' }), }), }, }, diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts b/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts index e58bc95b9a37..e45713e2b807 100644 --- a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts +++ b/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts @@ -127,7 +127,7 @@ export function registerJobsRoute(deps: RouteDependencies, legacy: ServerShim) { { id: schema.string(), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), }), }, 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 d1726f93108c..adbb5fa61895 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 @@ -26,13 +26,13 @@ export const signalParamsSchema = () => savedId: schema.nullable(schema.string()), timelineId: schema.nullable(schema.string()), timelineTitle: schema.nullable(schema.string()), - meta: schema.nullable(schema.object({}, { allowUnknowns: true })), + meta: schema.nullable(schema.object({}, { unknowns: 'allow' })), query: schema.nullable(schema.string()), - filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), + filters: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), riskScore: schema.number(), severity: schema.string(), - threat: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), + threat: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), to: schema.string(), type: schema.string(), references: schema.arrayOf(schema.string(), { defaultValue: [] }), diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts index 7d42149223b3..004ac36bad5b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts @@ -61,7 +61,7 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { this.router.post( { path: routePath, - validate: { body: configSchema.object({}, { allowUnknowns: true }) }, + validate: { body: configSchema.object({}, { unknowns: 'allow' }) }, options: { tags: ['access:siem'], }, diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index a84a24cea17d..e216574f8a02 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -71,7 +71,7 @@ export function createApi() { body: bodyRt || t.null }; - const anyObject = schema.object({}, { allowUnknowns: true }); + const anyObject = schema.object({}, { unknowns: 'allow' }); (router[routerMethod] as RouteRegistrar)( { diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.ts b/x-pack/plugins/canvas/server/routes/workpad/update.ts index 83b8fef48e9b..64736bcd57fd 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.ts @@ -120,7 +120,7 @@ export function initializeUpdateWorkpadAssetsRoute(deps: RouteInitializerDeps) { // ToDo: Currently the validation must be a schema.object // Because we don't know what keys the assets will have, we have to allow // unknowns and then validate in the handler - body: schema.object({}, { allowUnknowns: true }), + body: schema.object({}, { unknowns: 'allow' }), }, options: { body: { diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 04fe426bb2ec..27ee6fc58e20 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -141,4 +141,4 @@ export const sortToSnake = (sortField: string): SortFieldCase => { } }; -export const escapeHatch = schema.object({}, { allowUnknowns: true }); +export const escapeHatch = schema.object({}, { unknowns: 'allow' }); diff --git a/x-pack/plugins/file_upload/server/routes/file_upload.js b/x-pack/plugins/file_upload/server/routes/file_upload.js index acbc907729d9..d75f03132b40 100644 --- a/x-pack/plugins/file_upload/server/routes/file_upload.js +++ b/x-pack/plugins/file_upload/server/routes/file_upload.js @@ -28,12 +28,12 @@ export const bodySchema = schema.object( {}, { defaultValue: {}, - allowUnknowns: true, + unknowns: 'allow', } ) ), }, - { allowUnknowns: true } + { unknowns: 'allow' } ); const options = { @@ -48,7 +48,7 @@ export const idConditionalValidation = (body, boolHasId) => .object( { data: boolHasId - ? schema.arrayOf(schema.object({}, { allowUnknowns: true }), { minSize: 1 }) + ? schema.arrayOf(schema.object({}, { unknowns: 'allow' }), { minSize: 1 }) : schema.any(), settings: boolHasId ? schema.any() @@ -58,7 +58,7 @@ export const idConditionalValidation = (body, boolHasId) => defaultValue: { number_of_shards: 1, }, - allowUnknowns: true, + unknowns: 'allow', } ), mappings: boolHasId @@ -67,11 +67,11 @@ export const idConditionalValidation = (body, boolHasId) => {}, { defaultValue: {}, - allowUnknowns: true, + unknowns: 'allow', } ), }, - { allowUnknowns: true } + { unknowns: 'allow' } ) .validate(body); diff --git a/x-pack/plugins/graph/server/routes/explore.ts b/x-pack/plugins/graph/server/routes/explore.ts index 125378891151..ceced840bdbc 100644 --- a/x-pack/plugins/graph/server/routes/explore.ts +++ b/x-pack/plugins/graph/server/routes/explore.ts @@ -23,7 +23,7 @@ export function registerExploreRoute({ validate: { body: schema.object({ index: schema.string(), - query: schema.object({}, { allowUnknowns: true }), + query: schema.object({}, { unknowns: 'allow' }), }), }, }, diff --git a/x-pack/plugins/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts index 91b404dc7cb9..6e9fe508af3d 100644 --- a/x-pack/plugins/graph/server/routes/search.ts +++ b/x-pack/plugins/graph/server/routes/search.ts @@ -21,7 +21,7 @@ export function registerSearchRoute({ validate: { body: schema.object({ index: schema.string(), - body: schema.object({}, { allowUnknowns: true }), + body: schema.object({}, { unknowns: 'allow' }), }), }, }, diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts index fb5d41870eec..8bf2774ac38b 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts @@ -11,9 +11,9 @@ export const templateSchema = schema.object({ indexPatterns: schema.arrayOf(schema.string()), version: schema.maybe(schema.number()), order: schema.maybe(schema.number()), - settings: schema.maybe(schema.object({}, { allowUnknowns: true })), - aliases: schema.maybe(schema.object({}, { allowUnknowns: true })), - mappings: schema.maybe(schema.object({}, { allowUnknowns: true })), + settings: schema.maybe(schema.object({}, { unknowns: 'allow' })), + aliases: schema.maybe(schema.object({}, { unknowns: 'allow' })), + mappings: schema.maybe(schema.object({}, { unknowns: 'allow' })), ilmPolicy: schema.maybe( schema.object({ name: schema.maybe(schema.string()), diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 6ff749c04022..e2ff93ce356e 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -76,7 +76,7 @@ export class KibanaFramework { public registerGraphQLEndpoint(routePath: string, gqlSchema: GraphQLSchema) { // These endpoints are validated by GraphQL at runtime and with GraphQL generated types - const body = schema.object({}, { allowUnknowns: true }); + const body = schema.object({}, { unknowns: 'allow' }); type Body = TypeOf; const routeOptions = { diff --git a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts index 33328bdfebaf..7e9b7ada28c8 100644 --- a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts @@ -18,7 +18,7 @@ import { } from '../../../common/http_api/inventory_meta_api'; import { getCloudMetadata } from './lib/get_cloud_metadata'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const initInventoryMetaRoute = (libs: InfraBackendLibs) => { const { framework } = libs; diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts index 7eb7de57b2f9..6852a102afc8 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts @@ -19,7 +19,7 @@ import { import { throwErrors } from '../../../../common/runtime_types'; import { NoLogAnalysisResultsIndexError } from '../../../lib/log_analysis'; -const anyObject = schema.object({}, { allowUnknowns: true }); +const anyObject = schema.object({}, { unknowns: 'allow' }); export const initGetLogEntryCategoriesRoute = ({ framework, diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_datasets.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_datasets.ts index 813263302827..730e32dee2fb 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_datasets.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_datasets.ts @@ -19,7 +19,7 @@ import { throwErrors } from '../../../../common/runtime_types'; import { InfraBackendLibs } from '../../../lib/infra_types'; import { NoLogAnalysisResultsIndexError } from '../../../lib/log_analysis'; -const anyObject = schema.object({}, { allowUnknowns: true }); +const anyObject = schema.object({}, { unknowns: 'allow' }); export const initGetLogEntryCategoryDatasetsRoute = ({ framework, diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts index 67c6c9f5b992..44f466cc77c8 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts @@ -19,7 +19,7 @@ import { throwErrors } from '../../../../common/runtime_types'; import { InfraBackendLibs } from '../../../lib/infra_types'; import { NoLogAnalysisResultsIndexError } from '../../../lib/log_analysis'; -const anyObject = schema.object({}, { allowUnknowns: true }); +const anyObject = schema.object({}, { unknowns: 'allow' }); export const initGetLogEntryCategoryExamplesRoute = ({ framework, diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts index 6551316fd0c6..38dc0a790a7a 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts @@ -20,7 +20,7 @@ import { import { throwErrors } from '../../../../common/runtime_types'; import { NoLogAnalysisResultsIndexError } from '../../../lib/log_analysis'; -const anyObject = schema.object({}, { allowUnknowns: true }); +const anyObject = schema.object({}, { unknowns: 'allow' }); export const initGetLogEntryRateRoute = ({ framework, logEntryRateAnalysis }: InfraBackendLibs) => { framework.registerRoute( diff --git a/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts b/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts index fe579124cfe1..54ae0b4529da 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts @@ -19,7 +19,7 @@ import { import { throwErrors } from '../../../../common/runtime_types'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const initValidateLogAnalysisIndicesRoute = ({ framework }: InfraBackendLibs) => { framework.registerRoute( diff --git a/x-pack/plugins/infra/server/routes/log_entries/entries.ts b/x-pack/plugins/infra/server/routes/log_entries/entries.ts index 361535886ab2..93802468dd26 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/entries.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/entries.ts @@ -22,7 +22,7 @@ import { import { parseFilterQuery } from '../../utils/serialized_query'; import { LogEntriesParams } from '../../lib/domains/log_entries_domain'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) => { framework.registerRoute( diff --git a/x-pack/plugins/infra/server/routes/log_entries/highlights.ts b/x-pack/plugins/infra/server/routes/log_entries/highlights.ts index 8af81a6ee313..8ee412d5acdd 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/highlights.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/highlights.ts @@ -22,7 +22,7 @@ import { import { parseFilterQuery } from '../../utils/serialized_query'; import { LogEntriesParams } from '../../lib/domains/log_entries_domain'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const initLogEntriesHighlightsRoute = ({ framework, logEntries }: InfraBackendLibs) => { framework.registerRoute( diff --git a/x-pack/plugins/infra/server/routes/log_entries/item.ts b/x-pack/plugins/infra/server/routes/log_entries/item.ts index 22663cb2001f..3a6bdaf3804e 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/item.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/item.ts @@ -20,7 +20,7 @@ import { logEntriesItemResponseRT, } from '../../../common/http_api'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const initLogEntriesItemRoute = ({ framework, sources, logEntries }: InfraBackendLibs) => { framework.registerRoute( diff --git a/x-pack/plugins/infra/server/routes/log_entries/summary.ts b/x-pack/plugins/infra/server/routes/log_entries/summary.ts index 05643adbe781..3f5bc8e364a5 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/summary.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/summary.ts @@ -21,7 +21,7 @@ import { } from '../../../common/http_api/log_entries'; import { parseFilterQuery } from '../../utils/serialized_query'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const initLogEntriesSummaryRoute = ({ framework, logEntries }: InfraBackendLibs) => { framework.registerRoute( diff --git a/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts b/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts index ecccd931bb37..6c6f7a5a3dcd 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts @@ -21,7 +21,7 @@ import { } from '../../../common/http_api/log_entries'; import { parseFilterQuery } from '../../utils/serialized_query'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const initLogEntriesSummaryHighlightsRoute = ({ framework, diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index a1f6311a103e..03d28110d612 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -23,7 +23,7 @@ import { getCloudMetricsMetadata } from './lib/get_cloud_metric_metadata'; import { getNodeInfo } from './lib/get_node_info'; import { throwErrors } from '../../../common/runtime_types'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const initMetadataRoute = (libs: InfraBackendLibs) => { const { framework } = libs; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts index 64cdb9318b6e..c22095a31195 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts @@ -15,7 +15,7 @@ import { populateSeriesWithTSVBData } from './lib/populate_series_with_tsvb_data import { metricsExplorerRequestBodyRT, metricsExplorerResponseRT } from '../../../common/http_api'; import { throwErrors } from '../../../common/runtime_types'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const initMetricExplorerRoute = (libs: InfraBackendLibs) => { const { framework } = libs; diff --git a/x-pack/plugins/infra/server/routes/node_details/index.ts b/x-pack/plugins/infra/server/routes/node_details/index.ts index 4a09615f0a17..36906f6f4125 100644 --- a/x-pack/plugins/infra/server/routes/node_details/index.ts +++ b/x-pack/plugins/infra/server/routes/node_details/index.ts @@ -18,7 +18,7 @@ import { } from '../../../common/http_api/node_details_api'; import { throwErrors } from '../../../common/runtime_types'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { const { framework } = libs; diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index 5f28e41d80c2..e45b9884967d 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -14,7 +14,7 @@ import { parseFilterQuery } from '../../utils/serialized_query'; import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api'; import { throwErrors } from '../../../common/runtime_types'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const initSnapshotRoute = (libs: InfraBackendLibs) => { const { framework } = libs; diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index 57c168041353..b1964a915098 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -55,7 +55,7 @@ export async function existingFieldsRoute(setup: CoreSetup) { indexPatternId: schema.string(), }), body: schema.object({ - dslQuery: schema.object({}, { allowUnknowns: true }), + dslQuery: schema.object({}, { unknowns: 'allow' }), fromDate: schema.maybe(schema.string()), toDate: schema.maybe(schema.string()), timeFieldName: schema.maybe(schema.string()), diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index 786aba5efe3f..5c91be9dfbd7 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -24,7 +24,7 @@ export async function initFieldsRoute(setup: CoreSetup) { }), body: schema.object( { - dslQuery: schema.object({}, { allowUnknowns: true }), + dslQuery: schema.object({}, { unknowns: 'allow' }), fromDate: schema.string(), toDate: schema.string(), timeFieldName: schema.maybe(schema.string()), @@ -34,10 +34,10 @@ export async function initFieldsRoute(setup: CoreSetup) { type: schema.string(), esTypes: schema.maybe(schema.arrayOf(schema.string())), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), }, }, diff --git a/x-pack/plugins/searchprofiler/server/routes/profile.ts b/x-pack/plugins/searchprofiler/server/routes/profile.ts index c47ab81b2ab7..4af3f0519cbc 100644 --- a/x-pack/plugins/searchprofiler/server/routes/profile.ts +++ b/x-pack/plugins/searchprofiler/server/routes/profile.ts @@ -12,7 +12,7 @@ export const register = ({ router, getLicenseStatus, log }: RouteDependencies) = path: '/api/searchprofiler/profile', validate: { body: schema.object({ - query: schema.object({}, { allowUnknowns: true }), + query: schema.object({}, { unknowns: 'allow' }), index: schema.string(), }), }, diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts index c9856e9dff7f..19d197b63f54 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -21,7 +21,7 @@ export function defineCommonRoutes({ router, authc, basePath, logger }: RouteDef path, // Allow unknown query parameters as this endpoint can be hit by the 3rd-party with any // set of query string parameters (e.g. SAML/OIDC logout request parameters). - validate: { query: schema.object({}, { allowUnknowns: true }) }, + validate: { query: schema.object({}, { unknowns: 'allow' }) }, options: { authRequired: false }, }, async (context, request, response) => { diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts index 232fdd26f783..96c36af20e98 100644 --- a/x-pack/plugins/security/server/routes/authentication/oidc.ts +++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts @@ -103,7 +103,7 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route // The client MUST ignore unrecognized response parameters according to // https://openid.net/specs/openid-connect-core-1_0.html#AuthResponseValidation and // https://tools.ietf.org/html/rfc6749#section-4.1.2. - { allowUnknowns: true } + { unknowns: 'allow' } ), }, options: { authRequired: false }, @@ -178,7 +178,7 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route }, // Other parameters MAY be sent, if defined by extensions. Any parameters used that are not understood MUST // be ignored by the Client according to https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin. - { allowUnknowns: true } + { unknowns: 'allow' } ), }, options: { authRequired: false }, @@ -217,7 +217,7 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route }, // Other parameters MAY be sent, if defined by extensions. Any parameters used that are not understood MUST // be ignored by the Client according to https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin. - { allowUnknowns: true } + { unknowns: 'allow' } ), }, options: { authRequired: false }, diff --git a/x-pack/plugins/security/server/routes/role_mapping/post.ts b/x-pack/plugins/security/server/routes/role_mapping/post.ts index bf9112be4ad3..11149f38069a 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/post.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/post.ts @@ -36,8 +36,8 @@ export function defineRoleMappingPostRoutes(params: RouteDefinitionParams) { // and keeping this in sync (and testable!) with ES could prove problematic. // We do not interpret any of these rules within this route handler; // they are simply passed to ES for processing. - rules: schema.object({}, { allowUnknowns: true }), - metadata: schema.object({}, { allowUnknowns: true }), + rules: schema.object({}, { unknowns: 'allow' }), + metadata: schema.object({}, { unknowns: 'allow' }), }), }, }, diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index e2e162d298e4..ee1fe01ab1b2 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -28,7 +28,7 @@ export function defineLoginRoutes({ next: schema.maybe(schema.string()), msg: schema.maybe(schema.string()), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), }, options: { authRequired: false }, diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts index f6f8bb4de4d8..e5df0ec33db0 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts @@ -37,9 +37,9 @@ export const policySchema = schema.object({ config: schema.maybe(snapshotConfigSchema), retention: schema.maybe(snapshotRetentionSchema), isManagedPolicy: schema.boolean(), - stats: schema.maybe(schema.object({}, { allowUnknowns: true })), - lastFailure: schema.maybe(schema.object({}, { allowUnknowns: true })), - lastSuccess: schema.maybe(schema.object({}, { allowUnknowns: true })), + stats: schema.maybe(schema.object({}, { unknowns: 'allow' })), + lastFailure: schema.maybe(schema.object({}, { unknowns: 'allow' })), + lastSuccess: schema.maybe(schema.object({}, { unknowns: 'allow' })), }); const fsRepositorySettings = schema.object({ @@ -100,7 +100,7 @@ const hdsRepositorySettings = schema.object( readonly: schema.maybe(schema.boolean()), ['security.principal']: schema.maybe(schema.string()), }, - { allowUnknowns: true } + { unknowns: 'allow' } ); const hdsfRepository = schema.object({ @@ -158,7 +158,7 @@ const sourceRepository = schema.object({ { delegateType: schema.string(), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), ]), }); diff --git a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts index df6f62135bae..a1184cbebd13 100644 --- a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts @@ -11,7 +11,7 @@ import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -const bodySchema = schema.object({ pattern: schema.string() }, { allowUnknowns: true }); +const bodySchema = schema.object({ pattern: schema.string() }, { unknowns: 'allow' }); function getIndexNamesFromAliasesResponse(json: Record) { return reduce( diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts index 7aaa77c05a5f..14a14a6f64d7 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts @@ -19,8 +19,8 @@ import { Watch } from '../../../models/watch/index'; import { WatchHistoryItem } from '../../../models/watch_history_item/index'; const bodySchema = schema.object({ - executeDetails: schema.object({}, { allowUnknowns: true }), - watch: schema.object({}, { allowUnknowns: true }), + executeDetails: schema.object({}, { unknowns: 'allow' }), + watch: schema.object({}, { unknowns: 'allow' }), }); function executeWatch(dataClient: IScopedClusterClient, executeDetails: any, watchJson: any) { diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts index 572790f12a5f..61d167bb9bbc 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts @@ -22,7 +22,7 @@ const bodySchema = schema.object( type: schema.string(), isNew: schema.boolean(), }, - { allowUnknowns: true } + { unknowns: 'allow' } ); function fetchWatch(dataClient: IScopedClusterClient, watchId: string) { diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts index 200b35953b6f..90550731bf23 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts @@ -16,8 +16,8 @@ import { Watch } from '../../../models/watch/index'; import { VisualizeOptions } from '../../../models/visualize_options/index'; const bodySchema = schema.object({ - watch: schema.object({}, { allowUnknowns: true }), - options: schema.object({}, { allowUnknowns: true }), + watch: schema.object({}, { unknowns: 'allow' }), + options: schema.object({}, { unknowns: 'allow' }), }); function fetchVisualizeData(dataClient: IScopedClusterClient, index: any, body: any) { From 156066dc6fa512169f1af4244f5c4cdefb61be14 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 17 Mar 2020 12:34:03 -0400 Subject: [PATCH 049/115] [Fleet] Add config revision to fleet agents (#60292) --- .../common/types/models/agent.ts | 3 +- .../sections/fleet/agent_list_page/index.tsx | 45 +++++-- .../ingest_manager/server/saved_objects.ts | 3 +- .../server/services/agents/acks.ts | 14 +++ .../server/services/agents/checkin.test.ts | 117 ++++++++++++++++++ .../server/services/agents/checkin.ts | 35 +++++- .../server/services/agents/enroll.ts | 1 - .../server/services/agents/update.ts | 8 +- 8 files changed, 205 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index ad06e8d3c9c1..179cc3fc9eb5 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -56,8 +56,9 @@ interface AgentBase { access_api_key_id?: string; default_api_key?: string; config_id?: string; + config_revision?: number; + config_newest_revision?: number; last_checkin?: string; - config_updated_at?: string; actions: AgentAction[]; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index acf09dedc25f..14a579eb7259 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -26,6 +26,7 @@ import { EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, + EuiIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; @@ -289,6 +290,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }, { field: 'active', + width: '100px', name: i18n.translate('xpack.ingestManager.agentList.statusColumnTitle', { defaultMessage: 'Status', }), @@ -299,10 +301,10 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.ingestManager.agentList.configColumnTitle', { defaultMessage: 'Configuration', }), - render: (configId: string) => { + render: (configId: string, agent: Agent) => { const configName = agentConfigs.find(p => p.id === configId)?.name; return ( - + = () => { {configName || configId} - - - - - + {agent.config_revision && ( + + + + + + )} + {agent.config_revision && + agent.config_newest_revision && + agent.config_newest_revision > agent.config_revision && ( + + + +   + {true && ( + <> + + + )} + + + )} ); }, }, { field: 'local_metadata.agent_version', + width: '100px', name: i18n.translate('xpack.ingestManager.agentList.versionTitle', { defaultMessage: 'Version', }), diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index 860b95b58c7f..31cf173c3e4f 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -32,7 +32,8 @@ export const savedObjectMappings = { config_id: { type: 'keyword' }, last_updated: { type: 'date' }, last_checkin: { type: 'date' }, - config_updated_at: { type: 'date' }, + config_revision: { type: 'integer' }, + config_newest_revision: { type: 'integer' }, // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 default_api_key: { type: 'keyword' }, updated_at: { type: 'date' }, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts index 98a5f69f9d2b..cf9a47979ae8 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts @@ -51,8 +51,22 @@ export async function acknowledgeAgentActions( }); if (matchedUpdatedActions.length > 0) { + const configRevision = matchedUpdatedActions.reduce((acc, action) => { + if (action.type !== 'CONFIG_CHANGE') { + return acc; + } + const data = action.data ? JSON.parse(action.data as string) : {}; + + if (data?.config?.id !== agent.config_id) { + return acc; + } + + return data?.config?.revision > acc ? data?.config?.revision : acc; + }, agent.config_revision || 0); + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, { actions: matchedUpdatedActions, + config_revision: configRevision, }); } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts new file mode 100644 index 000000000000..d3e10fcb6b63 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts @@ -0,0 +1,117 @@ +/* + * 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 { shouldCreateConfigAction } from './checkin'; +import { Agent } from '../../types'; + +function getAgent(data: Partial) { + return { actions: [], ...data } as Agent; +} + +describe('Agent checkin service', () => { + describe('shouldCreateConfigAction', () => { + it('should return false if the agent do not have an assigned config', () => { + const res = shouldCreateConfigAction(getAgent({})); + + expect(res).toBeFalsy(); + }); + + it('should return true if this is agent first checkin', () => { + const res = shouldCreateConfigAction(getAgent({ config_id: 'config1' })); + + expect(res).toBeTruthy(); + }); + + it('should return false agent is already running latest revision', () => { + const res = shouldCreateConfigAction( + getAgent({ + config_id: 'config1', + last_checkin: '2018-01-02T00:00:00', + config_revision: 1, + config_newest_revision: 1, + }) + ); + + expect(res).toBeFalsy(); + }); + + it('should return false agent has already latest revision config change action', () => { + const res = shouldCreateConfigAction( + getAgent({ + config_id: 'config1', + last_checkin: '2018-01-02T00:00:00', + config_revision: 1, + config_newest_revision: 2, + actions: [ + { + id: 'action1', + type: 'CONFIG_CHANGE', + created_at: new Date().toISOString(), + data: JSON.stringify({ + config: { + id: 'config1', + revision: 2, + }, + }), + }, + ], + }) + ); + + expect(res).toBeFalsy(); + }); + + it('should return true agent has unrelated config change actions', () => { + const res = shouldCreateConfigAction( + getAgent({ + config_id: 'config1', + last_checkin: '2018-01-02T00:00:00', + config_revision: 1, + config_newest_revision: 2, + actions: [ + { + id: 'action1', + type: 'CONFIG_CHANGE', + created_at: new Date().toISOString(), + data: JSON.stringify({ + config: { + id: 'config2', + revision: 2, + }, + }), + }, + { + id: 'action1', + type: 'CONFIG_CHANGE', + created_at: new Date().toISOString(), + data: JSON.stringify({ + config: { + id: 'config1', + revision: 1, + }, + }), + }, + ], + }) + ); + + expect(res).toBeTruthy(); + }); + + it('should return true if this agent has a new revision', () => { + const res = shouldCreateConfigAction( + getAgent({ + config_id: 'config1', + last_checkin: '2018-01-02T00:00:00', + config_revision: 1, + config_newest_revision: 2, + }) + ); + + expect(res).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts index 0ff4af4ffe35..d80fff5d8ece 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts @@ -37,7 +37,7 @@ export async function agentCheckin( const actions = filterActionsForCheckin(agent); // Generate new agent config if config is updated - if (isNewAgentConfig(agent) && agent.config_id) { + if (agent.config_id && shouldCreateConfigAction(agent)) { const config = await agentConfigService.getFullConfig(soClient, agent.config_id); if (config) { // Assign output API keys @@ -149,12 +149,37 @@ function isActionEvent(event: AgentEvent) { ); } -function isNewAgentConfig(agent: Agent) { +export function shouldCreateConfigAction(agent: Agent): boolean { + if (!agent.config_id) { + return false; + } + const isFirstCheckin = !agent.last_checkin; - const isConfigUpdatedSinceLastCheckin = - agent.last_checkin && agent.config_updated_at && agent.last_checkin <= agent.config_updated_at; + if (isFirstCheckin) { + return true; + } + + const isAgentConfigOutdated = + agent.config_revision && + agent.config_newest_revision && + agent.config_revision < agent.config_newest_revision; + if (!isAgentConfigOutdated) { + return false; + } + + const isActionAlreadyGenerated = !!agent.actions.find(action => { + if (!action.data || action.type !== 'CONFIG_CHANGE') { + return false; + } + + const data = JSON.parse(action.data); + + return ( + data.config.id === agent.config_id && data.config.revision === agent.config_newest_revision + ); + }); - return isFirstCheckin || isConfigUpdatedSinceLastCheckin; + return !isActionAlreadyGenerated; } function filterActionsForCheckin(agent: Agent): AgentAction[] { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts index 0f73f71817eb..52547e9bcb0f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts @@ -37,7 +37,6 @@ export async function enroll( current_error_events: undefined, actions: [], access_api_key_id: undefined, - config_updated_at: undefined, last_checkin: undefined, default_api_key: undefined, }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/update.ts b/x-pack/plugins/ingest_manager/server/services/agents/update.ts index 9eabf0944bdc..59d0ad31d1a6 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/update.ts @@ -8,14 +8,18 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { listAgents } from './crud'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { unenrollAgents } from './unenroll'; +import { agentConfigService } from '../agent_config'; export async function updateAgentsForConfigId( soClient: SavedObjectsClientContract, configId: string ) { + const config = await agentConfigService.get(soClient, configId); + if (!config) { + throw new Error('Config not found'); + } let hasMore = true; let page = 1; - const now = new Date().toISOString(); while (hasMore) { const { agents } = await listAgents(soClient, { kuery: `agents.config_id:"${configId}"`, @@ -30,7 +34,7 @@ export async function updateAgentsForConfigId( const agentUpdate = agents.map(agent => ({ id: agent.id, type: AGENT_SAVED_OBJECT_TYPE, - attributes: { config_updated_at: now }, + attributes: { config_newest_revision: config.revision }, })); await soClient.bulkUpdate(agentUpdate); From cea277e7c288cb2d4bf90acca2d57b80da7438d4 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 17 Mar 2020 13:06:12 -0400 Subject: [PATCH 050/115] [SIEM][Detections Engine] - Add rule markdown field to rule create, detail, and edit flows (#60108) * add rule note markdown field to rule creation, rule details, and rule edit flows Co-authored-by: Gloria Hornero Co-authored-by: Elastic Machine --- .../signal_detection_rules.spec.ts | 108 ++-- .../siem/cypress/screens/rule_details.ts | 28 +- .../run_check_circular_deps_cli.js | 10 + .../detection_engine/rules/types.ts | 2 + .../rules/all/__mocks__/mock.ts | 153 +++++ .../__snapshots__/index.test.tsx.snap | 453 ++++++++++++++ .../description_step/helpers.test.tsx | 403 ++++++++++++ .../components/description_step/helpers.tsx | 91 ++- .../description_step/index.test.tsx | 297 ++++++++- .../components/description_step/index.tsx | 58 +- .../step_about_rule/default_value.ts | 1 + .../components/step_about_rule/index.test.tsx | 155 +++++ .../components/step_about_rule/index.tsx | 140 +++-- .../components/step_about_rule/schema.tsx | 20 +- .../step_about_rule/translations.ts | 7 + .../step_about_rule_details/index.test.tsx | 175 ++++++ .../step_about_rule_details/index.tsx | 147 +++++ .../step_about_rule_details/translations.ts | 27 + .../components/step_define_rule/index.tsx | 6 +- .../components/step_schedule_rule/index.tsx | 51 +- .../rules/create/helpers.test.ts | 589 ++++++++++++++++++ .../detection_engine/rules/create/helpers.ts | 20 +- .../detection_engine/rules/create/index.tsx | 6 +- .../detection_engine/rules/details/index.tsx | 58 +- .../detection_engine/rules/helpers.test.tsx | 291 +++++++++ .../pages/detection_engine/rules/helpers.tsx | 156 +++-- .../pages/detection_engine/rules/types.ts | 9 +- .../pages/detection_engine/translations.ts | 2 +- 28 files changed, 3166 insertions(+), 297 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts index 8c384c901066..ce73fe1b7c2a 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts @@ -7,30 +7,30 @@ import { newRule } from '../objects/rule'; import { - ABOUT_DESCRIPTION, - ABOUT_EXPECTED_URLS, ABOUT_FALSE_POSITIVES, ABOUT_MITRE, ABOUT_RISK, - ABOUT_RULE_DESCRIPTION, ABOUT_SEVERITY, + ABOUT_STEP, ABOUT_TAGS, ABOUT_TIMELINE, + ABOUT_URLS, DEFINITION_CUSTOM_QUERY, - DEFINITION_DESCRIPTION, DEFINITION_INDEX_PATTERNS, + DEFINITION_STEP, RULE_NAME_HEADER, - SCHEDULE_DESCRIPTION, SCHEDULE_LOOPBACK, SCHEDULE_RUNS, + SCHEDULE_STEP, + ABOUT_RULE_DESCRIPTION, } from '../screens/rule_details'; import { CUSTOM_RULES_BTN, ELASTIC_RULES_BTN, RISK_SCORE, RULE_NAME, - RULES_TABLE, RULES_ROW, + RULES_TABLE, SEVERITY, } from '../screens/signal_detection_rules'; @@ -127,10 +127,25 @@ describe('Signal detection rules', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER) - .invoke('text') - .should('eql', `${newRule.name} Beta`); - + let expectedUrls = ''; + newRule.referenceUrls.forEach(url => { + expectedUrls = expectedUrls + url; + }); + let expectedFalsePositives = ''; + newRule.falsePositivesExamples.forEach(falsePositive => { + expectedFalsePositives = expectedFalsePositives + falsePositive; + }); + let expectedTags = ''; + newRule.tags.forEach(tag => { + expectedTags = expectedTags + tag; + }); + let expectedMitre = ''; + newRule.mitre.forEach(mitre => { + expectedMitre = expectedMitre + mitre.tactic; + mitre.techniques.forEach(technique => { + expectedMitre = expectedMitre + technique; + }); + }); const expectedIndexPatterns = [ 'apm-*-transaction*', 'auditbeat-*', @@ -139,77 +154,60 @@ describe('Signal detection rules', () => { 'packetbeat-*', 'winlogbeat-*', ]; - cy.get(DEFINITION_INDEX_PATTERNS).then(patterns => { - cy.wrap(patterns).each((pattern, index) => { - cy.wrap(pattern) - .invoke('text') - .should('eql', expectedIndexPatterns[index]); - }); - }); - cy.get(DEFINITION_DESCRIPTION) - .eq(DEFINITION_CUSTOM_QUERY) + + cy.get(RULE_NAME_HEADER) .invoke('text') - .should('eql', `${newRule.customQuery} `); - cy.get(ABOUT_DESCRIPTION) - .eq(ABOUT_RULE_DESCRIPTION) + .should('eql', `${newRule.name} Beta`); + + cy.get(ABOUT_RULE_DESCRIPTION) .invoke('text') .should('eql', newRule.description); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_SEVERITY) .invoke('text') .should('eql', newRule.severity); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_RISK) .invoke('text') .should('eql', newRule.riskScore); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_TIMELINE) .invoke('text') .should('eql', 'Default blank timeline'); - - let expectedUrls = ''; - newRule.referenceUrls.forEach(url => { - expectedUrls = expectedUrls + url; - }); - cy.get(ABOUT_DESCRIPTION) - .eq(ABOUT_EXPECTED_URLS) + cy.get(ABOUT_STEP) + .eq(ABOUT_URLS) .invoke('text') .should('eql', expectedUrls); - - let expectedFalsePositives = ''; - newRule.falsePositivesExamples.forEach(falsePositive => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_FALSE_POSITIVES) .invoke('text') .should('eql', expectedFalsePositives); - - let expectedMitre = ''; - newRule.mitre.forEach(mitre => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach(technique => { - expectedMitre = expectedMitre + technique; - }); - }); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_MITRE) .invoke('text') .should('eql', expectedMitre); - - let expectedTags = ''; - newRule.tags.forEach(tag => { - expectedTags = expectedTags + tag; - }); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_TAGS) .invoke('text') .should('eql', expectedTags); - cy.get(SCHEDULE_DESCRIPTION) + + cy.get(DEFINITION_INDEX_PATTERNS).then(patterns => { + cy.wrap(patterns).each((pattern, index) => { + cy.wrap(pattern) + .invoke('text') + .should('eql', expectedIndexPatterns[index]); + }); + }); + cy.get(DEFINITION_STEP) + .eq(DEFINITION_CUSTOM_QUERY) + .invoke('text') + .should('eql', `${newRule.customQuery} `); + + cy.get(SCHEDULE_STEP) .eq(SCHEDULE_RUNS) .invoke('text') .should('eql', '5m'); - cy.get(SCHEDULE_DESCRIPTION) + cy.get(SCHEDULE_STEP) .eq(SCHEDULE_LOOPBACK) .invoke('text') .should('eql', '1m'); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts index 46da52cd0ddd..6c16735ba5f2 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts @@ -4,35 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ABOUT_DESCRIPTION = '[data-test-subj="aboutRule"] .euiDescriptionList__description'; +export const ABOUT_FALSE_POSITIVES = 4; -export const ABOUT_EXPECTED_URLS = 4; +export const ABOUT_MITRE = 5; -export const ABOUT_FALSE_POSITIVES = 5; +export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]'; -export const ABOUT_MITRE = 6; +export const ABOUT_RISK = 1; -export const ABOUT_RULE_DESCRIPTION = 0; +export const ABOUT_SEVERITY = 0; -export const ABOUT_RISK = 2; +export const ABOUT_STEP = '[data-test-subj="aboutRule"] .euiDescriptionList__description'; -export const ABOUT_SEVERITY = 1; +export const ABOUT_TAGS = 6; -export const ABOUT_TAGS = 7; +export const ABOUT_TIMELINE = 2; -export const ABOUT_TIMELINE = 3; +export const ABOUT_URLS = 3; export const DEFINITION_CUSTOM_QUERY = 1; -export const DEFINITION_DESCRIPTION = - '[data-test-subj="definition"] .euiDescriptionList__description'; - export const DEFINITION_INDEX_PATTERNS = - '[data-test-subj="definition"] .euiDescriptionList__description .euiBadge__text'; + '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description .euiBadge__text'; + +export const DEFINITION_STEP = + '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description'; export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]'; -export const SCHEDULE_DESCRIPTION = '[data-test-subj="schedule"] .euiDescriptionList__description'; +export const SCHEDULE_STEP = '[data-test-subj="schedule"] .euiDescriptionList__description'; export const SCHEDULE_RUNS = 0; diff --git a/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js b/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js index 8ca61b2397d8..f3a97f5b9c9b 100644 --- a/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js +++ b/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js @@ -17,6 +17,16 @@ run( [resolve(__dirname, '../../public'), resolve(__dirname, '../../common')], { fileExtensions: ['ts', 'js', 'tsx'], + excludeRegExp: [ + 'test.ts$', + 'test.tsx$', + 'containers/detection_engine/rules/types.ts$', + 'core/public/chrome/chrome_service.tsx$', + 'src/core/server/types.ts$', + 'src/core/server/saved_objects/types.ts$', + 'src/core/public/overlays/banners/banners_service.tsx$', + 'src/core/public/saved_objects/saved_objects_client.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 4d2aec4ee874..f962204c6b1b 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 @@ -33,6 +33,7 @@ export const NewRuleSchema = t.intersection([ threat: t.array(t.unknown), to: t.string, updated_by: t.string, + note: t.string, }), ]); @@ -86,6 +87,7 @@ export const RuleSchema = t.intersection([ status_date: t.string, timeline_id: t.string, timeline_title: t.string, + note: t.string, version: t.number, }), ]); 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 e2287e5eeeb3..5627d3381850 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 @@ -4,7 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ +import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; import { Rule, RuleError } from '../../../../../containers/detection_engine/rules'; +import { AboutStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; +import { FieldValueQueryBar } from '../../components/query_bar'; + +export const mockQueryBar: FieldValueQueryBar = { + query: { + query: 'test query', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', +}; export const mockRule = (id: string): Rule => ({ created_at: '2020-01-10T21:11:45.839Z', @@ -37,9 +70,129 @@ export const mockRule = (id: string): Rule => ({ to: 'now', type: 'saved_query', threat: [], + note: '# this is some markdown documentation', version: 1, }); +export const mockRuleWithEverything = (id: string): Rule => ({ + created_at: '2020-01-10T21:11:45.839Z', + updated_at: '2020-01-10T21:11:45.839Z', + created_by: 'elastic', + description: '24/7', + enabled: true, + false_positives: ['test'], + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + from: 'now-300s', + id, + immutable: false, + index: ['auditbeat-*'], + interval: '5m', + rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 21, + name: 'Query with rule-id', + query: 'user.name: root or user.name: admin', + references: ['www.test.co'], + saved_id: 'test123', + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + meta: { from: '0m' }, + severity: 'low', + updated_by: 'elastic', + tags: ['tag1', 'tag2'], + to: 'now', + type: 'saved_query', + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + note: '# this is some markdown documentation', + version: 1, +}); + +export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ + isNew, + name: 'Query with rule-id', + description: '24/7', + severity: 'low', + riskScore: 21, + references: ['www.test.co'], + falsePositives: ['test'], + tags: ['tag1', 'tag2'], + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Titled timeline', + }, + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + note: '# this is some markdown documentation', +}); + +export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ + isNew, + index: ['filebeat-'], + queryBar: mockQueryBar, +}); + +export const mockScheduleStepRule = (isNew = false, enabled = false): ScheduleStepRule => ({ + isNew, + enabled, + interval: '5m', + from: '6m', + to: 'now', +}); + export const mockRuleError = (id: string): RuleError => ({ rule_id: id, error: { status_code: 404, message: `id: "${id}" not found` }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000000..4d416e70a096 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap @@ -0,0 +1,453 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is "multi" 1`] = ` + + + , + "title": "Severity", + }, + Object { + "description": 21, + "title": "Risk score", + }, + Object { + "description": "Titled timeline", + "title": "Timeline template", + }, + ] + } + /> + + + +

+ , + "title": "Reference URLs", + }, + Object { + "description": +
    +
  • + test +
  • +
+
, + "title": "False positive examples", + }, + Object { + "description": + + + + + + + + + + + + + + , + "title": "MITRE ATT&CK™", + }, + Object { + "description": + + + tag1 + + + + + tag2 + + + , + "title": "Tags", + }, + Object { + "description": +
+ # this is some markdown documentation +
+
, + "title": "Investigation notes", + }, + ] + } + /> + + +`; + +exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is "single" 1`] = ` + + + , + "title": "Severity", + }, + Object { + "description": 21, + "title": "Risk score", + }, + Object { + "description": "Titled timeline", + "title": "Timeline template", + }, + Object { + "description": +
    +
  • + + www.test.co + +
  • +
+
, + "title": "Reference URLs", + }, + Object { + "description": +
    +
  • + test +
  • +
+
, + "title": "False positive examples", + }, + Object { + "description": + + + + + + + + + + + + + + , + "title": "MITRE ATT&CK™", + }, + Object { + "description": + + + tag1 + + + + + tag2 + + + , + "title": "Tags", + }, + Object { + "description": +
+ # this is some markdown documentation +
+
, + "title": "Investigation notes", + }, + ] + } + /> +
+
+`; + +exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is "singleSplit 1`] = ` + + + , + "title": "Severity", + }, + Object { + "description": 21, + "title": "Risk score", + }, + Object { + "description": "Titled timeline", + "title": "Timeline template", + }, + Object { + "description": +
    +
  • + + www.test.co + +
  • +
+
, + "title": "Reference URLs", + }, + Object { + "description": +
    +
  • + test +
  • +
+
, + "title": "False positive examples", + }, + Object { + "description": + + + + + + + + + + + + + + , + "title": "MITRE ATT&CK™", + }, + Object { + "description": + + + tag1 + + + + + tag2 + + + , + "title": "Tags", + }, + Object { + "description": +
+ # this is some markdown documentation +
+
, + "title": "Investigation notes", + }, + ] + } + type="column" + /> +
+
+`; 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 new file mode 100644 index 000000000000..56c9d6da1560 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx @@ -0,0 +1,403 @@ +/* + * 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 { shallow } from 'enzyme'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +import { coreMock } from '../../../../../../../../../../src/core/public/mocks'; +import { esFilters, FilterManager } from '../../../../../../../../../../src/plugins/data/public'; +import { SeverityBadge } from '../severity_badge'; + +import * as i18n from './translations'; +import { + isNotEmptyArray, + buildQueryBarDescription, + buildThreatDescription, + buildUnorderedListArrayDescription, + buildStringArrayDescription, + buildSeverityDescription, + buildUrlsDescription, + buildNoteDescription, +} from './helpers'; +import { ListItems } from './types'; + +const setupMock = coreMock.createSetup(); +const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case 'filters:pinnedByDefault': + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } +}; +setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); +const mockFilterManager = new FilterManager(setupMock.uiSettings); + +const mockQueryBar = { + query: { + query: 'test query', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', +}; + +describe('helpers', () => { + describe('isNotEmptyArray', () => { + test('returns false if empty array', () => { + const result = isNotEmptyArray([]); + expect(result).toBeFalsy(); + }); + + test('returns false if array of empty strings', () => { + const result = isNotEmptyArray(['', '']); + expect(result).toBeFalsy(); + }); + + test('returns true if array of string with space', () => { + const result = isNotEmptyArray([' ']); + expect(result).toBeTruthy(); + }); + + test('returns true if array with at least one non-empty string', () => { + const result = isNotEmptyArray(['', 'abc']); + expect(result).toBeTruthy(); + }); + }); + + describe('buildQueryBarDescription', () => { + test('returns empty array if no filters, query or savedId exist', () => { + const emptyMockQueryBar = { + query: { + query: '', + language: 'kuery', + }, + filters: [], + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: emptyMockQueryBar.filters, + filterManager: mockFilterManager, + query: emptyMockQueryBar.query, + savedId: emptyMockQueryBar.saved_id, + }); + expect(result).toEqual([]); + }); + + test('returns expected array of ListItems when filters exists, but no indexPatterns passed in', () => { + const mockQueryBarWithFilters = { + ...mockQueryBar, + query: { + query: '', + language: 'kuery', + }, + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithFilters.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithFilters.query, + savedId: mockQueryBarWithFilters.saved_id, + }); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} ); + expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); + }); + + test('returns expected array of ListItems when filters AND indexPatterns exist', () => { + const mockQueryBarWithFilters = { + ...mockQueryBar, + query: { + query: '', + language: 'kuery', + }, + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithFilters.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithFilters.query, + savedId: mockQueryBarWithFilters.saved_id, + indexPatterns: { fields: [{ name: 'test name', type: 'test type' }], title: 'test title' }, + }); + const wrapper = shallow(result[0].description as React.ReactElement); + const filterLabelComponent = wrapper.find(esFilters.FilterLabel).at(0); + + expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} ); + expect(filterLabelComponent.prop('valueLabel')).toEqual('file'); + expect(filterLabelComponent.prop('filter')).toEqual(mockQueryBar.filters[0]); + }); + + test('returns expected array of ListItems when "query.query" exists', () => { + const mockQueryBarWithQuery = { + ...mockQueryBar, + filters: [], + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithQuery.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithQuery.query, + savedId: mockQueryBarWithQuery.saved_id, + }); + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); + expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query.query} ); + }); + + test('returns expected array of ListItems when "savedId" exists', () => { + const mockQueryBarWithSavedId = { + ...mockQueryBar, + query: { + query: '', + language: 'kuery', + }, + filters: [], + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithSavedId.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithSavedId.query, + savedId: mockQueryBarWithSavedId.saved_id, + }); + expect(result[0].title).toEqual(<>{i18n.SAVED_ID_LABEL} ); + expect(result[0].description).toEqual(<>{mockQueryBarWithSavedId.saved_id} ); + }); + }); + + describe('buildThreatDescription', () => { + test('returns empty array if no threats', () => { + const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', threat: [] }); + expect(result).toHaveLength(0); + }); + + test('returns empty tactic link if no corresponding tactic id found', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual(''); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Audio Capture (T1123)' + ); + }); + + test('returns empty technique link if no corresponding technique id found', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123456' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual(''); + }); + + test('returns with corresponding tactic and technique link text', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Audio Capture (T1123)' + ); + }); + + test('returns corresponding number of tactic and technique links', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [ + { reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }, + { reference: 'https://test.com', name: 'Clipboard Data', id: 'T1115' }, + ], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + { + framework: 'MITRE ATTACK', + technique: [ + { reference: 'https://test.com', name: 'Automated Collection', id: 'T1119' }, + ], + tactic: { reference: 'https://test.com', name: 'Discovery', id: 'TA0007' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(wrapper.find('[data-test-subj="threatTacticLink"]')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]')).toHaveLength(3); + }); + }); + + describe('buildUnorderedListArrayDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildUnorderedListArrayDescription( + 'Test label', + 'falsePositives', + [] + ); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildUnorderedListArrayDescription( + 'Test label', + 'falsePositives', + ['', 'falsePositive1', 'falsePositive2'] + ); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="unorderedListArrayDescriptionItem"]')).toHaveLength(2); + }); + }); + + describe('buildStringArrayDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', []); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', [ + '', + 'tag1', + 'tag2', + ]); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="stringArrayDescriptionBadgeItem"]')).toHaveLength(2); + expect( + wrapper + .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') + .first() + .text() + ).toEqual('tag1'); + expect( + wrapper + .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') + .at(1) + .text() + ).toEqual('tag2'); + }); + }); + + describe('buildSeverityDescription', () => { + test('returns ListItem with passed in label and SeverityBadge component', () => { + const result: ListItems[] = buildSeverityDescription('Test label', 'Test description value'); + + expect(result[0].title).toEqual('Test label'); + expect(result[0].description).toEqual(); + }); + }); + + describe('buildUrlsDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildUrlsDescription('Test label', []); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildUrlsDescription('Test label', [ + 'www.test.com', + 'www.test2.com', + ]); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="urlsDescriptionReferenceLinkItem"]')).toHaveLength(2); + expect( + wrapper + .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') + .first() + .text() + ).toEqual('www.test.com'); + expect( + wrapper + .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') + .at(1) + .text() + ).toEqual('www.test2.com'); + }); + }); + + describe('buildNoteDescription', () => { + test('returns ListItem with passed in label and note content', () => { + const noteSample = + 'Cras mattism. [Pellentesque](https://elastic.co). ### Malesuada adipiscing tristique'; + const result: ListItems[] = buildNoteDescription('Test label', noteSample); + const wrapper = shallow(result[0].description as React.ReactElement); + const noteElement = wrapper.find('[data-test-subj="noteDescriptionItem"]').at(0); + + expect(result[0].title).toEqual('Test label'); + expect(noteElement.exists()).toBeTruthy(); + expect(noteElement.text()).toEqual(noteSample); + }); + + test('returns empty array if passed in note is empty string', () => { + const result: ListItems[] = buildNoteDescription('Test label', ''); + + expect(result).toHaveLength(0); + }); + }); +}); 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 df767fbd4ff8..bc454ecb1134 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 @@ -9,9 +9,10 @@ import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, - EuiLink, EuiButtonEmpty, EuiSpacer, + EuiLink, + EuiText, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; @@ -27,8 +28,12 @@ import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './t import { SeverityBadge } from '../severity_badge'; import ListTreeIcon from './assets/list_tree_icon.svg'; -const isNotEmptyArray = (values: string[]) => - !isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0; +const NoteDescriptionContainer = styled(EuiFlexItem)` + height: 105px; + overflow-y: hidden; +`; + +export const isNotEmptyArray = (values: string[]) => !isEmpty(values.join('')); const EuiBadgeWrap = styled(EuiBadge)` .euiBadge__text { @@ -106,13 +111,6 @@ const TechniqueLinkItem = styled(EuiButtonEmpty)` } `; -const ReferenceLinkItem = styled(EuiButtonEmpty)` - .euiIcon { - width: 12px; - height: 12px; - } -`; - export const buildThreatDescription = ({ label, threat }: BuildThreatDescription): ListItems[] => { if (threat.length > 0) { return [ @@ -124,7 +122,11 @@ export const buildThreatDescription = ({ label, threat }: BuildThreatDescription const tactic = tacticsOptions.find(t => t.id === singleThreat.tactic.id); return ( - + {tactic != null ? tactic.text : ''} @@ -133,6 +135,7 @@ export const buildThreatDescription = ({ label, threat }: BuildThreatDescription return ( - {values.map((val: string) => - isEmpty(val) ? null :
  • {val}
  • - )} - + +
      + {values.map(val => + isEmpty(val) ? null : ( +
    • + {val} +
    • + ) + )} +
    +
    ), }, ]; @@ -193,7 +202,9 @@ export const buildStringArrayDescription = ( {values.map((val: string) => isEmpty(val) ? null : ( - {val} + + {val} + ) )} @@ -218,21 +229,37 @@ export const buildUrlsDescription = (label: string, values: string[]): ListItems { title: label, description: ( - - {values.map((val: string) => ( - - - {val} - - - ))} - + +
      + {values + .filter(v => !isEmpty(v)) + .map((val, index) => ( +
    • + + {val} + +
    • + ))} +
    +
    + ), + }, + ]; + } + return []; +}; + +export const buildNoteDescription = (label: string, note: string): ListItems[] => { + if (note.trim() !== '') { + return [ + { + title: label, + description: ( + +
    + {note} +
    +
    ), }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx index 84c662dd0019..2c6f47fd27c4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx @@ -3,12 +3,88 @@ * 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 { shallow } from 'enzyme'; -import { addFilterStateIfNotThere } from './'; +import { + StepRuleDescriptionComponent, + addFilterStateIfNotThere, + buildListItems, + getDescriptionItem, +} from './'; -import { esFilters, Filter } from '../../../../../../../../../../src/plugins/data/public'; +import { + esFilters, + Filter, + FilterManager, +} from '../../../../../../../../../../src/plugins/data/public'; +import { mockAboutStepRule } from '../../all/__mocks__/mock'; +import { coreMock } from '../../../../../../../../../../src/core/public/mocks'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; +import * as i18n from './translations'; + +import { schema } from '../step_about_rule/schema'; +import { ListItems } from './types'; +import { AboutStepRule } from '../../types'; describe('description_step', () => { + const setupMock = coreMock.createSetup(); + const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case 'filters:pinnedByDefault': + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } + }; + let mockFilterManager: FilterManager; + let mockAboutStep: AboutStepRule; + + beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); + mockFilterManager = new FilterManager(setupMock.uiSettings); + mockAboutStep = mockAboutStepRule(); + }); + + describe('StepRuleDescriptionComponent', () => { + test('renders correctly against snapshot when columns is "multi"', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); + }); + + test('renders correctly against snapshot when columns is "single"', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); + }); + + test('renders correctly against snapshot when columns is "singleSplit', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); + expect( + wrapper + .find('[data-test-subj="singleSplitStepRuleDescriptionList"]') + .at(0) + .prop('type') + ).toEqual('column'); + }); + }); + describe('addFilterStateIfNotThere', () => { test('it does not change the state if it is global', () => { const filters: Filter[] = [ @@ -182,4 +258,221 @@ describe('description_step', () => { expect(output).toEqual(expected); }); }); + + describe('buildListItems', () => { + test('returns expected ListItems array when given valid inputs', () => { + const result: ListItems[] = buildListItems(mockAboutStep, schema, mockFilterManager); + + expect(result.length).toEqual(10); + }); + }); + + describe('getDescriptionItem', () => { + test('returns ListItem with all values enumerated when value[field] is an array', () => { + const result: ListItems[] = getDescriptionItem( + 'tags', + 'Tags label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Tags label'); + expect(typeof result[0].description).toEqual('object'); + }); + + test('returns ListItem with description of value[field] when value[field] is a string', () => { + const result: ListItems[] = getDescriptionItem( + 'description', + 'Description label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Description label'); + expect(result[0].description).toEqual('24/7'); + }); + + test('returns empty array when "value" is a non-existant property in "field"', () => { + const result: ListItems[] = getDescriptionItem( + 'jibberjabber', + 'JibberJabber label', + mockAboutStep, + mockFilterManager + ); + + expect(result.length).toEqual(0); + }); + + describe('queryBar', () => { + test('returns array of ListItems when queryBar exist', () => { + const mockQueryBar = { + isNew: false, + queryBar: { + query: { + query: 'user.name: root or user.name: admin', + language: 'kuery', + }, + filters: null, + saved_id: null, + }, + }; + const result: ListItems[] = getDescriptionItem( + 'queryBar', + 'Query bar label', + mockQueryBar, + mockFilterManager + ); + + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); + expect(result[0].description).toEqual(<>{mockQueryBar.queryBar.query.query} ); + }); + }); + + describe('threat', () => { + test('returns array of ListItems when threat exist', () => { + const result: ListItems[] = getDescriptionItem( + 'threat', + 'Threat label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Threat label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + + test('filters out threats with tactic.name of "none"', () => { + const mockStep = { + ...mockAboutStep, + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'none', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + const result: ListItems[] = getDescriptionItem( + 'threat', + 'Threat label', + mockStep, + mockFilterManager + ); + + expect(result.length).toEqual(0); + }); + }); + + describe('references', () => { + test('returns array of ListItems when references exist', () => { + const result: ListItems[] = getDescriptionItem( + 'references', + 'Reference label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Reference label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('falsePositives', () => { + test('returns array of ListItems when falsePositives exist', () => { + const result: ListItems[] = getDescriptionItem( + 'falsePositives', + 'False positives label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('False positives label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('severity', () => { + test('returns array of ListItems when severity exist', () => { + const result: ListItems[] = getDescriptionItem( + 'severity', + 'Severity label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Severity label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('riskScore', () => { + test('returns array of ListItems when riskScore exist', () => { + const result: ListItems[] = getDescriptionItem( + 'riskScore', + 'Risk score label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Risk score label'); + expect(result[0].description).toEqual(21); + }); + }); + + describe('timeline', () => { + test('returns timeline title if one exists', () => { + const result: ListItems[] = getDescriptionItem( + 'timeline', + 'Timeline label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Timeline label'); + expect(result[0].description).toEqual('Titled timeline'); + }); + + test('returns default timeline title if none exists', () => { + const mockStep = { + ...mockAboutStep, + timeline: { + id: '12345', + }, + }; + const result: ListItems[] = getDescriptionItem( + 'timeline', + 'Timeline label', + mockStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Timeline label'); + expect(result[0].description).toEqual(DEFAULT_TIMELINE_TITLE); + }); + }); + + describe('note', () => { + test('returns default "note" description', () => { + const result: ListItems[] = getDescriptionItem( + 'note', + 'Investigation notes', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Investigation notes'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + }); }); 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 cb5c98bb23f0..1d58ef801489 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 @@ -7,6 +7,7 @@ import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty, chunk, get, pick } from 'lodash/fp'; import React, { memo, useState } from 'react'; +import styled from 'styled-components'; import { IIndexPattern, @@ -28,18 +29,28 @@ import { buildThreatDescription, buildUnorderedListArrayDescription, buildUrlsDescription, + buildNoteDescription, } from './helpers'; +const DescriptionListContainer = styled(EuiDescriptionList)` + &.euiDescriptionList--column .euiDescriptionList__title { + width: 30%; + } + &.euiDescriptionList--column .euiDescriptionList__description { + width: 70%; + } +`; + interface StepRuleDescriptionProps { - direction?: 'row' | 'column'; + columns?: 'multi' | 'single' | 'singleSplit'; data: unknown; indexPatterns?: IIndexPattern; schema: FormSchema; } -const StepRuleDescriptionComponent: React.FC = ({ +export const StepRuleDescriptionComponent: React.FC = ({ data, - direction = 'row', + columns = 'multi', indexPatterns, schema, }) => { @@ -55,11 +66,14 @@ const StepRuleDescriptionComponent: React.FC = ({ [] ); - if (direction === 'row') { + if (columns === 'multi') { return ( {chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => ( - + ))} @@ -69,8 +83,16 @@ const StepRuleDescriptionComponent: React.FC = ({ return ( - - + + {columns === 'single' ? ( + + ) : ( + + )} ); @@ -78,7 +100,7 @@ const StepRuleDescriptionComponent: React.FC = ({ export const StepRuleDescription = memo(StepRuleDescriptionComponent); -const buildListItems = ( +export const buildListItems = ( data: unknown, schema: FormSchema, filterManager: FilterManager, @@ -108,7 +130,7 @@ export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => { }); }; -const getDescriptionItem = ( +export const getDescriptionItem = ( field: string, label: string, value: unknown, @@ -132,13 +154,6 @@ const getDescriptionItem = ( (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' ); return buildThreatDescription({ label, threat }); - } else if (field === 'description') { - return [ - { - title: label, - description: get(field, value), - }, - ]; } else if (field === 'references') { const urls: string[] = get(field, value); return buildUrlsDescription(label, urls); @@ -166,14 +181,9 @@ const getDescriptionItem = ( description: timeline.title ?? DEFAULT_TIMELINE_TITLE, }, ]; - } else if (field === 'riskScore') { - const description: string = get(field, value); - return [ - { - title: label, - description, - }, - ]; + } else if (field === 'note') { + const val: string = get(field, value); + return buildNoteDescription(label, val); } const description: string = get(field, value); if (!isEmpty(description)) { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts index d15cce15877b..417133f23061 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts @@ -29,4 +29,5 @@ export const stepAboutDefaultValue: AboutStepRule = { title: DEFAULT_TIMELINE_TITLE, }, threat: threatDefault, + note: '', }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx new file mode 100644 index 000000000000..0ed479e23515 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx @@ -0,0 +1,155 @@ +/* + * 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 { mount, shallow } from 'enzyme'; +import { ThemeProvider } from 'styled-components'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { StepAboutRule } from './'; +import { mockAboutStepRule } from '../../all/__mocks__/mock'; +import { StepRuleDescription } from '../description_step'; +import { stepAboutDefaultValue } from './default_value'; + +const theme = () => ({ eui: euiDarkVars, darkMode: true }); + +describe('StepAboutRuleComponent', () => { + test('it renders StepRuleDescription if isReadOnlyView is true and "name" property exists', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(StepRuleDescription).exists()).toBeTruthy(); + }); + + test('it prevents user from clicking continue if no "description" defined', () => { + const wrapper = mount( + + + + ); + + const nameInput = wrapper + .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') + .at(0); + nameInput.simulate('change', { target: { value: 'Test name text' } }); + + const descriptionInput = wrapper + .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') + .at(0); + const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); + nextButton.simulate('click'); + + expect( + wrapper + .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') + .at(0) + .props().value + ).toEqual('Test name text'); + expect(descriptionInput.props().value).toEqual(''); + expect( + wrapper + .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleDescription"] label') + .at(0) + .hasClass('euiFormLabel-isInvalid') + ).toBeTruthy(); + expect( + wrapper + .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleDescription"] EuiTextArea') + .at(0) + .prop('isInvalid') + ).toBeTruthy(); + }); + + test('it prevents user from clicking continue if no "name" defined', () => { + const wrapper = mount( + + + + ); + + const descriptionInput = wrapper + .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') + .at(0); + descriptionInput.simulate('change', { target: { value: 'Test description text' } }); + + const nameInput = wrapper + .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') + .at(0); + const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); + nextButton.simulate('click'); + + expect( + wrapper + .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') + .at(0) + .props().value + ).toEqual('Test description text'); + expect(nameInput.props().value).toEqual(''); + expect( + wrapper + .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleName"] label') + .at(0) + .hasClass('euiFormLabel-isInvalid') + ).toBeTruthy(); + expect( + wrapper + .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleName"] EuiFieldText') + .at(0) + .prop('isInvalid') + ).toBeTruthy(); + }); + + test('it allows user to click continue if "name" and "description" are defined', () => { + const wrapper = mount( + + + + ); + + const descriptionInput = wrapper + .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') + .at(0); + descriptionInput.simulate('change', { target: { value: 'Test description text' } }); + + const nameInput = wrapper + .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') + .at(0); + nameInput.simulate('change', { target: { value: 'Test name text' } }); + + const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); + nextButton.simulate('click'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index 4f06d4314c1f..bfb123f3f320 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -39,6 +39,7 @@ import { schema } from './schema'; import * as I18n from './translations'; import { PickTimeline } from '../pick_timeline'; import { StepContentWrapper } from '../step_content_wrapper'; +import { MarkdownEditorForm } from '../../../../../components/markdown_editor/form'; const CommonUseField = getUseField({ component: Field }); @@ -46,6 +47,12 @@ interface StepAboutRuleProps extends RuleStepProps { defaultValues?: AboutStepRule | null; } +const ThreeQuartersContainer = styled.div` + max-width: 740px; +`; + +ThreeQuartersContainer.displayName = 'ThreeQuartersContainer'; + const TagContainer = styled.div` margin-top: 16px; `; @@ -75,7 +82,7 @@ const AdvancedSettingsAccordionButton = ( const StepAboutRuleComponent: FC = ({ addPadding = false, defaultValues, - descriptionDirection = 'row', + descriptionColumns = 'singleSplit', isReadOnlyView, isUpdateView = false, isLoading, @@ -120,68 +127,74 @@ const StepAboutRuleComponent: FC = ({ }, [form]); return isReadOnlyView && myStepData.name != null ? ( - - + + ) : ( <>
    - - + + + - - - - - - - - + + + + + + + + + + + = ({ dataTestSubj: 'detectionEngineStepAboutRuleMitreThreat', }} /> + + + + {({ severity }) => { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index 42cf1e0d9564..7c1ab09b7309 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -95,7 +95,14 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel', { - defaultMessage: 'Investigate detections using this timeline template', + defaultMessage: 'Timeline template', + } + ), + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateHelpText', + { + defaultMessage: + 'Select an existing timeline to use as a template when investigating generated signals.', } ), }, @@ -184,4 +191,15 @@ export const schema: FormSchema = { ), labelAppend: OptionalFieldLabel, }, + note: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.noteLabel', { + defaultMessage: 'Investigation notes', + }), + helpText: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.noteHelpText', { + defaultMessage: + 'Provide helpful information for analysts that are performing a signal investigation. These notes will appear on both the rule details page and in timelines created from signals generated by this rule.', + }), + labelAppend: OptionalFieldLabel, + }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts index 3b6680fd4e68..dfa60268e903 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts @@ -68,3 +68,10 @@ export const URL_FORMAT_INVALID = i18n.translate( defaultMessage: 'Url is invalid format', } ); + +export const ADD_RULE_NOTE_HELP_TEXT = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutrule.noteHelpText', + { + defaultMessage: 'Add rule investigation notes...', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx new file mode 100644 index 000000000000..4a4e96ec7490 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx @@ -0,0 +1,175 @@ +/* + * 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 { mount, shallow } from 'enzyme'; +import { EuiProgress, EuiButtonGroup } from '@elastic/eui'; +import { ThemeProvider } from 'styled-components'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { StepAboutRuleToggleDetails } from './'; +import { mockAboutStepRule } from '../../all/__mocks__/mock'; +import { HeaderSection } from '../../../../../components/header_section'; +import { StepAboutRule } from '../step_about_rule/'; +import { AboutStepRule } from '../../types'; + +const theme = () => ({ eui: euiDarkVars, darkMode: true }); + +describe('StepAboutRuleToggleDetails', () => { + let mockRule: AboutStepRule; + + beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + mockRule = mockAboutStepRule(); + }); + + test('it renders loading component when "loading" is true', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiProgress).exists()).toBeTruthy(); + expect(wrapper.find(HeaderSection).exists()).toBeTruthy(); + }); + + test('it does not render details if stepDataDetails is null', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(StepAboutRule).exists()).toBeFalsy(); + }); + + test('it does not render details if stepData is null', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(StepAboutRule).exists()).toBeFalsy(); + }); + + describe('note value is empty string', () => { + test('it does not render toggle buttons', () => { + const mockAboutStepWithoutNote = { + ...mockRule, + note: '', + }; + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="stepAboutDetailsToggle"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="stepAboutDetailsNoteContent"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="stepAboutDetailsContent"]').exists()).toBeTruthy(); + }); + }); + + describe('note value does exist', () => { + test('it renders toggle buttons, defaulted to "details"', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(EuiButtonGroup).exists()).toBeTruthy(); + expect( + wrapper + .find('EuiButtonToggle[id="details"]') + .at(0) + .prop('isSelected') + ).toBeTruthy(); + expect( + wrapper + .find('EuiButtonToggle[id="notes"]') + .at(0) + .prop('isSelected') + ).toBeFalsy(); + }); + + test('it allows users to toggle between "details" and "note"', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('EuiButtonGroup[idSelected="details"]').exists()).toBeTruthy(); + expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeFalsy(); + + wrapper + .find('input[title="Investigation notes"]') + .at(0) + .simulate('change', { target: { value: 'notes' } }); + + expect(wrapper.find('EuiButtonGroup[idSelected="details"]').exists()).toBeFalsy(); + expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy(); + }); + + test('it displays notes markdown when user toggles to "notes"', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('input[title="Investigation notes"]') + .at(0) + .simulate('change', { target: { value: 'notes' } }); + + expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy(); + expect(wrapper.find('Markdown h1').text()).toEqual('this is some markdown documentation'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx new file mode 100644 index 000000000000..c61566cb841e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiPanel, + EuiProgress, + EuiButtonGroup, + EuiButtonGroupOption, + EuiSpacer, + EuiFlexItem, + EuiText, + EuiFlexGroup, + EuiResizeObserver, +} from '@elastic/eui'; +import React, { memo, useState } from 'react'; +import styled from 'styled-components'; +import { isEmpty } from 'lodash/fp'; + +import { HeaderSection } from '../../../../../components/header_section'; +import { Markdown } from '../../../../../components/markdown'; +import { AboutStepRule, AboutStepRuleDetails } from '../../types'; +import * as i18n from './translations'; +import { StepAboutRule } from '../step_about_rule/'; + +const MyPanel = styled(EuiPanel)` + position: relative; +`; + +const FlexGroupFullHeight = styled(EuiFlexGroup)` + height: 100%; +`; + +const VerticalOverflowContainer = styled.div((props: { maxHeight: number }) => ({ + 'max-height': `${props.maxHeight}px`, + 'overflow-y': 'hidden', +})); + +const VerticalOverflowContent = styled.div((props: { maxHeight: number }) => ({ + 'max-height': `${props.maxHeight}px`, +})); + +const AboutContent = styled.div` + height: 100%; +`; + +const toggleOptions: EuiButtonGroupOption[] = [ + { + id: 'details', + label: i18n.ABOUT_PANEL_DETAILS_TAB, + }, + { + id: 'notes', + label: i18n.ABOUT_PANEL_NOTES_TAB, + }, +]; + +interface StepPanelProps { + stepData: AboutStepRule | null; + stepDataDetails: AboutStepRuleDetails | null; + loading: boolean; +} + +const StepAboutRuleToggleDetailsComponent: React.FC = ({ + stepData, + stepDataDetails, + loading, +}) => { + const [selectedToggleOption, setToggleOption] = useState('details'); + const [aboutPanelHeight, setAboutPanelHeight] = useState(0); + + const onResize = (e: { height: number; width: number }) => { + setAboutPanelHeight(e.height); + }; + + return ( + + {loading && ( + <> + + + + )} + {stepData != null && stepDataDetails != null && ( + + + + {!isEmpty(stepDataDetails.note) && stepDataDetails.note.trim() !== '' && ( + { + setToggleOption(val); + }} + data-test-subj="stepAboutDetailsToggle" + /> + )} + + + + {selectedToggleOption === 'details' ? ( + + {resizeRef => ( + + + + + {stepDataDetails.description} + + + + + + + )} + + ) : ( + + + + + + )} + + + )} + + ); +}; + +export const StepAboutRuleToggleDetails = memo(StepAboutRuleToggleDetailsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts new file mode 100644 index 000000000000..fa725366210d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.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 { i18n } from '@kbn/i18n'; + +export const ABOUT_PANEL_DETAILS_TAB = i18n.translate( + 'xpack.siem.detectionEngine.details.stepAboutRule.detailsLabel', + { + defaultMessage: 'Details', + } +); + +export const ABOUT_TEXT = i18n.translate( + 'xpack.siem.detectionEngine.details.stepAboutRule.aboutText', + { + defaultMessage: 'About', + } +); + +export const ABOUT_PANEL_NOTES_TAB = i18n.translate( + 'xpack.siem.detectionEngine.details.stepAboutRule.investigationNotesLabel', + { + defaultMessage: 'Investigation notes', + } +); 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 490a8d9d194c..2327ac36a590 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 @@ -87,7 +87,7 @@ const getStepDefaultValue = ( const StepDefineRuleComponent: FC = ({ addPadding = false, defaultValues, - descriptionDirection = 'row', + descriptionColumns = 'singleSplit', isReadOnlyView, isLoading, isUpdateView = false, @@ -155,9 +155,9 @@ const StepDefineRuleComponent: FC = ({ }, []); return isReadOnlyView && myStepData?.queryBar != null ? ( - + = ({ addPadding = false, defaultValues, - descriptionDirection = 'row', + descriptionColumns = 'singleSplit', isReadOnlyView, isLoading, isUpdateView = false, @@ -80,31 +85,35 @@ const StepScheduleRuleComponent: FC = ({ return isReadOnlyView && myStepData != null ? ( - + ) : ( <> - - + + + + + + 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 new file mode 100644 index 000000000000..dbc5dd9bbe29 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts @@ -0,0 +1,589 @@ +/* + * 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 { NewRule } from '../../../../containers/detection_engine/rules'; +import { + DefineStepRuleJson, + ScheduleStepRuleJson, + AboutStepRuleJson, + AboutStepRule, + ScheduleStepRule, + DefineStepRule, +} from '../types'; +import { + getTimeTypeValue, + formatDefineStepData, + formatScheduleStepData, + formatAboutStepData, + formatRule, +} from './helpers'; +import { + mockDefineStepRule, + mockQueryBar, + mockScheduleStepRule, + mockAboutStepRule, +} from '../all/__mocks__/mock'; + +describe('helpers', () => { + describe('getTimeTypeValue', () => { + test('returns timeObj with value 0 if no time value found', () => { + const result = getTimeTypeValue('m'); + + expect(result).toEqual({ unit: 'm', value: 0 }); + }); + + test('returns timeObj with unit set to empty string if no expected time type found', () => { + const result = getTimeTypeValue('5l'); + + expect(result).toEqual({ unit: '', value: 5 }); + }); + + test('returns timeObj with unit of s and value 5 when time is 5s ', () => { + const result = getTimeTypeValue('5s'); + + expect(result).toEqual({ unit: 's', value: 5 }); + }); + + test('returns timeObj with unit of m and value 5 when time is 5m ', () => { + const result = getTimeTypeValue('5m'); + + expect(result).toEqual({ unit: 'm', value: 5 }); + }); + + test('returns timeObj with unit of h and value 5 when time is 5h ', () => { + const result = getTimeTypeValue('5h'); + + expect(result).toEqual({ unit: 'h', value: 5 }); + }); + + test('returns timeObj with value of 5 when time is float like 5.6m ', () => { + const result = getTimeTypeValue('5m'); + + expect(result).toEqual({ unit: 'm', value: 5 }); + }); + + test('returns timeObj with value of 0 and unit of "" if random string passed in', () => { + const result = getTimeTypeValue('random'); + + expect(result).toEqual({ unit: '', value: 0 }); + }); + }); + + describe('formatDefineStepData', () => { + let mockData: DefineStepRule; + + beforeEach(() => { + mockData = mockDefineStepRule(); + }); + + test('returns formatted object as DefineStepRuleJson', () => { + const result: DefineStepRuleJson = formatDefineStepData(mockData); + const expected = { + language: 'kuery', + filters: mockQueryBar.filters, + query: 'test query', + saved_id: 'test123', + index: ['filebeat-'], + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with no saved_id if no savedId provided', () => { + const mockStepData = { + ...mockData, + queryBar: { + ...mockData.queryBar, + saved_id: '', + }, + }; + const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + const expected = { + language: 'kuery', + filters: mockQueryBar.filters, + query: 'test query', + index: ['filebeat-'], + }; + + expect(result).toEqual(expected); + }); + }); + + describe('formatScheduleStepData', () => { + let mockData: ScheduleStepRule; + + beforeEach(() => { + mockData = mockScheduleStepRule(); + }); + + test('returns formatted object as ScheduleStepRuleJson', () => { + const result: ScheduleStepRuleJson = formatScheduleStepData(mockData); + const expected = { + enabled: false, + from: 'now-660s', + to: 'now', + interval: '5m', + meta: { + from: '6m', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with "to" as "now" if "to" not supplied', () => { + const mockStepData = { + ...mockData, + }; + delete mockStepData.to; + const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); + const expected = { + enabled: false, + from: 'now-660s', + to: 'now', + interval: '5m', + meta: { + from: '6m', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with "to" as "now" if "to" random string', () => { + const mockStepData = { + ...mockData, + to: 'random', + }; + const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); + const expected = { + enabled: false, + from: 'now-660s', + to: 'now', + interval: '5m', + meta: { + from: '6m', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object if "from" random string', () => { + const mockStepData = { + ...mockData, + from: 'random', + }; + const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); + const expected = { + enabled: false, + from: 'now-300s', + to: 'now', + interval: '5m', + meta: { + from: 'random', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object if "interval" random string', () => { + const mockStepData = { + ...mockData, + interval: 'random', + }; + const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); + const expected = { + enabled: false, + from: 'now-360s', + to: 'now', + interval: 'random', + meta: { + from: '6m', + }, + }; + + expect(result).toEqual(expected); + }); + }); + + describe('formatAboutStepData', () => { + let mockData: AboutStepRule; + + beforeEach(() => { + mockData = mockAboutStepRule(); + }); + + test('returns formatted object as AboutStepRuleJson', () => { + const result: AboutStepRuleJson = formatAboutStepData(mockData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with empty falsePositive and references filtered out', () => { + const mockStepData = { + ...mockData, + falsePositives: ['', 'test', ''], + references: ['www.test.co', ''], + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object without note if note is empty string', () => { + const mockStepData = { + ...mockData, + note: '', + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + references: ['www.test.co'], + risk_score: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object without timeline_id and timeline_title if timeline.id is null', () => { + const mockStepData = { + ...mockData, + }; + delete mockStepData.timeline.id; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with timeline_id and timeline_title if timeline.id is "', () => { + const mockStepData = { + ...mockData, + timeline: { + ...mockData.timeline, + id: '', + }, + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + timeline_id: '', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object without timeline_id and timeline_title if timeline.title is null', () => { + const mockStepData = { + ...mockData, + timeline: { + ...mockData.timeline, + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + }, + }; + delete mockStepData.timeline.title; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with timeline_id and timeline_title if timeline.title is "', () => { + const mockStepData = { + ...mockData, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: '', + }, + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: '1234', name: 'tactic1', reference: 'reference1' }, + technique: [{ id: '456', name: 'technique1', reference: 'technique reference' }], + }, + ], + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: '', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with threats filtered out where tactic.name is "none"', () => { + const mockStepData = { + ...mockData, + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'none', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: '1234', name: 'tactic1', reference: 'reference1' }, + technique: [{ id: '456', name: 'technique1', reference: 'technique reference' }], + }, + ], + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + }); + + describe('formatRule', () => { + let mockAbout: AboutStepRule; + let mockDefine: DefineStepRule; + let mockSchedule: ScheduleStepRule; + + beforeEach(() => { + mockAbout = mockAboutStepRule(); + mockDefine = mockDefineStepRule(); + mockSchedule = mockScheduleStepRule(); + }); + + test('returns NewRule with type of saved_query when saved_id exists', () => { + const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule); + + expect(result.type).toEqual('saved_query'); + }); + + test('returns NewRule with type of query when saved_id does not exist', () => { + const mockDefineStepRuleWithoutSavedId = { + ...mockDefine, + queryBar: { + ...mockDefine.queryBar, + saved_id: '', + }, + }; + const result: NewRule = formatRule(mockDefineStepRuleWithoutSavedId, mockAbout, mockSchedule); + + 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); + + expect(result.id).toBeUndefined(); + }); + }); +}); 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 de6678b42df6..07578e870bf2 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 @@ -19,7 +19,7 @@ import { FormatRuleType, } from '../types'; -const getTimeTypeValue = (time: string): { unit: string; value: number } => { +export const getTimeTypeValue = (time: string): { unit: string; value: number } => { const timeObj = { unit: '', value: 0, @@ -39,7 +39,7 @@ const getTimeTypeValue = (time: string): { unit: string; value: number } => { return timeObj; }; -const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { +export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { const { queryBar, isNew, ...rest } = defineStepData; const { filters, query, saved_id: savedId } = queryBar; return { @@ -51,7 +51,7 @@ const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJso }; }; -const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { +export const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { const { isNew, ...formatScheduleData } = scheduleData; if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) { const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue( @@ -71,8 +71,17 @@ const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRul }; }; -const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { - const { falsePositives, references, riskScore, threat, timeline, isNew, ...rest } = aboutStepData; +export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { + const { + falsePositives, + references, + riskScore, + threat, + timeline, + isNew, + note, + ...rest + } = aboutStepData; return { false_positives: falsePositives.filter(item => !isEmpty(item)), references: references.filter(item => !isEmpty(item)), @@ -93,6 +102,7 @@ const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => return { id, name, reference }; }), })), + ...(!isEmpty(note) ? { note } : {}), ...rest, }; }; 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 d816c7e86705..c9f44ab0048f 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 @@ -286,7 +286,7 @@ const CreateRulePageComponent: React.FC = () => { isLoading={isLoading || loading} setForm={setStepsForm} setStepData={setStepData} - descriptionDirection="row" + descriptionColumns="singleSplit" /> @@ -315,7 +315,7 @@ const CreateRulePageComponent: React.FC = () => { { defaultValues={ (stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule) ?? null } - descriptionDirection="row" + descriptionColumns="singleSplit" isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.scheduleRule]} isLoading={isLoading || loading} setForm={setStepsForm} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index e73852ec9128..a35caf4acf67 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -38,13 +38,13 @@ import { } from '../../../../containers/source'; import { SpyRoute } from '../../../../utils/route/spy_routes'; +import { StepAboutRuleToggleDetails } from '../components/step_about_rule_details/'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; import { SignalsHistogramPanel } from '../../components/signals_histogram_panel'; import { SignalsTable } from '../../components/signals'; import { useUserInfo } from '../../components/user_info'; import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page'; import { useSignalInfo } from '../../components/signals_info'; -import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; import { buildSignalsRuleIdFilter } from '../../components/signals/default_config'; @@ -105,13 +105,15 @@ const RuleDetailsPageComponent: FC = ({ // This is used to re-trigger api rule status when user de/activate rule const [ruleEnabled, setRuleEnabled] = useState(null); const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.signals); - const { aboutRuleData, defineRuleData, scheduleRuleData } = + const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = rule != null - ? getStepsData({ - rule, - detailsView: true, - }) - : { aboutRuleData: null, defineRuleData: null, scheduleRuleData: null }; + ? getStepsData({ rule, detailsView: true }) + : { + aboutRuleData: null, + modifiedAboutRuleDetailsData: null, + defineRuleData: null, + scheduleRuleData: null, + }; const [lastSignals] = useSignalInfo({ ruleId }); const userHasNoPermissions = canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; @@ -291,16 +293,23 @@ const RuleDetailsPageComponent: FC = ({
    {ruleError} - {tabs} - {ruleDetailTab === RuleDetailTabs.signals && ( - <> - + + + + + + + {defineRuleData != null && ( = ({ )} - - - - {aboutRuleData != null && ( - - )} - - - + {scheduleRuleData != null && ( = ({ - + + + + {tabs} + + {ruleDetailTab === RuleDetailTabs.signals && ( + <> { + describe('getStepsData', () => { + test('returns object with about, define, and schedule step properties formatted', () => { + const { + defineRuleData, + modifiedAboutRuleDetailsData, + aboutRuleData, + scheduleRuleData, + }: GetStepsData = getStepsData({ + rule: mockRuleWithEverything('test-id'), + }); + const defineRuleStepData = { + isNew: false, + index: ['auditbeat-*'], + queryBar: { + query: { + query: 'user.name: root or user.name: admin', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', + }, + }; + const aboutRuleStepData = { + description: '24/7', + falsePositives: ['test'], + isNew: false, + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + riskScore: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Titled timeline', + }, + }; + const scheduleRuleStepData = { enabled: true, from: '0s', interval: '5m', isNew: false }; + const aboutRuleDataDetailsData = { + note: '# this is some markdown documentation', + description: '24/7', + }; + + expect(defineRuleData).toEqual(defineRuleStepData); + expect(aboutRuleData).toEqual(aboutRuleStepData); + expect(scheduleRuleData).toEqual(scheduleRuleStepData); + expect(modifiedAboutRuleDetailsData).toEqual(aboutRuleDataDetailsData); + }); + }); + + describe('getAboutStepsData', () => { + test('returns timeline id and title of null if they do not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.timeline_id; + delete mockedRule.timeline_title; + const result: AboutStepRule = getAboutStepsData(mockedRule, false); + + expect(result.timeline.id).toBeNull(); + expect(result.timeline.title).toBeNull(); + }); + + test('returns name, description, and note as empty string if detailsView is true', () => { + const result: AboutStepRule = getAboutStepsData(mockRuleWithEverything('test-id'), true); + + expect(result.name).toEqual(''); + expect(result.description).toEqual(''); + expect(result.note).toEqual(''); + }); + + test('returns note as empty string if property does not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.note; + const result: AboutStepRule = getAboutStepsData(mockedRule, false); + + expect(result.note).toEqual(''); + }); + }); + + describe('determineDetailsValue', () => { + test('returns name, description, and note as empty string if detailsView is true', () => { + const result: Pick = determineDetailsValue( + mockRuleWithEverything('test-id'), + true + ); + const expected = { name: '', description: '', note: '' }; + + expect(result).toEqual(expected); + }); + + test('returns name, description, and note values if detailsView is false', () => { + const mockedRule = mockRuleWithEverything('test-id'); + const result: Pick = determineDetailsValue( + mockedRule, + false + ); + const expected = { + name: mockedRule.name, + description: mockedRule.description, + note: mockedRule.note, + }; + + expect(result).toEqual(expected); + }); + + test('returns note as empty string if property does not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.note; + const result: Pick = determineDetailsValue( + mockedRule, + false + ); + const expected = { name: mockedRule.name, description: mockedRule.description, note: '' }; + + expect(result).toEqual(expected); + }); + }); + + describe('getDefineStepsData', () => { + test('returns with saved_id if value exists on rule', () => { + const result: DefineStepRule = getDefineStepsData(mockRule('test-id')); + const expected = { + isNew: false, + index: ['auditbeat-*'], + queryBar: { + query: { + query: '', + language: 'kuery', + }, + filters: [], + saved_id: "Garrett's IP", + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns with saved_id of null if value does not exist on rule', () => { + const mockedRule = { + ...mockRule('test-id'), + }; + delete mockedRule.saved_id; + const result: DefineStepRule = getDefineStepsData(mockedRule); + const expected = { + isNew: false, + index: ['auditbeat-*'], + queryBar: { + query: { + query: '', + language: 'kuery', + }, + filters: [], + saved_id: null, + }, + }; + + expect(result).toEqual(expected); + }); + }); + + describe('getHumanizedDuration', () => { + test('returns from as seconds if from duration is less than a minute', () => { + const result = getHumanizedDuration('now-62s', '1m'); + + expect(result).toEqual('2s'); + }); + + test('returns from as minutes if from duration is less than an hour', () => { + const result = getHumanizedDuration('now-660s', '5m'); + + expect(result).toEqual('6m'); + }); + + test('returns from as hours if from duration is more than 60 minutes', () => { + const result = getHumanizedDuration('now-7400s', '5m'); + + expect(result).toEqual('1h'); + }); + + test('returns from as if from is not parsable as dateMath', () => { + const result = getHumanizedDuration('randomstring', '5m'); + + expect(result).toEqual('NaNh'); + }); + + test('returns from as 5m if interval is not parsable as dateMath', () => { + const result = getHumanizedDuration('now-300s', 'randomstring'); + + expect(result).toEqual('5m'); + }); + }); + + describe('getScheduleStepsData', () => { + test('returns expected ScheduleStep rule object', () => { + const mockedRule = { + ...mockRule('test-id'), + }; + const result: ScheduleStepRule = getScheduleStepsData(mockedRule); + const expected = { + isNew: false, + enabled: mockedRule.enabled, + interval: mockedRule.interval, + from: '0s', + }; + + expect(result).toEqual(expected); + }); + }); + + describe('getModifiedAboutDetailsData', () => { + test('returns object with "note" and "description" being those of passed in rule', () => { + const result: AboutStepRuleDetails = getModifiedAboutDetailsData( + mockRuleWithEverything('test-id') + ); + const aboutRuleDataDetailsData = { + note: '# this is some markdown documentation', + description: '24/7', + }; + + expect(result).toEqual(aboutRuleDataDetailsData); + }); + + test('returns "note" with empty string if "note" does not exist', () => { + const { note, ...mockRuleWithoutNote } = { ...mockRuleWithEverything('test-id') }; + const result: AboutStepRuleDetails = getModifiedAboutDetailsData(mockRuleWithoutNote); + + const aboutRuleDetailsData = { note: '', description: mockRuleWithoutNote.description }; + + expect(result).toEqual(aboutRuleDetailsData); + }); + }); +}); 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 85f3bcbd236e..1fc8a86a476f 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 @@ -5,19 +5,26 @@ */ import dateMath from '@elastic/datemath'; -import { get, pick } from 'lodash/fp'; +import { get } from 'lodash/fp'; 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 { FormData, FormHook, FormSchema } from '../../../shared_imports'; -import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types'; +import { + AboutStepRule, + AboutStepRuleDetails, + DefineStepRule, + IMitreEnterpriseAttack, + ScheduleStepRule, +} from './types'; -interface GetStepsData { - aboutRuleData: AboutStepRule | null; - defineRuleData: DefineStepRule | null; - scheduleRuleData: ScheduleStepRule | null; +export interface GetStepsData { + aboutRuleData: AboutStepRule; + modifiedAboutRuleDetailsData: AboutStepRuleDetails; + defineRuleData: DefineStepRule; + scheduleRuleData: ScheduleStepRule; } export const getStepsData = ({ @@ -27,58 +34,107 @@ export const getStepsData = ({ rule: Rule; detailsView?: boolean; }): GetStepsData => { - const defineRuleData: DefineStepRule | null = - rule != null - ? { - isNew: false, - index: rule.index, - queryBar: { - query: { query: rule.query as string, language: rule.language }, - filters: rule.filters as Filter[], - saved_id: rule.saved_id ?? null, - }, - } - : null; - const aboutRuleData: AboutStepRule | null = - rule != null - ? { - isNew: false, - ...pick(['description', 'name', 'references', 'severity', 'tags', 'threat'], rule), - ...(detailsView ? { name: '' } : {}), - threat: rule.threat as IMitreEnterpriseAttack[], - falsePositives: rule.false_positives, - riskScore: rule.risk_score, - timeline: { - id: rule.timeline_id ?? null, - title: rule.timeline_title ?? null, - }, - } - : null; - - const from = dateMath.parse(rule.from) ?? moment(); - const interval = dateMath.parse(`now-${rule.interval}`) ?? moment(); - - const fromDuration = moment.duration(interval.diff(from)); - let fromHumanize = `${Math.floor(fromDuration.asHours())}h`; + const defineRuleData: DefineStepRule = getDefineStepsData(rule); + const aboutRuleData: AboutStepRule = getAboutStepsData(rule, detailsView); + const modifiedAboutRuleDetailsData: AboutStepRuleDetails = getModifiedAboutDetailsData(rule); + const scheduleRuleData: ScheduleStepRule = getScheduleStepsData(rule); + + return { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData }; +}; + +export const getDefineStepsData = (rule: Rule): DefineStepRule => { + const { index, query, language, filters, saved_id: savedId } = rule; + + return { + isNew: false, + index, + queryBar: { + query: { + query, + language, + }, + filters: filters as Filter[], + saved_id: savedId ?? null, + }, + }; +}; + +export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => { + const { enabled, interval, from } = rule; + const fromHumanizedValue = getHumanizedDuration(from, interval); + + return { + isNew: false, + enabled, + interval, + from: fromHumanizedValue, + }; +}; + +export const getHumanizedDuration = (from: string, interval: string): string => { + const fromValue = dateMath.parse(from) ?? moment(); + const intervalValue = dateMath.parse(`now-${interval}`) ?? moment(); + + const fromDuration = moment.duration(intervalValue.diff(fromValue)); + const fromHumanize = `${Math.floor(fromDuration.asHours())}h`; if (fromDuration.asSeconds() < 60) { - fromHumanize = `${Math.floor(fromDuration.asSeconds())}s`; + return `${Math.floor(fromDuration.asSeconds())}s`; } else if (fromDuration.asMinutes() < 60) { - fromHumanize = `${Math.floor(fromDuration.asMinutes())}m`; + return `${Math.floor(fromDuration.asMinutes())}m`; } - const scheduleRuleData: ScheduleStepRule | null = - rule != null - ? { - isNew: false, - ...pick(['enabled', 'interval'], rule), - from: fromHumanize, - } - : null; + return fromHumanize; +}; + +export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRule => { + const { name, description, note } = determineDetailsValue(rule, detailsView); + const { + references, + severity, + false_positives: falsePositives, + risk_score: riskScore, + tags, + threat, + timeline_id: timelineId, + timeline_title: timelineTitle, + } = rule; + + return { + isNew: false, + name, + description, + note: note!, + references, + severity, + tags, + riskScore, + falsePositives, + threat: threat as IMitreEnterpriseAttack[], + timeline: { + id: timelineId ?? null, + title: timelineTitle ?? null, + }, + }; +}; + +export const determineDetailsValue = ( + rule: Rule, + detailsView: boolean +): Pick => { + const { name, description, note } = rule; + if (detailsView) { + return { name: '', description: '', note: '' }; + } - return { aboutRuleData, defineRuleData, scheduleRuleData }; + return { name, description, note: note ?? '' }; }; +export const getModifiedAboutDetailsData = (rule: Rule): AboutStepRuleDetails => ({ + note: rule.note ?? '', + description: rule.description, +}); + export const useQuery = () => new URLSearchParams(useLocation().search); export type PrePackagedRuleStatus = 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 34df20de1e46..aa50626a1231 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 @@ -36,7 +36,7 @@ export interface RuleStepData { export interface RuleStepProps { addPadding?: boolean; - descriptionDirection?: 'row' | 'column'; + descriptionColumns?: 'multi' | 'single' | 'singleSplit'; setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void; isReadOnlyView: boolean; isUpdateView?: boolean; @@ -58,6 +58,12 @@ export interface AboutStepRule extends StepRuleData { tags: string[]; timeline: FieldValueTimeline; threat: IMitreEnterpriseAttack[]; + note: string; +} + +export interface AboutStepRuleDetails { + note: string; + description: string; } export interface DefineStepRule extends StepRuleData { @@ -91,6 +97,7 @@ export interface AboutStepRuleJson { timeline_id?: string; timeline_title?: string; threat: IMitreEnterpriseAttack[]; + note?: string; } export interface ScheduleStepRuleJson { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts index dd4acaeaf5a0..39277b3d3c77 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts @@ -19,7 +19,7 @@ export const TOTAL_SIGNAL = i18n.translate('xpack.siem.detectionEngine.totalSign }); export const SIGNAL = i18n.translate('xpack.siem.detectionEngine.signalTitle', { - defaultMessage: 'Signals (SIEM Detections)', + defaultMessage: 'Detected signals', }); export const ALERT = i18n.translate('xpack.siem.detectionEngine.alertTitle', { From 79b04547dbc93aafb6b390a830a16628c286eeac Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 17 Mar 2020 18:14:02 +0100 Subject: [PATCH 051/115] [SIEM] Adds 'Closes one signal when more than one opened signals are selected' test again (#60380) * Revert "Revert "adds new test (#60064)"" This reverts commit 4a8fd0afee9262916348c51e98f2ac955e25b2ac. * waits for having 25 signals displayed --- .../cypress/integration/detections.spec.ts | 44 ++++++++++++++++++- .../siem/cypress/screens/detections.ts | 4 +- .../plugins/siem/cypress/tasks/detections.ts | 7 +++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts index 1624586d4ca1..de17f40a3ac7 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts @@ -5,12 +5,14 @@ */ import { NUMBER_OF_SIGNALS, + OPEN_CLOSE_SIGNALS_BTN, SELECTED_SIGNALS, SHOWING_SIGNALS, SIGNALS, } from '../screens/detections'; import { + closeFirstSignal, closeSignals, goToClosedSignals, goToOpenedSignals, @@ -26,7 +28,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS } from '../urls/navigation'; describe('Detections', () => { - before(() => { + beforeEach(() => { esArchiverLoad('signals'); loginAndWaitForPage(DETECTIONS); }); @@ -53,6 +55,7 @@ describe('Detections', () => { waitForSignals(); cy.reload(); waitForSignals(); + waitForSignalsToBeLoaded(); const expectedNumberOfSignalsAfterClosing = +numberOfSignals - numberOfSignalsToBeClosed; cy.get(NUMBER_OF_SIGNALS) @@ -111,4 +114,43 @@ describe('Detections', () => { .should('eql', expectedNumberOfOpenedSignals.toString()); }); }); + + it('Closes one signal when more than one opened signals are selected', () => { + waitForSignalsToBeLoaded(); + + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .then(numberOfSignals => { + const numberOfSignalsToBeClosed = 1; + const numberOfSignalsToBeSelected = 3; + + cy.get(OPEN_CLOSE_SIGNALS_BTN).should('have.attr', 'disabled'); + selectNumberOfSignals(numberOfSignalsToBeSelected); + cy.get(OPEN_CLOSE_SIGNALS_BTN).should('not.have.attr', 'disabled'); + + closeFirstSignal(); + cy.reload(); + waitForSignalsToBeLoaded(); + waitForSignals(); + + const expectedNumberOfSignals = +numberOfSignals - numberOfSignalsToBeClosed; + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eq', expectedNumberOfSignals.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${expectedNumberOfSignals.toString()} signals`); + + goToClosedSignals(); + waitForSignals(); + + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eql', numberOfSignalsToBeClosed.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${numberOfSignalsToBeClosed.toString()} signal`); + cy.get(SIGNALS).should('have.length', numberOfSignalsToBeClosed); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts index 8b5ba2357880..f388ac1215d0 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts @@ -12,7 +12,9 @@ export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal export const NUMBER_OF_SIGNALS = '[data-test-subj="server-side-event-count"]'; -export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] .siemLinkIcon__label'; +export const OPEN_CLOSE_SIGNAL_BTN = '[data-test-subj="update-signal-status-button"]'; + +export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] button'; export const OPENED_SIGNALS_BTN = '[data-test-subj="openSignals"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts index 21a0c136b90d..3416e3eb81de 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts @@ -8,6 +8,7 @@ import { CLOSED_SIGNALS_BTN, LOADING_SIGNALS_PANEL, MANAGE_SIGNAL_DETECTION_RULES_BTN, + OPEN_CLOSE_SIGNAL_BTN, OPEN_CLOSE_SIGNALS_BTN, OPENED_SIGNALS_BTN, SIGNALS, @@ -15,6 +16,12 @@ import { } from '../screens/detections'; import { REFRESH_BUTTON } from '../screens/siem_header'; +export const closeFirstSignal = () => { + cy.get(OPEN_CLOSE_SIGNAL_BTN) + .first() + .click({ force: true }); +}; + export const closeSignals = () => { cy.get(OPEN_CLOSE_SIGNALS_BTN).click({ force: true }); }; From 4da0cb36844a3ff368dbeda25b4cc0f9738af920 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Tue, 17 Mar 2020 10:24:58 -0700 Subject: [PATCH 052/115] [Ingest] Support `show_user` package registry flag (#60338) * Support registry `show_user` var definition property (elastic/package-registry#266) * Add tests --- .../ingest_manager/common/types/models/epm.ts | 1 + .../components/datasource_input_config.tsx | 7 ++- .../datasource_input_stream_config.tsx | 7 ++- .../create_datasource_page/services/index.ts | 6 ++ .../services/is_advanced_var.test.ts | 62 +++++++++++++++++++ .../services/is_advanced_var.ts | 13 ++++ 6 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/is_advanced_var.test.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/is_advanced_var.ts diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 6b8403b74a75..28786530db01 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -186,6 +186,7 @@ export interface RegistryVarsEntry { description?: string; type: string; required?: boolean; + show_user?: boolean; multi?: boolean; default?: string | string[]; os?: { 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 39f2f048ab88..69d219463844 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 @@ -15,6 +15,7 @@ import { EuiTitle, } from '@elastic/eui'; import { DatasourceInput, RegistryVarsEntry } from '../../../../types'; +import { isAdvancedVar } from '../services'; import { DatasourceInputVarField } from './datasource_input_var_field'; export const DatasourceInputConfig: React.FunctionComponent<{ @@ -30,10 +31,10 @@ export const DatasourceInputConfig: React.FunctionComponent<{ if (packageInputVars) { packageInputVars.forEach(varDef => { - if (varDef.required && !varDef.default) { - requiredVars.push(varDef); - } else { + if (isAdvancedVar(varDef)) { advancedVars.push(varDef); + } else { + requiredVars.push(varDef); } }); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx index e4b138932cb5..1f483f1911bc 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx @@ -16,6 +16,7 @@ import { EuiButtonEmpty, } from '@elastic/eui'; import { DatasourceInputStream, RegistryStream, RegistryVarsEntry } from '../../../../types'; +import { isAdvancedVar } from '../services'; import { DatasourceInputVarField } from './datasource_input_var_field'; export const DatasourceInputStreamConfig: React.FunctionComponent<{ @@ -31,10 +32,10 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ if (packageInputStream.vars && packageInputStream.vars.length) { packageInputStream.vars.forEach(varDef => { - if (varDef.required && !varDef.default) { - requiredVars.push(varDef); - } else { + if (isAdvancedVar(varDef)) { advancedVars.push(varDef); + } else { + requiredVars.push(varDef); } }); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts new file mode 100644 index 000000000000..44e5bfa41cb9 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts @@ -0,0 +1,6 @@ +/* + * 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 { isAdvancedVar } from './is_advanced_var'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/is_advanced_var.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/is_advanced_var.test.ts new file mode 100644 index 000000000000..67796d69863f --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/is_advanced_var.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { isAdvancedVar } from './is_advanced_var'; + +describe('Ingest Manager - isAdvancedVar', () => { + it('returns true for vars that should be show under advanced options', () => { + expect( + isAdvancedVar({ + name: 'mock_var', + type: 'text', + required: true, + default: 'default string', + }) + ).toBe(true); + + expect( + isAdvancedVar({ + name: 'mock_var', + type: 'text', + default: 'default string', + }) + ).toBe(true); + + expect( + isAdvancedVar({ + name: 'mock_var', + type: 'text', + }) + ).toBe(true); + }); + + it('returns false for vars that should be show by default', () => { + expect( + isAdvancedVar({ + name: 'mock_var', + type: 'text', + required: true, + default: 'default string', + show_user: true, + }) + ).toBe(false); + + expect( + isAdvancedVar({ + name: 'mock_var', + type: 'text', + required: true, + }) + ).toBe(false); + + expect( + isAdvancedVar({ + name: 'mock_var', + type: 'text', + show_user: true, + }) + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/is_advanced_var.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/is_advanced_var.ts new file mode 100644 index 000000000000..398f1d675c5d --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/is_advanced_var.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 { RegistryVarsEntry } from '../../../../types'; + +export const isAdvancedVar = (varDef: RegistryVarsEntry): boolean => { + if (varDef.show_user || (varDef.required && !varDef.default)) { + return false; + } + return true; +}; From 6b7731bb74b1f133a39730c1e4de847c655e9d7a Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Tue, 17 Mar 2020 10:41:06 -0700 Subject: [PATCH 053/115] [Reporting] Wholesale moves client to newest-platform (#58945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move over to new plugin space, working implementation * Fixing tests for report_listing snapshots * WIP: Fixing react-component tests * Fixing report_info_button tests * Fixing download linksies * WIP: Final working implementation * Fixing attachAction API + API URLs * Let the past die. Kill it if you have to. That’s the only way to become what you were meant to be. * Fixing stream-client for new platform APIs * Fixing types and tests * Fix broken mock * Adds back in warnings to report info button * kibana.json line-breaks on required plugins * Fixing broked snapshots * Fix license checks in client-side components * Adding back in warnings to report_listing component * Fix danglig unused import * Adds license checks for basic to our csv panel action * Fixes issues from prior fork * Move relative pathing to absolute * Fix POST URL copying as we've moved from static methods * Fix layoutId props * Fixes types for layoutId Co-authored-by: Elastic Machine --- .../workpad_header/workpad_export/index.ts | 2 +- x-pack/legacy/plugins/reporting/index.ts | 16 +- .../report_info_button.test.mocks.ts | 8 - .../public/components/report_listing.test.tsx | 77 --------- .../public/constants/job_statuses.tsx | 13 -- .../reporting/public/lib/download_report.ts | 23 --- .../reporting/public/lib/job_queue_client.ts | 89 ---------- .../reporting/public/lib/reporting_client.ts | 37 ----- .../reporting/public/register_feature.ts | 27 ---- .../public/views/management/index.js | 7 - .../public/views/management/jobs.html | 3 - .../reporting/public/views/management/jobs.js | 59 ------- .../public/views/management/management.js | 44 ----- x-pack/legacy/plugins/reporting/types.d.ts | 16 -- x-pack/plugins/reporting/common/poller.ts | 96 +++++++++++ x-pack/plugins/reporting/constants.ts | 25 ++- x-pack/plugins/reporting/index.d.ts | 16 ++ x-pack/plugins/reporting/kibana.json | 10 +- .../report_info_button.test.tsx.snap | 0 .../report_listing.test.tsx.snap | 0 .../public/components/general_error.tsx | 2 +- .../public/components/job_failure.tsx | 2 +- .../components/job_queue_client.test.mocks.ts | 17 ++ .../public/components/job_success.tsx | 2 +- .../components/job_warning_formulas.tsx | 2 +- .../components/job_warning_max_size.tsx | 2 +- .../public/components/report_error_button.tsx | 5 +- .../components/report_info_button.test.tsx | 32 ++-- .../public/components/report_info_button.tsx | 12 +- .../public/components/report_listing.test.tsx | 85 ++++++++++ .../public/components/report_listing.tsx | 83 ++++++---- .../components/reporting_panel_content.tsx | 55 ++++--- .../screen_capture_panel_content.tsx | 6 + x-pack/plugins/reporting/public/index.ts | 4 +- .../__snapshots__/stream_handler.test.ts.snap | 4 +- .../lib/job_completion_notifications.ts | 2 +- .../plugins/reporting/public/lib/job_queue.ts | 27 ---- .../public/lib/license_check.test.ts | 50 ++++++ .../reporting/public/lib/license_check.ts | 52 ++++++ .../public/lib/reporting_api_client.ts | 152 ++++++++++++++++++ .../public/lib/stream_handler.test.ts | 68 +++----- .../reporting/public/lib/stream_handler.ts | 47 +++--- .../panel_actions/get_csv_panel_action.tsx | 65 ++++---- x-pack/plugins/reporting/public/plugin.tsx | 127 +++++++++++++-- .../register_csv_reporting.tsx | 43 +++-- .../register_pdf_png_reporting.tsx} | 120 ++++++++------ 46 files changed, 920 insertions(+), 714 deletions(-) delete mode 100644 x-pack/legacy/plugins/reporting/public/components/report_info_button.test.mocks.ts delete mode 100644 x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/constants/job_statuses.tsx delete mode 100644 x-pack/legacy/plugins/reporting/public/lib/download_report.ts delete mode 100644 x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts delete mode 100644 x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts delete mode 100644 x-pack/legacy/plugins/reporting/public/register_feature.ts delete mode 100644 x-pack/legacy/plugins/reporting/public/views/management/index.js delete mode 100644 x-pack/legacy/plugins/reporting/public/views/management/jobs.html delete mode 100644 x-pack/legacy/plugins/reporting/public/views/management/jobs.js delete mode 100644 x-pack/legacy/plugins/reporting/public/views/management/management.js create mode 100644 x-pack/plugins/reporting/common/poller.ts rename x-pack/{legacy => }/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap (100%) create mode 100644 x-pack/plugins/reporting/public/components/job_queue_client.test.mocks.ts rename x-pack/{legacy => }/plugins/reporting/public/components/report_error_button.tsx (92%) rename x-pack/{legacy => }/plugins/reporting/public/components/report_info_button.test.tsx (60%) rename x-pack/{legacy => }/plugins/reporting/public/components/report_info_button.tsx (95%) create mode 100644 x-pack/plugins/reporting/public/components/report_listing.test.tsx rename x-pack/{legacy => }/plugins/reporting/public/components/report_listing.tsx (85%) rename x-pack/{legacy => }/plugins/reporting/public/components/reporting_panel_content.tsx (88%) rename x-pack/{legacy => }/plugins/reporting/public/components/screen_capture_panel_content.tsx (91%) rename x-pack/{legacy => }/plugins/reporting/public/lib/job_completion_notifications.ts (98%) delete mode 100644 x-pack/plugins/reporting/public/lib/job_queue.ts create mode 100644 x-pack/plugins/reporting/public/lib/license_check.test.ts create mode 100644 x-pack/plugins/reporting/public/lib/license_check.ts create mode 100644 x-pack/plugins/reporting/public/lib/reporting_api_client.ts rename x-pack/{legacy => }/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx (72%) rename x-pack/{legacy => }/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx (61%) rename x-pack/{legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx => plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx} (57%) diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts index 7f81adad6bf9..949264fcc9fd 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; -import * as jobCompletionNotifications from '../../../../../reporting/public/lib/job_completion_notifications'; +import { jobCompletionNotifications } from '../../../../../../../plugins/reporting/public'; // @ts-ignore Untyped local import { getWorkpad, getPages } from '../../../state/selectors/workpad'; // @ts-ignore Untyped local diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index 9ce4e807f8ef..89e98302cddc 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -10,7 +10,7 @@ import { resolve } from 'path'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; import { config as reportingConfig } from './config'; import { legacyInit } from './server/legacy'; -import { ReportingConfigOptions, ReportingPluginSpecOptions } from './types'; +import { ReportingPluginSpecOptions } from './types'; const kbToBase64Length = (kb: number) => { return Math.floor((kb * 1024 * 8) / 6); @@ -25,20 +25,6 @@ export const reporting = (kibana: any) => { config: reportingConfig, uiExports: { - shareContextMenuExtensions: [ - 'plugins/reporting/share_context_menu/register_csv_reporting', - 'plugins/reporting/share_context_menu/register_reporting', - ], - embeddableActions: ['plugins/reporting/panel_actions/get_csv_panel_action'], - home: ['plugins/reporting/register_feature'], - managementSections: ['plugins/reporting/views/management'], - injectDefaultVars(server: Legacy.Server, options?: ReportingConfigOptions) { - const config = server.config(); - return { - reportingPollConfig: options ? options.poll : {}, - enablePanelActionDownload: config.get('xpack.reporting.csv.enablePanelActionDownload'), - }; - }, uiSettingDefaults: { [UI_SETTINGS_CUSTOM_PDF_LOGO]: { name: i18n.translate('xpack.reporting.pdfFooterImageLabel', { diff --git a/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.mocks.ts b/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.mocks.ts deleted file mode 100644 index 9dd7cbb5fc56..000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.mocks.ts +++ /dev/null @@ -1,8 +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. - */ - -export const mockJobQueueClient = { list: jest.fn(), total: jest.fn(), getInfo: jest.fn() }; -jest.mock('../lib/job_queue_client', () => ({ jobQueueClient: mockJobQueueClient })); diff --git a/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx deleted file mode 100644 index d78eb5c409c1..000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx +++ /dev/null @@ -1,77 +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. - */ - -interface JobData { - _index: string; - _id: string; - _source: { - browser_type: string; - created_at: string; - jobtype: string; - created_by: string; - payload: { - type: string; - title: string; - }; - kibana_name?: string; // undefined if job is pending (not yet claimed by an instance) - kibana_id?: string; // undefined if job is pending (not yet claimed by an instance) - output?: { content_type: string; size: number }; // undefined if job is incomplete - completed_at?: string; // undefined if job is incomplete - }; -} - -jest.mock('ui/chrome', () => ({ - getInjected() { - return { - jobsRefresh: { - interval: 10, - intervalErrorMultiplier: 2, - }, - }; - }, -})); - -jest.mock('ui/kfetch', () => ({ - kfetch: ({ pathname }: { pathname: string }): Promise => { - if (pathname === '/api/reporting/jobs/list') { - return Promise.resolve([ - { _index: '.reporting-2019.08.18', _id: 'jzoik8dh1q2i89fb5f19znm6', _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.869Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik7tn1q2i89fb5f60e5ve', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.155Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5tb1q2i89fb5fckchny', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:21.551Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5a11q2i89fb5f130t2m', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:20.857Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik3ka1q2i89fb5fdx93g7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:18.634Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik2vt1q2i89fb5ffw723n', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:17.753Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik1851q2i89fb5fdge6e7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1080, height: 720 } }, type: 'canvas workpad', title: 'My Canvas Workpad - Dark', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:15.605Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijyre1q2i89fb5fa7xzvi', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:12.410Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijv5h1q2i89fb5ffklnhx', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:07.733Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jznhgk7r1bx789fb5f6hxok7', _score: null, _source: { kibana_name: 'spicy.local', browser_type: 'chromium', created_at: '2019-08-23T02:15:47.799Z', jobtype: 'printable_pdf', created_by: 'elastic', kibana_id: 'ca75e26c-2b7d-464f-aef0-babb67c735a0', output: { content_type: 'application/pdf', size: 877114 }, completed_at: '2019-08-23T02:15:57.707Z', payload: { type: 'dashboard (legacy)', title: 'tests-panels', }, max_attempts: 3, started_at: '2019-08-23T02:15:48.794Z', attempts: 1, status: 'completed', }, }, // prettier-ignore - ]); - } - - // query for jobs count - return Promise.resolve(18); - }, -})); - -import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ReportListing } from './report_listing'; - -describe('ReportListing', () => { - it('Report job listing with some items', () => { - const wrapper = mountWithIntl( - - ); - wrapper.update(); - const input = wrapper.find('[data-test-subj="reportJobListing"]'); - expect(input).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/public/constants/job_statuses.tsx b/x-pack/legacy/plugins/reporting/public/constants/job_statuses.tsx deleted file mode 100644 index 29c51217a5c6..000000000000 --- a/x-pack/legacy/plugins/reporting/public/constants/job_statuses.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export enum JobStatuses { - PENDING = 'pending', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', -} diff --git a/x-pack/legacy/plugins/reporting/public/lib/download_report.ts b/x-pack/legacy/plugins/reporting/public/lib/download_report.ts deleted file mode 100644 index 54194c87afab..000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/download_report.ts +++ /dev/null @@ -1,23 +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 { npStart } from 'ui/new_platform'; -import { API_BASE_URL } from '../../common/constants'; - -const { core } = npStart; - -export function getReportURL(jobId: string) { - const apiBaseUrl = core.http.basePath.prepend(API_BASE_URL); - const downloadLink = `${apiBaseUrl}/jobs/download/${jobId}`; - - return downloadLink; -} - -export function downloadReport(jobId: string) { - const location = getReportURL(jobId); - - window.open(location); -} diff --git a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts b/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts deleted file mode 100644 index 87d4174168b7..000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts +++ /dev/null @@ -1,89 +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 { npStart } from 'ui/new_platform'; -import { API_LIST_URL } from '../../common/constants'; - -const { core } = npStart; - -export interface JobQueueEntry { - _id: string; - _source: any; -} - -export interface JobContent { - content: string; - content_type: boolean; -} - -export interface JobInfo { - kibana_name: string; - kibana_id: string; - browser_type: string; - created_at: string; - priority: number; - jobtype: string; - created_by: string; - timeout: number; - output: { - content_type: string; - size: number; - warnings: string[]; - }; - process_expiration: string; - completed_at: string; - payload: { - layout: { id: string; dimensions: { width: number; height: number } }; - objects: Array<{ relativeUrl: string }>; - type: string; - title: string; - forceNow: string; - browserTimezone: string; - }; - meta: { - layout: string; - objectType: string; - }; - max_attempts: number; - started_at: string; - attempts: number; - status: string; -} - -class JobQueueClient { - public list = (page = 0, jobIds: string[] = []): Promise => { - const query = { page } as any; - if (jobIds.length > 0) { - // Only getting the first 10, to prevent URL overflows - query.ids = jobIds.slice(0, 10).join(','); - } - - return core.http.get(`${API_LIST_URL}/list`, { - query, - asSystemRequest: true, - }); - }; - - public total(): Promise { - return core.http.get(`${API_LIST_URL}/count`, { - asSystemRequest: true, - }); - } - - public getContent(jobId: string): Promise { - return core.http.get(`${API_LIST_URL}/output/${jobId}`, { - asSystemRequest: true, - }); - } - - public getInfo(jobId: string): Promise { - return core.http.get(`${API_LIST_URL}/info/${jobId}`, { - asSystemRequest: true, - }); - } -} - -export const jobQueueClient = new JobQueueClient(); diff --git a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts b/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts deleted file mode 100644 index d471dc57fc9e..000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts +++ /dev/null @@ -1,37 +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 { stringify } from 'query-string'; -import { npStart } from 'ui/new_platform'; -// @ts-ignore -import rison from 'rison-node'; -import { add } from './job_completion_notifications'; - -const { core } = npStart; -const API_BASE_URL = '/api/reporting/generate'; - -interface JobParams { - [paramName: string]: any; -} - -export const getReportingJobPath = (exportType: string, jobParams: JobParams) => { - const params = stringify({ jobParams: rison.encode(jobParams) }); - - return `${core.http.basePath.prepend(API_BASE_URL)}/${exportType}?${params}`; -}; - -export const createReportingJob = async (exportType: string, jobParams: any) => { - const jobParamsRison = rison.encode(jobParams); - const resp = await core.http.post(`${API_BASE_URL}/${exportType}`, { - method: 'POST', - body: JSON.stringify({ - jobParams: jobParamsRison, - }), - }); - - add(resp.job.id); - - return resp; -}; diff --git a/x-pack/legacy/plugins/reporting/public/register_feature.ts b/x-pack/legacy/plugins/reporting/public/register_feature.ts deleted file mode 100644 index 4e8d32facfce..000000000000 --- a/x-pack/legacy/plugins/reporting/public/register_feature.ts +++ /dev/null @@ -1,27 +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 { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; - -const { - plugins: { home }, -} = npSetup; - -home.featureCatalogue.register({ - id: 'reporting', - title: i18n.translate('xpack.reporting.registerFeature.reportingTitle', { - defaultMessage: 'Reporting', - }), - description: i18n.translate('xpack.reporting.registerFeature.reportingDescription', { - defaultMessage: 'Manage your reports generated from Discover, Visualize, and Dashboard.', - }), - icon: 'reportingApp', - path: '/app/kibana#/management/kibana/reporting', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, -}); diff --git a/x-pack/legacy/plugins/reporting/public/views/management/index.js b/x-pack/legacy/plugins/reporting/public/views/management/index.js deleted file mode 100644 index 0ed6fe09ef80..000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/index.js +++ /dev/null @@ -1,7 +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 './management'; diff --git a/x-pack/legacy/plugins/reporting/public/views/management/jobs.html b/x-pack/legacy/plugins/reporting/public/views/management/jobs.html deleted file mode 100644 index 5471513d64d9..000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/jobs.html +++ /dev/null @@ -1,3 +0,0 @@ - -
    -
    \ No newline at end of file diff --git a/x-pack/legacy/plugins/reporting/public/views/management/jobs.js b/x-pack/legacy/plugins/reporting/public/views/management/jobs.js deleted file mode 100644 index 7205fad8cca5..000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/jobs.js +++ /dev/null @@ -1,59 +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 { render, unmountComponentAtNode } from 'react-dom'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -import routes from 'ui/routes'; -import template from 'plugins/reporting/views/management/jobs.html'; - -import { ReportListing } from '../../components/report_listing'; -import { i18n } from '@kbn/i18n'; -import { I18nContext } from 'ui/i18n'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; - -const REACT_ANCHOR_DOM_ELEMENT_ID = 'reportListingAnchor'; - -routes.when('/management/kibana/reporting', { - template, - k7Breadcrumbs: () => [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.reporting.breadcrumb', { - defaultMessage: 'Reporting', - }), - }, - ], - controllerAs: 'jobsCtrl', - controller($scope, kbnUrl) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - - , - node - ); - }); - - $scope.$on('$destroy', () => { - const node = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID); - if (node) { - unmountComponentAtNode(node); - } - }); - }, -}); diff --git a/x-pack/legacy/plugins/reporting/public/views/management/management.js b/x-pack/legacy/plugins/reporting/public/views/management/management.js deleted file mode 100644 index 8643e6fa8b8b..000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/management.js +++ /dev/null @@ -1,44 +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 { management } from 'ui/management'; -import { i18n } from '@kbn/i18n'; -import routes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -import 'plugins/reporting/views/management/jobs'; - -routes.defaults(/\/management/, { - resolve: { - reportingManagementSection: function() { - const kibanaManagementSection = management.getSection('kibana'); - const showReportingLinks = xpackInfo.get('features.reporting.management.showLinks'); - - kibanaManagementSection.deregister('reporting'); - if (showReportingLinks) { - const enableReportingLinks = xpackInfo.get('features.reporting.management.enableLinks'); - const tooltipMessage = xpackInfo.get('features.reporting.management.message'); - - let url; - let tooltip; - if (enableReportingLinks) { - url = '#/management/kibana/reporting'; - } else { - tooltip = tooltipMessage; - } - - return kibanaManagementSection.register('reporting', { - order: 15, - display: i18n.translate('xpack.reporting.management.reportingTitle', { - defaultMessage: 'Reporting', - }), - url, - tooltip, - }); - } - }, - }, -}); diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index b4d49fd21f23..917e9d7daae4 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -23,22 +23,6 @@ export type Job = EventEmitter & { }; }; -export interface ReportingConfigOptions { - browser: BrowserConfig; - poll: { - jobCompletionNotifier: { - interval: number; - intervalErrorMultiplier: number; - }; - jobsRefresh: { - interval: number; - intervalErrorMultiplier: number; - }; - }; - queue: QueueConfig; - capture: CaptureConfig; -} - export interface NetworkPolicyRule { allow: boolean; protocol: string; diff --git a/x-pack/plugins/reporting/common/poller.ts b/x-pack/plugins/reporting/common/poller.ts new file mode 100644 index 000000000000..919d7273062a --- /dev/null +++ b/x-pack/plugins/reporting/common/poller.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 _ from 'lodash'; +import { PollerOptions } from '..'; + +// @TODO Maybe move to observables someday +export class Poller { + private readonly functionToPoll: () => Promise; + private readonly successFunction: (...args: any) => any; + private readonly errorFunction: (error: Error) => any; + private _isRunning: boolean; + private _timeoutId: NodeJS.Timeout | null; + private pollFrequencyInMillis: number; + private trailing: boolean; + private continuePollingOnError: boolean; + private pollFrequencyErrorMultiplier: number; + + constructor(options: PollerOptions) { + this.functionToPoll = options.functionToPoll; // Must return a Promise + this.successFunction = options.successFunction || _.noop; + this.errorFunction = options.errorFunction || _.noop; + this.pollFrequencyInMillis = options.pollFrequencyInMillis; + this.trailing = options.trailing || false; + this.continuePollingOnError = options.continuePollingOnError || false; + this.pollFrequencyErrorMultiplier = options.pollFrequencyErrorMultiplier || 1; + + this._timeoutId = null; + this._isRunning = false; + } + + getPollFrequency() { + return this.pollFrequencyInMillis; + } + + _poll() { + return this.functionToPoll() + .then(this.successFunction) + .then(() => { + if (!this._isRunning) { + return; + } + + this._timeoutId = setTimeout(this._poll.bind(this), this.pollFrequencyInMillis); + }) + .catch(e => { + this.errorFunction(e); + if (!this._isRunning) { + return; + } + + if (this.continuePollingOnError) { + this._timeoutId = setTimeout( + this._poll.bind(this), + this.pollFrequencyInMillis * this.pollFrequencyErrorMultiplier + ); + } else { + this.stop(); + } + }); + } + + start() { + if (this._isRunning) { + return; + } + + this._isRunning = true; + if (this.trailing) { + this._timeoutId = setTimeout(this._poll.bind(this), this.pollFrequencyInMillis); + } else { + this._poll(); + } + } + + stop() { + if (!this._isRunning) { + return; + } + + this._isRunning = false; + + if (this._timeoutId) { + clearTimeout(this._timeoutId); + } + + this._timeoutId = null; + } + + isRunning() { + return this._isRunning; + } +} diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts index fe5673a0b74b..8f47a0a6b2ac 100644 --- a/x-pack/plugins/reporting/constants.ts +++ b/x-pack/plugins/reporting/constants.ts @@ -14,8 +14,31 @@ export const JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG = { }, }; -export const API_BASE_URL = '/api/reporting/jobs'; +// Routes +export const API_BASE_URL = '/api/reporting'; +export const API_LIST_URL = `${API_BASE_URL}/jobs`; +export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; +export const API_GENERATE_IMMEDIATE = `${API_BASE_URL}/v1/generate/immediate/csv/saved-object`; export const REPORTING_MANAGEMENT_HOME = '/app/kibana#/management/kibana/reporting'; +// Statuses export const JOB_STATUS_FAILED = 'failed'; export const JOB_STATUS_COMPLETED = 'completed'; + +export enum JobStatuses { + PENDING = 'pending', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', +} + +// Types +export const PDF_JOB_TYPE = 'printable_pdf'; +export const PNG_JOB_TYPE = 'PNG'; +export const CSV_JOB_TYPE = 'csv'; +export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; +export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; + +// Actions +export const CSV_REPORTING_ACTION = 'downloadCsvReport'; diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts index 9559de4a5bb0..7c1a2ebd7d9d 100644 --- a/x-pack/plugins/reporting/index.d.ts +++ b/x-pack/plugins/reporting/index.d.ts @@ -57,3 +57,19 @@ export type DownloadReportFn = (jobId: JobId) => DownloadLink; type ManagementLink = string; export type ManagementLinkFn = () => ManagementLink; + +export interface PollerOptions { + functionToPoll: () => Promise; + pollFrequencyInMillis: number; + trailing?: boolean; + continuePollingOnError?: boolean; + pollFrequencyErrorMultiplier?: number; + successFunction?: (...args: any) => any; + errorFunction?: (error: Error) => any; +} + +export interface LicenseCheckResults { + enableLinks: boolean; + showLinks: boolean; + message: string; +} diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index 50f552b0d9fb..a7e2bd288f0b 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -2,7 +2,15 @@ "id": "reporting", "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": [], + "requiredPlugins": [ + "home", + "management", + "licensing", + "uiActions", + "embeddable", + "share", + "kibanaLegacy" + ], "server": false, "ui": true } diff --git a/x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap rename to x-pack/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap diff --git a/x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap rename to x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap diff --git a/x-pack/plugins/reporting/public/components/general_error.tsx b/x-pack/plugins/reporting/public/components/general_error.tsx index feb0ea0062ac..bc1ec901cc47 100644 --- a/x-pack/plugins/reporting/public/components/general_error.tsx +++ b/x-pack/plugins/reporting/public/components/general_error.tsx @@ -7,7 +7,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { ToastInput } from '../../../../../src/core/public'; +import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; export const getGeneralErrorToast = (errorText: string, err: Error): ToastInput => ({ diff --git a/x-pack/plugins/reporting/public/components/job_failure.tsx b/x-pack/plugins/reporting/public/components/job_failure.tsx index 7544cbf90645..628ecb56b9c2 100644 --- a/x-pack/plugins/reporting/public/components/job_failure.tsx +++ b/x-pack/plugins/reporting/public/components/job_failure.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { ToastInput } from '../../../../../src/core/public'; +import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { JobSummary, ManagementLinkFn } from '../../index.d'; diff --git a/x-pack/plugins/reporting/public/components/job_queue_client.test.mocks.ts b/x-pack/plugins/reporting/public/components/job_queue_client.test.mocks.ts new file mode 100644 index 000000000000..5e9614e27e2f --- /dev/null +++ b/x-pack/plugins/reporting/public/components/job_queue_client.test.mocks.ts @@ -0,0 +1,17 @@ +/* + * 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 const mockAPIClient = { + http: jest.fn(), + list: jest.fn(), + total: jest.fn(), + getInfo: jest.fn(), + getContent: jest.fn(), + getReportURL: jest.fn(), + downloadReport: jest.fn(), +}; + +jest.mock('../lib/reporting_api_client', () => mockAPIClient); diff --git a/x-pack/plugins/reporting/public/components/job_success.tsx b/x-pack/plugins/reporting/public/components/job_success.tsx index b538cef030e0..c2feac382ca7 100644 --- a/x-pack/plugins/reporting/public/components/job_success.tsx +++ b/x-pack/plugins/reporting/public/components/job_success.tsx @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ToastInput } from '../../../../../src/core/public'; +import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { JobId, JobSummary } from '../../index.d'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx index 7981237c9b78..22f656dbe738 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ToastInput } from '../../../../../src/core/public'; +import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { JobId, JobSummary } from '../../index.d'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx index caeda6fc0167..1abba8888bb8 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ToastInput } from '../../../../../src/core/public'; +import { ToastInput } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { JobId, JobSummary } from '../../index.d'; import { ReportLink } from './report_link'; diff --git a/x-pack/legacy/plugins/reporting/public/components/report_error_button.tsx b/x-pack/plugins/reporting/public/components/report_error_button.tsx similarity index 92% rename from x-pack/legacy/plugins/reporting/public/components/report_error_button.tsx rename to x-pack/plugins/reporting/public/components/report_error_button.tsx index 3e6fd07847f2..252dee9c619a 100644 --- a/x-pack/legacy/plugins/reporting/public/components/report_error_button.tsx +++ b/x-pack/plugins/reporting/public/components/report_error_button.tsx @@ -7,11 +7,12 @@ import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { JobContent, jobQueueClient } from '../lib/job_queue_client'; +import { JobContent, ReportingAPIClient } from '../lib/reporting_api_client'; interface Props { jobId: string; intl: InjectedIntl; + apiClient: ReportingAPIClient; } interface State { @@ -90,7 +91,7 @@ class ReportErrorButtonUi extends Component { private loadError = async () => { this.setState({ isLoading: true }); try { - const reportContent: JobContent = await jobQueueClient.getContent(this.props.jobId); + const reportContent: JobContent = await this.props.apiClient.getContent(this.props.jobId); if (this.mounted) { this.setState({ isLoading: false, error: reportContent.content }); } diff --git a/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.tsx b/x-pack/plugins/reporting/public/components/report_info_button.test.tsx similarity index 60% rename from x-pack/legacy/plugins/reporting/public/components/report_info_button.test.tsx rename to x-pack/plugins/reporting/public/components/report_info_button.test.tsx index 3b9c2a848542..2edd59e6de7a 100644 --- a/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.tsx +++ b/x-pack/plugins/reporting/public/components/report_info_button.test.tsx @@ -4,27 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockJobQueueClient } from './report_info_button.test.mocks'; - import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReportInfoButton } from './report_info_button'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; -describe('ReportInfoButton', () => { - beforeEach(() => { - mockJobQueueClient.getInfo = jest.fn(() => ({ - payload: { title: 'Test Job' }, - })); - }); +jest.mock('../lib/reporting_api_client'); +const httpSetup = {} as any; +const apiClient = new ReportingAPIClient(httpSetup); + +describe('ReportInfoButton', () => { it('handles button click flyout on click', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); expect(input).toMatchSnapshot(); }); - it('opens flyout with info', () => { - const wrapper = mountWithIntl(); + it('opens flyout with info', async () => { + const wrapper = mountWithIntl(); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); input.simulate('click'); @@ -32,17 +30,17 @@ describe('ReportInfoButton', () => { const flyout = wrapper.find('[data-test-subj="reportInfoFlyout"]'); expect(flyout).toMatchSnapshot(); - expect(mockJobQueueClient.getInfo).toHaveBeenCalledTimes(1); - expect(mockJobQueueClient.getInfo).toHaveBeenCalledWith('abc-456'); + expect(apiClient.getInfo).toHaveBeenCalledTimes(1); + expect(apiClient.getInfo).toHaveBeenCalledWith('abc-456'); }); it('opens flyout with fetch error info', () => { // simulate fetch failure - mockJobQueueClient.getInfo = jest.fn(() => { + apiClient.getInfo = jest.fn(() => { throw new Error('Could not fetch the job info'); }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); input.simulate('click'); @@ -50,7 +48,7 @@ describe('ReportInfoButton', () => { const flyout = wrapper.find('[data-test-subj="reportInfoFlyout"]'); expect(flyout).toMatchSnapshot(); - expect(mockJobQueueClient.getInfo).toHaveBeenCalledTimes(1); - expect(mockJobQueueClient.getInfo).toHaveBeenCalledWith('abc-789'); + expect(apiClient.getInfo).toHaveBeenCalledTimes(1); + expect(apiClient.getInfo).toHaveBeenCalledWith('abc-789'); }); }); diff --git a/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx b/x-pack/plugins/reporting/public/components/report_info_button.tsx similarity index 95% rename from x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx rename to x-pack/plugins/reporting/public/components/report_info_button.tsx index 7f5d070948e5..81a5af3b8795 100644 --- a/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/components/report_info_button.tsx @@ -17,11 +17,12 @@ import { } from '@elastic/eui'; import React, { Component, Fragment } from 'react'; import { get } from 'lodash'; -import { USES_HEADLESS_JOB_TYPES } from '../../common/constants'; -import { JobInfo, jobQueueClient } from '../lib/job_queue_client'; +import { USES_HEADLESS_JOB_TYPES } from '../../constants'; +import { JobInfo, ReportingAPIClient } from '../lib/reporting_api_client'; interface Props { jobId: string; + apiClient: ReportingAPIClient; } interface State { @@ -171,6 +172,7 @@ export class ReportInfoButton extends Component { description: USES_HEADLESS_JOB_TYPES.includes(jobType) ? info.browser_type || UNKNOWN : NA, }, ]; + if (warnings) { jobInfoStatus.push({ title: 'Errors', @@ -261,17 +263,17 @@ export class ReportInfoButton extends Component { private loadInfo = async () => { this.setState({ isLoading: true }); try { - const info: JobInfo = await jobQueueClient.getInfo(this.props.jobId); + const info: JobInfo = await this.props.apiClient.getInfo(this.props.jobId); if (this.mounted) { this.setState({ isLoading: false, info }); } - } catch (kfetchError) { + } catch (err) { if (this.mounted) { this.setState({ isLoading: false, calloutTitle: 'Unable to fetch report info', info: null, - error: kfetchError, + error: err, }); } } diff --git a/x-pack/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/plugins/reporting/public/components/report_listing.test.tsx new file mode 100644 index 000000000000..5cf894580eae --- /dev/null +++ b/x-pack/plugins/reporting/public/components/report_listing.test.tsx @@ -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 React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ReportListing } from './report_listing'; +import { Observable } from 'rxjs'; +import { ILicense } from '../../../licensing/public'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; + +const reportingAPIClient = { + list: () => + Promise.resolve([ + { _index: '.reporting-2019.08.18', _id: 'jzoik8dh1q2i89fb5f19znm6', _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.869Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik7tn1q2i89fb5f60e5ve', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.155Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik5tb1q2i89fb5fckchny', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:21.551Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik5a11q2i89fb5f130t2m', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:20.857Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik3ka1q2i89fb5fdx93g7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:18.634Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik2vt1q2i89fb5ffw723n', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:17.753Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik1851q2i89fb5fdge6e7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1080, height: 720 } }, type: 'canvas workpad', title: 'My Canvas Workpad - Dark', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:15.605Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoijyre1q2i89fb5fa7xzvi', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:12.410Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoijv5h1q2i89fb5ffklnhx', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:07.733Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jznhgk7r1bx789fb5f6hxok7', _score: null, _source: { kibana_name: 'spicy.local', browser_type: 'chromium', created_at: '2019-08-23T02:15:47.799Z', jobtype: 'printable_pdf', created_by: 'elastic', kibana_id: 'ca75e26c-2b7d-464f-aef0-babb67c735a0', output: { content_type: 'application/pdf', size: 877114 }, completed_at: '2019-08-23T02:15:57.707Z', payload: { type: 'dashboard (legacy)', title: 'tests-panels', }, max_attempts: 3, started_at: '2019-08-23T02:15:48.794Z', attempts: 1, status: 'completed', }, }, // prettier-ignore + ]), + total: () => Promise.resolve(18), +} as any; + +const validCheck = { + check: () => ({ + state: 'VALID', + message: '', + }), +}; + +const license$ = { + subscribe: (handler: any) => { + return handler(validCheck); + }, +} as Observable; + +const toasts = { + addDanger: jest.fn(), +} as any; + +describe('ReportListing', () => { + it('Report job listing with some items', () => { + const wrapper = mountWithIntl( + + ); + wrapper.update(); + const input = wrapper.find('[data-test-subj="reportJobListing"]'); + expect(input).toMatchSnapshot(); + }); + + it('subscribes to license changes, and unsubscribes on dismount', () => { + const unsubscribeMock = jest.fn(); + const subMock = { + subscribe: jest.fn().mockReturnValue({ + unsubscribe: unsubscribeMock, + }), + } as any; + + const wrapper = mountWithIntl( + } + redirect={jest.fn()} + toasts={toasts} + /> + ); + wrapper.update(); + expect(subMock.subscribe).toHaveBeenCalled(); + expect(unsubscribeMock).not.toHaveBeenCalled(); + wrapper.unmount(); + expect(unsubscribeMock).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx similarity index 85% rename from x-pack/legacy/plugins/reporting/public/components/report_listing.tsx rename to x-pack/plugins/reporting/public/components/report_listing.tsx index 54061eda94dc..13fca019f328 100644 --- a/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -6,11 +6,11 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import moment from 'moment'; import { get } from 'lodash'; +import moment from 'moment'; import React, { Component } from 'react'; -import chrome from 'ui/chrome'; -import { toastNotifications } from 'ui/notify'; +import { Subscription } from 'rxjs'; + import { EuiBasicTable, EuiButtonIcon, @@ -21,10 +21,13 @@ import { EuiTitle, EuiToolTip, } from '@elastic/eui'; -import { Poller } from '../../../../common/poller'; -import { JobStatuses } from '../constants/job_statuses'; -import { downloadReport } from '../lib/download_report'; -import { jobQueueClient, JobQueueEntry } from '../lib/job_queue_client'; + +import { ToastsSetup, ApplicationStart } from 'src/core/public'; +import { LicensingPluginSetup, ILicense } from '../../../licensing/public'; +import { Poller } from '../../common/poller'; +import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../constants'; +import { ReportingAPIClient, JobQueueEntry } from '../lib/reporting_api_client'; +import { checkLicense } from '../lib/license_check'; import { ReportErrorButton } from './report_error_button'; import { ReportInfoButton } from './report_info_button'; @@ -47,11 +50,11 @@ interface Job { } interface Props { - badLicenseMessage: string; - showLinks: boolean; - enableLinks: boolean; - redirect: (url: string) => void; intl: InjectedIntl; + apiClient: ReportingAPIClient; + license$: LicensingPluginSetup['license$']; + redirect: ApplicationStart['navigateToApp']; + toasts: ToastsSetup; } interface State { @@ -59,6 +62,9 @@ interface State { total: number; jobs: Job[]; isLoading: boolean; + showLinks: boolean; + enableLinks: boolean; + badLicenseMessage: string; } const jobStatusLabelsMap = new Map([ @@ -95,9 +101,10 @@ const jobStatusLabelsMap = new Map([ ]); class ReportListingUi extends Component { + private isInitialJobsFetch: boolean; + private licenseSubscription?: Subscription; private mounted?: boolean; private poller?: any; - private isInitialJobsFetch: boolean; constructor(props: Props) { super(props); @@ -107,6 +114,9 @@ class ReportListingUi extends Component { total: 0, jobs: [], isLoading: false, + showLinks: false, + enableLinks: false, + badLicenseMessage: '', }; this.isInitialJobsFetch = true; @@ -137,23 +147,41 @@ class ReportListingUi extends Component { public componentWillUnmount() { this.mounted = false; this.poller.stop(); + + if (this.licenseSubscription) { + this.licenseSubscription.unsubscribe(); + } } public componentDidMount() { this.mounted = true; - const { jobsRefresh } = chrome.getInjected('reportingPollConfig'); this.poller = new Poller({ functionToPoll: () => { return this.fetchJobs(); }, - pollFrequencyInMillis: jobsRefresh.interval, + pollFrequencyInMillis: + JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.interval, trailing: false, continuePollingOnError: true, - pollFrequencyErrorMultiplier: jobsRefresh.intervalErrorMultiplier, + pollFrequencyErrorMultiplier: + JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.intervalErrorMultiplier, }); this.poller.start(); + this.licenseSubscription = this.props.license$.subscribe(this.licenseHandler); } + private licenseHandler = (license: ILicense) => { + const { enableLinks, showLinks, message: badLicenseMessage } = checkLicense( + license.check('reporting', 'basic') + ); + + this.setState({ + enableLinks, + showLinks, + badLicenseMessage, + }); + }; + private renderTable() { const { intl } = this.props; @@ -275,7 +303,6 @@ class ReportListingUi extends Component {
    {statusLabel} {maxSizeReached} - {warnings}
    ); }, @@ -340,7 +367,7 @@ class ReportListingUi extends Component { const { intl } = this.props; const button = ( downloadReport(record.id)} + onClick={() => this.props.apiClient.downloadReport(record.id)} iconType="importAction" aria-label={intl.formatMessage({ id: 'xpack.reporting.listing.table.downloadReportAriaLabel', @@ -386,11 +413,11 @@ class ReportListingUi extends Component { return; } - return ; + return ; }; private renderInfoButton = (record: Job) => { - return ; + return ; }; private onTableChange = ({ page }: { page: { index: number } }) => { @@ -407,19 +434,19 @@ class ReportListingUi extends Component { let jobs: JobQueueEntry[]; let total: number; try { - jobs = await jobQueueClient.list(this.state.page); - total = await jobQueueClient.total(); + jobs = await this.props.apiClient.list(this.state.page); + total = await this.props.apiClient.total(); this.isInitialJobsFetch = false; - } catch (kfetchError) { + } catch (fetchError) { if (!this.licenseAllowsToShowThisPage()) { - toastNotifications.addDanger(this.props.badLicenseMessage); - this.props.redirect('/management'); + this.props.toasts.addDanger(this.state.badLicenseMessage); + this.props.redirect('kibana#/management'); return; } - if (kfetchError.res.status !== 401 && kfetchError.res.status !== 403) { - toastNotifications.addDanger( - kfetchError.res.statusText || + if (fetchError.message === 'Failed to fetch') { + this.props.toasts.addDanger( + fetchError.message || this.props.intl.formatMessage({ id: 'xpack.reporting.listing.table.requestFailedErrorMessage', defaultMessage: 'Request failed', @@ -463,7 +490,7 @@ class ReportListingUi extends Component { }; private licenseAllowsToShowThisPage = () => { - return this.props.showLinks && this.props.enableLinks; + return this.state.showLinks && this.state.enableLinks; }; private formatDate(timestamp: string) { diff --git a/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx similarity index 88% rename from x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx rename to x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index aaf4021302a9..cf107fd71287 100644 --- a/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -7,12 +7,14 @@ import { EuiButton, EuiCopy, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, ReactElement } from 'react'; -import { toastNotifications } from 'ui/notify'; import url from 'url'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; -import * as reportingClient from '../lib/reporting_client'; +import { ToastsSetup } from 'src/core/public'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; interface Props { + apiClient: ReportingAPIClient; + toasts: ToastsSetup; reportType: string; layoutId: string | undefined; objectId?: string; @@ -31,23 +33,6 @@ interface State { } class ReportingPanelContentUi extends Component { - public static getDerivedStateFromProps(nextProps: Props, prevState: State) { - if (nextProps.layoutId !== prevState.layoutId) { - return { - ...prevState, - absoluteUrl: ReportingPanelContentUi.getAbsoluteReportGenerationUrl(nextProps), - }; - } - return prevState; - } - - private static getAbsoluteReportGenerationUrl = (props: Props) => { - const relativePath = reportingClient.getReportingJobPath( - props.reportType, - props.getJobParams() - ); - return url.resolve(window.location.href, relativePath); - }; private mounted?: boolean; constructor(props: Props) { @@ -55,11 +40,29 @@ class ReportingPanelContentUi extends Component { this.state = { isStale: false, - absoluteUrl: '', + absoluteUrl: this.getAbsoluteReportGenerationUrl(props), layoutId: '', }; } + private getAbsoluteReportGenerationUrl = (props: Props) => { + const relativePath = this.props.apiClient.getReportingJobPath( + props.reportType, + props.getJobParams() + ); + return url.resolve(window.location.href, relativePath); + }; + + public componentDidUpdate(prevProps: Props, prevState: State) { + if (this.props.layoutId && this.props.layoutId !== prevState.layoutId) { + this.setState({ + ...prevState, + absoluteUrl: this.getAbsoluteReportGenerationUrl(this.props), + layoutId: this.props.layoutId, + }); + } + } + public componentWillUnmount() { window.removeEventListener('hashchange', this.markAsStale); window.removeEventListener('resize', this.setAbsoluteReportGenerationUrl); @@ -188,17 +191,17 @@ class ReportingPanelContentUi extends Component { if (!this.mounted) { return; } - const absoluteUrl = ReportingPanelContentUi.getAbsoluteReportGenerationUrl(this.props); + const absoluteUrl = this.getAbsoluteReportGenerationUrl(this.props); this.setState({ absoluteUrl }); }; private createReportingJob = () => { const { intl } = this.props; - return reportingClient + return this.props.apiClient .createReportingJob(this.props.reportType, this.props.getJobParams()) .then(() => { - toastNotifications.addSuccess({ + this.props.toasts.addSuccess({ title: intl.formatMessage( { id: 'xpack.reporting.panelContent.successfullyQueuedReportNotificationTitle', @@ -218,7 +221,7 @@ class ReportingPanelContentUi extends Component { }) .catch((error: any) => { if (error.message === 'not exportable') { - return toastNotifications.addWarning({ + return this.props.toasts.addWarning({ title: intl.formatMessage( { id: 'xpack.reporting.panelContent.whatCanBeExportedWarningTitle', @@ -248,7 +251,7 @@ class ReportingPanelContentUi extends Component { /> ); - toastNotifications.addDanger({ + this.props.toasts.addDanger({ title: intl.formatMessage({ id: 'xpack.reporting.panelContent.notification.reportingErrorTitle', defaultMessage: 'Reporting error', diff --git a/x-pack/legacy/plugins/reporting/public/components/screen_capture_panel_content.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx similarity index 91% rename from x-pack/legacy/plugins/reporting/public/components/screen_capture_panel_content.tsx rename to x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx index cf6bb9487636..9fb74a70ff1a 100644 --- a/x-pack/legacy/plugins/reporting/public/components/screen_capture_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx @@ -7,9 +7,13 @@ import { EuiSpacer, EuiSwitch } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; +import { ToastsSetup } from 'src/core/public'; import { ReportingPanelContent } from './reporting_panel_content'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; interface Props { + apiClient: ReportingAPIClient; + toasts: ToastsSetup; reportType: string; objectId?: string; objectType: string; @@ -38,6 +42,8 @@ export class ScreenCapturePanelContent extends Component { public render() { return ( + "path":
    => { - return http.fetch(`${API_BASE_URL}/list`, { - query: { page: 0, ids: jobIds.join(',') }, - method: 'GET', - }); - }; - - public getContent(http: HttpService, jobId: JobId): Promise { - return http - .fetch(`${API_BASE_URL}/output/${jobId}`, { - method: 'GET', - }) - .then((data: JobContent) => data.content); - } -} - -export const jobQueueClient = new JobQueue(); diff --git a/x-pack/plugins/reporting/public/lib/license_check.test.ts b/x-pack/plugins/reporting/public/lib/license_check.test.ts new file mode 100644 index 000000000000..24e14969d2c8 --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/license_check.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { checkLicense } from './license_check'; +import { LicenseCheck } from '../../../licensing/public'; + +describe('License check', () => { + it('enables and shows links when licenses are good mkay', () => { + expect(checkLicense({ state: 'VALID' } as LicenseCheck)).toEqual({ + enableLinks: true, + showLinks: true, + message: '', + }); + }); + + it('disables and shows links when licenses are not valid', () => { + expect(checkLicense({ state: 'INVALID' } as LicenseCheck)).toEqual({ + enableLinks: false, + showLinks: false, + message: 'Your license does not support Reporting. Please upgrade your license.', + }); + }); + + it('shows links, but disables them, on expired licenses', () => { + expect(checkLicense({ state: 'EXPIRED' } as LicenseCheck)).toEqual({ + enableLinks: false, + showLinks: true, + message: 'You cannot use Reporting because your license has expired.', + }); + }); + + it('shows links, but disables them, when license checks are unavailable', () => { + expect(checkLicense({ state: 'UNAVAILABLE' } as LicenseCheck)).toEqual({ + enableLinks: false, + showLinks: true, + message: + 'You cannot use Reporting because license information is not available at this time.', + }); + }); + + it('shows and enables links if state is not known', () => { + expect(checkLicense({ state: 'PONYFOO' } as any)).toEqual({ + enableLinks: true, + showLinks: true, + message: '', + }); + }); +}); diff --git a/x-pack/plugins/reporting/public/lib/license_check.ts b/x-pack/plugins/reporting/public/lib/license_check.ts new file mode 100644 index 000000000000..ca803fb38ef2 --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/license_check.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { LicenseCheckResults } from '../..'; +import { LICENSE_CHECK_STATE, LicenseCheck } from '../../../licensing/public'; + +export const checkLicense = (checkResults: LicenseCheck): LicenseCheckResults => { + switch (checkResults.state) { + case LICENSE_CHECK_STATE.Valid: { + return { + showLinks: true, + enableLinks: true, + message: '', + }; + } + + case LICENSE_CHECK_STATE.Invalid: { + return { + showLinks: false, + enableLinks: false, + message: 'Your license does not support Reporting. Please upgrade your license.', + }; + } + + case LICENSE_CHECK_STATE.Unavailable: { + return { + showLinks: true, + enableLinks: false, + message: + 'You cannot use Reporting because license information is not available at this time.', + }; + } + + case LICENSE_CHECK_STATE.Expired: { + return { + showLinks: true, + enableLinks: false, + message: 'You cannot use Reporting because your license has expired.', + }; + } + + default: { + return { + showLinks: true, + enableLinks: true, + message: '', + }; + } + } +}; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts new file mode 100644 index 000000000000..ddfeb144d3cd --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts @@ -0,0 +1,152 @@ +/* + * 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 { stringify } from 'query-string'; +import rison from 'rison-node'; + +import { HttpSetup } from 'src/core/public'; +import { add } from './job_completion_notifications'; +import { + API_LIST_URL, + API_BASE_URL, + API_BASE_GENERATE, + REPORTING_MANAGEMENT_HOME, +} from '../../constants'; +import { JobId, SourceJob } from '../..'; + +export interface JobQueueEntry { + _id: string; + _source: any; +} + +export interface JobContent { + content: string; + content_type: boolean; +} + +export interface JobInfo { + kibana_name: string; + kibana_id: string; + browser_type: string; + created_at: string; + priority: number; + jobtype: string; + created_by: string; + timeout: number; + output: { + content_type: string; + size: number; + warnings: string[]; + }; + process_expiration: string; + completed_at: string; + payload: { + layout: { id: string; dimensions: { width: number; height: number } }; + objects: Array<{ relativeUrl: string }>; + type: string; + title: string; + forceNow: string; + browserTimezone: string; + }; + meta: { + layout: string; + objectType: string; + }; + max_attempts: number; + started_at: string; + attempts: number; + status: string; +} + +interface JobParams { + [paramName: string]: any; +} + +export class ReportingAPIClient { + private http: HttpSetup; + + constructor(http: HttpSetup) { + this.http = http; + } + + public getReportURL(jobId: string) { + const apiBaseUrl = this.http.basePath.prepend(API_LIST_URL); + const downloadLink = `${apiBaseUrl}/download/${jobId}`; + + return downloadLink; + } + + public downloadReport(jobId: string) { + const location = this.getReportURL(jobId); + + window.open(location); + } + + public list = (page = 0, jobIds: string[] = []): Promise => { + const query = { page } as any; + if (jobIds.length > 0) { + // Only getting the first 10, to prevent URL overflows + query.ids = jobIds.slice(0, 10).join(','); + } + + return this.http.get(`${API_LIST_URL}/list`, { + query, + asSystemRequest: true, + }); + }; + + public total(): Promise { + return this.http.get(`${API_LIST_URL}/count`, { + asSystemRequest: true, + }); + } + + public getContent(jobId: string): Promise { + return this.http.get(`${API_LIST_URL}/output/${jobId}`, { + asSystemRequest: true, + }); + } + + public getInfo(jobId: string): Promise { + return this.http.get(`${API_LIST_URL}/info/${jobId}`, { + asSystemRequest: true, + }); + } + + public findForJobIds = (jobIds: JobId[]): Promise => { + return this.http.fetch(`${API_LIST_URL}/list`, { + query: { page: 0, ids: jobIds.join(',') }, + method: 'GET', + }); + }; + + public getReportingJobPath = (exportType: string, jobParams: JobParams) => { + const params = stringify({ jobParams: rison.encode(jobParams) }); + + return `${this.http.basePath.prepend(API_BASE_URL)}/${exportType}?${params}`; + }; + + public createReportingJob = async (exportType: string, jobParams: any) => { + const jobParamsRison = rison.encode(jobParams); + const resp = await this.http.post(`${API_BASE_GENERATE}/${exportType}`, { + method: 'POST', + body: JSON.stringify({ + jobParams: jobParamsRison, + }), + }); + + add(resp.job.id); + + return resp; + }; + + public getManagementLink = () => this.http.basePath.prepend(REPORTING_MANAGEMENT_HOME); + + public getDownloadLink = (jobId: JobId) => + this.http.basePath.prepend(`${API_LIST_URL}/download/${jobId}`); + + public getBasePath = () => this.http.basePath.get(); +} diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index aeba2ca5406b..3a2c7de9ad0f 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -5,9 +5,9 @@ */ import sinon, { stub } from 'sinon'; -import { HttpSetup, NotificationsStart } from '../../../../../src/core/public'; -import { SourceJob, JobSummary, HttpService } from '../../index.d'; -import { JobQueue } from './job_queue'; +import { NotificationsStart } from 'src/core/public'; +import { SourceJob, JobSummary } from '../../index.d'; +import { ReportingAPIClient } from './reporting_api_client'; import { ReportingNotifierStreamHandler } from './stream_handler'; Object.defineProperty(window, 'sessionStorage', { @@ -44,20 +44,16 @@ const mockJobsFound = [ }, ]; -const jobQueueClientMock: JobQueue = { - findForJobIds: async (http: HttpService, jobIds: string[]) => { +const jobQueueClientMock: ReportingAPIClient = { + findForJobIds: async (jobIds: string[]) => { return mockJobsFound as SourceJob[]; }, - getContent: () => { - return Promise.resolve('this is the completed report data'); + getContent: (): Promise => { + return Promise.resolve({ content: 'this is the completed report data' }); }, -}; - -const httpMock: HttpService = ({ - basePath: { - prepend: stub(), - }, -} as unknown) as HttpSetup; + getManagementLink: () => '/#management', + getDownloadLink: () => '/reporting/download/job-123', +} as any; const mockShowDanger = stub(); const mockShowSuccess = stub(); @@ -76,17 +72,13 @@ describe('stream handler', () => { }); it('constructs', () => { - const sh = new ReportingNotifierStreamHandler(httpMock, notificationsMock, jobQueueClientMock); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); expect(sh).not.toBe(null); }); describe('findChangedStatusJobs', () => { it('finds no changed status jobs from empty', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); const findJobs = sh.findChangedStatusJobs([]); findJobs.subscribe(data => { expect(data).toEqual({ completed: [], failed: [] }); @@ -95,11 +87,7 @@ describe('stream handler', () => { }); it('finds changed status jobs', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); const findJobs = sh.findChangedStatusJobs([ 'job-source-mock1', 'job-source-mock2', @@ -115,11 +103,7 @@ describe('stream handler', () => { describe('showNotifications', () => { it('show success', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); sh.showNotifications({ completed: [ { @@ -140,11 +124,7 @@ describe('stream handler', () => { }); it('show max length warning', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); sh.showNotifications({ completed: [ { @@ -166,11 +146,7 @@ describe('stream handler', () => { }); it('show csv formulas warning', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); sh.showNotifications({ completed: [ { @@ -192,11 +168,7 @@ describe('stream handler', () => { }); it('show failed job toast', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); sh.showNotifications({ completed: [], failed: [ @@ -217,11 +189,7 @@ describe('stream handler', () => { }); it('show multiple toast', done => { - const sh = new ReportingNotifierStreamHandler( - httpMock, - notificationsMock, - jobQueueClientMock - ); + const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock); sh.showNotifications({ completed: [ { diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index e58e90d3de8e..1aae30f6fdfb 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -11,19 +11,16 @@ import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUS_COMPLETED, JOB_STATUS_FAILED, - API_BASE_URL, - REPORTING_MANAGEMENT_HOME, } from '../../constants'; + import { JobId, JobSummary, JobStatusBuckets, - HttpService, NotificationsService, SourceJob, - DownloadReportFn, - ManagementLinkFn, } from '../../index.d'; + import { getSuccessToast, getFailureToast, @@ -31,7 +28,7 @@ import { getWarningMaxSizeToast, getGeneralErrorToast, } from '../components'; -import { jobQueueClient as defaultJobQueueClient } from './job_queue'; +import { ReportingAPIClient } from './reporting_api_client'; function updateStored(jobIds: JobId[]): void { sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobIds)); @@ -49,21 +46,7 @@ function summarizeJob(src: SourceJob): JobSummary { } export class ReportingNotifierStreamHandler { - private getManagementLink: ManagementLinkFn; - private getDownloadLink: DownloadReportFn; - - constructor( - private http: HttpService, - private notifications: NotificationsService, - private jobQueueClient = defaultJobQueueClient - ) { - this.getManagementLink = () => { - return http.basePath.prepend(REPORTING_MANAGEMENT_HOME); - }; - this.getDownloadLink = (jobId: JobId) => { - return http.basePath.prepend(`${API_BASE_URL}/download/${jobId}`); - }; - } + constructor(private notifications: NotificationsService, private apiClient: ReportingAPIClient) {} /* * Use Kibana Toast API to show our messages @@ -77,23 +60,33 @@ export class ReportingNotifierStreamHandler { for (const job of completedJobs) { if (job.csvContainsFormulas) { this.notifications.toasts.addWarning( - getWarningFormulasToast(job, this.getManagementLink, this.getDownloadLink) + getWarningFormulasToast( + job, + this.apiClient.getManagementLink, + this.apiClient.getDownloadLink + ) ); } else if (job.maxSizeReached) { this.notifications.toasts.addWarning( - getWarningMaxSizeToast(job, this.getManagementLink, this.getDownloadLink) + getWarningMaxSizeToast( + job, + this.apiClient.getManagementLink, + this.apiClient.getDownloadLink + ) ); } else { this.notifications.toasts.addSuccess( - getSuccessToast(job, this.getManagementLink, this.getDownloadLink) + getSuccessToast(job, this.apiClient.getManagementLink, this.apiClient.getDownloadLink) ); } } // no download link available for (const job of failedJobs) { - const content = await this.jobQueueClient.getContent(this.http, job.id); - this.notifications.toasts.addDanger(getFailureToast(content, job, this.getManagementLink)); + const { content } = await this.apiClient.getContent(job.id); + this.notifications.toasts.addDanger( + getFailureToast(content, job, this.apiClient.getManagementLink) + ); } return { completed: completedJobs, failed: failedJobs }; }; @@ -106,7 +99,7 @@ export class ReportingNotifierStreamHandler { * session storage) but have non-processing job status on the server */ public findChangedStatusJobs(storedJobs: JobId[]): Rx.Observable { - return Rx.from(this.jobQueueClient.findForJobIds(this.http, storedJobs)).pipe( + return Rx.from(this.apiClient.findForJobIds(storedJobs)).pipe( map((jobs: SourceJob[]) => { const completedJobs: JobSummary[] = []; const failedJobs: JobSummary[] = []; diff --git a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx similarity index 72% rename from x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx rename to x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 4c9cd890ee75..282ee75815fa 100644 --- a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -6,24 +6,21 @@ import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; - -import { npSetup, npStart } from 'ui/new_platform'; -import { - ActionByType, - IncompatibleActionError, -} from '../../../../../../src/plugins/ui_actions/public'; +import { CoreSetup } from 'src/core/public'; +import { Action, IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public'; +import { LicensingPluginSetup } from '../../../licensing/public'; +import { checkLicense } from '../lib/license_check'; import { ViewMode, IEmbeddable, - CONTEXT_MENU_TRIGGER, -} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; -import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/constants'; -import { ISearchEmbeddable } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types'; +} from '../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; -import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../common/constants'; +// @TODO: These import paths will need to be updated once discovery moves to non-legacy dir +import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/constants'; +import { ISearchEmbeddable } from '../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types'; -const { core } = npStart; +import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../constants'; function isSavedSearchEmbeddable( embeddable: IEmbeddable | ISearchEmbeddable @@ -31,23 +28,26 @@ function isSavedSearchEmbeddable( return embeddable.type === SEARCH_EMBEDDABLE_TYPE; } -export interface CSVActionContext { +interface ActionContext { embeddable: ISearchEmbeddable; } -declare module '../../../../../../src/plugins/ui_actions/public' { - export interface ActionContextMapping { - [CSV_REPORTING_ACTION]: CSVActionContext; - } -} - -class GetCsvReportPanelAction implements ActionByType { +export class GetCsvReportPanelAction implements Action { private isDownloading: boolean; - public readonly type = CSV_REPORTING_ACTION; + public readonly type = ''; public readonly id = CSV_REPORTING_ACTION; + private canDownloadCSV: boolean = false; + private core: CoreSetup; - constructor() { + constructor(core: CoreSetup, license$: LicensingPluginSetup['license$']) { this.isDownloading = false; + this.core = core; + + license$.subscribe(license => { + const results = license.check('reporting', 'basic'); + const { showLinks } = checkLicense(results); + this.canDownloadCSV = showLinks; + }); } public getIconType() { @@ -73,13 +73,17 @@ class GetCsvReportPanelAction implements ActionByType { + public isCompatible = async (context: ActionContext) => { + if (!this.canDownloadCSV) { + return false; + } + const { embeddable } = context; return embeddable.getInput().viewMode !== ViewMode.EDIT && embeddable.type === 'search'; }; - public execute = async (context: CSVActionContext) => { + public execute = async (context: ActionContext) => { const { embeddable } = context; if (!isSavedSearchEmbeddable(embeddable)) { @@ -97,7 +101,7 @@ class GetCsvReportPanelAction implements ActionByType { this.isDownloading = false; @@ -160,7 +164,7 @@ class GetCsvReportPanelAction implements ActionByType { private readonly stop$ = new Rx.ReplaySubject(1); - // FIXME: License checking: only active, non-expired licenses allowed - // Depends on https://github.com/elastic/kibana/pull/44922 + private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { + defaultMessage: 'Reporting', + }); + + private readonly breadcrumbText = i18n.translate('xpack.reporting.breadcrumb', { + defaultMessage: 'Reporting', + }); + constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup) {} + public setup( + core: CoreSetup, + { + home, + management, + licensing, + uiActions, + share, + }: { + home: HomePublicPluginSetup; + management: ManagementSetup; + licensing: LicensingPluginSetup; + uiActions: UiActionsSetup; + share: SharePluginSetup; + } + ) { + const { + http, + notifications: { toasts }, + getStartServices, + uiSettings, + } = core; + const { license$ } = licensing; + + const apiClient = new ReportingAPIClient(http); + const action = new GetCsvReportPanelAction(core, license$); + + home.featureCatalogue.register({ + id: 'reporting', + title: i18n.translate('xpack.reporting.registerFeature.reportingTitle', { + defaultMessage: 'Reporting', + }), + description: i18n.translate('xpack.reporting.registerFeature.reportingDescription', { + defaultMessage: 'Manage your reports generated from Discover, Visualize, and Dashboard.', + }), + icon: 'reportingApp', + path: '/app/kibana#/management/kibana/reporting', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, + }); + + management.sections.getSection('kibana')!.registerApp({ + id: 'reporting', + title: this.title, + order: 15, + mount: async params => { + const [start] = await getStartServices(); + params.setBreadcrumbs([{ text: this.breadcrumbText }]); + ReactDOM.render( + + + , + params.element + ); + + return () => { + ReactDOM.unmountComponentAtNode(params.element); + }; + }, + }); + + uiActions.registerAction(action); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, action); + + share.register(csvReportingProvider({ apiClient, toasts, license$ })); + share.register( + reportingPDFPNGProvider({ + apiClient, + toasts, + license$, + uiSettings, + }) + ); + } // FIXME: only perform these actions for authenticated routes // Depends on https://github.com/elastic/kibana/pull/39477 public start(core: CoreStart) { const { http, notifications } = core; - const streamHandler = new StreamHandler(http, notifications); + const apiClient = new ReportingAPIClient(http); + const streamHandler = new StreamHandler(notifications, apiClient); Rx.timer(0, JOBS_REFRESH_INTERVAL) .pipe( diff --git a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx similarity index 61% rename from x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx rename to x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 3c9d1d726258..9d4f475cde79 100644 --- a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -5,14 +5,34 @@ */ import { i18n } from '@kbn/i18n'; -// @ts-ignore: implicit any for JS file -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import React from 'react'; -import { npSetup } from 'ui/new_platform'; + +import { ToastsSetup } from 'src/core/public'; import { ReportingPanelContent } from '../components/reporting_panel_content'; -import { ShareContext } from '../../../../../../src/plugins/share/public'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { checkLicense } from '../lib/license_check'; +import { LicensingPluginSetup } from '../../../licensing/public'; +import { ShareContext } from '../../../../../src/plugins/share/public'; + +interface ReportingProvider { + apiClient: ReportingAPIClient; + toasts: ToastsSetup; + license$: LicensingPluginSetup['license$']; +} + +export const csvReportingProvider = ({ apiClient, toasts, license$ }: ReportingProvider) => { + let toolTipContent = ''; + let disabled = true; + let hasCSVReporting = false; + + license$.subscribe(license => { + const { enableLinks, showLinks, message } = checkLicense(license.check('reporting', 'basic')); + + toolTipContent = message; + hasCSVReporting = showLinks; + disabled = !enableLinks; + }); -function reportingProvider() { const getShareMenuItems = ({ objectType, objectId, @@ -32,7 +52,8 @@ function reportingProvider() { }; const shareActions = []; - if (xpackInfo.get('features.reporting.csv.showLinks', false)) { + + if (hasCSVReporting) { const panelTitle = i18n.translate('xpack.reporting.shareContextMenu.csvReportsButtonLabel', { defaultMessage: 'CSV Reports', }); @@ -41,8 +62,8 @@ function reportingProvider() { shareMenuItem: { name: panelTitle, icon: 'document', - toolTipContent: xpackInfo.get('features.reporting.csv.message'), - disabled: !xpackInfo.get('features.reporting.csv.enableLinks', false) ? true : false, + toolTipContent, + disabled, ['data-test-subj']: 'csvReportMenuItem', sortOrder: 1, }, @@ -51,6 +72,8 @@ function reportingProvider() { title: panelTitle, content: ( { + let toolTipContent = ''; + let disabled = true; + let hasPDFPNGReporting = false; -const { core } = npSetup; + license$.subscribe(license => { + const { enableLinks, showLinks, message } = checkLicense(license.check('reporting', 'gold')); + + toolTipContent = message; + hasPDFPNGReporting = showLinks; + disabled = !enableLinks; + }); -async function reportingProvider() { const getShareMenuItems = ({ objectType, objectId, @@ -29,24 +52,22 @@ async function reportingProvider() { } // Dashboard only mode does not currently support reporting // https://github.com/elastic/kibana/issues/18286 - if ( - objectType === 'dashboard' && - npStart.plugins.kibanaLegacy.dashboardConfig.getHideWriteControls() - ) { + // @TODO For NP + if (objectType === 'dashboard' && false) { return []; } const getReportingJobParams = () => { // Replace hashes with original RISON values. const relativeUrl = shareableUrl.replace( - window.location.origin + core.http.basePath.get(), + window.location.origin + apiClient.getBasePath(), '' ); const browserTimezone = - core.uiSettings.get('dateFormat:tz') === 'Browser' + uiSettings.get('dateFormat:tz') === 'Browser' ? moment.tz.guess() - : core.uiSettings.get('dateFormat:tz'); + : uiSettings.get('dateFormat:tz'); return { ...sharingData, @@ -59,14 +80,14 @@ async function reportingProvider() { const getPngJobParams = () => { // Replace hashes with original RISON values. const relativeUrl = shareableUrl.replace( - window.location.origin + core.http.basePath.get(), + window.location.origin + apiClient.getBasePath(), '' ); const browserTimezone = - core.uiSettings.get('dateFormat:tz') === 'Browser' + uiSettings.get('dateFormat:tz') === 'Browser' ? moment.tz.guess() - : core.uiSettings.get('dateFormat:tz'); + : uiSettings.get('dateFormat:tz'); return { ...sharingData, @@ -77,60 +98,69 @@ async function reportingProvider() { }; const shareActions = []; - if (xpackInfo.get('features.reporting.printablePdf.showLinks', false)) { - const panelTitle = i18n.translate('xpack.reporting.shareContextMenu.pdfReportsButtonLabel', { - defaultMessage: 'PDF Reports', - }); + + if (hasPDFPNGReporting) { + const pngPanelTitle = i18n.translate( + 'xpack.reporting.shareContextMenu.pngReportsButtonLabel', + { + defaultMessage: 'PNG Reports', + } + ); + + const pdfPanelTitle = i18n.translate( + 'xpack.reporting.shareContextMenu.pdfReportsButtonLabel', + { + defaultMessage: 'PDF Reports', + } + ); shareActions.push({ shareMenuItem: { - name: panelTitle, + name: pngPanelTitle, icon: 'document', - toolTipContent: xpackInfo.get('features.reporting.printablePdf.message'), - disabled: !xpackInfo.get('features.reporting.printablePdf.enableLinks', false) - ? true - : false, - ['data-test-subj']: 'pdfReportMenuItem', + toolTipContent, + disabled, + ['data-test-subj']: 'pngReportMenuItem', sortOrder: 10, }, panel: { - id: 'reportingPdfPanel', - title: panelTitle, + id: 'reportingPngPanel', + title: pngPanelTitle, content: ( ), }, }); - } - - if (xpackInfo.get('features.reporting.png.showLinks', false)) { - const panelTitle = 'PNG Reports'; shareActions.push({ shareMenuItem: { - name: panelTitle, + name: pdfPanelTitle, icon: 'document', - toolTipContent: xpackInfo.get('features.reporting.png.message'), - disabled: !xpackInfo.get('features.reporting.png.enableLinks', false) ? true : false, - ['data-test-subj']: 'pngReportMenuItem', + toolTipContent, + disabled, + ['data-test-subj']: 'pdfReportMenuItem', sortOrder: 10, }, panel: { - id: 'reportingPngPanel', - title: panelTitle, + id: 'reportingPdfPanel', + title: pdfPanelTitle, content: ( @@ -146,8 +176,4 @@ async function reportingProvider() { id: 'screenCaptureReports', getShareMenuItems, }; -} - -(async () => { - npSetup.plugins.share.register(await reportingProvider()); -})(); +}; From 89f9260da2eb0d54404a93380ea4eef6c4b16597 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Tue, 17 Mar 2020 10:41:23 -0700 Subject: [PATCH 054/115] FTR configurable test users (#52431) * initial implementation of configurable test users * user superuser by default to match master * referenced the configs in reporting and api integration * setting the minimum number of default roles * looking for x-pack tests with users and roles * add testUserService in dashboard mode tests * running only ciGroup7 * uncommenting - addign visualization * re-enabling all CI groups to run on CI * reinstating Jenkinsfile * disable Test user for OIDC config * improved logging and added Roles for OSS tests to get better info on the runs. * disable test_user for auth tests * don't fetch enabledPlugins when testuser disabled * fix es-lint * running oss tests with x-pack enabled * [revertme] build default dist for oss tests * updating NOTICE.txt file as it complained in the kibana intake tests * changed to pick OSS builds * trying a license change to trial * switch back to xpack builds * created a new sample data role and used it in homepage tests * revert test/scripts/jenkins_ci_group.sh * only refresh browser and wait for chrome if we are already on Kibana page * fix large_string test to use minimum set of roles and privileges * fix for date nanos custom timestamp with a configured role * changes to the files with addition of new roles for the test_user * reverting to OSS changes and few additions to the time_zone test to run as a test_user * changes to security * changes to the x-pack test to use elastic superuser * fix for chart_types test * fixes to area chart , input control test * fix for dashboard filtering test and a new config role * changes to handle the x-pack tests * additional role for date nanos mixed * added the logstash role to the accessibility tests * removed telemetry setting * docs+few changes to the tests * removed Page navigation * removed pageNavigation which was unused * test/accessibility/apps/management.ts * update management.ts * aria label, and other changes * accidentally checked in a piped file with results. * accidentally checked in a piped file with results. * accidentally checked in a piped file with results. * accidentally checked in a piped file with results. * accidentally checked in a piped file with results. * accidentally checked in a piped file with results. * accidentally checked in a piped file with results. * accidentally checked in a piped file with results. * reverted * unloading of logstash data, fixing aria label * aria-label * added the required role * fix for tsvb chart * fix for sample data test reverted home_page pageobject file * changes to sample data test and visualize index file to incorporate OSS changes * changes to describe() and some more changes to incorporate in settings_page * re-adding the after() * removed unwanted roles * replaced kibana_user with kibana_admin * added the check of deprecated kibana_user * testing with kibana_admin role * fix for discover test * incorporated the review comments * incorporated the review comments * incorporate review comments and added restoreDefaults() * removed describe.only * reverted the OSS logic change I had here- pulled into seperate PR * incorporated the review comments * incorporated review changes * adding hidden=true to find hidden kibanaChrome * change field.test.tsx to be same as that of master branch Co-authored-by: spalger Co-authored-by: Elastic Machine --- .../development-functional-tests.asciidoc | 19 ++ .../lib/config/schema.ts | 15 ++ test/common/services/security/role.ts | 2 - test/common/services/security/security.ts | 12 +- test/common/services/security/test_user.ts | 92 ++++++++++ test/functional/apps/context/_date_nanos.js | 7 +- .../context/_date_nanos_custom_timestamp.js | 11 +- .../apps/dashboard/dashboard_filtering.js | 6 + test/functional/apps/dashboard/index.js | 1 + test/functional/apps/dashboard/time_zones.js | 2 - test/functional/apps/discover/_date_nanos.js | 7 +- .../apps/discover/_date_nanos_mixed.js | 7 +- .../apps/discover/_discover_histogram.js | 7 + .../functional/apps/discover/_large_string.js | 3 + .../apps/getting_started/_shakespeare.js | 6 + test/functional/apps/home/_sample_data.ts | 6 + .../apps/management/_handle_alias.js | 3 + .../apps/management/_test_huge_fields.js | 3 + test/functional/apps/visualize/_area_chart.js | 11 +- .../apps/visualize/_experimental_vis.js | 2 +- .../apps/visualize/_linked_saved_searches.ts | 2 +- .../apps/visualize/_markdown_vis.js | 2 +- test/functional/apps/visualize/_tsvb_chart.ts | 4 + test/functional/apps/visualize/_vega_chart.js | 2 +- .../input_control_vis/input_control_range.ts | 3 + test/functional/config.js | 167 ++++++++++++++++++ test/functional/page_objects/common_page.ts | 17 +- test/functional/services/browser.ts | 2 + test/functional/services/test_subjects.ts | 2 + x-pack/test/api_integration/config.js | 1 + .../dashboard_mode/dashboard_view_mode.js | 21 ++- x-pack/test/functional/config.js | 19 ++ x-pack/test/oidc_api_integration/config.ts | 1 + x-pack/test/pki_api_integration/config.ts | 1 + .../reporting/configs/chromium_functional.js | 1 + x-pack/test/saml_api_integration/config.ts | 1 + x-pack/test/token_api_integration/config.js | 1 + 37 files changed, 433 insertions(+), 36 deletions(-) create mode 100644 test/common/services/security/test_user.ts diff --git a/docs/developer/core/development-functional-tests.asciidoc b/docs/developer/core/development-functional-tests.asciidoc index dcb3d65b8b83..51b5273851ce 100644 --- a/docs/developer/core/development-functional-tests.asciidoc +++ b/docs/developer/core/development-functional-tests.asciidoc @@ -178,6 +178,25 @@ To run tests on Firefox locally, use `config.firefox.js`: node scripts/functional_test_runner --config test/functional/config.firefox.js ----------- +[float] +===== Using the test_user service + +Tests should run at the positive security boundry condition, meaning that they should be run with the mimimum privileges required (and documented) and not as the superuser. + This prevents the type of regression where additional privleges accidentally become required to perform the same action. + +The functional UI tests now default to logging in with a user named `test_user` and the roles of this user can be changed dynamically without logging in and out. + +In order to achieve this a new service was introduced called `createTestUserService` (see `test/common/services/security/test_user.ts`). The purpose of this test user service is to create roles defined in the test config files and setRoles() or restoreDefaults(). + +An example of how to set the role like how its defined below: + +`await security.testUser.setRoles(['kibana_user', 'kibana_date_nanos']);` + +Here we are setting the `test_user` to have the `kibana_user` role and also role access to a specific data index (`kibana_date_nanos`). + +Tests should normally setRoles() in the before() and restoreDefaults() in the after(). + + [float] ===== Anatomy of a test file diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 28e8396d0beb..66f17ab579ec 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -255,5 +255,20 @@ export const schema = Joi.object() fixedHeaderHeight: Joi.number().default(50), }) .default(), + + // settings for the security service if there is no defaultRole defined, then default to superuser role. + security: Joi.object() + .keys({ + roles: Joi.object().default(), + defaultRoles: Joi.array() + .items(Joi.string()) + .when('$primary', { + is: true, + then: Joi.array().min(1), + }) + .default(['superuser']), + disableTestUser: Joi.boolean(), + }) + .default(), }) .default(); diff --git a/test/common/services/security/role.ts b/test/common/services/security/role.ts index 0e7572882f80..dfc6ff9b164e 100644 --- a/test/common/services/security/role.ts +++ b/test/common/services/security/role.ts @@ -43,7 +43,6 @@ export class Role { `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` ); } - this.log.debug(`created role ${name}`); } public async delete(name: string) { @@ -56,6 +55,5 @@ export class Role { )}` ); } - this.log.debug(`deleted role ${name}`); } } diff --git a/test/common/services/security/security.ts b/test/common/services/security/security.ts index 4eebb7b6697e..6ad0933a2a5a 100644 --- a/test/common/services/security/security.ts +++ b/test/common/services/security/security.ts @@ -23,15 +23,21 @@ import { Role } from './role'; import { User } from './user'; import { RoleMappings } from './role_mappings'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { createTestUserService } from './test_user'; -export function SecurityServiceProvider({ getService }: FtrProviderContext) { +export async function SecurityServiceProvider(context: FtrProviderContext) { + const { getService } = context; const log = getService('log'); const config = getService('config'); const url = formatUrl(config.get('servers.kibana')); + const role = new Role(url, log); + const user = new User(url, log); + const testUser = await createTestUserService(role, user, context); return new (class SecurityService { - role = new Role(url, log); roleMappings = new RoleMappings(url, log); - user = new User(url, log); + testUser = testUser; + role = role; + user = user; })(); } diff --git a/test/common/services/security/test_user.ts b/test/common/services/security/test_user.ts new file mode 100644 index 000000000000..7f01c64d291a --- /dev/null +++ b/test/common/services/security/test_user.ts @@ -0,0 +1,92 @@ +/* + * 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 { Role } from './role'; +import { User } from './user'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { Browser } from '../../../functional/services/browser'; +import { TestSubjects } from '../../../functional/services/test_subjects'; + +export async function createTestUserService( + role: Role, + user: User, + { getService, hasService }: FtrProviderContext +) { + const log = getService('log'); + const config = getService('config'); + // @ts-ignore browser service is not normally available in common. + const browser: Browser | void = hasService('browser') && getService('browser'); + const testSubjects: TestSubjects | void = + // @ts-ignore testSubject service is not normally available in common. + hasService('testSubjects') && getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + + const enabledPlugins = config.get('security.disableTestUser') + ? [] + : await kibanaServer.plugins.getEnabledIds(); + const isEnabled = () => { + return enabledPlugins.includes('security') && !config.get('security.disableTestUser'); + }; + if (isEnabled()) { + log.debug('===============creating roles and users==============='); + for (const [name, definition] of Object.entries(config.get('security.roles'))) { + // create the defined roles (need to map array to create roles) + await role.create(name, definition); + } + try { + // delete the test_user if present (will it error if the user doesn't exist?) + await user.delete('test_user'); + } catch (exception) { + log.debug('no test user to delete'); + } + + // create test_user with username and pwd + log.debug(`default roles = ${config.get('security.defaultRoles')}`); + await user.create('test_user', { + password: 'changeme', + roles: config.get('security.defaultRoles'), + full_name: 'test user', + }); + } + + return new (class TestUser { + async restoreDefaults() { + if (isEnabled()) { + await this.setRoles(config.get('security.defaultRoles')); + } + } + + async setRoles(roles: string[]) { + if (isEnabled()) { + log.debug(`set roles = ${roles}`); + await user.create('test_user', { + password: 'changeme', + roles, + full_name: 'test user', + }); + + if (browser && testSubjects) { + if (await testSubjects.exists('kibanaChrome', { allowHidden: true })) { + await browser.refresh(); + await testSubjects.find('kibanaChrome', config.get('timeouts.find') * 10); + } + } + } + } + })(); +} diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index d4acdb0b4d5c..bd132e3745ca 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -26,11 +26,13 @@ const TEST_STEP_SIZE = 3; export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const docTable = getService('docTable'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); describe('context view for date_nanos', () => { before(async function() { + await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await esArchiver.loadIfNeeded('date_nanos'); await kibanaServer.uiSettings.replace({ defaultIndex: TEST_INDEX_PATTERN }); await kibanaServer.uiSettings.update({ @@ -39,8 +41,9 @@ export default function({ getService, getPageObjects }) { }); }); - after(function unloadMakelogs() { - return esArchiver.unload('date_nanos'); + after(async function unloadMakelogs() { + await security.testUser.restoreDefaults(); + await esArchiver.unload('date_nanos'); }); it('displays predessors - anchor - successors in right order ', async function() { diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js index 046cca0aba8c..7834b29931a6 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js @@ -26,12 +26,14 @@ const TEST_STEP_SIZE = 3; export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const docTable = getService('docTable'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); // skipped due to a recent change in ES that caused search_after queries with data containing // custom timestamp formats like in the testdata to fail describe.skip('context view for date_nanos with custom timestamp', () => { before(async function() { + await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_custom']); await esArchiver.loadIfNeeded('date_nanos_custom'); await kibanaServer.uiSettings.replace({ defaultIndex: TEST_INDEX_PATTERN }); await kibanaServer.uiSettings.update({ @@ -40,10 +42,6 @@ export default function({ getService, getPageObjects }) { }); }); - after(function unloadMakelogs() { - return esArchiver.unload('date_nanos_custom'); - }); - it('displays predessors - anchor - successors in right order ', async function() { await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, '1'); const actualRowsText = await docTable.getRowsText(); @@ -54,5 +52,10 @@ export default function({ getService, getPageObjects }) { ]; expect(actualRowsText).to.eql(expectedRowsText); }); + + after(async function() { + await security.testUser.restoreDefaults(); + await esArchiver.unload('date_nanos_custom'); + }); }); } diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index ec8a48ca7491..f388993dcaf7 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -33,6 +33,7 @@ export default function({ getService, getPageObjects }) { const filterBar = getService('filterBar'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); @@ -41,6 +42,7 @@ export default function({ getService, getPageObjects }) { before(async () => { await esArchiver.load('dashboard/current/kibana'); + await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']); await kibanaServer.uiSettings.replace({ defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', }); @@ -49,6 +51,10 @@ export default function({ getService, getPageObjects }) { await PageObjects.dashboard.gotoDashboardLandingPage(); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + describe('adding a filter that excludes all data', () => { before(async () => { await PageObjects.dashboard.clickNewDashboard(); diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js index 13e863144539..5e96a55b1901 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -23,6 +23,7 @@ export default function({ getService, loadTestFile }) { async function loadCurrentData() { await browser.setWindowSize(1300, 900); + await esArchiver.unload('logstash_functional'); await esArchiver.loadIfNeeded('dashboard/current/data'); } diff --git a/test/functional/apps/dashboard/time_zones.js b/test/functional/apps/dashboard/time_zones.js index f374d6526fcf..b7698a7d6ac4 100644 --- a/test/functional/apps/dashboard/time_zones.js +++ b/test/functional/apps/dashboard/time_zones.js @@ -22,7 +22,6 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const pieChart = getService('pieChart'); - const browser = getService('browser'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['dashboard', 'timePicker', 'settings', 'common']); @@ -48,7 +47,6 @@ export default function({ getService, getPageObjects }) { after(async () => { await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'UTC' }); - await browser.refresh(); }); it('Exported dashboard adjusts EST time to UTC', async () => { diff --git a/test/functional/apps/discover/_date_nanos.js b/test/functional/apps/discover/_date_nanos.js index 9b06b9ac84cf..99a37cc18fea 100644 --- a/test/functional/apps/discover/_date_nanos.js +++ b/test/functional/apps/discover/_date_nanos.js @@ -23,6 +23,7 @@ export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const fromTime = 'Sep 22, 2019 @ 20:31:44.000'; const toTime = 'Sep 23, 2019 @ 03:31:44.000'; @@ -30,12 +31,14 @@ export default function({ getService, getPageObjects }) { before(async function() { await esArchiver.loadIfNeeded('date_nanos'); await kibanaServer.uiSettings.replace({ defaultIndex: 'date-nanos' }); + await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }); - after(function unloadMakelogs() { - return esArchiver.unload('date_nanos'); + after(async function unloadMakelogs() { + await security.testUser.restoreDefaults(); + await esArchiver.unload('date_nanos'); }); it('should show a timestamp with nanoseconds in the first result row', async function() { diff --git a/test/functional/apps/discover/_date_nanos_mixed.js b/test/functional/apps/discover/_date_nanos_mixed.js index 0bb6848db4d1..b88ae87601cc 100644 --- a/test/functional/apps/discover/_date_nanos_mixed.js +++ b/test/functional/apps/discover/_date_nanos_mixed.js @@ -23,6 +23,7 @@ export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const fromTime = 'Jan 1, 2019 @ 00:00:00.000'; const toTime = 'Jan 1, 2019 @ 23:59:59.999'; @@ -30,12 +31,14 @@ export default function({ getService, getPageObjects }) { before(async function() { await esArchiver.loadIfNeeded('date_nanos_mixed'); await kibanaServer.uiSettings.replace({ defaultIndex: 'timestamp-*' }); + await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_mixed']); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }); - after(function unloadMakelogs() { - return esArchiver.unload('date_nanos_mixed'); + after(async () => { + await security.testUser.restoreDefaults(); + esArchiver.unload('date_nanos_mixed'); }); it('shows a list of records of indices with date & date_nanos fields in the right order', async function() { diff --git a/test/functional/apps/discover/_discover_histogram.js b/test/functional/apps/discover/_discover_histogram.js index 931083866625..f815c505a8c2 100644 --- a/test/functional/apps/discover/_discover_histogram.js +++ b/test/functional/apps/discover/_discover_histogram.js @@ -25,6 +25,7 @@ export default function({ getService, getPageObjects }) { const browser = getService('browser'); const elasticChart = getService('elasticChart'); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const PageObjects = getPageObjects(['settings', 'common', 'discover', 'header', 'timePicker']); const defaultSettings = { defaultIndex: 'long-window-logstash-*', @@ -35,6 +36,11 @@ export default function({ getService, getPageObjects }) { before(async function() { log.debug('load kibana index with default index pattern'); await PageObjects.common.navigateToApp('home'); + await security.testUser.setRoles([ + 'kibana_admin', + 'test_logstash_reader', + 'long_window_logstash', + ]); await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('long_window_logstash'); await esArchiver.load('visualize'); @@ -56,6 +62,7 @@ export default function({ getService, getPageObjects }) { await esArchiver.unload('long_window_logstash'); await esArchiver.unload('visualize'); await esArchiver.unload('discover'); + await security.testUser.restoreDefaults(); }); it('should visualize monthly data with different day intervals', async () => { diff --git a/test/functional/apps/discover/_large_string.js b/test/functional/apps/discover/_large_string.js index a5052b240307..5e9048e2bc48 100644 --- a/test/functional/apps/discover/_large_string.js +++ b/test/functional/apps/discover/_large_string.js @@ -25,10 +25,12 @@ export default function({ getService, getPageObjects }) { const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); const queryBar = getService('queryBar'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover']); describe('test large strings', function() { before(async function() { + await security.testUser.setRoles(['kibana_admin', 'kibana_large_strings']); await esArchiver.load('empty_kibana'); await esArchiver.loadIfNeeded('hamlet'); await kibanaServer.uiSettings.replace({ defaultIndex: 'testlargestring' }); @@ -77,6 +79,7 @@ export default function({ getService, getPageObjects }) { }); after(async () => { + await security.testUser.restoreDefaults(); await esArchiver.unload('hamlet'); }); }); diff --git a/test/functional/apps/getting_started/_shakespeare.js b/test/functional/apps/getting_started/_shakespeare.js index 5af1676cf423..ded4eca90841 100644 --- a/test/functional/apps/getting_started/_shakespeare.js +++ b/test/functional/apps/getting_started/_shakespeare.js @@ -23,6 +23,7 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); const esArchiver = getService('esArchiver'); const retry = getService('retry'); + const security = getService('security'); const PageObjects = getPageObjects([ 'console', 'common', @@ -46,11 +47,16 @@ export default function({ getService, getPageObjects }) { 'Load empty_kibana and Shakespeare Getting Started data\n' + 'https://www.elastic.co/guide/en/kibana/current/tutorial-load-dataset.html' ); + await security.testUser.setRoles(['kibana_admin', 'test_shakespeare_reader']); await esArchiver.load('empty_kibana', { skipExisting: true }); log.debug('Load shakespeare data'); await esArchiver.loadIfNeeded('getting_started/shakespeare'); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should create shakespeare index pattern', async function() { log.debug('Create shakespeare index pattern'); await PageObjects.settings.createIndexPattern('shakes', null); diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 8bc528e04556..5812b9b96e42 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -25,6 +25,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const find = getService('find'); const log = getService('log'); + const security = getService('security'); const pieChart = getService('pieChart'); const renderable = getService('renderable'); const dashboardExpect = getService('dashboardExpect'); @@ -34,10 +35,15 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { this.tags('smoke'); before(async () => { + await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await PageObjects.common.navigateToUrl('home', 'tutorial_directory/sampleData'); await PageObjects.header.waitUntilLoadingHasFinished(); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should display registered flights sample data sets', async () => { await retry.try(async () => { const exists = await PageObjects.home.doesSampleDataSetExist('flights'); diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.js index 55f6b56d9f0d..4ef02f6c9e87 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.js @@ -23,11 +23,13 @@ export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); const retry = getService('retry'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'timePicker']); // FLAKY: https://github.com/elastic/kibana/issues/59717 describe.skip('Index patterns on aliases', function() { before(async function() { + await security.testUser.setRoles(['kibana_admin', 'test_alias_reader']); await esArchiver.loadIfNeeded('alias'); await esArchiver.load('empty_kibana'); await es.indices.updateAliases({ @@ -84,6 +86,7 @@ export default function({ getService, getPageObjects }) { }); after(async () => { + await security.testUser.restoreDefaults(); await esArchiver.unload('alias'); }); }); diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.js index 643cbcbe8948..bc280e51ae04 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.js @@ -21,6 +21,7 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings']); describe('test large number of fields', function() { @@ -28,6 +29,7 @@ export default function({ getService, getPageObjects }) { const EXPECTED_FIELD_COUNT = '10006'; before(async function() { + await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']); await esArchiver.loadIfNeeded('large_fields'); await PageObjects.settings.createIndexPattern('testhuge', 'date'); }); @@ -38,6 +40,7 @@ export default function({ getService, getPageObjects }) { }); after(async () => { + await security.testUser.restoreDefaults(); await esArchiver.unload('large_fields'); }); }); diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index 101b2d4f547d..bf836cfe778b 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -24,6 +24,7 @@ export default function({ getService, getPageObjects }) { const inspector = getService('inspector'); const browser = getService('browser'); const retry = getService('retry'); + const security = getService('security'); const PageObjects = getPageObjects([ 'common', 'visualize', @@ -58,7 +59,14 @@ export default function({ getService, getPageObjects }) { return PageObjects.visEditor.clickGo(); }; - before(initAreaChart); + before(async function() { + await security.testUser.setRoles([ + 'kibana_admin', + 'long_window_logstash', + 'test_logstash_reader', + ]); + await initAreaChart(); + }); it('should save and load with special characters', async function() { const vizNamewithSpecialChars = vizName1 + '/?&=%'; @@ -284,6 +292,7 @@ export default function({ getService, getPageObjects }) { .pop() .replace('embed=true', ''); await PageObjects.common.navigateToUrl('visualize', embedUrl); + await security.testUser.restoreDefaults(); }); }); diff --git a/test/functional/apps/visualize/_experimental_vis.js b/test/functional/apps/visualize/_experimental_vis.js index 2ce15cf913ef..c45a95abab86 100644 --- a/test/functional/apps/visualize/_experimental_vis.js +++ b/test/functional/apps/visualize/_experimental_vis.js @@ -23,7 +23,7 @@ export default ({ getService, getPageObjects }) => { const log = getService('log'); const PageObjects = getPageObjects(['visualize']); - describe('visualize app', function() { + describe('experimental visualizations in visualize app ', function() { this.tags('smoke'); describe('experimental visualizations', () => { diff --git a/test/functional/apps/visualize/_linked_saved_searches.ts b/test/functional/apps/visualize/_linked_saved_searches.ts index 345987a80339..ea42f7c67198 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.ts +++ b/test/functional/apps/visualize/_linked_saved_searches.ts @@ -32,7 +32,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { 'visChart', ]); - describe('visualize app', function describeIndexTests() { + describe('saved search visualizations from visualize app', function describeIndexTests() { describe('linked saved searched', () => { const savedSearchName = 'vis_saved_search'; diff --git a/test/functional/apps/visualize/_markdown_vis.js b/test/functional/apps/visualize/_markdown_vis.js index fee6c074af5d..649fe0a8e4c2 100644 --- a/test/functional/apps/visualize/_markdown_vis.js +++ b/test/functional/apps/visualize/_markdown_vis.js @@ -29,7 +29,7 @@ export default function({ getPageObjects, getService }) {

    Inline HTML that should not be rendered as html

    `; - describe('visualize app', () => { + describe('markdown app in visualize app', () => { before(async function() { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickMarkdownWidget(); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 6a4bed3ba589..867db66ac81d 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -25,11 +25,13 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const log = getService('log'); const inspector = getService('inspector'); + const security = getService('security'); const PageObjects = getPageObjects(['visualize', 'visualBuilder', 'timePicker', 'visChart']); describe('visual builder', function describeIndexTests() { this.tags('smoke'); beforeEach(async () => { + await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisualBuilder(); await PageObjects.visualBuilder.checkVisualBuilderIsPresent(); @@ -111,8 +113,10 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualBuilder.resetPage(); await PageObjects.visualBuilder.clickMetric(); await PageObjects.visualBuilder.checkMetricTabIsPresent(); + await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); }); after(async () => { + await security.testUser.restoreDefaults(); await esArchiver.unload('kibana_sample_data_flights'); }); diff --git a/test/functional/apps/visualize/_vega_chart.js b/test/functional/apps/visualize/_vega_chart.js index df0603c7f95f..7a19bde341cd 100644 --- a/test/functional/apps/visualize/_vega_chart.js +++ b/test/functional/apps/visualize/_vega_chart.js @@ -25,7 +25,7 @@ export default function({ getService, getPageObjects }) { const inspector = getService('inspector'); const log = getService('log'); - describe('visualize app', () => { + describe('vega chart in visualize app', () => { before(async () => { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); diff --git a/test/functional/apps/visualize/input_control_vis/input_control_range.ts b/test/functional/apps/visualize/input_control_vis/input_control_range.ts index f48ba7b54daf..8f079f5cc430 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_range.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_range.ts @@ -25,10 +25,12 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const find = getService('find'); + const security = getService('security'); const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']); describe('input control range', () => { before(async () => { + await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await esArchiver.load('kibana_sample_data_flights_index_pattern'); await visualize.navigateToNewVisualization(); await visualize.clickInputControlVis(); @@ -63,6 +65,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.loadIfNeeded('long_window_logstash'); await esArchiver.load('visualize'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await security.testUser.restoreDefaults(); }); }); } diff --git a/test/functional/config.js b/test/functional/config.js index e84b7e0a98a6..11399bd6187c 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -103,5 +103,172 @@ export default async function({ readConfigFile }) { browser: { type: 'chrome', }, + + security: { + roles: { + test_logstash_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['logstash*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + test_shakespeare_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['shakes*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + test_testhuge_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['testhuge*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + test_alias_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['alias*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + //for sample data - can remove but not add sample data.( not ml)- for ml use built in role. + kibana_sample_admin: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['kibana_sample*'], + privileges: ['read', 'view_index_metadata', 'manage', 'create_index', 'index'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + kibana_date_nanos: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['date-nanos'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + kibana_date_nanos_custom: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['date_nanos_custom_timestamp'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + kibana_date_nanos_mixed: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['date_nanos_mixed', 'timestamp-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + kibana_large_strings: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['testlargestring'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + long_window_logstash: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['long-window-logstash-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + animals: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['animals-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + }, + defaultRoles: ['test_logstash_reader', 'kibana_admin'], + }, }; } diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 60966511c1f9..5ee3726ddb44 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -105,13 +105,16 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo const wantedLoginPage = appUrl.includes('/login') || appUrl.includes('/logout'); if (loginPage && !wantedLoginPage) { - log.debug( - `Found login page. Logging in with username = ${config.get('servers.kibana.username')}` - ); - await PageObjects.shield.login( - config.get('servers.kibana.username'), - config.get('servers.kibana.password') - ); + log.debug('Found login page'); + if (config.get('security.disableTestUser')) { + await PageObjects.shield.login( + config.get('servers.kibana.username'), + config.get('servers.kibana.password') + ); + } else { + await PageObjects.shield.login('test_user', 'changeme'); + } + await find.byCssSelector( '[data-test-subj="kibanaChrome"] nav:not(.ng-hide)', 6 * defaultFindTimeout diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index 02349b4e6cca..5017947e95d0 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -21,6 +21,7 @@ import { cloneDeep } from 'lodash'; import { Key, Origin } from 'selenium-webdriver'; // @ts-ignore internal modules are not typed import { LegacyActionSequence } from 'selenium-webdriver/lib/actions'; +import { ProvidedType } from '@kbn/test/types/ftr'; import Jimp from 'jimp'; import { modifyUrl } from '../../../src/core/utils'; @@ -28,6 +29,7 @@ import { WebElementWrapper } from './lib/web_element_wrapper'; import { FtrProviderContext } from '../ftr_provider_context'; import { Browsers } from './remote/browsers'; +export type Browser = ProvidedType; export async function BrowserProvider({ getService }: FtrProviderContext) { const log = getService('log'); const { driver, browserType } = await getService('__webdriver__').init(); diff --git a/test/functional/services/test_subjects.ts b/test/functional/services/test_subjects.ts index d47b838c8d72..e5c2e61c48a0 100644 --- a/test/functional/services/test_subjects.ts +++ b/test/functional/services/test_subjects.ts @@ -19,6 +19,7 @@ import testSubjSelector from '@kbn/test-subj-selector'; import { map as mapAsync } from 'bluebird'; +import { ProvidedType } from '@kbn/test/types/ftr'; import { WebElementWrapper } from './lib/web_element_wrapper'; import { FtrProviderContext } from '../ftr_provider_context'; @@ -32,6 +33,7 @@ interface SetValueOptions { typeCharByChar?: boolean; } +export type TestSubjects = ProvidedType; export function TestSubjectsProvider({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 182a9105a7df..b62368bf2d60 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -15,6 +15,7 @@ export async function getApiIntegrationConfig({ readConfigFile }) { testFiles: [require.resolve('./apis')], services, servers: xPackFunctionalTestsConfig.get('servers'), + security: xPackFunctionalTestsConfig.get('security'), esArchiver: xPackFunctionalTestsConfig.get('esArchiver'), junit: { reportName: 'X-Pack API Integration Tests', diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index b9c0b0095b96..78cef80c7ca8 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -12,6 +12,7 @@ export default function({ getService, getPageObjects }) { const browser = getService('browser'); const log = getService('log'); const pieChart = getService('pieChart'); + const security = getService('security'); const testSubjects = getService('testSubjects'); const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardPanelActions = getService('dashboardPanelActions'); @@ -109,13 +110,15 @@ export default function({ getService, getPageObjects }) { await PageObjects.security.clickSaveEditUser(); }); - after('logout', async () => { - await PageObjects.security.forceLogout(); + after(async () => { + await security.testUser.restoreDefaults(); }); it('shows only the dashboard app link', async () => { + await security.testUser.setRoles(['test_logstash_reader', 'kibana_dashboard_only_user']); + await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.security.forceLogout(); - await PageObjects.security.login('dashuser', '123456'); + await PageObjects.security.login('test_user', 'changeme'); const appLinks = await appsMenu.readLinks(); expect(appLinks).to.have.length(1); @@ -194,8 +197,12 @@ export default function({ getService, getPageObjects }) { }); it('is loaded for a user who is assigned a non-dashboard mode role', async () => { - await PageObjects.security.forceLogout(); - await PageObjects.security.login('mixeduser', '123456'); + await security.testUser.setRoles([ + 'test_logstash_reader', + 'kibana_dashboard_only_user', + 'kibana_admin', + ]); + await PageObjects.header.waitUntilLoadingHasFinished(); if (await appsMenu.linkExists('Management')) { throw new Error('Expected management nav link to not be shown'); @@ -203,8 +210,8 @@ export default function({ getService, getPageObjects }) { }); it('is not loaded for a user who is assigned a superuser role', async () => { - await PageObjects.security.forceLogout(); - await PageObjects.security.login('mysuperuser', '123456'); + await security.testUser.setRoles(['kibana_dashboard_only_user', 'superuser']); + await PageObjects.header.waitUntilLoadingHasFinished(); if (!(await appsMenu.linkExists('Management'))) { throw new Error('Expected management nav link to be shown'); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 09ec403af742..1586908d8b5e 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -217,5 +217,24 @@ export default async function({ readConfigFile }) { junit: { reportName: 'Chrome X-Pack UI Functional Tests', }, + security: { + roles: { + test_logstash_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['logstash*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + }, + defaultRoles: ['superuser'], + }, }; } diff --git a/x-pack/test/oidc_api_integration/config.ts b/x-pack/test/oidc_api_integration/config.ts index 724ffd35cc9e..557dea4d51b0 100644 --- a/x-pack/test/oidc_api_integration/config.ts +++ b/x-pack/test/oidc_api_integration/config.ts @@ -17,6 +17,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { return { testFiles: [require.resolve('./apis/authorization_code_flow')], servers: xPackAPITestsConfig.get('servers'), + security: { disableTestUser: true }, services, junit: { reportName: 'X-Pack OpenID Connect API Integration Tests', diff --git a/x-pack/test/pki_api_integration/config.ts b/x-pack/test/pki_api_integration/config.ts index a445b3d4943b..21ae1b40efa1 100644 --- a/x-pack/test/pki_api_integration/config.ts +++ b/x-pack/test/pki_api_integration/config.ts @@ -28,6 +28,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { return { testFiles: [require.resolve('./apis')], servers, + security: { disableTestUser: true }, services, junit: { reportName: 'X-Pack PKI API Integration Tests', diff --git a/x-pack/test/reporting/configs/chromium_functional.js b/x-pack/test/reporting/configs/chromium_functional.js index 81a51f44c1c5..05c3b6c14294 100644 --- a/x-pack/test/reporting/configs/chromium_functional.js +++ b/x-pack/test/reporting/configs/chromium_functional.js @@ -33,5 +33,6 @@ export default async function({ readConfigFile }) { }, esArchiver: functionalConfig.get('esArchiver'), esTestCluster: functionalConfig.get('esTestCluster'), + security: { disableTestUser: true }, }; } diff --git a/x-pack/test/saml_api_integration/config.ts b/x-pack/test/saml_api_integration/config.ts index 6ea29b0d9e56..502d34d4c9e5 100644 --- a/x-pack/test/saml_api_integration/config.ts +++ b/x-pack/test/saml_api_integration/config.ts @@ -19,6 +19,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { return { testFiles: [require.resolve('./apis')], servers: xPackAPITestsConfig.get('servers'), + security: { disableTestUser: true }, services: { randomness: kibanaAPITestsConfig.get('services.randomness'), legacyEs: kibanaAPITestsConfig.get('services.legacyEs'), diff --git a/x-pack/test/token_api_integration/config.js b/x-pack/test/token_api_integration/config.js index db5ee6a9a1cb..84322ff9473f 100644 --- a/x-pack/test/token_api_integration/config.js +++ b/x-pack/test/token_api_integration/config.js @@ -10,6 +10,7 @@ export default async function({ readConfigFile }) { return { testFiles: [require.resolve('./auth')], servers: xPackAPITestsConfig.get('servers'), + security: { disableTestUser: true }, services: { legacyEs: xPackAPITestsConfig.get('services.legacyEs'), supertestWithoutAuth: xPackAPITestsConfig.get('services.supertestWithoutAuth'), From f875b7165e3ea0ba925f9a4dd89c39703a5e6bab Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 17 Mar 2020 13:41:46 -0400 Subject: [PATCH 055/115] do not update cell background if is label cell (#60308) --- .../classification_exploration/column_data.tsx | 4 +++- .../classification_exploration/evaluate_panel.tsx | 14 ++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx index 5a08dd159aff..baf7fd32b0f6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx @@ -15,6 +15,8 @@ interface ColumnData { error_count?: number; } +export const ACTUAL_CLASS_ID = 'actual_class'; + export function getColumnData(confusionMatrixData: ConfusionMatrix[]) { const colData: Partial = []; @@ -67,7 +69,7 @@ export function getColumnData(confusionMatrixData: ConfusionMatrix[]) { const columns: any = [ { - id: 'actual_class', + id: ACTUAL_CLASS_ID, display: , }, ]; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 23dd1ae288d8..7bf55f4ecf39 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -39,7 +39,7 @@ import { ANALYSIS_CONFIG_TYPE, } from '../../../../common/analytics'; import { LoadingPanel } from '../loading_panel'; -import { getColumnData } from './column_data'; +import { getColumnData, ACTUAL_CLASS_ID } from './column_data'; const defaultPanelWidth = 500; @@ -205,11 +205,13 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) const cellValue = columnsData[rowIndex][columnId]; // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { - setCellProps({ - style: { - backgroundColor: `rgba(0, 179, 164, ${cellValue})`, - }, - }); + if (columnId !== ACTUAL_CLASS_ID) { + setCellProps({ + style: { + backgroundColor: `rgba(0, 179, 164, ${cellValue})`, + }, + }); + } }, [rowIndex, columnId, setCellProps]); return ( {typeof cellValue === 'number' ? `${Math.round(cellValue * 100)}%` : cellValue} From 928454afa4ce0f7136c90b2a8756e0600b55e360 Mon Sep 17 00:00:00 2001 From: Nick Peihl Date: Tue, 17 Mar 2020 11:51:17 -0700 Subject: [PATCH 056/115] Update the ems-client dependency to 7.7.0 (#59936) * Update the ems-client dependency This PR adds the `appName` and `appVersion` parameters used by ems-client. The `appVersion` parameter replaces the now deprecated `kbnVersion` parameter in ems-client. * Review feedback * Fix borked merge Co-authored-by: Elastic Machine --- package.json | 2 +- src/legacy/ui/public/vis/map/service_settings.js | 3 ++- x-pack/legacy/plugins/maps/public/meta.js | 4 +++- x-pack/legacy/plugins/maps/server/routes.js | 4 +++- x-pack/package.json | 2 +- x-pack/plugins/maps/common/constants.ts | 1 + yarn.lock | 8 ++++---- 7 files changed, 15 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 261b3ad74d9b..13796dcbf2b7 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "@elastic/apm-rum": "^4.6.0", "@elastic/charts": "^17.1.1", "@elastic/datemath": "5.0.2", - "@elastic/ems-client": "7.6.0", + "@elastic/ems-client": "7.7.0", "@elastic/eui": "20.0.2", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", diff --git a/src/legacy/ui/public/vis/map/service_settings.js b/src/legacy/ui/public/vis/map/service_settings.js index 233ee526c439..9f3d21831e3d 100644 --- a/src/legacy/ui/public/vis/map/service_settings.js +++ b/src/legacy/ui/public/vis/map/service_settings.js @@ -47,7 +47,8 @@ uiModules this._showZoomMessage = true; this._emsClient = new EMSClient({ language: i18n.getLocale(), - kbnVersion: kbnVersion, + appVersion: kbnVersion, + appName: 'kibana', fileApiUrl: mapConfig.emsFileApiUrl, tileApiUrl: mapConfig.emsTileApiUrl, htmlSanitizer: $sanitize, diff --git a/x-pack/legacy/plugins/maps/public/meta.js b/x-pack/legacy/plugins/maps/public/meta.js index c5cfb582976c..4d81785ff7a0 100644 --- a/x-pack/legacy/plugins/maps/public/meta.js +++ b/x-pack/legacy/plugins/maps/public/meta.js @@ -9,6 +9,7 @@ import { EMS_FILES_CATALOGUE_PATH, EMS_TILES_CATALOGUE_PATH, EMS_GLYPHS_PATH, + EMS_APP_NAME, } from '../common/constants'; import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; @@ -56,7 +57,8 @@ export function getEMSClient() { emsClient = new EMSClient({ language: i18n.getLocale(), - kbnVersion: chrome.getInjected('kbnPkgVersion'), + appVersion: chrome.getInjected('kbnPkgVersion'), + appName: EMS_APP_NAME, tileApiUrl, fileApiUrl, landingPageUrl: chrome.getInjected('emsLandingPageUrl'), diff --git a/x-pack/legacy/plugins/maps/server/routes.js b/x-pack/legacy/plugins/maps/server/routes.js index 757750dbb081..7ca659148449 100644 --- a/x-pack/legacy/plugins/maps/server/routes.js +++ b/x-pack/legacy/plugins/maps/server/routes.js @@ -5,6 +5,7 @@ */ import { + EMS_APP_NAME, EMS_CATALOGUE_PATH, EMS_FILES_API_PATH, EMS_FILES_CATALOGUE_PATH, @@ -38,7 +39,8 @@ export function initRoutes(server, licenseUid) { if (mapConfig.includeElasticMapsService) { emsClient = new EMSClient({ language: i18n.getLocale(), - kbnVersion: serverConfig.get('pkg.version'), + appVersion: serverConfig.get('pkg.version'), + appName: EMS_APP_NAME, fileApiUrl: mapConfig.emsFileApiUrl, tileApiUrl: mapConfig.emsTileApiUrl, landingPageUrl: mapConfig.emsLandingPageUrl, diff --git a/x-pack/package.json b/x-pack/package.json index 3c8aa435c3e4..20b9dcc6cd0b 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -179,7 +179,7 @@ "@babel/runtime": "^7.5.5", "@elastic/apm-rum-react": "^0.3.2", "@elastic/datemath": "5.0.2", - "@elastic/ems-client": "7.6.0", + "@elastic/ems-client": "7.7.0", "@elastic/eui": "20.0.2", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.1.0", diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index b1483cefa43b..b608151d26ae 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -10,6 +10,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; +export const EMS_APP_NAME = 'kibana'; export const EMS_CATALOGUE_PATH = 'ems/catalogue'; export const EMS_FILES_CATALOGUE_PATH = 'ems/files'; diff --git a/yarn.lock b/yarn.lock index 1e5c160a7eb1..ea153e2a9c63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1916,10 +1916,10 @@ once "^1.4.0" pump "^3.0.0" -"@elastic/ems-client@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.6.0.tgz#ca548aba1a1f5170a1892de129b537b5248c74be" - integrity sha512-oBtLH24qIgTaMhlSske49FTd35Y0nv+PlZCZaHkBhOH+ScsTDL3LO2lbIcSmcYQod43Ly34v/xwJvFCTxojVEQ== +"@elastic/ems-client@7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.7.0.tgz#7d36d716dd941f060b9fcdae94f186a9aecc5cc2" + integrity sha512-JatsSyLik/8MTEOEimzEZ3NYjvGL1YzjbGujuSCgaXhPRqzu/wvMLEL8dlVpmYFZ7ALbGNsVdho4Hr8tngsIMw== dependencies: lodash "^4.17.15" node-fetch "^1.7.3" From 9f31565b885f149c4473ba7408d5625c9b538690 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 17 Mar 2020 19:25:01 +0000 Subject: [PATCH 057/115] [ML] Fixing custom urls to dashboards (#60355) * [ML] Fixing custom urls to dashboards * missing file Co-authored-by: Elastic Machine --- x-pack/plugins/ml/kibana.json | 3 +- x-pack/plugins/ml/public/application/app.tsx | 1 + .../components/custom_url_editor/utils.js | 139 +++++++++--------- .../application/util/dependency_cache.ts | 11 ++ x-pack/plugins/ml/public/plugin.ts | 3 + 5 files changed, 85 insertions(+), 72 deletions(-) diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 2f8863fc1d7c..b6db289f4be6 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -12,7 +12,8 @@ "features", "home", "licensing", - "usageCollection" + "usageCollection", + "share" ], "optionalPlugins": [ "security", diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 206189c79696..259771548839 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -41,6 +41,7 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { application: coreStart.application, http: coreStart.http, security: deps.security, + urlGenerators: deps.share.urlGenerators, }); const mlLicense = setLicenseCache(deps.licensing); diff --git a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js index e9b39058c23a..a7734289314a 100644 --- a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js @@ -7,10 +7,9 @@ import { TIME_RANGE_TYPE, URL_TYPE } from './constants'; import rison from 'rison-node'; -// import url from 'url'; +import url from 'url'; -// import { npStart } from 'ui/new_platform'; -// import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../../src/plugins/dashboard_embeddable_container/public'; +import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../../src/plugins/dashboard/public'; import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; import { getPartitioningFieldNames } from '../../../../../common/util/job_utils'; @@ -19,7 +18,7 @@ import { replaceTokensInUrlValue, isValidLabel } from '../../../util/custom_url_ import { ml } from '../../../services/ml_api_service'; import { mlJobService } from '../../../services/job_service'; import { escapeForElasticsearchQuery } from '../../../util/string_utils'; -// import { getSavedObjectsClient } from '../../../util/dependency_cache'; +import { getSavedObjectsClient, getGetUrlGenerator } from '../../../util/dependency_cache'; export function getNewCustomUrlDefaults(job, dashboards, indexPatterns) { // Returns the settings object in the format used by the custom URL editor @@ -119,7 +118,7 @@ export function buildCustomUrlFromSettings(settings) { // Dashboard URL returns a Promise as a query is made to obtain the full dashboard config. // So wrap the other two return types in a Promise for consistent return type. if (settings.type === URL_TYPE.KIBANA_DASHBOARD) { - // return buildDashboardUrlFromSettings(settings); + return buildDashboardUrlFromSettings(settings); } else if (settings.type === URL_TYPE.KIBANA_DISCOVER) { return Promise.resolve(buildDiscoverUrlFromSettings(settings)); } else { @@ -132,72 +131,70 @@ export function buildCustomUrlFromSettings(settings) { } } -// function buildDashboardUrlFromSettings(settings) { -// // Get the complete list of attributes for the selected dashboard (query, filters). -// return new Promise((resolve, reject) => { -// const { dashboardId, queryFieldNames } = settings.kibanaSettings; - -// const savedObjectsClient = getSavedObjectsClient(); -// savedObjectsClient -// .get('dashboard', dashboardId) -// .then(response => { -// // Use the filters from the saved dashboard if there are any. -// // let filters = []; - -// // Use the query from the dashboard only if no job entities are selected. -// let query = undefined; - -// const searchSourceJSON = response.get('kibanaSavedObjectMeta.searchSourceJSON'); -// if (searchSourceJSON !== undefined) { -// const searchSourceData = JSON.parse(searchSourceJSON); -// if (searchSourceData.filter !== undefined) { -// filters = searchSourceData.filter; -// } -// query = searchSourceData.query; -// } - -// const queryFromEntityFieldNames = buildAppStateQueryParam(queryFieldNames); -// if (queryFromEntityFieldNames !== undefined) { -// query = queryFromEntityFieldNames; -// } - -// const generator = npStart.plugins.share.urlGenerators.getUrlGenerator( -// DASHBOARD_APP_URL_GENERATOR -// ); - -// return generator -// .createUrl({ -// dashboardId, -// timeRange: { -// from: '$earliest$', -// to: '$latest$', -// mode: 'absolute', -// }, -// filters, -// query, -// // Don't hash the URL since this string will be 1. shown to the user and 2. used as a -// // template to inject the time parameters. -// useHash: false, -// }) -// .then(urlValue => { -// const urlToAdd = { -// url_name: settings.label, -// url_value: decodeURIComponent(`kibana${url.parse(urlValue).hash}`), -// time_range: TIME_RANGE_TYPE.AUTO, -// }; - -// if (settings.timeRange.type === TIME_RANGE_TYPE.INTERVAL) { -// urlToAdd.time_range = settings.timeRange.interval; -// } - -// resolve(urlToAdd); -// }); -// }) -// .catch(resp => { -// reject(resp); -// }); -// }); -// } +function buildDashboardUrlFromSettings(settings) { + // Get the complete list of attributes for the selected dashboard (query, filters). + return new Promise((resolve, reject) => { + const { dashboardId, queryFieldNames } = settings.kibanaSettings; + + const savedObjectsClient = getSavedObjectsClient(); + savedObjectsClient + .get('dashboard', dashboardId) + .then(response => { + // Use the filters from the saved dashboard if there are any. + let filters = []; + + // Use the query from the dashboard only if no job entities are selected. + let query = undefined; + + const searchSourceJSON = response.get('kibanaSavedObjectMeta.searchSourceJSON'); + if (searchSourceJSON !== undefined) { + const searchSourceData = JSON.parse(searchSourceJSON); + if (searchSourceData.filter !== undefined) { + filters = searchSourceData.filter; + } + query = searchSourceData.query; + } + + const queryFromEntityFieldNames = buildAppStateQueryParam(queryFieldNames); + if (queryFromEntityFieldNames !== undefined) { + query = queryFromEntityFieldNames; + } + + const getUrlGenerator = getGetUrlGenerator(); + const generator = getUrlGenerator(DASHBOARD_APP_URL_GENERATOR); + return generator + .createUrl({ + dashboardId, + timeRange: { + from: '$earliest$', + to: '$latest$', + mode: 'absolute', + }, + filters, + query, + // Don't hash the URL since this string will be 1. shown to the user and 2. used as a + // template to inject the time parameters. + useHash: false, + }) + .then(urlValue => { + const urlToAdd = { + url_name: settings.label, + url_value: decodeURIComponent(`kibana${url.parse(urlValue).hash}`), + time_range: TIME_RANGE_TYPE.AUTO, + }; + + if (settings.timeRange.type === TIME_RANGE_TYPE.INTERVAL) { + urlToAdd.time_range = settings.timeRange.interval; + } + + resolve(urlToAdd); + }); + }) + .catch(resp => { + reject(resp); + }); + }); +} function buildDiscoverUrlFromSettings(settings) { const { discoverIndexPatternId, queryFieldNames } = settings.kibanaSettings; diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index 5343c51b525d..d5605d3bca65 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -21,6 +21,7 @@ import { ChromeRecentlyAccessed, IBasePath, } from 'kibana/public'; +import { SharePluginStart } from 'src/plugins/share/public'; import { SecurityPluginSetup } from '../../../../security/public'; export interface DependencyCache { @@ -40,6 +41,7 @@ export interface DependencyCache { http: HttpStart | null; security: SecurityPluginSetup | null; i18n: I18nStart | null; + urlGenerators: SharePluginStart['urlGenerators'] | null; } const cache: DependencyCache = { @@ -59,6 +61,7 @@ const cache: DependencyCache = { http: null, security: null, i18n: null, + urlGenerators: null, }; export function setDependencyCache(deps: Partial) { @@ -78,6 +81,7 @@ export function setDependencyCache(deps: Partial) { cache.http = deps.http || null; cache.security = deps.security || null; cache.i18n = deps.i18n || null; + cache.urlGenerators = deps.urlGenerators || null; } export function getTimefilter() { @@ -191,6 +195,13 @@ export function getI18n() { return cache.i18n; } +export function getGetUrlGenerator() { + if (cache.urlGenerators === null) { + throw new Error("urlGenerators hasn't been initialized"); + } + return cache.urlGenerators.getUrlGenerator; +} + export function clearCache() { console.log('clearing dependency cache'); // eslint-disable-line no-console Object.keys(cache).forEach(k => { diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index ef85a36494df..624e877bda49 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { Plugin, CoreStart, CoreSetup, AppMountParameters } from 'kibana/public'; import { ManagementSetup } from 'src/plugins/management/public'; +import { SharePluginStart } from 'src/plugins/share/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { SecurityPluginSetup } from '../../security/public'; @@ -17,6 +18,7 @@ import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; export interface MlStartDependencies { data: DataPublicPluginStart; + share: SharePluginStart; } export interface MlSetupDependencies { security: SecurityPluginSetup; @@ -41,6 +43,7 @@ export class MlPlugin implements Plugin { coreStart, { data: pluginsStart.data, + share: pluginsStart.share, security: pluginsSetup.security, licensing: pluginsSetup.licensing, management: pluginsSetup.management, From a8e33a87b57b141a925ddc5c8a7bea72c51832a9 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Tue, 17 Mar 2020 13:27:10 -0600 Subject: [PATCH 058/115] Update app arch CODEOWNERS items. (#60396) --- .github/CODEOWNERS | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index accc99170bb7..132a99fb0a15 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -29,33 +29,32 @@ /src/plugins/dashboard/ @elastic/kibana-app # App Architecture +/examples/url_generators_examples/ @elastic/kibana-app-arch +/examples/url_generators_explorer/ @elastic/kibana-app-arch /packages/kbn-interpreter/ @elastic/kibana-app-arch -/src/legacy/core_plugins/data/ @elastic/kibana-app-arch -/src/legacy/core_plugins/elasticsearch/lib/create_proxy.js @elastic/kibana-app-arch /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch /src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch /src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch /src/legacy/core_plugins/kibana/public/management/ @elastic/kibana-app-arch -/src/legacy/core_plugins/kibana/server/field_formats/ @elastic/kibana-app-arch /src/legacy/core_plugins/kibana/server/routes/api/management/ @elastic/kibana-app-arch -/src/legacy/core_plugins/kibana/server/routes/api/suggestions/ @elastic/kibana-app-arch /src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch /src/legacy/server/index_patterns/ @elastic/kibana-app-arch +/src/plugins/advanced_settings/ @elastic/kibana-app-arch /src/plugins/bfetch/ @elastic/kibana-app-arch /src/plugins/data/ @elastic/kibana-app-arch /src/plugins/embeddable/ @elastic/kibana-app-arch /src/plugins/expressions/ @elastic/kibana-app-arch /src/plugins/inspector/ @elastic/kibana-app-arch /src/plugins/kibana_react/ @elastic/kibana-app-arch +/src/plugins/kibana_react/public/code_editor @elastic/kibana-canvas /src/plugins/kibana_utils/ @elastic/kibana-app-arch /src/plugins/management/ @elastic/kibana-app-arch /src/plugins/navigation/ @elastic/kibana-app-arch +/src/plugins/share/ @elastic/kibana-app-arch /src/plugins/ui_actions/ @elastic/kibana-app-arch /src/plugins/visualizations/ @elastic/kibana-app-arch -/src/plugins/share/ @elastic/kibana-app-arch -/examples/url_generators_examples/ @elastic/kibana-app-arch -/examples/url_generators_explorer/ @elastic/kibana-app-arch /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch +/x-pack/plugins/data_enhanced/ @elastic/kibana-app-arch /x-pack/plugins/drilldowns/ @elastic/kibana-app-arch # APM From 631d93da50335a1db1519bb7fc5dfb35210da758 Mon Sep 17 00:00:00 2001 From: Karen Metts <35154725+karenzone@users.noreply.github.com> Date: Tue, 17 Mar 2020 16:11:40 -0400 Subject: [PATCH 059/115] Remove link to old settings (#60326) --- docs/settings/monitoring-settings.asciidoc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 8586d26e9a07..6645f49029a5 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -20,9 +20,7 @@ which support the same values as <>. To control how data is collected from your {es} nodes, you configure {ref}/monitoring-settings.html[`xpack.monitoring.collection` settings] in `elasticsearch.yml`. To control how monitoring data is collected -from Logstash, you configure -{logstash-ref}/monitoring-internal-collection.html#monitoring-settings[`xpack.monitoring` settings] -in `logstash.yml`. +from Logstash, configure monitoring settings in `logstash.yml`. For more information, see {ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. From 2367d749c102faf1f039a5c03d2d16eff43f06f9 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 17 Mar 2020 15:24:59 -0700 Subject: [PATCH 060/115] upgrade react-use (#60427) Co-authored-by: spalger Co-authored-by: Elastic Machine --- package.json | 2 +- x-pack/package.json | 2 +- yarn.lock | 42 +++++++++++++++++++++++++++--------------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 13796dcbf2b7..583e99158da7 100644 --- a/package.json +++ b/package.json @@ -239,7 +239,7 @@ "react-resize-detector": "^4.2.0", "react-router-dom": "^5.1.2", "react-sizeme": "^2.3.6", - "react-use": "^13.13.0", + "react-use": "^13.27.0", "reactcss": "1.2.3", "redux": "^4.0.5", "redux-actions": "^2.6.5", diff --git a/x-pack/package.json b/x-pack/package.json index 20b9dcc6cd0b..6f15b46e28f5 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -317,7 +317,7 @@ "react-sticky": "^6.0.3", "react-syntax-highlighter": "^5.7.0", "react-tiny-virtual-list": "^2.2.0", - "react-use": "^13.13.0", + "react-use": "^13.27.0", "react-vis": "^1.8.1", "react-visibility-sensor": "^5.1.1", "recompose": "^0.26.0", diff --git a/yarn.lock b/yarn.lock index ea153e2a9c63..a7e29935c7ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4678,6 +4678,11 @@ dependencies: "@types/sizzle" "*" +"@types/js-cookie@2.2.5": + version "2.2.5" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.5.tgz#38dfaacae8623b37cc0b0d27398e574e3fc28b1e" + integrity sha512-cpmwBRcHJmmZx0OGU7aPVwGWGbs4iKwVYchk9iuMtxNCA2zorwdaTz4GkLgs2WGxiRZRFKnV1k6tRUHX7tBMxg== + "@types/js-search@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@types/js-search/-/js-search-1.4.0.tgz#f2d4afa176a4fc7b17fb46a1593847887fa1fb7b" @@ -5768,10 +5773,10 @@ dependencies: tslib "^1.9.3" -"@xobotyi/scrollbar-width@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.5.0.tgz#488210bff634548040dc22a72f62722a85b134e1" - integrity sha512-BK+HR1D00F2xh7n4+5en8/dMkG13uvIXLmEbsjtc1702b7+VwXkvlBDKoRPJMbkRN5hD7VqWa3nS9fNT8JG3CA== +"@xobotyi/scrollbar-width@1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.4.tgz#a7dce20b7465bcad29cd6bbb557695e4ea7863cb" + integrity sha512-o12FCQt/X5n3pgKEWGpt0f/7Eg4mfv3uRwPUrctiOT8ZuxbH3cNLGWfH/8y6KxVJg4L2885ucuXQ6XECZzUiJA== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -13667,10 +13672,10 @@ fast-safe-stringify@2.x.x, fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0. resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== -fast-shallow-equal@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-0.1.1.tgz#44d01324d7fd31e00a67bb02b9396e283d526c22" - integrity sha512-XVP6nhaXLYOH6JZCWBcNaeEer9GJ5/8cJWUP+OLmgwWgEkJp5Kpl/fdpJ01zl0mpLxrk7f5J3hIv+GmjTCi7Mg== +fast-shallow-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b" + integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw== fast-stream-to-buffer@^1.0.0: version "1.0.0" @@ -18498,6 +18503,11 @@ js-base64@^2.1.8: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.5.tgz#e293cd3c7c82f070d700fc7a1ca0a2e69f101f92" integrity sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ== +js-cookie@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" + integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== + js-levenshtein@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.3.tgz#3ef627df48ec8cf24bacf05c0f184ff30ef413c5" @@ -24919,16 +24929,18 @@ react-transition-group@^2.2.1: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" -react-use@^13.13.0: - version "13.13.0" - resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.13.0.tgz#5d133c4d4d8d3f21f6ccf4ccbe54fbcd6fdafb36" - integrity sha512-J3/h5wvL6vXmecAvEnninCC3DviLMRWcQrEnouTliwws1b376DQKEgIFuTXlF8c3SKpXBQJdDDm1RpluokW6ag== +react-use@^13.27.0: + version "13.27.0" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.27.0.tgz#53a619dc9213e2cbe65d6262e8b0e76641ade4aa" + integrity sha512-2lyTyqJWyvnaP/woVtDcFS4B5pUYz0FQWI9pVHk/6TBWom2x3/ziJthkEn/LbCA9Twv39xSQU7Dn0zdIWfsNTQ== dependencies: - "@xobotyi/scrollbar-width" "1.5.0" + "@types/js-cookie" "2.2.5" + "@xobotyi/scrollbar-width" "1.9.4" copy-to-clipboard "^3.2.0" - fast-shallow-equal "^0.1.1" + fast-deep-equal "^3.1.1" + fast-shallow-equal "^1.0.0" + js-cookie "^2.2.1" nano-css "^5.2.1" - react-fast-compare "^2.0.4" resize-observer-polyfill "^1.5.1" screenfull "^5.0.0" set-harmonic-interval "^1.0.1" From 32b3861580720503a7864cb92b30ff84e4e7f8de Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 17 Mar 2020 15:25:44 -0700 Subject: [PATCH 061/115] [kbn/pm] don't fail when plugins are outside repo (#60164) * [kbn/pm] don't fail when plugins are outside repo * remove unused import Co-authored-by: spalger Co-authored-by: Elastic Machine --- packages/kbn-pm/dist/index.js | 1244 +++++++++-------- packages/kbn-pm/package.json | 1 + packages/kbn-pm/src/utils/kibana.ts | 12 + .../kbn-pm/src/utils/project_checksums.ts | 26 +- yarn.lock | 2 +- 5 files changed, 681 insertions(+), 604 deletions(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 338d48930052..16fc0d891185 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __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__(704); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(705); /* 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"]; }); @@ -57074,7 +57074,7 @@ async function getChangesForProjects(projects, kbn, log) { log.verbose('getting changed files'); const { stdout - } = await execa__WEBPACK_IMPORTED_MODULE_3___default()('git', ['ls-files', '-dmt', '--', ...Array.from(projects.values()).map(p => p.path)], { + } = await execa__WEBPACK_IMPORTED_MODULE_3___default()('git', ['ls-files', '-dmt', '--', ...Array.from(projects.values()).filter(p => kbn.isPartOfRepo(p)).map(p => p.path)], { cwd: kbn.getAbsolute() }); const output = stdout.trim(); @@ -57117,6 +57117,11 @@ async function getChangesForProjects(projects, kbn, log) { const changesByProject = new Map(); for (const project of sortedRelevantProjects) { + if (kbn.isOutsideRepo(project)) { + changesByProject.set(project, undefined); + continue; + } + const ownChanges = new Map(); const prefix = kbn.getRelative(project.path); @@ -57141,6 +57146,10 @@ async function getChangesForProjects(projects, kbn, log) { async function getLatestSha(project, kbn) { + if (kbn.isOutsideRepo(project)) { + return; + } + const { stdout } = await execa__WEBPACK_IMPORTED_MODULE_3___default()('git', ['log', '-n', '1', '--pretty=format:%H', '--', project.path], { @@ -57200,7 +57209,7 @@ async function getChecksum(project, changes, yarnLock, kbn, log) { log.verbose(`[${project.name}] local sha:`, sha); } - if (Array.from(changes.values()).includes('invalid')) { + if (!changes || Array.from(changes.values()).includes('invalid')) { log.warning(`[${project.name}] unable to determine local changes, caching disabled`); return; } @@ -79162,8 +79171,10 @@ __webpack_require__.r(__webpack_exports__); /* 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___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(501); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(579); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(704); +/* 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); 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; } @@ -79192,6 +79203,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope + /** * Helper class for dealing with a set of projects as children of * the Kibana project. The kbn/pm is currently implemented to be @@ -79206,7 +79218,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope class Kibana { static async loadFrom(rootPath) { - return new Kibana((await Object(_projects__WEBPACK_IMPORTED_MODULE_2__["getProjects"])(rootPath, Object(_config__WEBPACK_IMPORTED_MODULE_3__["getProjectPaths"])({ + return new Kibana((await Object(_projects__WEBPACK_IMPORTED_MODULE_3__["getProjects"])(rootPath, Object(_config__WEBPACK_IMPORTED_MODULE_4__["getProjectPaths"])({ rootPath })))); } @@ -79265,7 +79277,7 @@ class Kibana { getProjectAndDeps(name) { const project = this.getProject(name); - return Object(_projects__WEBPACK_IMPORTED_MODULE_2__["includeTransitiveProjects"])([project], this.allWorkspaceProjects); + return Object(_projects__WEBPACK_IMPORTED_MODULE_3__["includeTransitiveProjects"])([project], this.allWorkspaceProjects); } /** filter the projects to just those matching certain paths/include/exclude tags */ @@ -79274,7 +79286,7 @@ class Kibana { const allProjects = this.getAllProjects(); const filteredProjects = new Map(); const pkgJsonPaths = Array.from(allProjects.values()).map(p => p.packageJsonLocation); - const filteredPkgJsonGlobs = Object(_config__WEBPACK_IMPORTED_MODULE_3__["getProjectPaths"])(_objectSpread({}, options, { + const filteredPkgJsonGlobs = Object(_config__WEBPACK_IMPORTED_MODULE_4__["getProjectPaths"])(_objectSpread({}, options, { rootPath: this.kibanaProject.path })).map(g => path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(g, 'package.json')); const matchingPkgJsonPaths = multimatch__WEBPACK_IMPORTED_MODULE_1___default()(pkgJsonPaths, filteredPkgJsonGlobs); @@ -79292,6 +79304,14 @@ class Kibana { return filteredProjects; } + isPartOfRepo(project) { + return project.path === this.kibanaProject.path || is_path_inside__WEBPACK_IMPORTED_MODULE_2___default()(project.path, this.kibanaProject.path); + } + + isOutsideRepo(project) { + return !this.isPartOfRepo(project); + } + } /***/ }), @@ -79385,14 +79405,42 @@ module.exports = arrify; /***/ }), /* 704 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const path = __webpack_require__(16); + +module.exports = (childPath, parentPath) => { + childPath = path.resolve(childPath); + parentPath = path.resolve(parentPath); + + if (process.platform === 'win32') { + childPath = childPath.toLowerCase(); + parentPath = parentPath.toLowerCase(); + } + + if (childPath === parentPath) { + return false; + } + + childPath += path.sep; + parentPath += path.sep; + + return childPath.startsWith(parentPath); +}; + + +/***/ }), +/* 705 */ /***/ (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__(705); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706); /* 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__(928); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(929); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -79417,13 +79465,13 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 705 */ +/* 706 */ /***/ (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__(706); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(707); /* 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___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); @@ -79565,7 +79613,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 706 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79573,13 +79621,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__(707); -const arrify = __webpack_require__(709); -const globby = __webpack_require__(710); +const pAll = __webpack_require__(708); +const arrify = __webpack_require__(710); +const globby = __webpack_require__(711); const isGlob = __webpack_require__(605); -const cpFile = __webpack_require__(913); -const junk = __webpack_require__(925); -const CpyError = __webpack_require__(926); +const cpFile = __webpack_require__(914); +const junk = __webpack_require__(926); +const CpyError = __webpack_require__(927); const defaultOptions = { ignoreJunk: true @@ -79698,12 +79746,12 @@ module.exports = (source, destination, { /***/ }), -/* 707 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(708); +const pMap = __webpack_require__(709); module.exports = (iterable, options) => pMap(iterable, element => element(), options); // TODO: Remove this for the next major release @@ -79711,7 +79759,7 @@ module.exports.default = module.exports; /***/ }), -/* 708 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79790,7 +79838,7 @@ module.exports.default = pMap; /***/ }), -/* 709 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79820,17 +79868,17 @@ module.exports = arrify; /***/ }), -/* 710 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -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 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 DEFAULT_FILTER = () => false; @@ -79975,12 +80023,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 711 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(712); +var arrayUniq = __webpack_require__(713); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -79988,7 +80036,7 @@ module.exports = function () { /***/ }), -/* 712 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80057,7 +80105,7 @@ if ('Set' in global) { /***/ }), -/* 713 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -80106,13 +80154,13 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(503) var minimatch = __webpack_require__(505) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(714) +var inherits = __webpack_require__(715) 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__(716) -var common = __webpack_require__(717) +var globSync = __webpack_require__(717) +var common = __webpack_require__(718) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -80853,7 +80901,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 714 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -80863,12 +80911,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(715); + module.exports = __webpack_require__(716); } /***/ }), -/* 715 */ +/* 716 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -80901,7 +80949,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 716 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync @@ -80911,12 +80959,12 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(503) var minimatch = __webpack_require__(505) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(713).Glob +var Glob = __webpack_require__(714).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__(717) +var common = __webpack_require__(718) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -81393,7 +81441,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 717 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -81639,10 +81687,10 @@ function childrenIgnored (self, path) { /***/ }), -/* 718 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(719); +const pkg = __webpack_require__(720); module.exports = pkg.async; module.exports.default = pkg.async; @@ -81655,19 +81703,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 719 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -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); +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); /** * Synchronous API. */ @@ -81733,7 +81781,7 @@ function isString(source) { /***/ }), -/* 720 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81771,13 +81819,13 @@ exports.prepare = prepare; /***/ }), -/* 721 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(722); +var patternUtils = __webpack_require__(723); /** * Generate tasks based on parent directory of each pattern. */ @@ -81868,16 +81916,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 722 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var globParent = __webpack_require__(723); -var isGlob = __webpack_require__(726); -var micromatch = __webpack_require__(727); +var globParent = __webpack_require__(724); +var isGlob = __webpack_require__(727); +var micromatch = __webpack_require__(728); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -82023,15 +82071,15 @@ exports.matchAny = matchAny; /***/ }), -/* 723 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(16); -var isglob = __webpack_require__(724); -var pathDirname = __webpack_require__(725); +var isglob = __webpack_require__(725); +var pathDirname = __webpack_require__(726); var isWin32 = __webpack_require__(11).platform() === 'win32'; module.exports = function globParent(str) { @@ -82054,7 +82102,7 @@ module.exports = function globParent(str) { /***/ }), -/* 724 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -82085,7 +82133,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 725 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82235,7 +82283,7 @@ module.exports.win32 = win32; /***/ }), -/* 726 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -82287,7 +82335,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 727 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82298,18 +82346,18 @@ module.exports = function isGlob(str, options) { */ var util = __webpack_require__(29); -var braces = __webpack_require__(728); -var toRegex = __webpack_require__(830); -var extend = __webpack_require__(838); +var braces = __webpack_require__(729); +var toRegex = __webpack_require__(831); +var extend = __webpack_require__(839); /** * Local dependencies */ -var compilers = __webpack_require__(841); -var parsers = __webpack_require__(873); -var cache = __webpack_require__(874); -var utils = __webpack_require__(875); +var compilers = __webpack_require__(842); +var parsers = __webpack_require__(874); +var cache = __webpack_require__(875); +var utils = __webpack_require__(876); var MAX_LENGTH = 1024 * 64; /** @@ -83171,7 +83219,7 @@ module.exports = micromatch; /***/ }), -/* 728 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83181,18 +83229,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(729); -var unique = __webpack_require__(741); -var extend = __webpack_require__(738); +var toRegex = __webpack_require__(730); +var unique = __webpack_require__(742); +var extend = __webpack_require__(739); /** * Local dependencies */ -var compilers = __webpack_require__(742); -var parsers = __webpack_require__(757); -var Braces = __webpack_require__(767); -var utils = __webpack_require__(743); +var compilers = __webpack_require__(743); +var parsers = __webpack_require__(758); +var Braces = __webpack_require__(768); +var utils = __webpack_require__(744); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -83496,15 +83544,15 @@ module.exports = braces; /***/ }), -/* 729 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(730); -var extend = __webpack_require__(738); -var not = __webpack_require__(740); +var define = __webpack_require__(731); +var extend = __webpack_require__(739); +var not = __webpack_require__(741); var MAX_LENGTH = 1024 * 64; /** @@ -83651,7 +83699,7 @@ module.exports.makeRe = makeRe; /***/ }), -/* 730 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83664,7 +83712,7 @@ module.exports.makeRe = makeRe; -var isDescriptor = __webpack_require__(731); +var isDescriptor = __webpack_require__(732); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -83689,7 +83737,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 731 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83702,9 +83750,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(732); -var isAccessor = __webpack_require__(733); -var isData = __webpack_require__(736); +var typeOf = __webpack_require__(733); +var isAccessor = __webpack_require__(734); +var isData = __webpack_require__(737); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -83718,7 +83766,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 732 */ +/* 733 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -83871,7 +83919,7 @@ function isBuffer(val) { /***/ }), -/* 733 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83884,7 +83932,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(734); +var typeOf = __webpack_require__(735); // accessor descriptor properties var accessor = { @@ -83947,10 +83995,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 734 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -84069,7 +84117,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 735 */ +/* 736 */ /***/ (function(module, exports) { /*! @@ -84096,7 +84144,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 736 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84109,7 +84157,7 @@ function isSlowBuffer (obj) { -var typeOf = __webpack_require__(737); +var typeOf = __webpack_require__(738); // data descriptor properties var data = { @@ -84158,10 +84206,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 737 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -84280,13 +84328,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 738 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(739); +var isObject = __webpack_require__(740); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -84320,7 +84368,7 @@ function hasOwn(obj, key) { /***/ }), -/* 739 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84340,13 +84388,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 740 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(738); +var extend = __webpack_require__(739); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -84413,7 +84461,7 @@ module.exports = toRegex; /***/ }), -/* 741 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84463,13 +84511,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 742 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(743); +var utils = __webpack_require__(744); module.exports = function(braces, options) { braces.compiler @@ -84752,25 +84800,25 @@ function hasQueue(node) { /***/ }), -/* 743 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(744); +var splitString = __webpack_require__(745); var utils = module.exports; /** * Module dependencies */ -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.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.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -85102,7 +85150,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 744 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85115,7 +85163,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(745); +var extend = __webpack_require__(746); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -85280,14 +85328,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 745 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(746); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(747); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -85347,7 +85395,7 @@ function isEnum(obj, key) { /***/ }), -/* 746 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85360,7 +85408,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -85368,7 +85416,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 747 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85381,7 +85429,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(749); function isObjectObject(o) { return isObject(o) === true @@ -85412,7 +85460,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 748 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85431,7 +85479,7 @@ module.exports = function isObject(val) { /***/ }), -/* 749 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85478,7 +85526,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 750 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85507,7 +85555,7 @@ function flat(arr, res) { /***/ }), -/* 751 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85521,10 +85569,10 @@ function flat(arr, res) { var util = __webpack_require__(29); -var isNumber = __webpack_require__(752); -var extend = __webpack_require__(738); -var repeat = __webpack_require__(754); -var toRegex = __webpack_require__(755); +var isNumber = __webpack_require__(753); +var extend = __webpack_require__(739); +var repeat = __webpack_require__(755); +var toRegex = __webpack_require__(756); /** * Return a range of numbers or letters. @@ -85722,7 +85770,7 @@ module.exports = fillRange; /***/ }), -/* 752 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85735,7 +85783,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(754); module.exports = function isNumber(num) { var type = typeOf(num); @@ -85751,10 +85799,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 753 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -85873,7 +85921,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 754 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85950,7 +85998,7 @@ function repeat(str, num) { /***/ }), -/* 755 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85963,8 +86011,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(754); -var isNumber = __webpack_require__(752); +var repeat = __webpack_require__(755); +var isNumber = __webpack_require__(753); var cache = {}; function toRegexRange(min, max, options) { @@ -86251,7 +86299,7 @@ module.exports = toRegexRange; /***/ }), -/* 756 */ +/* 757 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86276,14 +86324,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 757 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(758); -var utils = __webpack_require__(743); +var Node = __webpack_require__(759); +var utils = __webpack_require__(744); /** * Braces parsers @@ -86643,15 +86691,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 758 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(748); -var define = __webpack_require__(759); -var utils = __webpack_require__(766); +var isObject = __webpack_require__(749); +var define = __webpack_require__(760); +var utils = __webpack_require__(767); var ownNames; /** @@ -87142,7 +87190,7 @@ exports = module.exports = Node; /***/ }), -/* 759 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87155,7 +87203,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -87180,7 +87228,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 760 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87193,9 +87241,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(761); -var isAccessor = __webpack_require__(762); -var isData = __webpack_require__(764); +var typeOf = __webpack_require__(762); +var isAccessor = __webpack_require__(763); +var isData = __webpack_require__(765); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -87209,7 +87257,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 761 */ +/* 762 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87344,7 +87392,7 @@ function isBuffer(val) { /***/ }), -/* 762 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87357,7 +87405,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(763); +var typeOf = __webpack_require__(764); // accessor descriptor properties var accessor = { @@ -87420,7 +87468,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 763 */ +/* 764 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87555,7 +87603,7 @@ function isBuffer(val) { /***/ }), -/* 764 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87568,7 +87616,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(765); +var typeOf = __webpack_require__(766); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -87611,7 +87659,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 765 */ +/* 766 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87746,13 +87794,13 @@ function isBuffer(val) { /***/ }), -/* 766 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(754); var utils = module.exports; /** @@ -88772,17 +88820,17 @@ function assert(val, message) { /***/ }), -/* 767 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -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); +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); /** * Customize Snapdragon parser and renderer @@ -88883,17 +88931,17 @@ module.exports = Braces; /***/ }), -/* 768 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -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 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 regexCache = {}; var cache = {}; @@ -89064,20 +89112,20 @@ module.exports.Parser = Parser; /***/ }), -/* 769 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -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); +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); /** * Optionally define a custom `cache` namespace to use. @@ -89506,7 +89554,7 @@ module.exports.namespace = namespace; /***/ }), -/* 770 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89519,7 +89567,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -89544,21 +89592,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 771 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -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); +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); /** * Create a `Cache` constructor that when instantiated will @@ -89812,7 +89860,7 @@ module.exports.namespace = namespace; /***/ }), -/* 772 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { @@ -89981,7 +90029,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 773 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89994,8 +90042,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(774); -var mapVisit = __webpack_require__(775); +var visit = __webpack_require__(775); +var mapVisit = __webpack_require__(776); module.exports = function(collection, method, val) { var result; @@ -90018,7 +90066,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 774 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90031,7 +90079,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(749); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -90058,14 +90106,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 775 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var visit = __webpack_require__(774); +var visit = __webpack_require__(775); /** * Map `visit` over an array of objects. @@ -90102,7 +90150,7 @@ function isObject(val) { /***/ }), -/* 776 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90115,7 +90163,7 @@ function isObject(val) { -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(754); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -90142,16 +90190,16 @@ function filter(arr) { /***/ }), -/* 777 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(739); -var union = __webpack_require__(778); -var get = __webpack_require__(779); -var set = __webpack_require__(780); +var isObject = __webpack_require__(740); +var union = __webpack_require__(779); +var get = __webpack_require__(780); +var set = __webpack_require__(781); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -90179,7 +90227,7 @@ function arrayify(val) { /***/ }), -/* 778 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90215,7 +90263,7 @@ module.exports = function union(init) { /***/ }), -/* 779 */ +/* 780 */ /***/ (function(module, exports) { /*! @@ -90271,7 +90319,7 @@ function toString(val) { /***/ }), -/* 780 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90284,10 +90332,10 @@ function toString(val) { -var split = __webpack_require__(744); -var extend = __webpack_require__(738); -var isPlainObject = __webpack_require__(747); -var isObject = __webpack_require__(739); +var split = __webpack_require__(745); +var extend = __webpack_require__(739); +var isPlainObject = __webpack_require__(748); +var isObject = __webpack_require__(740); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -90333,7 +90381,7 @@ function isValidKey(key) { /***/ }), -/* 781 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90346,8 +90394,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(748); -var has = __webpack_require__(782); +var isObject = __webpack_require__(749); +var has = __webpack_require__(783); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -90372,7 +90420,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 782 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90385,9 +90433,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(783); -var hasValues = __webpack_require__(785); -var get = __webpack_require__(779); +var isObject = __webpack_require__(784); +var hasValues = __webpack_require__(786); +var get = __webpack_require__(780); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -90398,7 +90446,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 783 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90411,7 +90459,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(784); +var isArray = __webpack_require__(785); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -90419,7 +90467,7 @@ module.exports = function isObject(val) { /***/ }), -/* 784 */ +/* 785 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -90430,7 +90478,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 785 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90473,7 +90521,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 786 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90486,9 +90534,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(748); -var hasValues = __webpack_require__(787); -var get = __webpack_require__(779); +var isObject = __webpack_require__(749); +var hasValues = __webpack_require__(788); +var get = __webpack_require__(780); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -90496,7 +90544,7 @@ module.exports = function(val, prop) { /***/ }), -/* 787 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90509,8 +90557,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(788); -var isNumber = __webpack_require__(752); +var typeOf = __webpack_require__(789); +var isNumber = __webpack_require__(753); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -90563,10 +90611,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 788 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -90688,14 +90736,14 @@ module.exports = function kindOf(val) { /***/ }), -/* 789 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(790); -var forIn = __webpack_require__(791); +var isExtendable = __webpack_require__(791); +var forIn = __webpack_require__(792); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -90759,7 +90807,7 @@ module.exports = mixinDeep; /***/ }), -/* 790 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90772,7 +90820,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -90780,7 +90828,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 791 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90803,7 +90851,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 792 */ +/* 793 */ /***/ (function(module, exports) { /*! @@ -90830,14 +90878,14 @@ module.exports = pascalcase; /***/ }), -/* 793 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var utils = __webpack_require__(794); +var utils = __webpack_require__(795); /** * Expose class utils @@ -91202,7 +91250,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 794 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91216,10 +91264,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(778); -utils.define = __webpack_require__(730); -utils.isObj = __webpack_require__(748); -utils.staticExtend = __webpack_require__(795); +utils.union = __webpack_require__(779); +utils.define = __webpack_require__(731); +utils.isObj = __webpack_require__(749); +utils.staticExtend = __webpack_require__(796); /** @@ -91230,7 +91278,7 @@ module.exports = utils; /***/ }), -/* 795 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91243,8 +91291,8 @@ module.exports = utils; -var copy = __webpack_require__(796); -var define = __webpack_require__(730); +var copy = __webpack_require__(797); +var define = __webpack_require__(731); var util = __webpack_require__(29); /** @@ -91327,15 +91375,15 @@ module.exports = extend; /***/ }), -/* 796 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(753); -var copyDescriptor = __webpack_require__(797); -var define = __webpack_require__(730); +var typeOf = __webpack_require__(754); +var copyDescriptor = __webpack_require__(798); +var define = __webpack_require__(731); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -91508,7 +91556,7 @@ module.exports.has = has; /***/ }), -/* 797 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91596,16 +91644,16 @@ function isObject(val) { /***/ }), -/* 798 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(799); -var define = __webpack_require__(730); -var debug = __webpack_require__(801)('snapdragon:compiler'); -var utils = __webpack_require__(807); +var use = __webpack_require__(800); +var define = __webpack_require__(731); +var debug = __webpack_require__(802)('snapdragon:compiler'); +var utils = __webpack_require__(808); /** * Create a new `Compiler` with the given `options`. @@ -91759,7 +91807,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(826); + var sourcemaps = __webpack_require__(827); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -91780,7 +91828,7 @@ module.exports = Compiler; /***/ }), -/* 799 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91793,7 +91841,7 @@ module.exports = Compiler; -var utils = __webpack_require__(800); +var utils = __webpack_require__(801); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -91908,7 +91956,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 800 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91922,8 +91970,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(730); -utils.isObject = __webpack_require__(748); +utils.define = __webpack_require__(731); +utils.isObject = __webpack_require__(749); utils.isString = function(val) { @@ -91938,7 +91986,7 @@ module.exports = utils; /***/ }), -/* 801 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91947,14 +91995,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(802); + module.exports = __webpack_require__(803); } else { - module.exports = __webpack_require__(805); + module.exports = __webpack_require__(806); } /***/ }), -/* 802 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91963,7 +92011,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(803); +exports = module.exports = __webpack_require__(804); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -92145,7 +92193,7 @@ function localstorage() { /***/ }), -/* 803 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { @@ -92161,7 +92209,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(804); +exports.humanize = __webpack_require__(805); /** * The currently active debug mode names, and names to skip. @@ -92353,7 +92401,7 @@ function coerce(val) { /***/ }), -/* 804 */ +/* 805 */ /***/ (function(module, exports) { /** @@ -92511,7 +92559,7 @@ function plural(ms, n, name) { /***/ }), -/* 805 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -92527,7 +92575,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(803); +exports = module.exports = __webpack_require__(804); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -92706,7 +92754,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(806); + var net = __webpack_require__(807); stream = new net.Socket({ fd: fd, readable: false, @@ -92765,13 +92813,13 @@ exports.enable(load()); /***/ }), -/* 806 */ +/* 807 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 807 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92781,9 +92829,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(738); -exports.SourceMap = __webpack_require__(808); -exports.sourceMapResolve = __webpack_require__(819); +exports.extend = __webpack_require__(739); +exports.SourceMap = __webpack_require__(809); +exports.sourceMapResolve = __webpack_require__(820); /** * Convert backslash in the given string to forward slashes @@ -92826,7 +92874,7 @@ exports.last = function(arr, n) { /***/ }), -/* 808 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -92834,13 +92882,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__(809).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(815).SourceMapConsumer; -exports.SourceNode = __webpack_require__(818).SourceNode; +exports.SourceMapGenerator = __webpack_require__(810).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(816).SourceMapConsumer; +exports.SourceNode = __webpack_require__(819).SourceNode; /***/ }), -/* 809 */ +/* 810 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -92850,10 +92898,10 @@ exports.SourceNode = __webpack_require__(818).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(810); -var util = __webpack_require__(812); -var ArraySet = __webpack_require__(813).ArraySet; -var MappingList = __webpack_require__(814).MappingList; +var base64VLQ = __webpack_require__(811); +var util = __webpack_require__(813); +var ArraySet = __webpack_require__(814).ArraySet; +var MappingList = __webpack_require__(815).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -93262,7 +93310,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 810 */ +/* 811 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93302,7 +93350,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(811); +var base64 = __webpack_require__(812); // 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, @@ -93408,7 +93456,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 811 */ +/* 812 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93481,7 +93529,7 @@ exports.decode = function (charCode) { /***/ }), -/* 812 */ +/* 813 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93904,7 +93952,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 813 */ +/* 814 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93914,7 +93962,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); +var util = __webpack_require__(813); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -94031,7 +94079,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 814 */ +/* 815 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94041,7 +94089,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); +var util = __webpack_require__(813); /** * Determine whether mappingB is after mappingA with respect to generated @@ -94116,7 +94164,7 @@ exports.MappingList = MappingList; /***/ }), -/* 815 */ +/* 816 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94126,11 +94174,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -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; +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; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -95204,7 +95252,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 816 */ +/* 817 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95321,7 +95369,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 817 */ +/* 818 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95441,7 +95489,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 818 */ +/* 819 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95451,8 +95499,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(809).SourceMapGenerator; -var util = __webpack_require__(812); +var SourceMapGenerator = __webpack_require__(810).SourceMapGenerator; +var util = __webpack_require__(813); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -95860,17 +95908,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 819 */ +/* 820 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -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) +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) @@ -96168,7 +96216,7 @@ module.exports = { /***/ }), -/* 820 */ +/* 821 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -96231,7 +96279,7 @@ void (function(root, factory) { /***/ }), -/* 821 */ +/* 822 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96249,13 +96297,13 @@ module.exports = resolveUrl /***/ }), -/* 822 */ +/* 823 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(823) +var decodeUriComponent = __webpack_require__(824) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -96266,7 +96314,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 823 */ +/* 824 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96367,7 +96415,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 824 */ +/* 825 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96390,7 +96438,7 @@ module.exports = urix /***/ }), -/* 825 */ +/* 826 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96404,7 +96452,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 826 */ +/* 827 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96412,8 +96460,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(23); var path = __webpack_require__(16); -var define = __webpack_require__(730); -var utils = __webpack_require__(807); +var define = __webpack_require__(731); +var utils = __webpack_require__(808); /** * Expose `mixin()`. @@ -96556,19 +96604,19 @@ exports.comment = function(node) { /***/ }), -/* 827 */ +/* 828 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(799); +var use = __webpack_require__(800); var util = __webpack_require__(29); -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); +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); /** * Create a new `Parser` with the given `input` and `options`. @@ -97096,7 +97144,7 @@ module.exports = Parser; /***/ }), -/* 828 */ +/* 829 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97203,13 +97251,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 829 */ +/* 830 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(730); +var define = __webpack_require__(731); /** * Store position for a node @@ -97224,16 +97272,16 @@ module.exports = function Position(start, parser) { /***/ }), -/* 830 */ +/* 831 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(831); -var define = __webpack_require__(837); -var extend = __webpack_require__(838); -var not = __webpack_require__(840); +var safe = __webpack_require__(832); +var define = __webpack_require__(838); +var extend = __webpack_require__(839); +var not = __webpack_require__(841); var MAX_LENGTH = 1024 * 64; /** @@ -97386,10 +97434,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 831 */ +/* 832 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(832); +var parse = __webpack_require__(833); var types = parse.types; module.exports = function (re, opts) { @@ -97435,13 +97483,13 @@ function isRegExp (x) { /***/ }), -/* 832 */ +/* 833 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(833); -var types = __webpack_require__(834); -var sets = __webpack_require__(835); -var positions = __webpack_require__(836); +var util = __webpack_require__(834); +var types = __webpack_require__(835); +var sets = __webpack_require__(836); +var positions = __webpack_require__(837); module.exports = function(regexpStr) { @@ -97723,11 +97771,11 @@ module.exports.types = types; /***/ }), -/* 833 */ +/* 834 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); -var sets = __webpack_require__(835); +var types = __webpack_require__(835); +var sets = __webpack_require__(836); // All of these are private and only used by randexp. @@ -97840,7 +97888,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 834 */ +/* 835 */ /***/ (function(module, exports) { module.exports = { @@ -97856,10 +97904,10 @@ module.exports = { /***/ }), -/* 835 */ +/* 836 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); +var types = __webpack_require__(835); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -97944,10 +97992,10 @@ exports.anyChar = function() { /***/ }), -/* 836 */ +/* 837 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); +var types = __webpack_require__(835); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -97967,7 +98015,7 @@ exports.end = function() { /***/ }), -/* 837 */ +/* 838 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97980,8 +98028,8 @@ exports.end = function() { -var isobject = __webpack_require__(748); -var isDescriptor = __webpack_require__(760); +var isobject = __webpack_require__(749); +var isDescriptor = __webpack_require__(761); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -98012,14 +98060,14 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 838 */ +/* 839 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(839); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(840); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -98079,7 +98127,7 @@ function isEnum(obj, key) { /***/ }), -/* 839 */ +/* 840 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98092,7 +98140,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -98100,14 +98148,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 840 */ +/* 841 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(838); -var safe = __webpack_require__(831); +var extend = __webpack_require__(839); +var safe = __webpack_require__(832); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -98179,14 +98227,14 @@ module.exports = toRegex; /***/ }), -/* 841 */ +/* 842 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(842); -var extglob = __webpack_require__(857); +var nanomatch = __webpack_require__(843); +var extglob = __webpack_require__(858); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -98263,7 +98311,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 842 */ +/* 843 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98274,17 +98322,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(29); -var toRegex = __webpack_require__(729); -var extend = __webpack_require__(843); +var toRegex = __webpack_require__(730); +var extend = __webpack_require__(844); /** * Local dependencies */ -var compilers = __webpack_require__(845); -var parsers = __webpack_require__(846); -var cache = __webpack_require__(849); -var utils = __webpack_require__(851); +var compilers = __webpack_require__(846); +var parsers = __webpack_require__(847); +var cache = __webpack_require__(850); +var utils = __webpack_require__(852); var MAX_LENGTH = 1024 * 64; /** @@ -99108,14 +99156,14 @@ module.exports = nanomatch; /***/ }), -/* 843 */ +/* 844 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(844); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(845); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -99175,7 +99223,7 @@ function isEnum(obj, key) { /***/ }), -/* 844 */ +/* 845 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99188,7 +99236,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -99196,7 +99244,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 845 */ +/* 846 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99542,15 +99590,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 846 */ +/* 847 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(740); -var toRegex = __webpack_require__(729); -var isOdd = __webpack_require__(847); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(730); +var isOdd = __webpack_require__(848); /** * Characters to use in negation regex (we want to "not" match @@ -99936,7 +99984,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 847 */ +/* 848 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99949,7 +99997,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(848); +var isNumber = __webpack_require__(849); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -99963,7 +100011,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 848 */ +/* 849 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99991,14 +100039,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 849 */ +/* 850 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(850))(); +module.exports = new (__webpack_require__(851))(); /***/ }), -/* 850 */ +/* 851 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100011,7 +100059,7 @@ module.exports = new (__webpack_require__(850))(); -var MapCache = __webpack_require__(828); +var MapCache = __webpack_require__(829); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -100133,7 +100181,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 851 */ +/* 852 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100146,14 +100194,14 @@ var path = __webpack_require__(16); * Module dependencies */ -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); +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); /** * Returns true if the given value is effectively an empty string @@ -100519,7 +100567,7 @@ utils.unixify = function(options) { /***/ }), -/* 852 */ +/* 853 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -100547,7 +100595,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 853 */ +/* 854 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100560,8 +100608,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(748); -var isDescriptor = __webpack_require__(760); +var isobject = __webpack_require__(749); +var isDescriptor = __webpack_require__(761); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -100592,7 +100640,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 854 */ +/* 855 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100646,7 +100694,7 @@ function diffArray(one, two) { /***/ }), -/* 855 */ +/* 856 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100659,7 +100707,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(749); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -100688,7 +100736,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 856 */ +/* 857 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -100823,7 +100871,7 @@ function isBuffer(val) { /***/ }), -/* 857 */ +/* 858 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100833,18 +100881,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(738); -var unique = __webpack_require__(741); -var toRegex = __webpack_require__(729); +var extend = __webpack_require__(739); +var unique = __webpack_require__(742); +var toRegex = __webpack_require__(730); /** * Local dependencies */ -var compilers = __webpack_require__(858); -var parsers = __webpack_require__(869); -var Extglob = __webpack_require__(872); -var utils = __webpack_require__(871); +var compilers = __webpack_require__(859); +var parsers = __webpack_require__(870); +var Extglob = __webpack_require__(873); +var utils = __webpack_require__(872); var MAX_LENGTH = 1024 * 64; /** @@ -101161,13 +101209,13 @@ module.exports = extglob; /***/ }), -/* 858 */ +/* 859 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(859); +var brackets = __webpack_require__(860); /** * Extglob compilers @@ -101337,7 +101385,7 @@ module.exports = function(extglob) { /***/ }), -/* 859 */ +/* 860 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101347,17 +101395,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(860); -var parsers = __webpack_require__(862); +var compilers = __webpack_require__(861); +var parsers = __webpack_require__(863); /** * Module dependencies */ -var debug = __webpack_require__(864)('expand-brackets'); -var extend = __webpack_require__(738); -var Snapdragon = __webpack_require__(768); -var toRegex = __webpack_require__(729); +var debug = __webpack_require__(865)('expand-brackets'); +var extend = __webpack_require__(739); +var Snapdragon = __webpack_require__(769); +var toRegex = __webpack_require__(730); /** * Parses the given POSIX character class `pattern` and returns a @@ -101555,13 +101603,13 @@ module.exports = brackets; /***/ }), -/* 860 */ +/* 861 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(861); +var posix = __webpack_require__(862); module.exports = function(brackets) { brackets.compiler @@ -101649,7 +101697,7 @@ module.exports = function(brackets) { /***/ }), -/* 861 */ +/* 862 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101678,14 +101726,14 @@ module.exports = { /***/ }), -/* 862 */ +/* 863 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(863); -var define = __webpack_require__(730); +var utils = __webpack_require__(864); +var define = __webpack_require__(731); /** * Text regex @@ -101904,14 +101952,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 863 */ +/* 864 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(729); -var regexNot = __webpack_require__(740); +var toRegex = __webpack_require__(730); +var regexNot = __webpack_require__(741); var cached; /** @@ -101945,7 +101993,7 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 864 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -101954,14 +102002,14 @@ exports.createRegex = function(pattern, include) { */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(865); + module.exports = __webpack_require__(866); } else { - module.exports = __webpack_require__(868); + module.exports = __webpack_require__(869); } /***/ }), -/* 865 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -101970,7 +102018,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(866); +exports = module.exports = __webpack_require__(867); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -102152,7 +102200,7 @@ function localstorage() { /***/ }), -/* 866 */ +/* 867 */ /***/ (function(module, exports, __webpack_require__) { @@ -102168,7 +102216,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(867); +exports.humanize = __webpack_require__(868); /** * The currently active debug mode names, and names to skip. @@ -102360,7 +102408,7 @@ function coerce(val) { /***/ }), -/* 867 */ +/* 868 */ /***/ (function(module, exports) { /** @@ -102518,7 +102566,7 @@ function plural(ms, n, name) { /***/ }), -/* 868 */ +/* 869 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -102534,7 +102582,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(866); +exports = module.exports = __webpack_require__(867); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -102713,7 +102761,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(806); + var net = __webpack_require__(807); stream = new net.Socket({ fd: fd, readable: false, @@ -102772,15 +102820,15 @@ exports.enable(load()); /***/ }), -/* 869 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(859); -var define = __webpack_require__(870); -var utils = __webpack_require__(871); +var brackets = __webpack_require__(860); +var define = __webpack_require__(871); +var utils = __webpack_require__(872); /** * Characters to use in text regex (we want to "not" match @@ -102935,7 +102983,7 @@ module.exports = parsers; /***/ }), -/* 870 */ +/* 871 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102948,7 +102996,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -102973,14 +103021,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 871 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(740); -var Cache = __webpack_require__(850); +var regex = __webpack_require__(741); +var Cache = __webpack_require__(851); /** * Utils @@ -103049,7 +103097,7 @@ utils.createRegex = function(str) { /***/ }), -/* 872 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103059,16 +103107,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(768); -var define = __webpack_require__(870); -var extend = __webpack_require__(738); +var Snapdragon = __webpack_require__(769); +var define = __webpack_require__(871); +var extend = __webpack_require__(739); /** * Local dependencies */ -var compilers = __webpack_require__(858); -var parsers = __webpack_require__(869); +var compilers = __webpack_require__(859); +var parsers = __webpack_require__(870); /** * Customize Snapdragon parser and renderer @@ -103134,16 +103182,16 @@ module.exports = Extglob; /***/ }), -/* 873 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(857); -var nanomatch = __webpack_require__(842); -var regexNot = __webpack_require__(740); -var toRegex = __webpack_require__(830); +var extglob = __webpack_require__(858); +var nanomatch = __webpack_require__(843); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(831); var not; /** @@ -103224,14 +103272,14 @@ function textRegex(pattern) { /***/ }), -/* 874 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(850))(); +module.exports = new (__webpack_require__(851))(); /***/ }), -/* 875 */ +/* 876 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103244,13 +103292,13 @@ var path = __webpack_require__(16); * Module dependencies */ -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); +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); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -103547,7 +103595,7 @@ utils.unixify = function(options) { /***/ }), -/* 876 */ +/* 877 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -103682,7 +103730,7 @@ function isBuffer(val) { /***/ }), -/* 877 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103701,9 +103749,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_stream_1 = __webpack_require__(895); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_stream_1 = __webpack_require__(896); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -103764,15 +103812,15 @@ exports.default = ReaderAsync; /***/ }), -/* 878 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(879); -const readdirAsync = __webpack_require__(887); -const readdirStream = __webpack_require__(890); +const readdirSync = __webpack_require__(880); +const readdirAsync = __webpack_require__(888); +const readdirStream = __webpack_require__(891); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -103856,7 +103904,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 879 */ +/* 880 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103864,11 +103912,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(880); +const DirectoryReader = __webpack_require__(881); let syncFacade = { - fs: __webpack_require__(885), - forEach: __webpack_require__(886), + fs: __webpack_require__(886), + forEach: __webpack_require__(887), sync: true }; @@ -103897,7 +103945,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 880 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103906,9 +103954,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__(881); -const stat = __webpack_require__(883); -const call = __webpack_require__(884); +const normalizeOptions = __webpack_require__(882); +const stat = __webpack_require__(884); +const call = __webpack_require__(885); /** * Asynchronously reads the contents of a directory and streams the results @@ -104284,14 +104332,14 @@ module.exports = DirectoryReader; /***/ }), -/* 881 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(882); +const globToRegExp = __webpack_require__(883); module.exports = normalizeOptions; @@ -104468,7 +104516,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 882 */ +/* 883 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -104605,13 +104653,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 883 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(884); +const call = __webpack_require__(885); module.exports = stat; @@ -104686,7 +104734,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 884 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104747,14 +104795,14 @@ function callOnce (fn) { /***/ }), -/* 885 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(884); +const call = __webpack_require__(885); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -104818,7 +104866,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 886 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104847,7 +104895,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 887 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104855,12 +104903,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(888); -const DirectoryReader = __webpack_require__(880); +const maybe = __webpack_require__(889); +const DirectoryReader = __webpack_require__(881); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(889), + forEach: __webpack_require__(890), async: true }; @@ -104902,7 +104950,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 888 */ +/* 889 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104929,7 +104977,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 889 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104965,7 +105013,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 890 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104973,11 +105021,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(880); +const DirectoryReader = __webpack_require__(881); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(889), + forEach: __webpack_require__(890), async: true }; @@ -104997,16 +105045,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 891 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(892); -var entry_1 = __webpack_require__(894); -var pathUtil = __webpack_require__(893); +var deep_1 = __webpack_require__(893); +var entry_1 = __webpack_require__(895); +var pathUtil = __webpack_require__(894); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -105072,14 +105120,14 @@ exports.default = Reader; /***/ }), -/* 892 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(893); -var patternUtils = __webpack_require__(722); +var pathUtils = __webpack_require__(894); +var patternUtils = __webpack_require__(723); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -105162,7 +105210,7 @@ exports.default = DeepFilter; /***/ }), -/* 893 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105193,14 +105241,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 894 */ +/* 895 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(893); -var patternUtils = __webpack_require__(722); +var pathUtils = __webpack_require__(894); +var patternUtils = __webpack_require__(723); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -105285,7 +105333,7 @@ exports.default = EntryFilter; /***/ }), -/* 895 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105305,8 +105353,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(896); -var fs_1 = __webpack_require__(900); +var fsStat = __webpack_require__(897); +var fs_1 = __webpack_require__(901); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -105356,14 +105404,14 @@ exports.default = FileSystemStream; /***/ }), -/* 896 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(897); -const statProvider = __webpack_require__(899); +const optionsManager = __webpack_require__(898); +const statProvider = __webpack_require__(900); /** * Asynchronous API. */ @@ -105394,13 +105442,13 @@ exports.statSync = statSync; /***/ }), -/* 897 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(898); +const fsAdapter = __webpack_require__(899); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -105413,7 +105461,7 @@ exports.prepare = prepare; /***/ }), -/* 898 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105436,7 +105484,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 899 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105488,7 +105536,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 900 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105519,7 +105567,7 @@ exports.default = FileSystem; /***/ }), -/* 901 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105539,9 +105587,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_stream_1 = __webpack_require__(895); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_stream_1 = __webpack_require__(896); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -105609,7 +105657,7 @@ exports.default = ReaderStream; /***/ }), -/* 902 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105628,9 +105676,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_sync_1 = __webpack_require__(903); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_sync_1 = __webpack_require__(904); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -105690,7 +105738,7 @@ exports.default = ReaderSync; /***/ }), -/* 903 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105709,8 +105757,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(896); -var fs_1 = __webpack_require__(900); +var fsStat = __webpack_require__(897); +var fs_1 = __webpack_require__(901); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -105756,7 +105804,7 @@ exports.default = FileSystemSync; /***/ }), -/* 904 */ +/* 905 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105772,7 +105820,7 @@ exports.flatten = flatten; /***/ }), -/* 905 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105793,13 +105841,13 @@ exports.merge = merge; /***/ }), -/* 906 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(907); +const pathType = __webpack_require__(908); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -105865,13 +105913,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 907 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(908); +const pify = __webpack_require__(909); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -105914,7 +105962,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 908 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106005,17 +106053,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 909 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(718); -const gitIgnore = __webpack_require__(910); -const pify = __webpack_require__(911); -const slash = __webpack_require__(912); +const fastGlob = __webpack_require__(719); +const gitIgnore = __webpack_require__(911); +const pify = __webpack_require__(912); +const slash = __webpack_require__(913); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -106113,7 +106161,7 @@ module.exports.sync = options => { /***/ }), -/* 910 */ +/* 911 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -106582,7 +106630,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 911 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106657,7 +106705,7 @@ module.exports = (input, options) => { /***/ }), -/* 912 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106675,17 +106723,17 @@ module.exports = input => { /***/ }), -/* 913 */ +/* 914 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const pEvent = __webpack_require__(914); -const CpFileError = __webpack_require__(917); -const fs = __webpack_require__(921); -const ProgressEmitter = __webpack_require__(924); +const pEvent = __webpack_require__(915); +const CpFileError = __webpack_require__(918); +const fs = __webpack_require__(922); +const ProgressEmitter = __webpack_require__(925); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -106799,12 +106847,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 914 */ +/* 915 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(915); +const pTimeout = __webpack_require__(916); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -107095,12 +107143,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 915 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(916); +const pFinally = __webpack_require__(917); class TimeoutError extends Error { constructor(message) { @@ -107146,7 +107194,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 916 */ +/* 917 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107168,12 +107216,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 917 */ +/* 918 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(918); +const NestedError = __webpack_require__(919); class CpFileError extends NestedError { constructor(message, nested) { @@ -107187,10 +107235,10 @@ module.exports = CpFileError; /***/ }), -/* 918 */ +/* 919 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(919); +var inherits = __webpack_require__(920); var NestedError = function (message, nested) { this.nested = nested; @@ -107241,7 +107289,7 @@ module.exports = NestedError; /***/ }), -/* 919 */ +/* 920 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -107249,12 +107297,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(920); + module.exports = __webpack_require__(921); } /***/ }), -/* 920 */ +/* 921 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -107283,16 +107331,16 @@ if (typeof Object.create === 'function') { /***/ }), -/* 921 */ +/* 922 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(922); -const pEvent = __webpack_require__(914); -const CpFileError = __webpack_require__(917); +const makeDir = __webpack_require__(923); +const pEvent = __webpack_require__(915); +const CpFileError = __webpack_require__(918); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -107389,7 +107437,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 922 */ +/* 923 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107397,7 +107445,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__(923); +const semver = __webpack_require__(924); const defaults = { mode: 0o777 & (~process.umask()), @@ -107546,7 +107594,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 923 */ +/* 924 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -109148,7 +109196,7 @@ function coerce (version, options) { /***/ }), -/* 924 */ +/* 925 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109189,7 +109237,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 925 */ +/* 926 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109235,12 +109283,12 @@ exports.default = module.exports; /***/ }), -/* 926 */ +/* 927 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(927); +const NestedError = __webpack_require__(928); class CpyError extends NestedError { constructor(message, nested) { @@ -109254,7 +109302,7 @@ module.exports = CpyError; /***/ }), -/* 927 */ +/* 928 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(29).inherits; @@ -109310,7 +109358,7 @@ module.exports = NestedError; /***/ }), -/* 928 */ +/* 929 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 444d46307b05..278fdbd2bc9a 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -48,6 +48,7 @@ "globby": "^8.0.1", "has-ansi": "^3.0.0", "indent-string": "^3.2.0", + "is-path-inside": "^3.0.2", "lodash.clonedeepwith": "^4.5.0", "log-symbols": "^2.2.0", "multimatch": "^4.0.0", diff --git a/packages/kbn-pm/src/utils/kibana.ts b/packages/kbn-pm/src/utils/kibana.ts index 36f697d19fc1..58af98b2a92d 100644 --- a/packages/kbn-pm/src/utils/kibana.ts +++ b/packages/kbn-pm/src/utils/kibana.ts @@ -20,6 +20,7 @@ import Path from 'path'; import multimatch from 'multimatch'; +import isPathInside from 'is-path-inside'; import { ProjectMap, getProjects, includeTransitiveProjects } from './projects'; import { Project } from './project'; @@ -121,4 +122,15 @@ export class Kibana { return filteredProjects; } + + isPartOfRepo(project: Project) { + return ( + project.path === this.kibanaProject.path || + isPathInside(project.path, this.kibanaProject.path) + ); + } + + isOutsideRepo(project: Project) { + return !this.isPartOfRepo(project); + } } diff --git a/packages/kbn-pm/src/utils/project_checksums.ts b/packages/kbn-pm/src/utils/project_checksums.ts index 2fd24c8fc957..572f2adb19bd 100644 --- a/packages/kbn-pm/src/utils/project_checksums.ts +++ b/packages/kbn-pm/src/utils/project_checksums.ts @@ -43,7 +43,14 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too const { stdout } = await execa( 'git', - ['ls-files', '-dmt', '--', ...Array.from(projects.values()).map(p => p.path)], + [ + 'ls-files', + '-dmt', + '--', + ...Array.from(projects.values()) + .filter(p => kbn.isPartOfRepo(p)) + .map(p => p.path), + ], { cwd: kbn.getAbsolute(), } @@ -84,9 +91,14 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too } const sortedRelevantProjects = Array.from(projects.values()).sort(projectBySpecificitySorter); - const changesByProject = new Map(); + const changesByProject = new Map(); for (const project of sortedRelevantProjects) { + if (kbn.isOutsideRepo(project)) { + changesByProject.set(project, undefined); + continue; + } + const ownChanges: Changes = new Map(); const prefix = kbn.getRelative(project.path); @@ -114,6 +126,10 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too /** Get the latest commit sha for a project */ async function getLatestSha(project: Project, kbn: Kibana) { + if (kbn.isOutsideRepo(project)) { + return; + } + const { stdout } = await execa( 'git', ['log', '-n', '1', '--pretty=format:%H', '--', project.path], @@ -175,7 +191,7 @@ function resolveDepsForProject(project: Project, yarnLock: YarnLock, kbn: Kibana */ async function getChecksum( project: Project, - changes: Changes, + changes: Changes | undefined, yarnLock: YarnLock, kbn: Kibana, log: ToolingLog @@ -185,7 +201,7 @@ async function getChecksum( log.verbose(`[${project.name}] local sha:`, sha); } - if (Array.from(changes.values()).includes('invalid')) { + if (!changes || Array.from(changes.values()).includes('invalid')) { log.warning(`[${project.name}] unable to determine local changes, caching disabled`); return; } @@ -248,7 +264,7 @@ export async function getAllChecksums(kbn: Kibana, log: ToolingLog) { Array.from(projects.values()).map(async project => { cacheKeys.set( project.name, - await getChecksum(project, changesByProject.get(project)!, yarnLock, kbn, log) + await getChecksum(project, changesByProject.get(project), yarnLock, kbn, log) ); }) ); diff --git a/yarn.lock b/yarn.lock index a7e29935c7ab..eaee706101a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17576,7 +17576,7 @@ is-path-inside@^2.1.0: dependencies: path-is-inside "^1.0.2" -is-path-inside@^3.0.1: +is-path-inside@^3.0.1, is-path-inside@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== From f168b6abb844c6f4c76db0d7bb3018f60ccba89d Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Tue, 17 Mar 2020 15:26:29 -0700 Subject: [PATCH 062/115] Add additional safeguards for data source wizard step 2 (#60426) Co-authored-by: Elastic Machine --- .../create_datasource_page/step_configure_datasource.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx index 484ea3f1d94a..b45beef4a8b5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx @@ -183,7 +183,10 @@ export const StepConfigureDatasource: React.FunctionComponent<{ // Step B, configure inputs (and their streams) // Assume packages only export one datasource for now const ConfigureInputs = - packageInfo.datasources && packageInfo.datasources[0] ? ( + packageInfo.datasources && + packageInfo.datasources[0] && + packageInfo.datasources[0].inputs && + packageInfo.datasources[0].inputs.length ? ( {packageInfo.datasources[0].inputs.map(packageInput => { const datasourceInput = datasource.inputs.find(input => input.type === packageInput.type); From 3e0b6fb65d916104c3d80874bab6dd2f5844f193 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 17 Mar 2020 18:55:56 -0400 Subject: [PATCH 063/115] [IM] Use EuiCodeBlock to render index mapping (#60420) --- .../detail_panel/show_json/show_json.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.js index 50c0e331e3db..7b7ca0842708 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { EuiCodeEditor } from '@elastic/eui'; +import { EuiCodeBlock } from '@elastic/eui'; import 'brace/theme/textmate'; @@ -25,17 +25,6 @@ export class ShowJson extends React.PureComponent { return null; } const json = JSON.stringify(data, null, 2); - return ( - - ); + return {json}; } } From bc16fcd984d772edcb381f28ce654d73d0ae7a08 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Tue, 17 Mar 2020 16:15:27 -0700 Subject: [PATCH 064/115] Update ingest management team handle (#60457) --- .github/CODEOWNERS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 132a99fb0a15..df3a56dd3513 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -74,9 +74,9 @@ # Observability UIs /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui /x-pack/plugins/infra/ @elastic/logs-metrics-ui -/x-pack/plugins/ingest_manager/ @elastic/ingest -/x-pack/legacy/plugins/ingest_manager/ @elastic/ingest -/x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest +/x-pack/plugins/ingest_manager/ @elastic/ingest-management +/x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management +/x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest-management /x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui # Machine Learning From 0d23c516ce4da540bacef2ab5536f8fd35bd1198 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 18 Mar 2020 00:23:50 +0100 Subject: [PATCH 065/115] [Console] Fix bool filter autocompletions and refactor (#60361) The autocomplete lib component was controlling state that belongs to the legacy editor model and so it was moved there. The fix for filter autocompletion inside of "bool" was just adding "[" around the filter scoped entry. * Remove reference to model code from autocomplete lib * Also renamed the __tests__ dir to __jest__ to avoid re-running in mocha. --- .../legacy_core_editor/legacy_core_editor.ts | 56 ++++++- .../__tests__/integration.test.js | 145 +++++++++--------- .../models/sense_editor/sense_editor.ts | 1 + .../url_autocomplete.test.js | 1 - .../url_params.test.js | 4 - .../public/lib/autocomplete/autocomplete.ts | 71 ++------- .../public/lib/autocomplete/body_completer.js | 1 - .../console/public/lib/autocomplete/engine.js | 2 +- .../console/public/types/core_editor.ts | 12 ++ .../lib/spec_definitions/es_6_0/query/dsl.js | 8 +- 10 files changed, 161 insertions(+), 140 deletions(-) rename src/plugins/console/public/lib/autocomplete/{__tests__ => __jest__}/url_autocomplete.test.js (99%) rename src/plugins/console/public/lib/autocomplete/{__tests__ => __jest__}/url_params.test.js (95%) diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index 47947e985092..49093dd3527b 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -18,9 +18,17 @@ */ import ace from 'brace'; -import { Editor as IAceEditor } from 'brace'; +import { Editor as IAceEditor, IEditSession as IAceEditSession } from 'brace'; import $ from 'jquery'; -import { CoreEditor, Position, Range, Token, TokensProvider, EditorEvent } from '../../../types'; +import { + CoreEditor, + Position, + Range, + Token, + TokensProvider, + EditorEvent, + AutoCompleterFunction, +} from '../../../types'; import { AceTokensProvider } from '../../../lib/ace_token_provider'; import * as curl from '../sense_editor/curl'; import smartResize from './smart_resize'; @@ -354,4 +362,48 @@ export class LegacyCoreEditor implements CoreEditor { } } } + + registerAutocompleter(getCompletions: AutoCompleterFunction): void { + // Hook into Ace + + // disable standard context based autocompletion. + // @ts-ignore + ace.define('ace/autocomplete/text_completer', ['require', 'exports', 'module'], function( + require: any, + exports: any + ) { + exports.getCompletions = function( + innerEditor: any, + session: any, + pos: any, + prefix: any, + callback: any + ) { + callback(null, []); + }; + }); + + const langTools = ace.acequire('ace/ext/language_tools'); + + langTools.setCompleters([ + { + identifierRegexps: [ + /[a-zA-Z_0-9\.\$\-\u00A2-\uFFFF]/, // adds support for dot character + ], + getCompletions: ( + DO_NOT_USE_1: IAceEditor, + DO_NOT_USE_2: IAceEditSession, + pos: { row: number; column: number }, + prefix: string, + callback: (...args: any[]) => void + ) => { + const position: Position = { + lineNumber: pos.row + 1, + column: pos.column + 1, + }; + getCompletions(position, prefix, callback); + }, + }, + ]); + } } diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js index 1a09b6b00da9..c5a0c2ebddf7 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js @@ -84,93 +84,90 @@ describe('Integration', () => { changeListener: function() {}, }; // mimic auto complete - senseEditor.autocomplete._test.getCompletions( - senseEditor, - null, - { row: cursor.lineNumber - 1, column: cursor.column - 1 }, - '', - function(err, terms) { - if (testToRun.assertThrows) { - done(); - return; - } + senseEditor.autocomplete._test.getCompletions(senseEditor, null, cursor, '', function( + err, + terms + ) { + if (testToRun.assertThrows) { + done(); + return; + } - if (err) { - throw err; - } + if (err) { + throw err; + } - if (testToRun.no_context) { - expect(!terms || terms.length === 0).toBeTruthy(); - } else { - expect(terms).not.toBeNull(); - expect(terms.length).toBeGreaterThan(0); - } + if (testToRun.no_context) { + expect(!terms || terms.length === 0).toBeTruthy(); + } else { + expect(terms).not.toBeNull(); + expect(terms.length).toBeGreaterThan(0); + } - if (!terms || terms.length === 0) { - done(); - return; - } + if (!terms || terms.length === 0) { + done(); + return; + } - if (testToRun.autoCompleteSet) { - const expectedTerms = _.map(testToRun.autoCompleteSet, function(t) { - if (typeof t !== 'object') { - t = { name: t }; - } - return t; - }); - if (terms.length !== expectedTerms.length) { - expect(_.pluck(terms, 'name')).toEqual(_.pluck(expectedTerms, 'name')); - } else { - const filteredActualTerms = _.map(terms, function(actualTerm, i) { - const expectedTerm = expectedTerms[i]; - const filteredTerm = {}; - _.each(expectedTerm, function(v, p) { - filteredTerm[p] = actualTerm[p]; - }); - return filteredTerm; - }); - expect(filteredActualTerms).toEqual(expectedTerms); + if (testToRun.autoCompleteSet) { + const expectedTerms = _.map(testToRun.autoCompleteSet, function(t) { + if (typeof t !== 'object') { + t = { name: t }; } + return t; + }); + if (terms.length !== expectedTerms.length) { + expect(_.pluck(terms, 'name')).toEqual(_.pluck(expectedTerms, 'name')); + } else { + const filteredActualTerms = _.map(terms, function(actualTerm, i) { + const expectedTerm = expectedTerms[i]; + const filteredTerm = {}; + _.each(expectedTerm, function(v, p) { + filteredTerm[p] = actualTerm[p]; + }); + return filteredTerm; + }); + expect(filteredActualTerms).toEqual(expectedTerms); } + } - const context = terms[0].context; - const { - cursor: { lineNumber, column }, - } = testToRun; - senseEditor.autocomplete._test.addReplacementInfoToContext( - context, - { lineNumber, column }, - terms[0].value - ); + const context = terms[0].context; + const { + cursor: { lineNumber, column }, + } = testToRun; + senseEditor.autocomplete._test.addReplacementInfoToContext( + context, + { lineNumber, column }, + terms[0].value + ); - function ac(prop, propTest) { - if (typeof testToRun[prop] !== 'undefined') { - if (propTest) { - propTest(context[prop], testToRun[prop], prop); - } else { - expect(context[prop]).toEqual(testToRun[prop]); - } + function ac(prop, propTest) { + if (typeof testToRun[prop] !== 'undefined') { + if (propTest) { + propTest(context[prop], testToRun[prop], prop); + } else { + expect(context[prop]).toEqual(testToRun[prop]); } } + } - function posCompare(actual, expected) { - expect(actual.lineNumber).toEqual(expected.lineNumber + lineOffset); - expect(actual.column).toEqual(expected.column); - } - - function rangeCompare(actual, expected, name) { - posCompare(actual.start, expected.start, name + '.start'); - posCompare(actual.end, expected.end, name + '.end'); - } + function posCompare(actual, expected) { + expect(actual.lineNumber).toEqual(expected.lineNumber + lineOffset); + expect(actual.column).toEqual(expected.column); + } - ac('prefixToAdd'); - ac('suffixToAdd'); - ac('addTemplate'); - ac('textBoxPosition', posCompare); - ac('rangeToReplace', rangeCompare); - done(); + function rangeCompare(actual, expected, name) { + posCompare(actual.start, expected.start, name + '.start'); + posCompare(actual.end, expected.end, name + '.end'); } - ); + + ac('prefixToAdd'); + ac('suffixToAdd'); + ac('addTemplate'); + ac('textBoxPosition', posCompare); + ac('rangeToReplace', rangeCompare); + done(); + }); }); } diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts index f559f5dfcd70..b1444bdf2bba 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts @@ -44,6 +44,7 @@ export class SenseEditor { coreEditor, parser: this.parser, }); + this.coreEditor.registerAutocompleter(this.autocomplete.getCompletions); this.coreEditor.on( 'tokenizerUpdate', this.highlightCurrentRequestsAndUpdateActionBar.bind(this) diff --git a/src/plugins/console/public/lib/autocomplete/__tests__/url_autocomplete.test.js b/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js similarity index 99% rename from src/plugins/console/public/lib/autocomplete/__tests__/url_autocomplete.test.js rename to src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js index 40fcd551fb6f..0758a7569556 100644 --- a/src/plugins/console/public/lib/autocomplete/__tests__/url_autocomplete.test.js +++ b/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import '../../../application/models/sense_editor/sense_editor.test.mocks'; const _ = require('lodash'); import { diff --git a/src/plugins/console/public/lib/autocomplete/__tests__/url_params.test.js b/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js similarity index 95% rename from src/plugins/console/public/lib/autocomplete/__tests__/url_params.test.js rename to src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js index ce2a2553b19e..72fce53c4f1f 100644 --- a/src/plugins/console/public/lib/autocomplete/__tests__/url_params.test.js +++ b/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js @@ -16,10 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import '../../../application/models/sense_editor/sense_editor.test.mocks'; -import 'brace'; -import 'brace/mode/javascript'; -import 'brace/mode/json'; const _ = require('lodash'); import { UrlParams } from '../../autocomplete/url_params'; import { populateContext } from '../../autocomplete/engine'; diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts index e09024ccfc85..d4f10ff4e427 100644 --- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts +++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts @@ -18,9 +18,9 @@ */ import _ from 'lodash'; -import ace, { Editor as AceEditor, IEditSession } from 'brace'; import { i18n } from '@kbn/i18n'; +// TODO: All of these imports need to be moved to the core editor so that it can inject components from there. import { getTopLevelUrlCompleteComponents, getEndpointBodyCompleteComponents, @@ -39,7 +39,7 @@ import { createTokenIterator } from '../../application/factories'; import { Position, Token, Range, CoreEditor } from '../../types'; -let LAST_EVALUATED_TOKEN: any = null; +let lastEvaluatedToken: any = null; function isUrlParamsToken(token: any) { switch ((token || {}).type) { @@ -889,7 +889,7 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor if (!currentToken) { if (pos.lineNumber === 1) { - LAST_EVALUATED_TOKEN = null; + lastEvaluatedToken = null; return; } currentToken = { position: { column: 0, lineNumber: 0 }, value: '', type: '' }; // empty row @@ -902,26 +902,26 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor if (parser.isEmptyToken(nextToken)) { // Empty line, or we're not on the edge of current token. Save the current position as base currentToken.position.column = pos.column; - LAST_EVALUATED_TOKEN = currentToken; + lastEvaluatedToken = currentToken; } else { nextToken.position.lineNumber = pos.lineNumber; - LAST_EVALUATED_TOKEN = nextToken; + lastEvaluatedToken = nextToken; } return; } - if (!LAST_EVALUATED_TOKEN) { - LAST_EVALUATED_TOKEN = currentToken; + if (!lastEvaluatedToken) { + lastEvaluatedToken = currentToken; return; // wait for the next typing. } if ( - LAST_EVALUATED_TOKEN.position.column !== currentToken.position.column || - LAST_EVALUATED_TOKEN.position.lineNumber !== currentToken.position.lineNumber || - LAST_EVALUATED_TOKEN.value === currentToken.value + lastEvaluatedToken.position.column !== currentToken.position.column || + lastEvaluatedToken.position.lineNumber !== currentToken.position.lineNumber || + lastEvaluatedToken.value === currentToken.value ) { // not on the same place or nothing changed, cache and wait for the next time - LAST_EVALUATED_TOKEN = currentToken; + lastEvaluatedToken = currentToken; return; } @@ -935,7 +935,7 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor return; } - LAST_EVALUATED_TOKEN = currentToken; + lastEvaluatedToken = currentToken; editor.execCommand('startAutocomplete'); }, 100); @@ -947,17 +947,7 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor } } - function getCompletions( - DO_NOT_USE: AceEditor, - DO_NOT_USE_SESSION: IEditSession, - pos: { row: number; column: number }, - prefix: string, - callback: (...args: any[]) => void - ) { - const position: Position = { - lineNumber: pos.row + 1, - column: pos.column + 1, - }; + function getCompletions(position: Position, prefix: string, callback: (...args: any[]) => void) { try { const context = getAutoCompleteContext(editor, position); if (!context) { @@ -1028,39 +1018,12 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor editor.on('changeSelection', editorChangeListener); - // Hook into Ace - - // disable standard context based autocompletion. - // @ts-ignore - ace.define('ace/autocomplete/text_completer', ['require', 'exports', 'module'], function( - require: any, - exports: any - ) { - exports.getCompletions = function( - innerEditor: any, - session: any, - pos: any, - prefix: any, - callback: any - ) { - callback(null, []); - }; - }); - - const langTools = ace.acequire('ace/ext/language_tools'); - - langTools.setCompleters([ - { - identifierRegexps: [ - /[a-zA-Z_0-9\.\$\-\u00A2-\uFFFF]/, // adds support for dot character - ], - getCompletions, - }, - ]); - return { + getCompletions, + // TODO: This needs to be cleaned up _test: { - getCompletions, + getCompletions: (_editor: any, _editSession: any, pos: any, prefix: any, callback: any) => + getCompletions(pos, prefix, callback), addReplacementInfoToContext, addChangeListener: () => editor.on('changeSelection', editorChangeListener), removeChangeListener: () => editor.off('changeSelection', editorChangeListener), diff --git a/src/plugins/console/public/lib/autocomplete/body_completer.js b/src/plugins/console/public/lib/autocomplete/body_completer.js index e23a58780a36..1aa315c50b9b 100644 --- a/src/plugins/console/public/lib/autocomplete/body_completer.js +++ b/src/plugins/console/public/lib/autocomplete/body_completer.js @@ -115,7 +115,6 @@ class ScopeResolver extends SharedComponent { next: [], }; const components = this.resolveLinkToComponents(context, editor); - _.each(components, function(component) { const componentResult = component.match(token, context, editor); if (componentResult && componentResult.next) { diff --git a/src/plugins/console/public/lib/autocomplete/engine.js b/src/plugins/console/public/lib/autocomplete/engine.js index f4df8af871eb..7b64d91c9537 100644 --- a/src/plugins/console/public/lib/autocomplete/engine.js +++ b/src/plugins/console/public/lib/autocomplete/engine.js @@ -43,7 +43,7 @@ export function wrapComponentWithDefaults(component, defaults) { const tracer = function() { if (window.engine_trace) { - console.log.call(console, arguments); + console.log.call(console, ...arguments); } }; diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts index 79dc3ca74200..84a2c64a8088 100644 --- a/src/plugins/console/public/types/core_editor.ts +++ b/src/plugins/console/public/types/core_editor.ts @@ -29,6 +29,12 @@ export type EditorEvent = | 'change' | 'changeSelection'; +export type AutoCompleterFunction = ( + pos: Position, + prefix: string, + callback: (...args: any[]) => void +) => void; + export interface Position { /** * The line number, not zero-indexed. @@ -256,4 +262,10 @@ export interface CoreEditor { * Register a keyboard shortcut and provide a function to be called. */ registerKeyboardShortcut(opts: { keys: any; fn: () => void; name: string }): void; + + /** + * Register a completions function that will be called when the editor + * detects a change + */ + registerAutocompleter(getCompletions: AutoCompleterFunction): void; } diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js b/src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js index a5f0d15dee0e..16b952fe0fe4 100644 --- a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js +++ b/src/plugins/console/server/lib/spec_definitions/es_6_0/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 8412ab61b4f7ecbdca6bb05225c80023d9bd4745 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 17 Mar 2020 16:27:45 -0700 Subject: [PATCH 066/115] Revert "[Console] Fix bool filter autocompletions and refactor (#60361)" This reverts commit 0d23c516ce4da540bacef2ab5536f8fd35bd1198. --- .../legacy_core_editor/legacy_core_editor.ts | 56 +------ .../__tests__/integration.test.js | 145 +++++++++--------- .../models/sense_editor/sense_editor.ts | 1 - .../url_autocomplete.test.js | 1 + .../url_params.test.js | 4 + .../public/lib/autocomplete/autocomplete.ts | 71 +++++++-- .../public/lib/autocomplete/body_completer.js | 1 + .../console/public/lib/autocomplete/engine.js | 2 +- .../console/public/types/core_editor.ts | 12 -- .../lib/spec_definitions/es_6_0/query/dsl.js | 8 +- 10 files changed, 140 insertions(+), 161 deletions(-) rename src/plugins/console/public/lib/autocomplete/{__jest__ => __tests__}/url_autocomplete.test.js (99%) rename src/plugins/console/public/lib/autocomplete/{__jest__ => __tests__}/url_params.test.js (95%) diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index 49093dd3527b..47947e985092 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -18,17 +18,9 @@ */ import ace from 'brace'; -import { Editor as IAceEditor, IEditSession as IAceEditSession } from 'brace'; +import { Editor as IAceEditor } from 'brace'; import $ from 'jquery'; -import { - CoreEditor, - Position, - Range, - Token, - TokensProvider, - EditorEvent, - AutoCompleterFunction, -} from '../../../types'; +import { CoreEditor, Position, Range, Token, TokensProvider, EditorEvent } from '../../../types'; import { AceTokensProvider } from '../../../lib/ace_token_provider'; import * as curl from '../sense_editor/curl'; import smartResize from './smart_resize'; @@ -362,48 +354,4 @@ export class LegacyCoreEditor implements CoreEditor { } } } - - registerAutocompleter(getCompletions: AutoCompleterFunction): void { - // Hook into Ace - - // disable standard context based autocompletion. - // @ts-ignore - ace.define('ace/autocomplete/text_completer', ['require', 'exports', 'module'], function( - require: any, - exports: any - ) { - exports.getCompletions = function( - innerEditor: any, - session: any, - pos: any, - prefix: any, - callback: any - ) { - callback(null, []); - }; - }); - - const langTools = ace.acequire('ace/ext/language_tools'); - - langTools.setCompleters([ - { - identifierRegexps: [ - /[a-zA-Z_0-9\.\$\-\u00A2-\uFFFF]/, // adds support for dot character - ], - getCompletions: ( - DO_NOT_USE_1: IAceEditor, - DO_NOT_USE_2: IAceEditSession, - pos: { row: number; column: number }, - prefix: string, - callback: (...args: any[]) => void - ) => { - const position: Position = { - lineNumber: pos.row + 1, - column: pos.column + 1, - }; - getCompletions(position, prefix, callback); - }, - }, - ]); - } } diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js index c5a0c2ebddf7..1a09b6b00da9 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js @@ -84,90 +84,93 @@ describe('Integration', () => { changeListener: function() {}, }; // mimic auto complete - senseEditor.autocomplete._test.getCompletions(senseEditor, null, cursor, '', function( - err, - terms - ) { - if (testToRun.assertThrows) { - done(); - return; - } + senseEditor.autocomplete._test.getCompletions( + senseEditor, + null, + { row: cursor.lineNumber - 1, column: cursor.column - 1 }, + '', + function(err, terms) { + if (testToRun.assertThrows) { + done(); + return; + } - if (err) { - throw err; - } + if (err) { + throw err; + } - if (testToRun.no_context) { - expect(!terms || terms.length === 0).toBeTruthy(); - } else { - expect(terms).not.toBeNull(); - expect(terms.length).toBeGreaterThan(0); - } + if (testToRun.no_context) { + expect(!terms || terms.length === 0).toBeTruthy(); + } else { + expect(terms).not.toBeNull(); + expect(terms.length).toBeGreaterThan(0); + } - if (!terms || terms.length === 0) { - done(); - return; - } + if (!terms || terms.length === 0) { + done(); + return; + } - if (testToRun.autoCompleteSet) { - const expectedTerms = _.map(testToRun.autoCompleteSet, function(t) { - if (typeof t !== 'object') { - t = { name: t }; - } - return t; - }); - if (terms.length !== expectedTerms.length) { - expect(_.pluck(terms, 'name')).toEqual(_.pluck(expectedTerms, 'name')); - } else { - const filteredActualTerms = _.map(terms, function(actualTerm, i) { - const expectedTerm = expectedTerms[i]; - const filteredTerm = {}; - _.each(expectedTerm, function(v, p) { - filteredTerm[p] = actualTerm[p]; - }); - return filteredTerm; + if (testToRun.autoCompleteSet) { + const expectedTerms = _.map(testToRun.autoCompleteSet, function(t) { + if (typeof t !== 'object') { + t = { name: t }; + } + return t; }); - expect(filteredActualTerms).toEqual(expectedTerms); + if (terms.length !== expectedTerms.length) { + expect(_.pluck(terms, 'name')).toEqual(_.pluck(expectedTerms, 'name')); + } else { + const filteredActualTerms = _.map(terms, function(actualTerm, i) { + const expectedTerm = expectedTerms[i]; + const filteredTerm = {}; + _.each(expectedTerm, function(v, p) { + filteredTerm[p] = actualTerm[p]; + }); + return filteredTerm; + }); + expect(filteredActualTerms).toEqual(expectedTerms); + } } - } - const context = terms[0].context; - const { - cursor: { lineNumber, column }, - } = testToRun; - senseEditor.autocomplete._test.addReplacementInfoToContext( - context, - { lineNumber, column }, - terms[0].value - ); + const context = terms[0].context; + const { + cursor: { lineNumber, column }, + } = testToRun; + senseEditor.autocomplete._test.addReplacementInfoToContext( + context, + { lineNumber, column }, + terms[0].value + ); - function ac(prop, propTest) { - if (typeof testToRun[prop] !== 'undefined') { - if (propTest) { - propTest(context[prop], testToRun[prop], prop); - } else { - expect(context[prop]).toEqual(testToRun[prop]); + function ac(prop, propTest) { + if (typeof testToRun[prop] !== 'undefined') { + if (propTest) { + propTest(context[prop], testToRun[prop], prop); + } else { + expect(context[prop]).toEqual(testToRun[prop]); + } } } - } - function posCompare(actual, expected) { - expect(actual.lineNumber).toEqual(expected.lineNumber + lineOffset); - expect(actual.column).toEqual(expected.column); - } + function posCompare(actual, expected) { + expect(actual.lineNumber).toEqual(expected.lineNumber + lineOffset); + expect(actual.column).toEqual(expected.column); + } - function rangeCompare(actual, expected, name) { - posCompare(actual.start, expected.start, name + '.start'); - posCompare(actual.end, expected.end, name + '.end'); - } + function rangeCompare(actual, expected, name) { + posCompare(actual.start, expected.start, name + '.start'); + posCompare(actual.end, expected.end, name + '.end'); + } - ac('prefixToAdd'); - ac('suffixToAdd'); - ac('addTemplate'); - ac('textBoxPosition', posCompare); - ac('rangeToReplace', rangeCompare); - done(); - }); + ac('prefixToAdd'); + ac('suffixToAdd'); + ac('addTemplate'); + ac('textBoxPosition', posCompare); + ac('rangeToReplace', rangeCompare); + done(); + } + ); }); } diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts index b1444bdf2bba..f559f5dfcd70 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts @@ -44,7 +44,6 @@ export class SenseEditor { coreEditor, parser: this.parser, }); - this.coreEditor.registerAutocompleter(this.autocomplete.getCompletions); this.coreEditor.on( 'tokenizerUpdate', this.highlightCurrentRequestsAndUpdateActionBar.bind(this) diff --git a/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js b/src/plugins/console/public/lib/autocomplete/__tests__/url_autocomplete.test.js similarity index 99% rename from src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js rename to src/plugins/console/public/lib/autocomplete/__tests__/url_autocomplete.test.js index 0758a7569556..40fcd551fb6f 100644 --- a/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js +++ b/src/plugins/console/public/lib/autocomplete/__tests__/url_autocomplete.test.js @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import '../../../application/models/sense_editor/sense_editor.test.mocks'; const _ = require('lodash'); import { diff --git a/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js b/src/plugins/console/public/lib/autocomplete/__tests__/url_params.test.js similarity index 95% rename from src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js rename to src/plugins/console/public/lib/autocomplete/__tests__/url_params.test.js index 72fce53c4f1f..ce2a2553b19e 100644 --- a/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js +++ b/src/plugins/console/public/lib/autocomplete/__tests__/url_params.test.js @@ -16,6 +16,10 @@ * specific language governing permissions and limitations * under the License. */ +import '../../../application/models/sense_editor/sense_editor.test.mocks'; +import 'brace'; +import 'brace/mode/javascript'; +import 'brace/mode/json'; const _ = require('lodash'); import { UrlParams } from '../../autocomplete/url_params'; import { populateContext } from '../../autocomplete/engine'; diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts index d4f10ff4e427..e09024ccfc85 100644 --- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts +++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts @@ -18,9 +18,9 @@ */ import _ from 'lodash'; +import ace, { Editor as AceEditor, IEditSession } from 'brace'; import { i18n } from '@kbn/i18n'; -// TODO: All of these imports need to be moved to the core editor so that it can inject components from there. import { getTopLevelUrlCompleteComponents, getEndpointBodyCompleteComponents, @@ -39,7 +39,7 @@ import { createTokenIterator } from '../../application/factories'; import { Position, Token, Range, CoreEditor } from '../../types'; -let lastEvaluatedToken: any = null; +let LAST_EVALUATED_TOKEN: any = null; function isUrlParamsToken(token: any) { switch ((token || {}).type) { @@ -889,7 +889,7 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor if (!currentToken) { if (pos.lineNumber === 1) { - lastEvaluatedToken = null; + LAST_EVALUATED_TOKEN = null; return; } currentToken = { position: { column: 0, lineNumber: 0 }, value: '', type: '' }; // empty row @@ -902,26 +902,26 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor if (parser.isEmptyToken(nextToken)) { // Empty line, or we're not on the edge of current token. Save the current position as base currentToken.position.column = pos.column; - lastEvaluatedToken = currentToken; + LAST_EVALUATED_TOKEN = currentToken; } else { nextToken.position.lineNumber = pos.lineNumber; - lastEvaluatedToken = nextToken; + LAST_EVALUATED_TOKEN = nextToken; } return; } - if (!lastEvaluatedToken) { - lastEvaluatedToken = currentToken; + if (!LAST_EVALUATED_TOKEN) { + LAST_EVALUATED_TOKEN = currentToken; return; // wait for the next typing. } if ( - lastEvaluatedToken.position.column !== currentToken.position.column || - lastEvaluatedToken.position.lineNumber !== currentToken.position.lineNumber || - lastEvaluatedToken.value === currentToken.value + LAST_EVALUATED_TOKEN.position.column !== currentToken.position.column || + LAST_EVALUATED_TOKEN.position.lineNumber !== currentToken.position.lineNumber || + LAST_EVALUATED_TOKEN.value === currentToken.value ) { // not on the same place or nothing changed, cache and wait for the next time - lastEvaluatedToken = currentToken; + LAST_EVALUATED_TOKEN = currentToken; return; } @@ -935,7 +935,7 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor return; } - lastEvaluatedToken = currentToken; + LAST_EVALUATED_TOKEN = currentToken; editor.execCommand('startAutocomplete'); }, 100); @@ -947,7 +947,17 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor } } - function getCompletions(position: Position, prefix: string, callback: (...args: any[]) => void) { + function getCompletions( + DO_NOT_USE: AceEditor, + DO_NOT_USE_SESSION: IEditSession, + pos: { row: number; column: number }, + prefix: string, + callback: (...args: any[]) => void + ) { + const position: Position = { + lineNumber: pos.row + 1, + column: pos.column + 1, + }; try { const context = getAutoCompleteContext(editor, position); if (!context) { @@ -1018,12 +1028,39 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor editor.on('changeSelection', editorChangeListener); + // Hook into Ace + + // disable standard context based autocompletion. + // @ts-ignore + ace.define('ace/autocomplete/text_completer', ['require', 'exports', 'module'], function( + require: any, + exports: any + ) { + exports.getCompletions = function( + innerEditor: any, + session: any, + pos: any, + prefix: any, + callback: any + ) { + callback(null, []); + }; + }); + + const langTools = ace.acequire('ace/ext/language_tools'); + + langTools.setCompleters([ + { + identifierRegexps: [ + /[a-zA-Z_0-9\.\$\-\u00A2-\uFFFF]/, // adds support for dot character + ], + getCompletions, + }, + ]); + return { - getCompletions, - // TODO: This needs to be cleaned up _test: { - getCompletions: (_editor: any, _editSession: any, pos: any, prefix: any, callback: any) => - getCompletions(pos, prefix, callback), + getCompletions, addReplacementInfoToContext, addChangeListener: () => editor.on('changeSelection', editorChangeListener), removeChangeListener: () => editor.off('changeSelection', editorChangeListener), diff --git a/src/plugins/console/public/lib/autocomplete/body_completer.js b/src/plugins/console/public/lib/autocomplete/body_completer.js index 1aa315c50b9b..e23a58780a36 100644 --- a/src/plugins/console/public/lib/autocomplete/body_completer.js +++ b/src/plugins/console/public/lib/autocomplete/body_completer.js @@ -115,6 +115,7 @@ class ScopeResolver extends SharedComponent { next: [], }; const components = this.resolveLinkToComponents(context, editor); + _.each(components, function(component) { const componentResult = component.match(token, context, editor); if (componentResult && componentResult.next) { diff --git a/src/plugins/console/public/lib/autocomplete/engine.js b/src/plugins/console/public/lib/autocomplete/engine.js index 7b64d91c9537..f4df8af871eb 100644 --- a/src/plugins/console/public/lib/autocomplete/engine.js +++ b/src/plugins/console/public/lib/autocomplete/engine.js @@ -43,7 +43,7 @@ export function wrapComponentWithDefaults(component, defaults) { const tracer = function() { if (window.engine_trace) { - console.log.call(console, ...arguments); + console.log.call(console, arguments); } }; diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts index 84a2c64a8088..79dc3ca74200 100644 --- a/src/plugins/console/public/types/core_editor.ts +++ b/src/plugins/console/public/types/core_editor.ts @@ -29,12 +29,6 @@ export type EditorEvent = | 'change' | 'changeSelection'; -export type AutoCompleterFunction = ( - pos: Position, - prefix: string, - callback: (...args: any[]) => void -) => void; - export interface Position { /** * The line number, not zero-indexed. @@ -262,10 +256,4 @@ export interface CoreEditor { * Register a keyboard shortcut and provide a function to be called. */ registerKeyboardShortcut(opts: { keys: any; fn: () => void; name: string }): void; - - /** - * Register a completions function that will be called when the editor - * detects a change - */ - registerAutocompleter(getCompletions: AutoCompleterFunction): void; } diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js b/src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js index 16b952fe0fe4..a5f0d15dee0e 100644 --- a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js +++ b/src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js @@ -281,11 +281,9 @@ export function queryDsl(api) { __scope_link: '.', }, ], - filter: [ - { - __scope_link: 'GLOBAL.filter', - }, - ], + filter: { + __scope_link: 'GLOBAL.filter', + }, minimum_should_match: 1, boost: 1.0, }, From 2e6c76fda7517314543fabe5e4aa1999aab4c631 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 17 Mar 2020 16:33:37 -0700 Subject: [PATCH 067/115] Disabled edit alert button on management ui for non registered UI alert types (#60439) --- x-pack/plugins/alerting/server/plugin.ts | 10 ---------- .../alerts_list/components/alerts_list.test.tsx | 14 +++++++++++++- .../alerts_list/components/alerts_list.tsx | 4 +++- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 885391325fcd..b4b2de19ef24 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -173,16 +173,6 @@ export class AlertingPlugin { muteAlertInstanceRoute(router, this.licenseState); unmuteAlertInstanceRoute(router, this.licenseState); - alertTypeRegistry.register({ - id: 'test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - name: 'Test', - executor: async options => { - return { status: 'ok' }; - }, - }); - return { registerType: alertTypeRegistry.register.bind(alertTypeRegistry), }; 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 865ab6ea04ce..f8f0c278c81e 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 @@ -242,6 +242,8 @@ describe('alerts_list component with items', () => { alertTypeRegistry: alertTypeRegistry as any, }; + alertTypeRegistry.has.mockReturnValue(true); + wrapper = mountWithIntl( @@ -257,11 +259,15 @@ describe('alerts_list component with items', () => { expect(loadActionTypes).toHaveBeenCalled(); } - it('renders table of connectors', async () => { + it('renders table of alerts', async () => { await setup(); expect(wrapper.find('EuiBasicTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); }); + it('renders edit button for registered alert types', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="alertsTableCell-editLink"]').length).toBeGreaterThan(0); + }); }); describe('alerts_list component empty with show only capability', () => { @@ -455,6 +461,8 @@ describe('alerts_list with show only capability', () => { alertTypeRegistry: alertTypeRegistry as any, }; + alertTypeRegistry.has.mockReturnValue(false); + wrapper = mountWithIntl( @@ -473,4 +481,8 @@ describe('alerts_list with show only capability', () => { expect(wrapper.find('EuiTableRow')).toHaveLength(2); // TODO: check delete button }); + it('not renders edit button for non registered alert types', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="alertsTableCell-editLink"]').length).toBe(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 2975b1ef6eba..8d8fc177b57a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -225,7 +225,7 @@ export const AlertsList: React.FunctionComponent = () => { ? [ { render: (item: AlertTableItem) => { - return ( + return alertTypeRegistry.has(item.alertTypeId) ? ( { id="xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editLinkTitle" /> + ) : ( + <> ); }, }, From 4deea08f23c63870daedc5612698be8ea17103d2 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 17 Mar 2020 18:15:26 -0700 Subject: [PATCH 068/115] Publish getIsNavDrawerLocked$ method on core chrome service. (#60191) * Remove isCollapsed, getIsCollapsed, and global_nav_state. --- ...blic.chromestart.getisnavdrawerlocked_.md} | 8 ++-- .../kibana-plugin-core-public.chromestart.md | 3 +- ...-core-public.chromestart.setiscollapsed.md | 24 ---------- src/core/public/chrome/chrome_service.mock.ts | 5 +-- src/core/public/chrome/chrome_service.test.ts | 40 ++--------------- src/core/public/chrome/chrome_service.tsx | 43 ++++++++---------- src/core/public/chrome/ui/header/header.tsx | 27 ++++++++--- .../chrome/ui/header/header_wrapper.tsx | 45 ------------------- src/core/public/chrome/ui/header/index.ts | 1 - src/core/public/chrome/ui/index.ts | 1 - src/core/public/public.api.md | 3 +- src/legacy/ui/public/chrome/chrome.js | 1 - .../chrome/services/global_nav_state.js | 45 ------------------- src/legacy/ui/public/chrome/services/index.js | 20 --------- 14 files changed, 50 insertions(+), 216 deletions(-) rename docs/development/core/public/{kibana-plugin-core-public.chromestart.getiscollapsed_.md => kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md} (51%) delete mode 100644 docs/development/core/public/kibana-plugin-core-public.chromestart.setiscollapsed.md delete mode 100644 src/core/public/chrome/ui/header/header_wrapper.tsx delete mode 100644 src/legacy/ui/public/chrome/services/global_nav_state.js delete mode 100644 src/legacy/ui/public/chrome/services/index.js diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getiscollapsed_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md similarity index 51% rename from docs/development/core/public/kibana-plugin-core-public.chromestart.getiscollapsed_.md rename to docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md index 205f863526e2..78a4442a651e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.getiscollapsed_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md @@ -1,15 +1,15 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getIsCollapsed$](./kibana-plugin-core-public.chromestart.getiscollapsed_.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getIsNavDrawerLocked$](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) -## ChromeStart.getIsCollapsed$() method +## ChromeStart.getIsNavDrawerLocked$() method -Get an observable of the current collapsed state of the chrome. +Get an observable of the current locked state of the nav drawer. Signature: ```typescript -getIsCollapsed$(): Observable; +getIsNavDrawerLocked$(): Observable; ``` Returns: diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index 7d9d47df544d..c179e089d7cf 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -56,7 +56,7 @@ core.chrome.setHelpExtension(elem => { | [getBrand$()](./kibana-plugin-core-public.chromestart.getbrand_.md) | Get an observable of the current brand information. | | [getBreadcrumbs$()](./kibana-plugin-core-public.chromestart.getbreadcrumbs_.md) | Get an observable of the current list of breadcrumbs | | [getHelpExtension$()](./kibana-plugin-core-public.chromestart.gethelpextension_.md) | Get an observable of the current custom help conttent | -| [getIsCollapsed$()](./kibana-plugin-core-public.chromestart.getiscollapsed_.md) | Get an observable of the current collapsed state of the chrome. | +| [getIsNavDrawerLocked$()](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) | Get an observable of the current locked state of the nav drawer. | | [getIsVisible$()](./kibana-plugin-core-public.chromestart.getisvisible_.md) | Get an observable of the current visibility state of the chrome. | | [removeApplicationClass(className)](./kibana-plugin-core-public.chromestart.removeapplicationclass.md) | Remove a className added with addApplicationClass(). If className is unknown it is ignored. | | [setAppTitle(appTitle)](./kibana-plugin-core-public.chromestart.setapptitle.md) | Sets the current app's title | @@ -65,6 +65,5 @@ core.chrome.setHelpExtension(elem => { | [setBreadcrumbs(newBreadcrumbs)](./kibana-plugin-core-public.chromestart.setbreadcrumbs.md) | Override the current set of breadcrumbs | | [setHelpExtension(helpExtension)](./kibana-plugin-core-public.chromestart.sethelpextension.md) | Override the current set of custom help content | | [setHelpSupportUrl(url)](./kibana-plugin-core-public.chromestart.sethelpsupporturl.md) | Override the default support URL shown in the help menu | -| [setIsCollapsed(isCollapsed)](./kibana-plugin-core-public.chromestart.setiscollapsed.md) | Set the collapsed state of the chrome navigation. | | [setIsVisible(isVisible)](./kibana-plugin-core-public.chromestart.setisvisible.md) | Set the temporary visibility for the chrome. This does nothing if the chrome is hidden by default and should be used to hide the chrome for things like full-screen modes with an exit button. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setiscollapsed.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setiscollapsed.md deleted file mode 100644 index b1843ef326d9..000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.setiscollapsed.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [setIsCollapsed](./kibana-plugin-core-public.chromestart.setiscollapsed.md) - -## ChromeStart.setIsCollapsed() method - -Set the collapsed state of the chrome navigation. - -Signature: - -```typescript -setIsCollapsed(isCollapsed: boolean): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| isCollapsed | boolean | | - -Returns: - -`void` - diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index bd932c5961ec..89007461b63e 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -61,8 +61,6 @@ const createStartContractMock = () => { getBrand$: jest.fn(), setIsVisible: jest.fn(), getIsVisible$: jest.fn(), - setIsCollapsed: jest.fn(), - getIsCollapsed$: jest.fn(), addApplicationClass: jest.fn(), removeApplicationClass: jest.fn(), getApplicationClasses$: jest.fn(), @@ -73,15 +71,16 @@ const createStartContractMock = () => { getHelpExtension$: jest.fn(), setHelpExtension: jest.fn(), setHelpSupportUrl: jest.fn(), + getIsNavDrawerLocked$: jest.fn(), }; startContract.navLinks.getAll.mockReturnValue([]); startContract.getBrand$.mockReturnValue(new BehaviorSubject({} as ChromeBrand)); startContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false)); - startContract.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false)); startContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name'])); startContract.getBadge$.mockReturnValue(new BehaviorSubject({} as ChromeBadge)); startContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb])); startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined)); + startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); return startContract; }; diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 9018b2197363..bf531aaa00fa 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -259,40 +259,6 @@ describe('start', () => { }); }); - describe('is collapsed', () => { - it('updates/emits isCollapsed', async () => { - const { chrome, service } = await start(); - const promise = chrome - .getIsCollapsed$() - .pipe(toArray()) - .toPromise(); - - chrome.setIsCollapsed(true); - chrome.setIsCollapsed(false); - chrome.setIsCollapsed(true); - service.stop(); - - await expect(promise).resolves.toMatchInlineSnapshot(` - Array [ - false, - true, - false, - true, - ] - `); - }); - - it('only stores true in localStorage', async () => { - const { chrome } = await start(); - - chrome.setIsCollapsed(true); - expect(store.size).toBe(1); - - chrome.setIsCollapsed(false); - expect(store.size).toBe(0); - }); - }); - describe('application classes', () => { it('updates/emits the application classes', async () => { const { chrome, service } = await start(); @@ -442,12 +408,12 @@ describe('start', () => { }); describe('stop', () => { - it('completes applicationClass$, isCollapsed$, breadcrumbs$, isVisible$, and brand$ observables', async () => { + it('completes applicationClass$, getIsNavDrawerLocked, breadcrumbs$, isVisible$, and brand$ observables', async () => { const { chrome, service } = await start(); const promise = Rx.combineLatest( chrome.getBrand$(), chrome.getApplicationClasses$(), - chrome.getIsCollapsed$(), + chrome.getIsNavDrawerLocked$(), chrome.getBreadcrumbs$(), chrome.getIsVisible$(), chrome.getHelpExtension$() @@ -465,7 +431,7 @@ describe('stop', () => { Rx.combineLatest( chrome.getBrand$(), chrome.getApplicationClasses$(), - chrome.getIsCollapsed$(), + chrome.getIsNavDrawerLocked$(), chrome.getBreadcrumbs$(), chrome.getIsVisible$(), chrome.getHelpExtension$() diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 2b0b115ce068..7c9b644b8b98 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -34,14 +34,14 @@ import { ChromeNavLinks, NavLinksService } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { NavControlsService, ChromeNavControls } from './nav_controls'; import { DocTitleService, ChromeDocTitle } from './doc_title'; -import { LoadingIndicator, HeaderWrapper as Header } from './ui'; +import { LoadingIndicator, Header } from './ui'; import { DocLinksStart } from '../doc_links'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; import { KIBANA_ASK_ELASTIC_LINK } from './constants'; import { IUiSettingsClient } from '../ui_settings'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; -const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed'; +const IS_LOCKED_KEY = 'core.chrome.isLocked'; /** @public */ export interface ChromeBadge { @@ -146,18 +146,25 @@ export class ChromeService { const appTitle$ = new BehaviorSubject('Kibana'); const brand$ = new BehaviorSubject({}); - const isCollapsed$ = new BehaviorSubject(!!localStorage.getItem(IS_COLLAPSED_KEY)); const applicationClasses$ = new BehaviorSubject>(new Set()); const helpExtension$ = new BehaviorSubject(undefined); const breadcrumbs$ = new BehaviorSubject([]); const badge$ = new BehaviorSubject(undefined); const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK); + const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true'); const navControls = this.navControls.start(); const navLinks = this.navLinks.start({ application, http }); const recentlyAccessed = await this.recentlyAccessed.start({ http }); const docTitle = this.docTitle.start({ document: window.document }); + const setIsNavDrawerLocked = (isLocked: boolean) => { + isNavDrawerLocked$.next(isLocked); + localStorage.setItem(IS_LOCKED_KEY, `${isLocked}`); + }; + + const getIsNavDrawerLocked$ = isNavDrawerLocked$.pipe(takeUntil(this.stop$)); + if (!this.params.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) { notifications.toasts.addWarning( i18n.translate('core.chrome.legacyBrowserWarning', { @@ -193,6 +200,8 @@ export class ChromeService { recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} navControlsRight$={navControls.getRight$()} + onIsLockedUpdate={setIsNavDrawerLocked} + isLocked$={getIsNavDrawerLocked$} /> ), @@ -214,17 +223,6 @@ export class ChromeService { setIsVisible: (isVisible: boolean) => this.toggleHidden$.next(!isVisible), - getIsCollapsed$: () => isCollapsed$.pipe(takeUntil(this.stop$)), - - setIsCollapsed: (isCollapsed: boolean) => { - isCollapsed$.next(isCollapsed); - if (isCollapsed) { - localStorage.setItem(IS_COLLAPSED_KEY, 'true'); - } else { - localStorage.removeItem(IS_COLLAPSED_KEY); - } - }, - getApplicationClasses$: () => applicationClasses$.pipe( map(set => [...set]), @@ -262,6 +260,8 @@ export class ChromeService { }, setHelpSupportUrl: (url: string) => helpSupportUrl$.next(url), + + getIsNavDrawerLocked$: () => getIsNavDrawerLocked$, }; } @@ -353,16 +353,6 @@ export interface ChromeStart { */ setIsVisible(isVisible: boolean): void; - /** - * Get an observable of the current collapsed state of the chrome. - */ - getIsCollapsed$(): Observable; - - /** - * Set the collapsed state of the chrome navigation. - */ - setIsCollapsed(isCollapsed: boolean): void; - /** * Get the current set of classNames that will be set on the application container. */ @@ -413,6 +403,11 @@ export interface ChromeStart { * @param url The updated support URL */ setHelpSupportUrl(url: string): void; + + /** + * Get an observable of the current locked state of the nav drawer. + */ + getIsNavDrawerLocked$(): Observable; } /** @internal */ diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index c9a583f39b30..4dec084fd8a8 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -30,6 +30,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Component, createRef } from 'react'; +import classnames from 'classnames'; import * as Rx from 'rxjs'; import { ChromeBadge, @@ -68,8 +69,8 @@ export interface HeaderProps { navControlsLeft$: Rx.Observable; navControlsRight$: Rx.Observable; basePath: HttpStart['basePath']; - isLocked?: boolean; - onIsLockedUpdate?: OnIsLockedUpdate; + isLocked$: Rx.Observable; + onIsLockedUpdate: OnIsLockedUpdate; } interface State { @@ -81,6 +82,7 @@ interface State { navControlsLeft: readonly ChromeNavControl[]; navControlsRight: readonly ChromeNavControl[]; currentAppId: string | undefined; + isLocked: boolean; } export class Header extends Component { @@ -99,6 +101,7 @@ export class Header extends Component { navControlsLeft: [], navControlsRight: [], currentAppId: '', + isLocked: false, }; } @@ -109,11 +112,12 @@ export class Header extends Component { this.props.forceAppSwitcherNavigation$, this.props.navLinks$, this.props.recentlyAccessed$, - // Types for combineLatest only handle up to 6 inferred types so we combine these two separately. + // Types for combineLatest only handle up to 6 inferred types so we combine these separately. Rx.combineLatest( this.props.navControlsLeft$, this.props.navControlsRight$, - this.props.application.currentAppId$ + this.props.application.currentAppId$, + this.props.isLocked$ ) ).subscribe({ next: ([ @@ -122,7 +126,7 @@ export class Header extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId], + [navControlsLeft, navControlsRight, currentAppId, isLocked], ]) => { this.setState({ appTitle, @@ -133,6 +137,7 @@ export class Header extends Component { navControlsLeft, navControlsRight, currentAppId, + isLocked, }); }, }); @@ -181,8 +186,16 @@ export class Header extends Component { return null; } + const className = classnames( + 'chrHeaderWrapper', + { + 'chrHeaderWrapper--navIsLocked': this.state.isLocked, + }, + 'hide-for-sharing' + ); + return ( -
    +
    @@ -220,7 +233,7 @@ export class Header extends Component { = props => { - const initialIsLocked = localStorage.getItem(IS_LOCKED_KEY); - const [isLocked, setIsLocked] = useState(initialIsLocked === 'true'); - const setIsLockedStored = (locked: boolean) => { - localStorage.setItem(IS_LOCKED_KEY, `${locked}`); - setIsLocked(locked); - }; - const className = classnames( - 'chrHeaderWrapper', - { - 'chrHeaderWrapper--navIsLocked': isLocked, - }, - 'hide-for-sharing' - ); - return ( -
    -
    -
    - ); -}; diff --git a/src/core/public/chrome/ui/header/index.ts b/src/core/public/chrome/ui/header/index.ts index 4521f1f74b31..49e002a66d93 100644 --- a/src/core/public/chrome/ui/header/index.ts +++ b/src/core/public/chrome/ui/header/index.ts @@ -18,7 +18,6 @@ */ export { Header, HeaderProps } from './header'; -export { HeaderWrapper } from './header_wrapper'; export { ChromeHelpExtensionMenuLink, ChromeHelpExtensionMenuCustomLink, diff --git a/src/core/public/chrome/ui/index.ts b/src/core/public/chrome/ui/index.ts index 81b2fdfb0fcc..460e19b7d978 100644 --- a/src/core/public/chrome/ui/index.ts +++ b/src/core/public/chrome/ui/index.ts @@ -20,7 +20,6 @@ export { LoadingIndicator } from './loading_indicator'; export { Header, - HeaderWrapper, ChromeHelpExtensionMenuLink, ChromeHelpExtensionMenuCustomLink, ChromeHelpExtensionMenuDiscussLink, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index fa5dc745e693..7428280b2dcc 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -337,7 +337,7 @@ export interface ChromeStart { getBrand$(): Observable; getBreadcrumbs$(): Observable; getHelpExtension$(): Observable; - getIsCollapsed$(): Observable; + getIsNavDrawerLocked$(): Observable; getIsVisible$(): Observable; navControls: ChromeNavControls; navLinks: ChromeNavLinks; @@ -349,7 +349,6 @@ export interface ChromeStart { setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; setHelpExtension(helpExtension?: ChromeHelpExtension): void; setHelpSupportUrl(url: string): void; - setIsCollapsed(isCollapsed: boolean): void; setIsVisible(isVisible: boolean): void; } diff --git a/src/legacy/ui/public/chrome/chrome.js b/src/legacy/ui/public/chrome/chrome.js index 3355870eabfe..7a75ad906a87 100644 --- a/src/legacy/ui/public/chrome/chrome.js +++ b/src/legacy/ui/public/chrome/chrome.js @@ -28,7 +28,6 @@ import '../private'; import '../promises'; import '../directives/storage'; import '../directives/watch_multi'; -import './services'; import '../react_components'; import '../i18n'; diff --git a/src/legacy/ui/public/chrome/services/global_nav_state.js b/src/legacy/ui/public/chrome/services/global_nav_state.js deleted file mode 100644 index 5a67806852fe..000000000000 --- a/src/legacy/ui/public/chrome/services/global_nav_state.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 { distinctUntilChanged } from 'rxjs/operators'; -import { npStart } from 'ui/new_platform'; -import { uiModules } from '../../modules'; - -const newPlatformChrome = npStart.core.chrome; - -uiModules.get('kibana').service('globalNavState', $rootScope => { - let isOpen = false; - newPlatformChrome - .getIsCollapsed$() - .pipe(distinctUntilChanged()) - .subscribe(isCollapsed => { - $rootScope.$evalAsync(() => { - isOpen = !isCollapsed; - $rootScope.$broadcast('globalNavState:change'); - }); - }); - - return { - isOpen: () => isOpen, - - setOpen: newValue => { - newPlatformChrome.setIsCollapsed(!newValue); - }, - }; -}); diff --git a/src/legacy/ui/public/chrome/services/index.js b/src/legacy/ui/public/chrome/services/index.js deleted file mode 100644 index 3b3967f51b2f..000000000000 --- a/src/legacy/ui/public/chrome/services/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 './global_nav_state'; From c1435db29f8d90688ac2a645c5778ad2f438823f Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 17 Mar 2020 18:15:58 -0700 Subject: [PATCH 069/115] Edits UI text for ML nodes and job button (#60184) * Edits UI text for ML nodes and job button * Update x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js Co-Authored-By: Brandon Morelli * Update x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js Co-Authored-By: Brandon Morelli Co-authored-by: Elastic Machine Co-authored-by: Brandon Morelli --- .../ServiceIntegrations/MachineLearningFlyout/view.tsx | 2 +- .../__snapshots__/explorer_no_jobs_found.test.js.snap | 2 +- .../explorer_no_jobs_found/explorer_no_jobs_found.js | 5 +---- .../jobs_list/components/jobs_stats_bar/jobs_stats_bar.js | 2 +- .../jobs_list/components/new_job_button/new_job_button.js | 2 +- .../overview/components/anomaly_detection_panel/utils.ts | 2 +- x-pack/plugins/ml/public/application/services/job_service.js | 2 +- 7 files changed, 7 insertions(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx index 31fc4db8f1a2..cff190cd98a1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx @@ -209,7 +209,7 @@ export function MachineLearningFlyoutView({ {i18n.translate( 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel', { - defaultMessage: 'Create new job' + defaultMessage: 'Create job' } )} diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap index 8aec3c8336da..c6503a639997 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap @@ -9,7 +9,7 @@ exports[`ExplorerNoInfluencersFound snapshot 1`] = ` href="ml#/jobs" > diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js index 5cce2e1eece9..6f391f9746f2 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js @@ -23,10 +23,7 @@ export const ExplorerNoJobsFound = () => ( } actions={ - + } data-test-subj="mlNoJobsFound" diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js index 08155f3f4edb..3c791ff65897 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js @@ -15,7 +15,7 @@ function createJobStats(jobsSummaryList) { const jobStats = { activeNodes: { label: i18n.translate('xpack.ml.jobsList.statsBar.activeMLNodesLabel', { - defaultMessage: 'Active ML Nodes', + defaultMessage: 'Active ML nodes', }), value: 0, show: true, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js index cacca839a4f5..1297ca5b9afd 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js @@ -29,7 +29,7 @@ export function NewJobButton() { > ); diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts index eab40c0f577f..b030a1ef45ab 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts @@ -72,7 +72,7 @@ export function getStatsBarData(jobsList: any) { const jobStats = { activeNodes: { label: i18n.translate('xpack.ml.overviewJobsList.statsBar.activeMLNodesLabel', { - defaultMessage: 'Active ML Nodes', + defaultMessage: 'Active ML nodes', }), value: 0, show: true, diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index f092e85bef5c..e087740ec0e9 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -45,7 +45,7 @@ class JobService { this.jobStats = { activeNodes: { label: i18n.translate('xpack.ml.jobService.activeMLNodesLabel', { - defaultMessage: 'Active ML Nodes', + defaultMessage: 'Active ML nodes', }), value: 0, show: true, From 2207e0ab265fe0a7c204fdd54a3edbea732b283f Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 17 Mar 2020 18:20:00 -0700 Subject: [PATCH 070/115] Index Action - Moved index params fields to connector config (#60349) * Moved index params fields to connector config * Fixed type check issue * Fixing functional tests * Fixed due to comments * Fixed functional tests * Fixed tests and type check --- .../builtin_action_types/es_index.test.ts | 121 +++---- .../server/builtin_action_types/es_index.ts | 34 +- .../public/application/boot.tsx | 2 +- .../builtin_action_types/es_index.test.tsx | 39 +-- .../builtin_action_types/es_index.tsx | 297 ++++++++++++++---- .../components/builtin_action_types/types.ts | 7 +- .../threshold/expression.tsx | 95 +----- .../builtin_alert_types/threshold/types.ts | 6 - .../threshold/visualization.tsx | 2 +- .../action_connector_form.test.tsx | 1 + .../action_connector_form.tsx | 4 + .../connector_add_flyout.tsx | 1 + .../connector_add_modal.tsx | 1 + .../connector_edit_flyout.tsx | 1 + .../public/common/index_controls/index.ts | 90 ++++++ .../lib/index_threshold_api.ts} | 3 +- .../triggers_actions_ui/public/types.ts | 6 +- .../actions/builtin_action_types/es_index.ts | 119 ++++--- .../actions/builtin_action_types/es_index.ts | 32 +- 19 files changed, 528 insertions(+), 333 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts rename x-pack/plugins/triggers_actions_ui/public/{application/components/builtin_alert_types/threshold/lib/api.ts => common/lib/index_threshold_api.ts} (96%) diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index 0be198347725..7eded9bb4096 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -43,18 +43,46 @@ describe('actionTypeRegistry.get() works', () => { describe('config validation', () => { test('config validation succeeds when config is valid', () => { - const config: Record = {}; + const config: Record = { + index: 'testing-123', + refresh: false, + }; expect(validateConfig(actionType, config)).toEqual({ ...config, - index: null, + index: 'testing-123', + refresh: false, }); - config.index = 'testing-123'; + config.executionTimeField = 'field-123'; expect(validateConfig(actionType, config)).toEqual({ ...config, index: 'testing-123', + refresh: false, + executionTimeField: 'field-123', }); + + delete config.index; + + expect(() => { + validateConfig(actionType, { index: 666 }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: [index]: expected value of type [string] but got [number]"` + ); + delete config.executionTimeField; + + expect(() => { + validateConfig(actionType, { index: 'testing-123', executionTimeField: true }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: [executionTimeField]: expected value of type [string] but got [boolean]"` + ); + + delete config.refresh; + expect(() => { + validateConfig(actionType, { index: 'testing-123', refresh: 'foo' }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: [refresh]: expected value of type [boolean] but got [string]"` + ); }); test('config validation fails when config is not valid', () => { @@ -65,46 +93,16 @@ describe('config validation', () => { expect(() => { validateConfig(actionType, baseConfig); }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: [indeX]: definition for this key is missing"` + `"error validating action type config: [index]: expected value of type [string] but got [undefined]"` ); - - delete baseConfig.user; - baseConfig.index = 666; - - expect(() => { - validateConfig(actionType, baseConfig); - }).toThrowErrorMatchingInlineSnapshot(` -"error validating action type config: [index]: types that failed validation: -- [index.0]: expected value of type [string] but got [number] -- [index.1]: expected value to equal [null]" -`); }); }); describe('params validation', () => { test('params validation succeeds when params is valid', () => { const params: Record = { - index: 'testing-123', - executionTimeField: 'field-used-for-time', - refresh: true, documents: [{ rando: 'thing' }], }; - expect(validateParams(actionType, params)).toMatchInlineSnapshot(` - Object { - "documents": Array [ - Object { - "rando": "thing", - }, - ], - "executionTimeField": "field-used-for-time", - "index": "testing-123", - "refresh": true, - } - `); - - delete params.index; - delete params.refresh; - delete params.executionTimeField; expect(validateParams(actionType, params)).toMatchInlineSnapshot(` Object { "documents": Array [ @@ -129,24 +127,6 @@ describe('params validation', () => { `"error validating action params: [documents]: expected value of type [array] but got [undefined]"` ); - expect(() => { - validateParams(actionType, { index: 666 }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [index]: expected value of type [string] but got [number]"` - ); - - expect(() => { - validateParams(actionType, { executionTimeField: true }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [executionTimeField]: expected value of type [string] but got [boolean]"` - ); - - expect(() => { - validateParams(actionType, { refresh: 'foo' }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [refresh]: expected value of type [boolean] but got [string]"` - ); - expect(() => { validateParams(actionType, { documents: ['should be an object'] }); }).toThrowErrorMatchingInlineSnapshot( @@ -162,13 +142,10 @@ describe('execute()', () => { let params: ActionParamsType; let executorOptions: ActionTypeExecutorOptions; - // minimal params, index via param - config = { index: null }; + // minimal params + config = { index: 'index-value', refresh: false, executionTimeField: undefined }; params = { - index: 'index-via-param', documents: [{ jim: 'bob' }], - executionTimeField: undefined, - refresh: undefined, }; const actionId = 'some-id'; @@ -190,19 +167,17 @@ describe('execute()', () => { "jim": "bob", }, ], - "index": "index-via-param", + "index": "index-value", + "refresh": false, }, ], ] `); - // full params (except index), index via config - config = { index: 'index-via-config' }; + // full params + config = { index: 'index-value', executionTimeField: 'field_to_use_for_time', refresh: true }; params = { - index: undefined, documents: [{ jimbob: 'jr' }], - executionTimeField: 'field_to_use_for_time', - refresh: true, }; executorOptions = { actionId, config, secrets, params, services }; @@ -226,20 +201,17 @@ describe('execute()', () => { "jimbob": "jr", }, ], - "index": "index-via-config", + "index": "index-value", "refresh": true, }, ], ] `); - // minimal params, index via config and param - config = { index: 'index-via-config' }; + // minimal params + config = { index: 'index-value', executionTimeField: undefined, refresh: false }; params = { - index: 'index-via-param', documents: [{ jim: 'bob' }], - executionTimeField: undefined, - refresh: undefined, }; executorOptions = { actionId, config, secrets, params, services }; @@ -259,19 +231,17 @@ describe('execute()', () => { "jim": "bob", }, ], - "index": "index-via-config", + "index": "index-value", + "refresh": false, }, ], ] `); // multiple documents - config = { index: null }; + config = { index: 'index-value', executionTimeField: undefined, refresh: false }; params = { - index: 'index-via-param', documents: [{ a: 1 }, { b: 2 }], - executionTimeField: undefined, - refresh: undefined, }; executorOptions = { actionId, config, secrets, params, services }; @@ -297,7 +267,8 @@ describe('execute()', () => { "b": 2, }, ], - "index": "index-via-param", + "index": "index-value", + "refresh": false, }, ], ] diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index f8217046b2ea..b1fe5e3af2d1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -8,7 +8,6 @@ import { curry } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; -import { nullableType } from './lib/nullable'; import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; @@ -17,7 +16,9 @@ import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from export type ActionTypeConfigType = TypeOf; const ConfigSchema = schema.object({ - index: nullableType(schema.string()), + index: schema.string(), + refresh: schema.boolean({ defaultValue: false }), + executionTimeField: schema.maybe(schema.string()), }); // params definition @@ -28,9 +29,6 @@ export type ActionParamsType = TypeOf; // - timeout not added here, as this seems to be a generic thing we want to do // eventually: https://github.com/elastic/kibana/projects/26#card-24087404 const ParamsSchema = schema.object({ - index: schema.maybe(schema.string()), - executionTimeField: schema.maybe(schema.string()), - refresh: schema.maybe(schema.boolean()), documents: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), }); @@ -60,27 +58,12 @@ async function executor( const params = execOptions.params as ActionParamsType; const services = execOptions.services; - if (config.index == null && params.index == null) { - const message = i18n.translate('xpack.actions.builtin.esIndex.indexParamRequiredErrorMessage', { - defaultMessage: 'index param needs to be set because not set in config for action', - }); - return { - status: 'error', - actionId, - message, - }; - } - - if (config.index != null && params.index != null) { - logger.debug(`index passed in params overridden by index set in config for action ${actionId}`); - } - - const index = config.index || params.index; + const index = config.index; const bulkBody = []; for (const document of params.documents) { - if (params.executionTimeField != null) { - document[params.executionTimeField] = new Date(); + if (config.executionTimeField != null) { + document[config.executionTimeField] = new Date(); } bulkBody.push({ index: {} }); @@ -92,9 +75,7 @@ async function executor( body: bulkBody, }; - if (params.refresh != null) { - bulkParams.refresh = params.refresh; - } + bulkParams.refresh = config.refresh; let result; try { @@ -103,6 +84,7 @@ async function executor( const message = i18n.translate('xpack.actions.builtin.esIndex.errorIndexingErrorMessage', { defaultMessage: 'error indexing documents', }); + logger.error(`error indexing documents: ${err.message}`); return { status: 'error', actionId, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx b/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx index a458472c6d75..c157f923e444 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx @@ -9,7 +9,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { SavedObjectsClientContract } from 'src/core/public'; import { App, AppDeps } from './app'; -import { setSavedObjectsClient } from '../application/components/builtin_alert_types/threshold/lib/api'; +import { setSavedObjectsClient } from '../common/lib/index_threshold_api'; interface BootDeps extends AppDeps { element: HTMLElement; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx index d44787f0c4ed..f1d4790e67bb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx @@ -9,6 +9,7 @@ import { TypeRegistry } from '../../type_registry'; import { registerBuiltInActionTypes } from './index'; import { ActionTypeModel, ActionParamsProps } from '../../../types'; import { IndexActionParams, EsIndexActionConnector } from './types'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; const ACTION_TYPE_ID = '.index'; let actionTypeModel: ActionTypeModel; @@ -38,16 +39,15 @@ describe('index connector validation', () => { name: 'es_index', config: { index: 'test_es_index', + refresh: false, + executionTimeField: '1', }, } as EsIndexActionConnector; expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: {}, - }); - - delete actionConnector.config.index; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: {}, + errors: { + index: [], + }, }); }); }); @@ -55,9 +55,6 @@ describe('index connector validation', () => { describe('action params validation', () => { test('action params validation succeeds when action params is valid', () => { const actionParams = { - index: 'test', - refresh: false, - executionTimeField: '1', documents: ['test'], }; @@ -75,6 +72,8 @@ describe('action params validation', () => { describe('IndexActionConnectorFields renders', () => { test('all connector fields is rendered', () => { + const mocks = coreMock.createSetup(); + expect(actionTypeModel.actionConnectorFields).not.toBeNull(); if (!actionTypeModel.actionConnectorFields) { return; @@ -87,23 +86,21 @@ describe('IndexActionConnectorFields renders', () => { name: 'es_index', config: { index: 'test', + refresh: false, + executionTimeField: 'test1', }, } as EsIndexActionConnector; const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} + http={mocks.http} /> ); - expect(wrapper.find('[data-test-subj="indexInput"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="indexInput"]') - .first() - .prop('value') - ).toBe('test'); + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy(); }); }); @@ -117,8 +114,6 @@ describe('IndexParamsFields renders', () => { ActionParamsProps >; const actionParams = { - index: 'test_index', - refresh: false, documents: ['test'], }; const wrapper = mountWithIntl( @@ -129,13 +124,11 @@ describe('IndexParamsFields renders', () => { index={0} /> ); - expect(wrapper.find('[data-test-subj="indexInput"]').length > 0).toBeTruthy(); expect( wrapper - .find('[data-test-subj="indexInput"]') + .find('[data-test-subj="actionIndexDoc"]') .first() .prop('value') - ).toBe('test_index'); - expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy(); + ).toBe('"test"'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx index 6af54d2bf15b..b3e62e022c41 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx @@ -3,8 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; -import { EuiFieldText, EuiFormRow, EuiSwitch, EuiSpacer } from '@elastic/eui'; +import React, { Fragment, useState, useEffect } from 'react'; +import { + EuiFormRow, + EuiSwitch, + EuiSpacer, + EuiCodeEditor, + EuiComboBox, + EuiComboBoxOptionOption, + EuiSelect, + EuiTitle, + EuiIconTip, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { @@ -14,6 +24,13 @@ import { ActionParamsProps, } from '../../../types'; import { IndexActionParams, EsIndexActionConnector } from './types'; +import { getTimeFieldOptions } from '../../../common/lib/get_time_options'; +import { + firstFieldOption, + getFields, + getIndexOptions, + getIndexPatterns, +} from '../../../common/index_controls'; export function getActionType(): ActionTypeModel { return { @@ -25,8 +42,23 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Index data into Elasticsearch.', } ), - validateConnector: (): ValidationResult => { - return { errors: {} }; + validateConnector: (action: EsIndexActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + index: new Array(), + }; + validationResult.errors = errors; + if (!action.config.index) { + errors.index.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText', + { + defaultMessage: 'Index is required.', + } + ) + ); + } + return validationResult; }, actionConnectorFields: IndexActionConnectorFields, actionParamsFields: IndexParamsFields, @@ -38,33 +70,189 @@ export function getActionType(): ActionTypeModel { const IndexActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig }) => { - const { index } = action.config; +>> = ({ action, editActionConfig, errors, http }) => { + const { index, refresh, executionTimeField } = action.config; + const [hasTimeFieldCheckbox, setTimeFieldCheckboxState] = useState( + executionTimeField !== undefined + ); + + const [indexPatterns, setIndexPatterns] = useState([]); + const [indexOptions, setIndexOptions] = useState([]); + const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); + const [isIndiciesLoading, setIsIndiciesLoading] = useState(false); + + useEffect(() => { + const indexPatternsFunction = async () => { + setIndexPatterns(await getIndexPatterns()); + if (index) { + const currentEsFields = await getFields(http!, [index]); + const timeFields = getTimeFieldOptions(currentEsFields as any); + setTimeFieldOptions([firstFieldOption, ...timeFields]); + } + }; + indexPatternsFunction(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( - - + +
    + +
    +
    + + ) => { - editActionConfig('index', e.target.value); - }} - onBlur={() => { - if (!index) { - editActionConfig('index', ''); + label={ + + } + isInvalid={errors.index.length > 0 && index !== undefined} + error={errors.index} + helpText={ + + } + > + 0 && index !== undefined} + noSuggestions={!indexOptions.length} + options={indexOptions} + data-test-subj="connectorIndexesComboBox" + selectedOptions={ + index + ? [ + { + value: index, + label: index, + }, + ] + : [] } + onChange={async (selected: EuiComboBoxOptionOption[]) => { + editActionConfig('index', selected[0].value); + const indices = selected.map(s => s.value as string); + + // reset time field and expression fields if indices are deleted + if (indices.length === 0) { + setTimeFieldOptions([]); + return; + } + const currentEsFields = await getFields(http!, indices); + const timeFields = getTimeFieldOptions(currentEsFields as any); + + setTimeFieldOptions([firstFieldOption, ...timeFields]); + }} + onSearchChange={async search => { + setIsIndiciesLoading(true); + setIndexOptions(await getIndexOptions(http!, search, indexPatterns)); + setIsIndiciesLoading(false); + }} + onBlur={() => { + if (!index) { + editActionConfig('index', ''); + } + }} + /> + + + { + editActionConfig('refresh', e.target.checked); }} + label={ + <> + {' '} + + + } + /> + + { + setTimeFieldCheckboxState(!hasTimeFieldCheckbox); + }} + label={ + <> + + + + } /> -
    + + {hasTimeFieldCheckbox ? ( + <> + + } + > + { + editActionConfig('executionTimeField', e.target.value); + }} + onBlur={() => { + if (executionTimeField === undefined) { + editActionConfig('executionTimeField', ''); + } + }} + /> + + + ) : null} + ); }; @@ -73,47 +261,48 @@ const IndexParamsFields: React.FunctionComponent { - const { refresh } = actionParams; + const { documents } = actionParams; + + function onDocumentsChange(updatedDocuments: string) { + try { + const documentsJSON = JSON.parse(updatedDocuments); + editAction('documents', [documentsJSON], index); + // eslint-disable-next-line no-empty + } catch (e) {} + } return ( - ) => { - editAction('index', e.target.value, index); + 0 ? documents[0] : {}, null, 2)} + onChange={onDocumentsChange} + width="100%" + height="auto" + minLines={6} + maxLines={30} + isReadOnly={false} + setOptions={{ + showLineNumbers: true, + tabSize: 2, }} - onBlur={() => { - if (!actionParams.index) { - editAction('index', '', index); - } + editorProps={{ + $blockScrolling: Infinity, }} + showGutter={true} /> - - { - editAction('refresh', e.target.checked, index); - }} - label={ - - } - /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index 45a08b2d5263..c0ddd6791e90 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -39,9 +39,6 @@ export interface PagerDutyActionParams { } export interface IndexActionParams { - index?: string; - refresh?: boolean; - executionTimeField?: string; documents: string[]; } @@ -85,7 +82,9 @@ export interface EmailActionConnector extends ActionConnector { } interface EsIndexConfig { - index?: string; + index: string; + executionTimeField?: string; + refresh?: boolean; } export interface EsIndexActionConnector extends ActionConnector { 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 2bf779e55061..5c7f48de81f7 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 @@ -23,12 +23,13 @@ import { EuiEmptyPrompt, EuiText, } from '@elastic/eui'; -import { COMPARATORS, builtInComparators } from '../../../../common/constants'; import { - getMatchingIndicesForThresholdAlertType, - getThresholdAlertTypeFields, - loadIndexPatterns, -} from './lib/api'; + firstFieldOption, + getIndexPatterns, + getIndexOptions, + getFields, +} from '../../../../common/index_controls'; +import { COMPARATORS, builtInComparators } from '../../../../common/constants'; import { getTimeFieldOptions } from '../../../../common/lib/get_time_options'; import { ThresholdVisualization } from './visualization'; import { WhenExpression } from '../../../../common'; @@ -95,15 +96,6 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent expressionFieldsWithValidation.includes(errorKey) && errors[errorKey].length >= 1 ); - const getIndexPatterns = async () => { - const indexPatternObjects = await loadIndexPatterns(); - const titles = indexPatternObjects.map((indexPattern: any) => indexPattern.attributes.title); - setIndexPatterns(titles); - }; - const expressionErrorMessage = i18n.translate( 'xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage', { @@ -150,7 +136,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent 0) { - const currentEsFields = await getFields(index); + const currentEsFields = await getFields(http, index); const timeFields = getTimeFieldOptions(currentEsFields as any); setEsFields(currentEsFields); @@ -158,12 +144,11 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent { - return await getThresholdAlertTypeFields({ indexes, http }); - }; - useEffect(() => { - getIndexPatterns(); + const indexPatternsFunction = async () => { + setIndexPatterns(await getIndexPatterns()); + }; + indexPatternsFunction(); }, []); useEffect(() => { @@ -171,60 +156,6 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent; - } - - const getIndexOptions = async (pattern: string, indexPatternsParam: string[]) => { - const options: IOption[] = []; - - if (!pattern) { - return options; - } - - const matchingIndices = (await getMatchingIndicesForThresholdAlertType({ - pattern, - http, - })) as string[]; - const matchingIndexPatterns = indexPatternsParam.filter(anIndexPattern => { - return anIndexPattern.includes(pattern); - }) as string[]; - - if (matchingIndices.length || matchingIndexPatterns.length) { - const matchingOptions = _.uniq([...matchingIndices, ...matchingIndexPatterns]); - - options.push({ - label: i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.indicesAndIndexPatternsLabel', - { - defaultMessage: 'Based on your indices and index patterns', - } - ), - options: matchingOptions.map(match => { - return { - label: match, - value: match, - }; - }), - }); - } - - options.push({ - label: i18n.translate('xpack.triggersActionsUI.sections.alertAdd.threshold.chooseLabel', { - defaultMessage: 'Choose…', - }), - options: [ - { - value: pattern, - label: pattern, - }, - ], - }); - - return options; - }; - const indexPopover = ( @@ -285,7 +216,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent { setIsIndiciesLoading(true); - setIndexOptions(await getIndexOptions(search, indexPatterns)); + setIndexOptions(await getIndexOptions(http, search, indexPatterns)); setIsIndiciesLoading(false); }} onBlur={() => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts index d5b64f1489b8..356b0fbbc084 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts @@ -4,12 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - TimeSeriesResult, - TimeSeriesResultRow, - MetricResult, -} from '../../../../../../alerting_builtins/common/alert_types/index_threshold'; - export interface Comparator { text: string; value: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx index f27e35fe7609..0bcaa8312746 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx @@ -23,7 +23,7 @@ import { import moment from 'moment-timezone'; import { EuiCallOut, EuiLoadingChart, EuiSpacer, EuiEmptyPrompt, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getThresholdAlertVisualizationData } from './lib/api'; +import { getThresholdAlertVisualizationData } from '../../../../common/lib/index_threshold_api'; import { AggregationType, Comparator } from '../../../../common/types'; import { AlertsContextValue } from '../../../context/alerts_context'; import { IndexThresholdAlertParams } from './types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx index f68cc5759fb5..1c70e42e7ae7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -68,6 +68,7 @@ describe('action_connector_form', () => { dispatch={() => {}} errors={{ name: [] }} actionTypeRegistry={deps.actionTypeRegistry} + http={deps.http} /> ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index e221fff64048..57333d803279 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -15,6 +15,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { HttpSetup } from 'kibana/public'; import { ReducerAction } from './connector_reducer'; import { ActionConnector, IErrorObject, ActionTypeModel } from '../../../types'; import { TypeRegistry } from '../../type_registry'; @@ -47,6 +48,7 @@ interface ActionConnectorProps { }; errors: IErrorObject; actionTypeRegistry: TypeRegistry; + http: HttpSetup; } export const ActionConnectorForm = ({ @@ -56,6 +58,7 @@ export const ActionConnectorForm = ({ serverError, errors, actionTypeRegistry, + http, }: ActionConnectorProps) => { const setActionProperty = (key: string, value: any) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); @@ -148,6 +151,7 @@ export const ActionConnectorForm = ({ errors={errors} editActionConfig={setActionConfigProperty} editActionSecrets={setActionSecretsProperty} + http={http} /> ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index f265a1de6f56..9aea2419ec61 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -104,6 +104,7 @@ export const ConnectorAddFlyout = ({ dispatch={dispatch} errors={errors} actionTypeRegistry={actionTypeRegistry} + http={http} /> ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index c7f52fb462cc..977a908fd86f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -152,6 +152,7 @@ export const ConnectorAddModal = ({ serverError={serverError} errors={errors} actionTypeRegistry={actionTypeRegistry} + http={http} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index d0dcff9ef6a9..39c0b7255a7b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -135,6 +135,7 @@ export const ConnectorEditFlyout = ({ actionTypeName={connector.actionType} dispatch={dispatch} actionTypeRegistry={actionTypeRegistry} + http={http} /> 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 new file mode 100644 index 000000000000..32fb35d6adeb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/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 { HttpSetup } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { + loadIndexPatterns, + getMatchingIndicesForThresholdAlertType, + getThresholdAlertTypeFields, +} from '../lib/index_threshold_api'; + +export interface IOption { + label: string; + options: Array<{ value: string; label: string }>; +} + +export const getIndexPatterns = async () => { + const indexPatternObjects = await loadIndexPatterns(); + return indexPatternObjects.map((indexPattern: any) => indexPattern.attributes.title); +}; + +export const getIndexOptions = async ( + http: HttpSetup, + pattern: string, + indexPatternsParam: string[] +) => { + const options: IOption[] = []; + + if (!pattern) { + return options; + } + + const matchingIndices = (await getMatchingIndicesForThresholdAlertType({ + pattern, + http, + })) as string[]; + const matchingIndexPatterns = indexPatternsParam.filter(anIndexPattern => { + return anIndexPattern.includes(pattern); + }) as string[]; + + if (matchingIndices.length || matchingIndexPatterns.length) { + const matchingOptions = _.uniq([...matchingIndices, ...matchingIndexPatterns]); + + options.push({ + label: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indicesAndIndexPatternsLabel', + { + defaultMessage: 'Based on your index patterns', + } + ), + options: matchingOptions.map(match => { + return { + label: match, + value: match, + }; + }), + }); + } + + options.push({ + label: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel', + { + defaultMessage: 'Choose…', + } + ), + options: [ + { + value: pattern, + label: pattern, + }, + ], + }); + + return options; +}; + +export const getFields = async (http: HttpSetup, indexes: string[]) => { + return await getThresholdAlertTypeFields({ indexes, http }); +}; + +export const firstFieldOption = { + text: i18n.translate('xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldOptionLabel', { + defaultMessage: 'Select a field', + }), + value: '', +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/api.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/index_threshold_api.ts similarity index 96% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/api.ts rename to x-pack/plugins/triggers_actions_ui/public/common/lib/index_threshold_api.ts index 064f05b415d4..9ec198a43646 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/index_threshold_api.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { HttpSetup } from 'kibana/public'; -import { TimeSeriesResult } from '../types'; -export { TimeSeriesResult } from '../types'; +import { TimeSeriesResult } from '../../../../alerting_builtins/common/alert_types/index_threshold'; const INDEX_THRESHOLD_API_ROOT = '/api/alerting_builtins/index_threshold'; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index d9681e2474f0..900521830571 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.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 { HttpSetup } from 'kibana/public'; import { ActionGroup } from '../../alerting/common'; import { ActionType } from '../../actions/common'; import { TypeRegistry } from './application/type_registry'; @@ -20,11 +21,12 @@ export type AlertTypeIndex = Record; export type ActionTypeRegistryContract = PublicMethodsOf>; export type AlertTypeRegistryContract = PublicMethodsOf>; -export interface ActionConnectorFieldsProps { - action: TActionCOnnector; +export interface ActionConnectorFieldsProps { + action: TActionConnector; editActionConfig: (property: string, value: any) => void; editActionSecrets: (property: string, value: any) => void; errors: { [key: string]: string[] }; + http?: HttpSetup; } export interface ActionParamsProps { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts index 1aa0f8e2c9f1..6d76a00d39b9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts @@ -31,7 +31,9 @@ export default function indexTest({ getService }: FtrProviderContext) { .send({ name: 'An index action', actionTypeId: '.index', - config: {}, + config: { + index: ES_TEST_INDEX_NAME, + }, secrets: {}, }) .expect(200); @@ -41,7 +43,8 @@ export default function indexTest({ getService }: FtrProviderContext) { name: 'An index action', actionTypeId: '.index', config: { - index: null, + index: ES_TEST_INDEX_NAME, + refresh: false, }, }); createdActionID = createdAction.id; @@ -55,10 +58,10 @@ export default function indexTest({ getService }: FtrProviderContext) { id: fetchedAction.id, name: 'An index action', actionTypeId: '.index', - config: { index: null }, + config: { index: ES_TEST_INDEX_NAME, refresh: false }, }); - // create action with index config + // create action with all config props const { body: createdActionWithIndex } = await supertest .post('/api/action') .set('kbn-xsrf', 'foo') @@ -67,6 +70,8 @@ export default function indexTest({ getService }: FtrProviderContext) { actionTypeId: '.index', config: { index: ES_TEST_INDEX_NAME, + refresh: true, + executionTimeField: 'test', }, }) .expect(200); @@ -77,6 +82,8 @@ export default function indexTest({ getService }: FtrProviderContext) { actionTypeId: '.index', config: { index: ES_TEST_INDEX_NAME, + refresh: true, + executionTimeField: 'test', }, }); createdActionIDWithIndex = createdActionWithIndex.id; @@ -92,6 +99,8 @@ export default function indexTest({ getService }: FtrProviderContext) { actionTypeId: '.index', config: { index: ES_TEST_INDEX_NAME, + refresh: true, + executionTimeField: 'test', }, }); }); @@ -111,20 +120,31 @@ export default function indexTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: [index]: types that failed validation:\n- [index.0]: expected value of type [string] but got [number]\n- [index.1]: expected value to equal [null]', + 'error validating action type config: [index]: expected value of type [string] but got [number]', }); }); }); it('should execute successly when expected for a single body', async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'An index action', + actionTypeId: '.index', + config: { + index: ES_TEST_INDEX_NAME, + refresh: true, + }, + secrets: {}, + }) + .expect(200); const { body: result } = await supertest - .post(`/api/action/${createdActionID}/_execute`) + .post(`/api/action/${createdAction.id}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { - index: ES_TEST_INDEX_NAME, documents: [{ testing: [1, 2, 3] }], - refresh: true, }, }) .expect(200); @@ -136,14 +156,25 @@ export default function indexTest({ getService }: FtrProviderContext) { }); it('should execute successly when expected for with multiple bodies', async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'An index action', + actionTypeId: '.index', + config: { + index: ES_TEST_INDEX_NAME, + refresh: true, + }, + secrets: {}, + }) + .expect(200); const { body: result } = await supertest - .post(`/api/action/${createdActionID}/_execute`) + .post(`/api/action/${createdAction.id}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { - index: ES_TEST_INDEX_NAME, documents: [{ testing: [1, 2, 3] }, { Testing: [4, 5, 6] }], - refresh: true, }, }) .expect(200); @@ -169,12 +200,25 @@ export default function indexTest({ getService }: FtrProviderContext) { }); it('should execute successly with refresh false', async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'An index action', + actionTypeId: '.index', + config: { + index: ES_TEST_INDEX_NAME, + refresh: false, + executionTimeField: 'test', + }, + secrets: {}, + }) + .expect(200); const { body: result } = await supertest - .post(`/api/action/${createdActionID}/_execute`) + .post(`/api/action/${createdAction.id}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { - index: ES_TEST_INDEX_NAME, documents: [{ refresh: 'not set' }], }, }) @@ -185,57 +229,32 @@ export default function indexTest({ getService }: FtrProviderContext) { items = await getTestIndexItems(es); expect(items.length).to.be.lessThan(2); - const { body: result2 } = await supertest - .post(`/api/action/${createdActionID}/_execute`) + const { body: createdActionWithRefresh } = await supertest + .post('/api/action') .set('kbn-xsrf', 'foo') .send({ - params: { + name: 'An index action', + actionTypeId: '.index', + config: { index: ES_TEST_INDEX_NAME, - documents: [{ refresh: 'true' }], refresh: true, }, + secrets: {}, }) .expect(200); - expect(result2.status).to.eql('ok'); - - items = await getTestIndexItems(es); - expect(items.length).to.eql(2); - }); - - it('should execute unsuccessfully when expected', async () => { - let response; - let result; - - response = await supertest - .post(`/api/action/${createdActionID}/_execute`) + const { body: result2 } = await supertest + .post(`/api/action/${createdActionWithRefresh.id}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { - indeX: ES_TEST_INDEX_NAME, - documents: [{ testing: [1, 2, 3] }], + documents: [{ refresh: 'true' }], }, }) .expect(200); - result = response.body; - expect(result.status).to.equal('error'); - expect(result.message).to.eql( - 'error validating action params: [indeX]: definition for this key is missing' - ); + expect(result2.status).to.eql('ok'); - response = await supertest - .post(`/api/action/${createdActionID}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - documents: [{ testing: [1, 2, 3] }], - }, - }) - .expect(200); - result = response.body; - expect(result.status).to.equal('error'); - expect(result.message).to.eql( - 'index param needs to be set because not set in config for action' - ); + items = await getTestIndexItems(es); + expect(items.length).to.eql(2); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts index 79e0da3a4c68..5cc3d7275a7b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts @@ -31,7 +31,7 @@ export default function indexTest({ getService }: FtrProviderContext) { .send({ name: 'An index action', actionTypeId: '.index', - config: {}, + config: { index: ES_TEST_INDEX_NAME }, secrets: {}, }) .expect(200); @@ -41,7 +41,8 @@ export default function indexTest({ getService }: FtrProviderContext) { name: 'An index action', actionTypeId: '.index', config: { - index: null, + index: ES_TEST_INDEX_NAME, + refresh: false, }, }); createdActionID = createdAction.id; @@ -55,10 +56,10 @@ export default function indexTest({ getService }: FtrProviderContext) { id: fetchedAction.id, name: 'An index action', actionTypeId: '.index', - config: { index: null }, + config: { index: ES_TEST_INDEX_NAME, refresh: false }, }); - // create action with index config + // create action with all config props const { body: createdActionWithIndex } = await supertest .post('/api/action') .set('kbn-xsrf', 'foo') @@ -67,6 +68,8 @@ export default function indexTest({ getService }: FtrProviderContext) { actionTypeId: '.index', config: { index: ES_TEST_INDEX_NAME, + refresh: true, + executionTimeField: 'test', }, }) .expect(200); @@ -77,6 +80,8 @@ export default function indexTest({ getService }: FtrProviderContext) { actionTypeId: '.index', config: { index: ES_TEST_INDEX_NAME, + refresh: true, + executionTimeField: 'test', }, }); createdActionIDWithIndex = createdActionWithIndex.id; @@ -92,19 +97,32 @@ export default function indexTest({ getService }: FtrProviderContext) { actionTypeId: '.index', config: { index: ES_TEST_INDEX_NAME, + refresh: true, + executionTimeField: 'test', }, }); }); it('should execute successly when expected for a single body', async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'An index action', + actionTypeId: '.index', + config: { + index: ES_TEST_INDEX_NAME, + refresh: true, + }, + secrets: {}, + }) + .expect(200); const { body: result } = await supertest - .post(`/api/action/${createdActionID}/_execute`) + .post(`/api/action/${createdAction.id}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { - index: ES_TEST_INDEX_NAME, documents: [{ testing: [1, 2, 3] }], - refresh: true, }, }) .expect(200); From ac5e323af872f0db601724c3c22455bbcd946c45 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 17 Mar 2020 18:47:54 -0700 Subject: [PATCH 071/115] [Search service] Asynchronous ES search strategy (#53538) * 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 * Simplify search logic * Fix merge error * Review feedback * Fix issue with hits.total Co-authored-by: Elastic Machine --- ...bana-plugin-plugins-data-server.icancel.md | 11 ---- ...lugin-plugins-data-server.isearchcancel.md | 11 ++++ .../kibana-plugin-plugins-data-server.md | 2 +- src/plugins/data/server/index.ts | 2 +- .../search/i_route_handler_search_context.ts | 4 +- src/plugins/data/server/search/i_search.ts | 4 +- .../data/server/search/i_search_strategy.ts | 4 +- src/plugins/data/server/search/index.ts | 8 ++- src/plugins/data/server/server.api.md | 12 ++-- .../public/search/es_search_strategy.ts | 16 ++++-- .../server/search/es_search_strategy.ts | 56 ++++++++++++++++--- 11 files changed, 90 insertions(+), 40 deletions(-) delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.icancel.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.icancel.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.icancel.md deleted file mode 100644 index 27141c68ae1a..000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.icancel.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ICancel](./kibana-plugin-plugins-data-server.icancel.md) - -## ICancel type - -Signature: - -```typescript -export declare type ICancel = (id: string) => Promise; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md new file mode 100644 index 000000000000..99c30515e8da --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchCancel](./kibana-plugin-plugins-data-server.isearchcancel.md) + +## ISearchCancel type + +Signature: + +```typescript +export declare type ISearchCancel = (id: string) => Promise; +``` 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 12d53f1a35ea..e756eb9b7290 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 @@ -67,9 +67,9 @@ | Type Alias | Description | | --- | --- | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-server.fieldformatsgetconfigfn.md) | | -| [ICancel](./kibana-plugin-plugins-data-server.icancel.md) | | | [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/server/index.ts b/src/plugins/data/server/index.ts index 0165486fc2de..5038b4226fad 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -166,7 +166,7 @@ export { ParsedInterval } from '../common'; export { ISearch, - ICancel, + ISearchCancel, ISearchOptions, IRequestTypesMap, IResponseTypesMap, diff --git a/src/plugins/data/server/search/i_route_handler_search_context.ts b/src/plugins/data/server/search/i_route_handler_search_context.ts index 89862781b826..9888c774ea10 100644 --- a/src/plugins/data/server/search/i_route_handler_search_context.ts +++ b/src/plugins/data/server/search/i_route_handler_search_context.ts @@ -17,9 +17,9 @@ * under the License. */ -import { ISearchGeneric, ICancelGeneric } from './i_search'; +import { ISearchGeneric, ISearchCancelGeneric } from './i_search'; export interface IRouteHandlerSearchContext { search: ISearchGeneric; - cancel: ICancelGeneric; + cancel: ISearchCancelGeneric; } diff --git a/src/plugins/data/server/search/i_search.ts b/src/plugins/data/server/search/i_search.ts index ea014c5e136d..fa4aa72ac728 100644 --- a/src/plugins/data/server/search/i_search.ts +++ b/src/plugins/data/server/search/i_search.ts @@ -42,7 +42,7 @@ export type ISearchGeneric = Promise; -export type ICancelGeneric = ( +export type ISearchCancelGeneric = ( id: string, strategy?: T ) => Promise; @@ -52,4 +52,4 @@ export type ISearch = ( options?: ISearchOptions ) => Promise; -export type ICancel = (id: string) => Promise; +export type ISearchCancel = (id: string) => Promise; diff --git a/src/plugins/data/server/search/i_search_strategy.ts b/src/plugins/data/server/search/i_search_strategy.ts index 4cfc9608383a..9b405034f883 100644 --- a/src/plugins/data/server/search/i_search_strategy.ts +++ b/src/plugins/data/server/search/i_search_strategy.ts @@ -18,7 +18,7 @@ */ import { APICaller } from 'kibana/server'; -import { ISearch, ICancel, ISearchGeneric } from './i_search'; +import { ISearch, ISearchCancel, ISearchGeneric } from './i_search'; import { TStrategyTypes } from './strategy_types'; import { ISearchContext } from './i_search_context'; @@ -28,7 +28,7 @@ import { ISearchContext } from './i_search_context'; */ export interface ISearchStrategy { search: ISearch; - cancel?: ICancel; + cancel?: ISearchCancel; } /** diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 385e96ee803b..15738a3befb2 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -21,7 +21,13 @@ export { ISearchSetup } from './i_search_setup'; export { ISearchContext } from './i_search_context'; -export { ISearch, ICancel, ISearchOptions, IRequestTypesMap, IResponseTypesMap } from './i_search'; +export { + ISearch, + ISearchCancel, + ISearchOptions, + IRequestTypesMap, + IResponseTypesMap, +} from './i_search'; export { TStrategyTypes } from './strategy_types'; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 2a2d9bb414c1..178b2949a945 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -329,12 +329,6 @@ export function getDefaultSearchParams(config: SharedGlobalConfig): { restTotalHitsAsInt: boolean; }; -// Warning: (ae-forgotten-export) The symbol "TStrategyTypes" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ICancel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ICancel = (id: string) => Promise; - // Warning: (ae-missing-release-tag) "IFieldFormatsRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -507,11 +501,17 @@ export interface IResponseTypesMap { [ES_SEARCH_STRATEGY]: IEsSearchResponse; } +// Warning: (ae-forgotten-export) The symbol "TStrategyTypes" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ISearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export type ISearch = (request: IRequestTypesMap[T], options?: ISearchOptions) => Promise; +// Warning: (ae-missing-release-tag) "ISearchCancel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ISearchCancel = (id: string) => Promise; + // Warning: (ae-missing-release-tag) "ISearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts index 25c6a789cca9..c493e8ce8678 100644 --- a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts @@ -10,16 +10,17 @@ import { TSearchStrategyProvider, ISearchContext, ISearch, - SYNC_SEARCH_STRATEGY, getEsPreference, } from '../../../../../src/plugins/data/public'; import { IEnhancedEsSearchRequest, EnhancedSearchParams } from '../../common'; +import { ASYNC_SEARCH_STRATEGY } from './async_search_strategy'; +import { IAsyncSearchOptions } from './types'; export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider = ( context: ISearchContext ) => { - const syncStrategyProvider = context.getSearchStrategy(SYNC_SEARCH_STRATEGY); - const { search: syncSearch } = syncStrategyProvider(context); + const asyncStrategyProvider = context.getSearchStrategy(ASYNC_SEARCH_STRATEGY); + const { search: asyncSearch } = asyncStrategyProvider(context); const search: ISearch = ( request: IEnhancedEsSearchRequest, @@ -32,9 +33,12 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider; + const asyncOptions: IAsyncSearchOptions = { pollInterval: 0, ...options }; + + return asyncSearch( + { ...request, serverStrategy: ES_SEARCH_STRATEGY }, + asyncOptions + ) as Observable; }; return { search }; diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 69b357196dc3..11f0b9a0dc83 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -14,10 +14,16 @@ import { TSearchStrategyProvider, ISearch, ISearchOptions, + ISearchCancel, getDefaultSearchParams, } from '../../../../../src/plugins/data/server'; import { IEnhancedEsSearchRequest } from '../../common'; +export interface AsyncSearchResponse { + id: string; + response: SearchResponse; +} + export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider = ( context: ISearchContext, caller: APICaller @@ -28,28 +34,62 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider { const config = await context.config$.pipe(first()).toPromise(); const defaultParams = getDefaultSearchParams(config); - const params = { ...defaultParams, ...request.params }; + const params = { ...defaultParams, trackTotalHits: true, ...request.params }; - const rawResponse = (await (request.indexType === 'rollup' + const response = await (request.indexType === 'rollup' ? rollupSearch(caller, { ...request, params }, options) - : caller('search', params, options))) as SearchResponse; + : asyncSearch(caller, { ...request, params }, options)); + + const rawResponse = + request.indexType === 'rollup' + ? (response as SearchResponse) + : (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; - return { total, loaded, rawResponse }; + return { id, total, loaded, rawResponse }; }; - return { search }; + const cancel: ISearchCancel = async id => { + const method = 'DELETE'; + const path = `_async_search/${id}`; + await caller('transport.request', { method, path }); + }; + + return { search, cancel }; }; -function rollupSearch( +function asyncSearch( + caller: APICaller, + request: IEnhancedEsSearchRequest, + options?: ISearchOptions +) { + const { body = undefined, index = undefined, ...params } = request.id ? {} : request.params; + + // If we have an ID, then just poll for that ID, otherwise send the entire request body + const method = request.id ? 'GET' : 'POST'; + const path = request.id ? `_async_search/${request.id}` : `${index}/_async_search`; + + // Wait up to 1s for the response to return + const query = toSnakeCase({ waitForCompletion: '1s', ...params }); + + return caller('transport.request', { method, path, body, query }, options); +} + +async function rollupSearch( caller: APICaller, request: IEnhancedEsSearchRequest, options?: ISearchOptions ) { + const { body, index, ...params } = request.params; const method = 'POST'; - const path = `${request.params.index}/_rollup_search`; - const { body, ...params } = request.params; + const path = `${index}/_rollup_search`; const query = toSnakeCase(params); return caller('transport.request', { method, path, body, query }, options); } From 65a111f189d371e7c67f562eb78df29b45dafcc7 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Tue, 17 Mar 2020 23:31:41 -0400 Subject: [PATCH 072/115] Task/host enhancements (#59671) functional tests and ui updates to endpoint host details --- .../endpoint/common/generate_data.test.ts | 12 +- .../plugins/endpoint/common/generate_data.ts | 8 +- x-pack/plugins/endpoint/common/types.ts | 12 +- .../endpoint/components/header_nav.tsx | 8 +- .../public/applications/endpoint/index.tsx | 86 ++++++------ .../applications/endpoint/store/action.ts | 4 +- .../endpoint/store/hosts/action.ts | 40 ++++++ .../endpoint/store/hosts/index.test.ts | 73 ++++++++++ .../store/{managing => hosts}/index.ts | 6 +- .../{managing => hosts}/middleware.test.ts | 37 ++--- .../store/{managing => hosts}/middleware.ts | 33 ++--- .../mock_host_result_list.ts | 12 +- .../store/{managing => hosts}/reducer.ts | 22 ++- .../endpoint/store/hosts/selectors.ts | 60 ++++++++ .../applications/endpoint/store/index.ts | 6 +- .../endpoint/store/managing/action.ts | 39 ------ .../endpoint/store/managing/index.test.ts | 93 ------------- .../endpoint/store/managing/selectors.ts | 60 -------- .../applications/endpoint/store/reducer.ts | 4 +- .../public/applications/endpoint/types.ts | 16 +-- .../endpoint/view/formatted_date_time.tsx | 24 ++++ .../view/{managing => hosts}/details.tsx | 71 ++++++---- .../view/{managing => hosts}/hooks.ts | 8 +- .../view/{managing => hosts}/index.test.tsx | 31 +++-- .../view/{managing => hosts}/index.tsx | 129 ++++++++++-------- .../url_from_query_params.ts | 4 +- .../endpoint/view/policy/policy_list.tsx | 25 +--- .../endpoint/scripts/resolver_generator.ts | 16 +-- .../endpoint/server/routes/metadata.test.ts | 28 ++-- .../endpoint/server/routes/metadata.ts | 26 ++-- .../api_integration/apis/endpoint/metadata.ts | 42 +++--- .../feature_controls/endpoint_spaces.ts | 19 +-- .../functional/apps/endpoint/header_nav.ts | 14 +- .../endpoint/{management.ts => host_list.ts} | 63 ++++++++- x-pack/test/functional/apps/endpoint/index.ts | 2 +- .../functional/page_objects/endpoint_page.ts | 35 ++++- 36 files changed, 623 insertions(+), 545 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts rename x-pack/plugins/endpoint/public/applications/endpoint/store/{managing => hosts}/index.ts (60%) rename x-pack/plugins/endpoint/public/applications/endpoint/store/{managing => hosts}/middleware.test.ts (63%) rename x-pack/plugins/endpoint/public/applications/endpoint/store/{managing => hosts}/middleware.ts (59%) rename x-pack/plugins/endpoint/public/applications/endpoint/store/{managing => hosts}/mock_host_result_list.ts (82%) rename x-pack/plugins/endpoint/public/applications/endpoint/store/{managing => hosts}/reducer.ts (66%) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/formatted_date_time.tsx rename x-pack/plugins/endpoint/public/applications/endpoint/view/{managing => hosts}/details.tsx (62%) rename x-pack/plugins/endpoint/public/applications/endpoint/view/{managing => hosts}/hooks.ts (61%) rename x-pack/plugins/endpoint/public/applications/endpoint/view/{managing => hosts}/index.test.tsx (75%) rename x-pack/plugins/endpoint/public/applications/endpoint/view/{managing => hosts}/index.tsx (55%) rename x-pack/plugins/endpoint/public/applications/endpoint/view/{managing => hosts}/url_from_query_params.ts (78%) rename x-pack/test/functional/apps/endpoint/{management.ts => host_list.ts} (58%) diff --git a/x-pack/plugins/endpoint/common/generate_data.test.ts b/x-pack/plugins/endpoint/common/generate_data.test.ts index a687d7af1c59..dfb906c7af60 100644 --- a/x-pack/plugins/endpoint/common/generate_data.test.ts +++ b/x-pack/plugins/endpoint/common/generate_data.test.ts @@ -21,8 +21,8 @@ describe('data generator', () => { const generator1 = new EndpointDocGenerator('seed'); const generator2 = new EndpointDocGenerator('seed'); const timestamp = new Date().getTime(); - const metadata1 = generator1.generateEndpointMetadata(timestamp); - const metadata2 = generator2.generateEndpointMetadata(timestamp); + const metadata1 = generator1.generateHostMetadata(timestamp); + const metadata2 = generator2.generateHostMetadata(timestamp); expect(metadata1).toEqual(metadata2); }); @@ -30,14 +30,14 @@ describe('data generator', () => { const generator1 = new EndpointDocGenerator('seed'); const generator2 = new EndpointDocGenerator('different seed'); const timestamp = new Date().getTime(); - const metadata1 = generator1.generateEndpointMetadata(timestamp); - const metadata2 = generator2.generateEndpointMetadata(timestamp); + const metadata1 = generator1.generateHostMetadata(timestamp); + const metadata2 = generator2.generateHostMetadata(timestamp); expect(metadata1).not.toEqual(metadata2); }); - it('creates endpoint metadata documents', () => { + it('creates host metadata documents', () => { const timestamp = new Date().getTime(); - const metadata = generator.generateEndpointMetadata(timestamp); + const metadata = generator.generateHostMetadata(timestamp); expect(metadata['@timestamp']).toEqual(timestamp); expect(metadata.event.created).toEqual(timestamp); expect(metadata.endpoint).not.toBeNull(); diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 36896e5af681..2e1d6074d0c2 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -6,7 +6,7 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; -import { AlertEvent, EndpointEvent, EndpointMetadata, OSFields, HostFields } from './types'; +import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields } from './types'; export type Event = AlertEvent | EndpointEvent; @@ -104,8 +104,8 @@ export class EndpointDocGenerator { this.commonInfo = this.createHostData(); } - // This function will create new values for all the host fields, so documents from a different endpoint can be created - // This provides a convenient way to make documents from multiple endpoints that are all tied to a single seed value + // This function will create new values for all the host fields, so documents from a different host can be created + // This provides a convenient way to make documents from multiple hosts that are all tied to a single seed value public randomizeHostData() { this.commonInfo = this.createHostData(); } @@ -129,7 +129,7 @@ export class EndpointDocGenerator { }; } - public generateEndpointMetadata(ts = new Date().getTime()): EndpointMetadata { + public generateHostMetadata(ts = new Date().getTime()): HostMetadata { return { '@timestamp': ts, event: { diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index aa326c663965..e423de56bf81 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -83,10 +83,10 @@ export interface AlertResultList { prev: string | null; } -export interface EndpointResultList { - /* the endpoints restricted by the page size */ - endpoints: EndpointMetadata[]; - /* the total number of unique endpoints in the index */ +export interface HostResultList { + /* the hosts restricted by the page size */ + hosts: HostMetadata[]; + /* the total number of unique hosts in the index */ total: number; /* the page size requested */ request_page_size: number; @@ -243,7 +243,7 @@ interface AlertMetadata { */ export type AlertData = AlertEvent & AlertMetadata; -export interface EndpointMetadata { +export type HostMetadata = Immutable<{ '@timestamp': number; event: { created: number; @@ -258,7 +258,7 @@ export interface EndpointMetadata { version: string; }; host: HostFields; -} +}>; /** * Represents `total` response from Elasticsearch after ES 7.0. diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx index f7d6551f9093..1bafcbec93f5 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx @@ -24,11 +24,11 @@ export const navTabs: NavTabs[] = [ href: '/', }, { - id: 'management', - name: i18n.translate('xpack.endpoint.headerNav.management', { - defaultMessage: 'Management', + id: 'hosts', + name: i18n.translate('xpack.endpoint.headerNav.hosts', { + defaultMessage: 'Hosts', }), - href: '/management', + href: '/hosts', }, { id: 'alerts', diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index cec51f570f95..997113754f95 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -11,15 +11,17 @@ import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; import { Route, Switch, BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import { Store } from 'redux'; +import { useObservable } from 'react-use'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { RouteCapture } from './view/route_capture'; import { EndpointPluginStartDependencies } from '../../plugin'; import { appStoreFactory } from './store'; import { AlertIndex } from './view/alerts'; -import { ManagementList } from './view/managing'; +import { HostList } from './view/hosts'; import { PolicyList } from './view/policy'; import { PolicyDetails } from './view/policy'; import { HeaderNavigation } from './components/header_nav'; +import { EuiThemeProvider } from '../../../../../legacy/common/eui_styled_components'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. @@ -48,43 +50,49 @@ interface RouterProps { } const AppRoot: React.FunctionComponent = React.memo( - ({ basename, store, coreStart: { http, notifications }, depsStart: { data } }) => ( - - - - - - - - ( -

    - -

    - )} - /> - - - - - ( - { + const isDarkMode = useObservable(uiSettings.get$('theme:darkMode')); + + return ( + + + + + + + + + ( +

    + +

    + )} + /> + + + + + ( + + )} /> - )} - /> -
    -
    -
    -
    -
    -
    - ) +
    +
    +
    + +
    +
    +
    + ); + } ); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts index 85215238dbef..2dce8ead3858 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ManagementAction } from './managing'; +import { HostAction } from './hosts'; import { AlertAction } from './alerts'; import { RoutingAction } from './routing'; import { PolicyListAction } from './policy_list'; import { PolicyDetailsAction } from './policy_details'; export type AppAction = - | ManagementAction + | HostAction | AlertAction | RoutingAction | PolicyListAction diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts new file mode 100644 index 000000000000..dee35aa3b895 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HostListPagination, ServerApiError } from '../../types'; +import { HostResultList, HostMetadata } from '../../../../../common/types'; + +interface ServerReturnedHostList { + type: 'serverReturnedHostList'; + payload: HostResultList; +} + +interface ServerReturnedHostDetails { + type: 'serverReturnedHostDetails'; + payload: HostMetadata; +} + +interface ServerFailedToReturnHostDetails { + type: 'serverFailedToReturnHostDetails'; + payload: ServerApiError; +} + +interface UserPaginatedHostList { + type: 'userPaginatedHostList'; + payload: HostListPagination; +} + +// Why is FakeActionWithNoPayload here, see: https://github.com/elastic/endpoint-app-team/issues/273 +interface FakeActionWithNoPayload { + type: 'fakeActionWithNoPayLoad'; +} + +export type HostAction = + | ServerReturnedHostList + | ServerReturnedHostDetails + | ServerFailedToReturnHostDetails + | UserPaginatedHostList + | FakeActionWithNoPayload; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts new file mode 100644 index 000000000000..9aff66cdfb75 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts @@ -0,0 +1,73 @@ +/* + * 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 { createStore, Dispatch, Store } from 'redux'; +import { HostAction, hostListReducer } from './index'; +import { HostListState } from '../../types'; +import { listData } from './selectors'; +import { mockHostResultList } from './mock_host_result_list'; + +describe('HostList store concerns', () => { + let store: Store; + let dispatch: Dispatch; + const createTestStore = () => { + store = createStore(hostListReducer); + dispatch = store.dispatch; + }; + + const loadDataToStore = () => { + dispatch({ + type: 'serverReturnedHostList', + payload: mockHostResultList({ request_page_size: 1, request_page_index: 1, total: 10 }), + }); + }; + + describe('# Reducers', () => { + beforeEach(() => { + createTestStore(); + }); + + test('it creates default state', () => { + expect(store.getState()).toEqual({ + hosts: [], + pageSize: 10, + pageIndex: 0, + total: 0, + loading: false, + }); + }); + + test('it handles `serverReturnedHostList', () => { + const payload = mockHostResultList({ + request_page_size: 1, + request_page_index: 1, + total: 10, + }); + dispatch({ + type: 'serverReturnedHostList', + payload, + }); + + const currentState = store.getState(); + expect(currentState.hosts).toEqual(payload.hosts); + expect(currentState.pageSize).toEqual(payload.request_page_size); + expect(currentState.pageIndex).toEqual(payload.request_page_index); + expect(currentState.total).toEqual(payload.total); + }); + }); + + describe('# Selectors', () => { + beforeEach(() => { + createTestStore(); + loadDataToStore(); + }); + + test('it selects `hostListData`', () => { + const currentState = store.getState(); + expect(listData(currentState)).toEqual(currentState.hosts); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.ts similarity index 60% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.ts index f0bfe27c9e30..e80d7a82dc8c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { managementListReducer } from './reducer'; -export { ManagementAction } from './action'; -export { managementMiddlewareFactory } from './middleware'; +export { hostListReducer } from './reducer'; +export { HostAction } from './action'; +export { hostMiddlewareFactory } from './middleware'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts similarity index 63% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts index 459a1789a58d..a1973a38b653 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts @@ -7,51 +7,40 @@ import { CoreStart, HttpSetup } from 'kibana/public'; import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { History, createBrowserHistory } from 'history'; -import { managementListReducer, managementMiddlewareFactory } from './index'; -import { EndpointMetadata, EndpointResultList } from '../../../../../common/types'; -import { EndpointDocGenerator } from '../../../../../common/generate_data'; -import { ManagementListState } from '../../types'; +import { hostListReducer, hostMiddlewareFactory } from './index'; +import { HostResultList } from '../../../../../common/types'; +import { HostListState } from '../../types'; import { AppAction } from '../action'; import { listData } from './selectors'; import { DepsStartMock, depsStartMock } from '../../mocks'; +import { mockHostResultList } from './mock_host_result_list'; -describe('endpoint list saga', () => { +describe('host list middleware', () => { const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms)); let fakeCoreStart: jest.Mocked; let depsStart: DepsStartMock; let fakeHttpServices: jest.Mocked; - let store: Store; + let store: Store; let getState: typeof store['getState']; let dispatch: Dispatch; - const generator = new EndpointDocGenerator(); - // https://github.com/elastic/endpoint-app-team/issues/131 - const generateEndpoint = (): EndpointMetadata => { - return generator.generateEndpointMetadata(); - }; - let history: History; - const getEndpointListApiResponse = (): EndpointResultList => { - return { - endpoints: [generateEndpoint()], - request_page_size: 1, - request_page_index: 1, - total: 10, - }; + const getEndpointListApiResponse = (): HostResultList => { + return mockHostResultList({ request_page_size: 1, request_page_index: 1, total: 10 }); }; beforeEach(() => { fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); depsStart = depsStartMock(); fakeHttpServices = fakeCoreStart.http as jest.Mocked; store = createStore( - managementListReducer, - applyMiddleware(managementMiddlewareFactory(fakeCoreStart, depsStart)) + hostListReducer, + applyMiddleware(hostMiddlewareFactory(fakeCoreStart, depsStart)) ); getState = store.getState; dispatch = store.dispatch; history = createBrowserHistory(); }); - test('it handles `userChangedUrl`', async () => { + test('handles `userChangedUrl`', async () => { const apiResponse = getEndpointListApiResponse(); fakeHttpServices.post.mockResolvedValue(apiResponse); expect(fakeHttpServices.post).not.toHaveBeenCalled(); @@ -60,7 +49,7 @@ describe('endpoint list saga', () => { type: 'userChangedUrl', payload: { ...history.location, - pathname: '/management', + pathname: '/hosts', }, }); await sleep(); @@ -69,6 +58,6 @@ describe('endpoint list saga', () => { paging_properties: [{ page_index: 0 }, { page_size: 10 }], }), }); - expect(listData(getState())).toEqual(apiResponse.endpoints); + expect(listData(getState())).toEqual(apiResponse.hosts); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts similarity index 59% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts index 1131e8d769fc..9481b6633f12 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts @@ -5,39 +5,30 @@ */ import { MiddlewareFactory } from '../../types'; -import { - pageIndex, - pageSize, - isOnManagementPage, - hasSelectedHost, - uiQueryParams, -} from './selectors'; -import { ManagementListState } from '../../types'; +import { pageIndex, pageSize, isOnHostPage, hasSelectedHost, uiQueryParams } from './selectors'; +import { HostListState } from '../../types'; import { AppAction } from '../action'; -export const managementMiddlewareFactory: MiddlewareFactory = coreStart => { +export const hostMiddlewareFactory: MiddlewareFactory = coreStart => { return ({ getState, dispatch }) => next => async (action: AppAction) => { next(action); const state = getState(); if ( (action.type === 'userChangedUrl' && - isOnManagementPage(state) && + isOnHostPage(state) && hasSelectedHost(state) !== true) || - action.type === 'userPaginatedManagementList' + action.type === 'userPaginatedHostList' ) { - const managementPageIndex = pageIndex(state); - const managementPageSize = pageSize(state); + const hostPageIndex = pageIndex(state); + const hostPageSize = pageSize(state); const response = await coreStart.http.post('/api/endpoint/metadata', { body: JSON.stringify({ - paging_properties: [ - { page_index: managementPageIndex }, - { page_size: managementPageSize }, - ], + paging_properties: [{ page_index: hostPageIndex }, { page_size: hostPageSize }], }), }); - response.request_page_index = managementPageIndex; + response.request_page_index = hostPageIndex; dispatch({ - type: 'serverReturnedManagementList', + type: 'serverReturnedHostList', payload: response, }); } @@ -46,12 +37,12 @@ export const managementMiddlewareFactory: MiddlewareFactory try { const response = await coreStart.http.get(`/api/endpoint/metadata/${selectedHost}`); dispatch({ - type: 'serverReturnedManagementDetails', + type: 'serverReturnedHostDetails', payload: response, }); } catch (error) { dispatch({ - type: 'serverFailedToReturnManagementDetails', + type: 'serverFailedToReturnHostDetails', payload: error, }); } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/mock_host_result_list.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts similarity index 82% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/managing/mock_host_result_list.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts index 61833d1dfb95..db39ecf44831 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/mock_host_result_list.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointResultList } from '../../../../../common/types'; +import { HostResultList } from '../../../../../common/types'; import { EndpointDocGenerator } from '../../../../../common/generate_data'; export const mockHostResultList: (options?: { total?: number; request_page_size?: number; request_page_index?: number; -}) => EndpointResultList = (options = {}) => { +}) => HostResultList = (options = {}) => { const { total = 1, request_page_size: requestPageSize = 10, @@ -24,13 +24,13 @@ export const mockHostResultList: (options?: { // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); - const endpoints = []; + const hosts = []; for (let index = 0; index < actualCountToReturn; index++) { const generator = new EndpointDocGenerator('seed'); - endpoints.push(generator.generateEndpointMetadata()); + hosts.push(generator.generateHostMetadata()); } - const mock: EndpointResultList = { - endpoints, + const mock: HostResultList = { + hosts, total, request_page_size: requestPageSize, request_page_index: requestPageIndex, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts similarity index 66% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts index 582aa6b7138c..fd70317a9f37 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts @@ -5,12 +5,12 @@ */ import { Reducer } from 'redux'; -import { ManagementListState } from '../../types'; +import { HostListState } from '../../types'; import { AppAction } from '../action'; -const initialState = (): ManagementListState => { +const initialState = (): HostListState => { return { - endpoints: [], + hosts: [], pageSize: 10, pageIndex: 0, total: 0, @@ -21,38 +21,36 @@ const initialState = (): ManagementListState => { }; }; -export const managementListReducer: Reducer = ( +export const hostListReducer: Reducer = ( state = initialState(), action ) => { - if (action.type === 'serverReturnedManagementList') { + if (action.type === 'serverReturnedHostList') { const { - endpoints, + hosts, total, request_page_size: pageSize, request_page_index: pageIndex, } = action.payload; return { ...state, - endpoints, + hosts, total, pageSize, pageIndex, loading: false, }; - } else if (action.type === 'serverReturnedManagementDetails') { + } else if (action.type === 'serverReturnedHostDetails') { return { ...state, details: action.payload, }; - } else if (action.type === 'serverFailedToReturnManagementDetails') { + } else if (action.type === 'serverFailedToReturnHostDetails') { return { ...state, detailsError: action.payload, }; - } else if (action.type === 'userExitedManagementList') { - return initialState(); - } else if (action.type === 'userPaginatedManagementList') { + } else if (action.type === 'userPaginatedHostList') { return { ...state, ...action.payload, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts new file mode 100644 index 000000000000..ebe310cb5119 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts @@ -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 querystring from 'querystring'; +import { createSelector } from 'reselect'; +import { Immutable } from '../../../../../common/types'; +import { HostListState, HostIndexUIQueryParams } from '../../types'; + +export const listData = (state: HostListState) => state.hosts; + +export const pageIndex = (state: HostListState) => state.pageIndex; + +export const pageSize = (state: HostListState) => state.pageSize; + +export const totalHits = (state: HostListState) => state.total; + +export const isLoading = (state: HostListState) => state.loading; + +export const detailsError = (state: HostListState) => state.detailsError; + +export const detailsData = (state: HostListState) => { + return state.details; +}; + +export const isOnHostPage = (state: HostListState) => + state.location ? state.location.pathname === '/hosts' : false; + +export const uiQueryParams: ( + state: HostListState +) => Immutable = createSelector( + (state: HostListState) => state.location, + (location: HostListState['location']) => { + const data: HostIndexUIQueryParams = {}; + if (location) { + // Removes the `?` from the beginning of query string if it exists + const query = querystring.parse(location.search.slice(1)); + + const keys: Array = ['selected_host']; + + for (const key of keys) { + const value = query[key]; + if (typeof value === 'string') { + data[key] = value; + } else if (Array.isArray(value)) { + data[key] = value[value.length - 1]; + } + } + } + return data; + } +); + +export const hasSelectedHost: (state: HostListState) => boolean = createSelector( + uiQueryParams, + ({ selected_host: selectedHost }) => { + return selectedHost !== undefined; + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts index c051be2bb83c..efa79b163d3b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -16,7 +16,7 @@ import { import { CoreStart } from 'kibana/public'; import { appReducer } from './reducer'; import { alertMiddlewareFactory } from './alerts/middleware'; -import { managementMiddlewareFactory } from './managing'; +import { hostMiddlewareFactory } from './hosts'; import { policyListMiddlewareFactory } from './policy_list'; import { policyDetailsMiddlewareFactory } from './policy_details'; import { GlobalState } from '../types'; @@ -69,8 +69,8 @@ export const appStoreFactory: (middlewareDeps?: { middleware = composeWithReduxDevTools( applyMiddleware( substateMiddlewareFactory( - globalState => globalState.managementList, - managementMiddlewareFactory(coreStart, depsStart) + globalState => globalState.hostList, + hostMiddlewareFactory(coreStart, depsStart) ), substateMiddlewareFactory( globalState => globalState.policyList, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts deleted file mode 100644 index a42e23e57d10..000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts +++ /dev/null @@ -1,39 +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 { ManagementListPagination, ServerApiError } from '../../types'; -import { EndpointResultList, EndpointMetadata } from '../../../../../common/types'; - -interface ServerReturnedManagementList { - type: 'serverReturnedManagementList'; - payload: EndpointResultList; -} - -interface ServerReturnedManagementDetails { - type: 'serverReturnedManagementDetails'; - payload: EndpointMetadata; -} - -interface ServerFailedToReturnManagementDetails { - type: 'serverFailedToReturnManagementDetails'; - payload: ServerApiError; -} - -interface UserExitedManagementList { - type: 'userExitedManagementList'; -} - -interface UserPaginatedManagementList { - type: 'userPaginatedManagementList'; - payload: ManagementListPagination; -} - -export type ManagementAction = - | ServerReturnedManagementList - | ServerReturnedManagementDetails - | ServerFailedToReturnManagementDetails - | UserExitedManagementList - | UserPaginatedManagementList; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts deleted file mode 100644 index e435fded13f4..000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts +++ /dev/null @@ -1,93 +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 { createStore, Dispatch, Store } from 'redux'; -import { ManagementAction, managementListReducer } from './index'; -import { EndpointMetadata } from '../../../../../common/types'; -import { EndpointDocGenerator } from '../../../../../common/generate_data'; -import { ManagementListState } from '../../types'; -import { listData } from './selectors'; - -describe('endpoint_list store concerns', () => { - let store: Store; - let dispatch: Dispatch; - const generator = new EndpointDocGenerator(); - const createTestStore = () => { - store = createStore(managementListReducer); - dispatch = store.dispatch; - }; - const generateEndpoint = (): EndpointMetadata => { - return generator.generateEndpointMetadata(); - }; - const loadDataToStore = () => { - dispatch({ - type: 'serverReturnedManagementList', - payload: { - endpoints: [generateEndpoint()], - request_page_size: 1, - request_page_index: 1, - total: 10, - }, - }); - }; - - describe('# Reducers', () => { - beforeEach(() => { - createTestStore(); - }); - - test('it creates default state', () => { - expect(store.getState()).toEqual({ - endpoints: [], - pageSize: 10, - pageIndex: 0, - total: 0, - loading: false, - }); - }); - - test('it handles `serverReturnedManagementList', () => { - const payload = { - endpoints: [generateEndpoint()], - request_page_size: 1, - request_page_index: 1, - total: 10, - }; - dispatch({ - type: 'serverReturnedManagementList', - payload, - }); - - const currentState = store.getState(); - expect(currentState.endpoints).toEqual(payload.endpoints); - expect(currentState.pageSize).toEqual(payload.request_page_size); - expect(currentState.pageIndex).toEqual(payload.request_page_index); - expect(currentState.total).toEqual(payload.total); - }); - - test('it handles `userExitedManagementListPage`', () => { - loadDataToStore(); - - expect(store.getState().total).toEqual(10); - - dispatch({ type: 'userExitedManagementList' }); - expect(store.getState().endpoints.length).toEqual(0); - expect(store.getState().pageIndex).toEqual(0); - }); - }); - - describe('# Selectors', () => { - beforeEach(() => { - createTestStore(); - loadDataToStore(); - }); - - test('it selects `managementListData`', () => { - const currentState = store.getState(); - expect(listData(currentState)).toEqual(currentState.endpoints); - }); - }); -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts deleted file mode 100644 index a7776f09fe2b..000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts +++ /dev/null @@ -1,60 +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 querystring from 'querystring'; -import { createSelector } from 'reselect'; -import { Immutable } from '../../../../../common/types'; -import { ManagementListState, ManagingIndexUIQueryParams } from '../../types'; - -export const listData = (state: ManagementListState) => state.endpoints; - -export const pageIndex = (state: ManagementListState) => state.pageIndex; - -export const pageSize = (state: ManagementListState) => state.pageSize; - -export const totalHits = (state: ManagementListState) => state.total; - -export const isLoading = (state: ManagementListState) => state.loading; - -export const detailsError = (state: ManagementListState) => state.detailsError; - -export const detailsData = (state: ManagementListState) => { - return state.details; -}; - -export const isOnManagementPage = (state: ManagementListState) => - state.location ? state.location.pathname === '/management' : false; - -export const uiQueryParams: ( - state: ManagementListState -) => Immutable = createSelector( - (state: ManagementListState) => state.location, - (location: ManagementListState['location']) => { - const data: ManagingIndexUIQueryParams = {}; - if (location) { - // Removes the `?` from the beginning of query string if it exists - const query = querystring.parse(location.search.slice(1)); - - const keys: Array = ['selected_host']; - - for (const key of keys) { - const value = query[key]; - if (typeof value === 'string') { - data[key] = value; - } else if (Array.isArray(value)) { - data[key] = value[value.length - 1]; - } - } - } - return data; - } -); - -export const hasSelectedHost: (state: ManagementListState) => boolean = createSelector( - uiQueryParams, - ({ selected_host: selectedHost }) => { - return selectedHost !== undefined; - } -); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts index e655a8d5e46d..c8b2d0867672 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { combineReducers, Reducer } from 'redux'; -import { managementListReducer } from './managing'; +import { hostListReducer } from './hosts'; import { AppAction } from './action'; import { alertListReducer } from './alerts'; import { GlobalState } from '../types'; @@ -12,7 +12,7 @@ import { policyListReducer } from './policy_list'; import { policyDetailsReducer } from './policy_details'; export const appReducer: Reducer = combineReducers({ - managementList: managementListReducer, + hostList: hostListReducer, alertList: alertListReducer, policyList: policyListReducer, policyDetails: policyDetailsReducer, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 91be6e4936db..3045f42a93fe 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -7,7 +7,7 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { IIndexPattern } from 'src/plugins/data/public'; import { - EndpointMetadata, + HostMetadata, AlertData, AlertResultList, Immutable, @@ -25,22 +25,22 @@ export type MiddlewareFactory = ( api: MiddlewareAPI, S> ) => (next: Dispatch) => (action: AppAction) => unknown; -export interface ManagementListState { - endpoints: EndpointMetadata[]; - total: number; +export interface HostListState { + hosts: HostMetadata[]; pageSize: number; pageIndex: number; + total: number; loading: boolean; detailsError?: ServerApiError; - details?: Immutable; + details?: Immutable; location?: Immutable; } -export interface ManagementListPagination { +export interface HostListPagination { pageIndex: number; pageSize: number; } -export interface ManagingIndexUIQueryParams { +export interface HostIndexUIQueryParams { selected_host?: string; } @@ -92,7 +92,7 @@ export interface PolicyDetailsState { } export interface GlobalState { - readonly managementList: ManagementListState; + readonly hostList: HostListState; readonly alertList: AlertListState; readonly policyList: PolicyListState; readonly policyDetails: PolicyDetailsState; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/formatted_date_time.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/formatted_date_time.tsx new file mode 100644 index 000000000000..dcf97b4b2b22 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/formatted_date_time.tsx @@ -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 React from 'react'; +import { FormattedDate, FormattedTime, FormattedRelative } from '@kbn/i18n/react'; + +export const FormattedDateAndTime: React.FC<{ date: Date }> = ({ date }) => { + // If date is greater than or equal to 1h (ago), then show it as a date + // else, show it as relative to "now" + return Date.now() - date.getTime() >= 3.6e6 ? ( + <> + + {' @'} + + + ) : ( + <> + + + ); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx similarity index 62% rename from x-pack/plugins/endpoint/public/applications/endpoint/view/managing/details.tsx rename to x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx index 9f2a73204271..37080e856835 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx @@ -13,38 +13,46 @@ import { EuiDescriptionList, EuiLoadingContent, EuiHorizontalRule, + EuiHealth, EuiSpacer, + EuiListGroup, + EuiListGroupItem, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; +import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { useManagementListSelector } from './hooks'; +import { HostMetadata } from '../../../../../common/types'; +import { useHostListSelector } from './hooks'; import { urlFromQueryParams } from './url_from_query_params'; -import { uiQueryParams, detailsData, detailsError } from './../../store/managing/selectors'; +import { FormattedDateAndTime } from '../formatted_date_time'; +import { uiQueryParams, detailsData, detailsError } from './../../store/hosts/selectors'; -const HostDetails = memo(() => { - const details = useManagementListSelector(detailsData); - if (details === undefined) { - return null; +const HostIds = styled(EuiListGroupItem)` + margin-top: 0; + .euiListGroupItem__text { + padding: 0; } +`; +const HostDetails = memo(({ details }: { details: HostMetadata }) => { const detailsResultsUpper = useMemo(() => { return [ { - title: i18n.translate('xpack.endpoint.management.details.os', { + title: i18n.translate('xpack.endpoint.host.details.os', { defaultMessage: 'OS', }), description: details.host.os.full, }, { - title: i18n.translate('xpack.endpoint.management.details.lastSeen', { + title: i18n.translate('xpack.endpoint.host.details.lastSeen', { defaultMessage: 'Last Seen', }), - description: details['@timestamp'], + description: , }, { - title: i18n.translate('xpack.endpoint.management.details.alerts', { + title: i18n.translate('xpack.endpoint.host.details.alerts', { defaultMessage: 'Alerts', }), description: '0', @@ -55,62 +63,67 @@ const HostDetails = memo(() => { const detailsResultsLower = useMemo(() => { return [ { - title: i18n.translate('xpack.endpoint.management.details.policy', { + title: i18n.translate('xpack.endpoint.host.details.policy', { defaultMessage: 'Policy', }), description: details.endpoint.policy.id, }, { - title: i18n.translate('xpack.endpoint.management.details.policyStatus', { + title: i18n.translate('xpack.endpoint.host.details.policyStatus', { defaultMessage: 'Policy Status', }), - description: 'active', + description: active, }, { - title: i18n.translate('xpack.endpoint.management.details.ipAddress', { + title: i18n.translate('xpack.endpoint.host.details.ipAddress', { defaultMessage: 'IP Address', }), - description: details.host.ip, + description: ( + + {details.host.ip.map((ip: string, index: number) => ( + + ))} + + ), }, { - title: i18n.translate('xpack.endpoint.management.details.hostname', { + title: i18n.translate('xpack.endpoint.host.details.hostname', { defaultMessage: 'Hostname', }), description: details.host.hostname, }, { - title: i18n.translate('xpack.endpoint.management.details.sensorVersion', { + title: i18n.translate('xpack.endpoint.host.details.sensorVersion', { defaultMessage: 'Sensor Version', }), description: details.agent.version, }, ]; }, [details.agent.version, details.endpoint.policy.id, details.host.hostname, details.host.ip]); - return ( <> ); }); -export const ManagementDetails = () => { +export const HostDetailsFlyout = () => { const history = useHistory(); const { notifications } = useKibana(); - const queryParams = useManagementListSelector(uiQueryParams); + const queryParams = useHostListSelector(uiQueryParams); const { selected_host: selectedHost, ...queryParamsWithoutSelectedHost } = queryParams; - const details = useManagementListSelector(detailsData); - const error = useManagementListSelector(detailsError); + const details = useHostListSelector(detailsData); + const error = useHostListSelector(detailsError); const handleFlyoutClose = useCallback(() => { history.push(urlFromQueryParams(queryParamsWithoutSelectedHost)); @@ -121,13 +134,13 @@ export const ManagementDetails = () => { notifications.toasts.danger({ title: ( ), body: ( ), @@ -137,10 +150,10 @@ export const ManagementDetails = () => { }, [error, notifications.toasts]); return ( - + -

    +

    {details === undefined ? : details.host.hostname}

    @@ -151,7 +164,7 @@ export const ManagementDetails = () => { ) : ( - + )}
    diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts similarity index 61% rename from x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts index a0720fbd8aee..99a0073f46c7 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts @@ -5,12 +5,10 @@ */ import { useSelector } from 'react-redux'; -import { GlobalState, ManagementListState } from '../../types'; +import { GlobalState, HostListState } from '../../types'; -export function useManagementListSelector( - selector: (state: ManagementListState) => TSelected -) { +export function useHostListSelector(selector: (state: HostListState) => TSelected) { return useSelector(function(state: GlobalState) { - return selector(state.managementList); + return selector(state.hostList); }); } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx similarity index 75% rename from x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.test.tsx rename to x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx index ced27ae8945b..f6dfae99c1b1 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx @@ -8,15 +8,16 @@ import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; import { Provider } from 'react-redux'; import { I18nProvider } from '@kbn/i18n/react'; +import { EuiThemeProvider } from '../../../../../../../legacy/common/eui_styled_components'; import { appStoreFactory } from '../../store'; import { RouteCapture } from '../route_capture'; import { createMemoryHistory, MemoryHistory } from 'history'; import { Router } from 'react-router-dom'; import { AppAction } from '../../types'; -import { ManagementList } from './index'; -import { mockHostResultList } from '../../store/managing/mock_host_result_list'; +import { HostList } from './index'; +import { mockHostResultList } from '../../store/hosts/mock_host_result_list'; -describe('when on the managing page', () => { +describe('when on the hosts page', () => { let render: () => reactTestingLibrary.RenderResult; let history: MemoryHistory; let store: ReturnType; @@ -28,11 +29,13 @@ describe('when on the managing page', () => { return reactTestingLibrary.render( - - - - - + + + + + + + ); @@ -41,7 +44,7 @@ describe('when on the managing page', () => { it('should show a table', async () => { const renderResult = render(); - const table = await renderResult.findByTestId('managementListTable'); + const table = await renderResult.findByTestId('hostListTable'); expect(table).not.toBeNull(); }); @@ -49,7 +52,7 @@ describe('when on the managing page', () => { it('should not show the flyout', () => { const renderResult = render(); expect.assertions(1); - return renderResult.findByTestId('managementDetailsFlyout').catch(e => { + return renderResult.findByTestId('hostDetailsFlyout').catch(e => { expect(e).not.toBeNull(); }); }); @@ -57,14 +60,14 @@ describe('when on the managing page', () => { beforeEach(() => { reactTestingLibrary.act(() => { const action: AppAction = { - type: 'serverReturnedManagementList', + type: 'serverReturnedHostList', payload: mockHostResultList(), }; store.dispatch(action); }); }); - it('should render the management summary row in the table', async () => { + it('should render the host summary row in the table', async () => { const renderResult = render(); const rows = await renderResult.findAllByRole('row'); expect(rows).toHaveLength(2); @@ -81,7 +84,7 @@ describe('when on the managing page', () => { }); it('should show the flyout', () => { - return renderResult.findByTestId('managementDetailsFlyout').then(flyout => { + return renderResult.findByTestId('hostDetailsFlyout').then(flyout => { expect(flyout).not.toBeNull(); }); }); @@ -100,7 +103,7 @@ describe('when on the managing page', () => { }); it('should show the flyout', () => { const renderResult = render(); - return renderResult.findByTestId('managementDetailsFlyout').then(flyout => { + return renderResult.findByTestId('hostDetailsFlyout').then(flyout => { expect(flyout).not.toBeNull(); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx similarity index 55% rename from x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx rename to x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx index ba9a931a233b..94625b8c6619 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx @@ -10,28 +10,29 @@ import { useHistory } from 'react-router-dom'; import { EuiPage, EuiPageBody, + EuiPageHeader, EuiPageContent, - EuiPageContentBody, - EuiPageContentHeader, - EuiPageContentHeaderSection, + EuiHorizontalRule, EuiTitle, EuiBasicTable, - EuiTextColor, + EuiText, EuiLink, + EuiHealth, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { createStructuredSelector } from 'reselect'; -import { ManagementDetails } from './details'; -import * as selectors from '../../store/managing/selectors'; -import { ManagementAction } from '../../store/managing/action'; -import { useManagementListSelector } from './hooks'; +import { HostDetailsFlyout } from './details'; +import * as selectors from '../../store/hosts/selectors'; +import { HostAction } from '../../store/hosts/action'; +import { useHostListSelector } from './hooks'; import { CreateStructuredSelector } from '../../types'; import { urlFromQueryParams } from './url_from_query_params'; const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); -export const ManagementList = () => { - const dispatch = useDispatch<(a: ManagementAction) => void>(); +export const HostList = () => { + const dispatch = useDispatch<(a: HostAction) => void>(); const history = useHistory(); const { listData, @@ -41,7 +42,7 @@ export const ManagementList = () => { isLoading, uiQueryParams: queryParams, hasSelectedHost, - } = useManagementListSelector(selector); + } = useHostListSelector(selector); const paginationSetup = useMemo(() => { return { @@ -57,7 +58,7 @@ export const ManagementList = () => { ({ page }: { page: { index: number; size: number } }) => { const { index, size } = page; dispatch({ - type: 'userPaginatedManagementList', + type: 'userPaginatedHostList', payload: { pageIndex: index, pageSize: size }, }); }, @@ -68,7 +69,7 @@ export const ManagementList = () => { return [ { field: '', - name: i18n.translate('xpack.endpoint.management.list.host', { + name: i18n.translate('xpack.endpoint.host.list.hostname', { defaultMessage: 'Hostname', }), render: ({ host: { hostname, id } }: { host: { hostname: string; id: string } }) => { @@ -89,7 +90,7 @@ export const ManagementList = () => { }, { field: '', - name: i18n.translate('xpack.endpoint.management.list.policy', { + name: i18n.translate('xpack.endpoint.host.list.policy', { defaultMessage: 'Policy', }), render: () => { @@ -98,37 +99,38 @@ export const ManagementList = () => { }, { field: '', - name: i18n.translate('xpack.endpoint.management.list.policyStatus', { + name: i18n.translate('xpack.endpoint.host.list.policyStatus', { defaultMessage: 'Policy Status', }), render: () => { - return 'Policy Status'; + return Policy Status; }, }, { field: '', - name: i18n.translate('xpack.endpoint.management.list.alerts', { + name: i18n.translate('xpack.endpoint.host.list.alerts', { defaultMessage: 'Alerts', }), + dataType: 'number', render: () => { return '0'; }, }, { field: 'host.os.name', - name: i18n.translate('xpack.endpoint.management.list.os', { + name: i18n.translate('xpack.endpoint.host.list.os', { defaultMessage: 'Operating System', }), }, { field: 'host.ip', - name: i18n.translate('xpack.endpoint.management.list.ip', { + name: i18n.translate('xpack.endpoint.host.list.ip', { defaultMessage: 'IP Address', }), }, { field: '', - name: i18n.translate('xpack.endpoint.management.list.sensorVersion', { + name: i18n.translate('xpack.endpoint.host.list.sensorVersion', { defaultMessage: 'Sensor Version', }), render: () => { @@ -137,9 +139,10 @@ export const ManagementList = () => { }, { field: '', - name: i18n.translate('xpack.endpoint.management.list.lastActive', { + name: i18n.translate('xpack.endpoint.host.list.lastActive', { defaultMessage: 'Last Active', }), + dataType: 'date', render: () => { return 'xxxx'; }, @@ -148,45 +151,59 @@ export const ManagementList = () => { }, [queryParams, history]); return ( - <> - {hasSelectedHost && } - + + {hasSelectedHost && } + - - - - -

    - -

    -
    -

    - - - -

    -
    -
    - - + +

    + +

    +
    + + + + + -
    + + +
    - +
    ); }; + +const HostPage = styled.div` + .hostPage { + padding: 0; + } + .hostHeader { + background-color: ${props => props.theme.eui.euiColorLightestShade}; + border-bottom: ${props => props.theme.eui.euiBorderThin}; + padding: ${props => + props.theme.eui.euiSizeXL + + ' ' + + 0 + + props.theme.eui.euiSizeXL + + ' ' + + props.theme.eui.euiSizeL}; + margin-bottom: 0; + } + .hostPageContent { + border: none; + } +`; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/url_from_query_params.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/url_from_query_params.ts similarity index 78% rename from x-pack/plugins/endpoint/public/applications/endpoint/view/managing/url_from_query_params.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/url_from_query_params.ts index ea6a4c6f684a..225aad8cab02 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/url_from_query_params.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/url_from_query_params.ts @@ -5,10 +5,10 @@ */ import querystring from 'querystring'; -import { EndpointAppLocation, ManagingIndexUIQueryParams } from '../../types'; +import { EndpointAppLocation, HostIndexUIQueryParams } from '../../types'; export function urlFromQueryParams( - queryParams: ManagingIndexUIQueryParams + queryParams: HostIndexUIQueryParams ): Partial { const search = querystring.stringify(queryParams); return { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx index cf573da3703c..e7ce53679bbe 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx @@ -20,17 +20,12 @@ import { EuiLink, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - FormattedMessage, - FormattedDate, - FormattedTime, - FormattedNumber, - FormattedRelative, -} from '@kbn/i18n/react'; +import { FormattedMessage, FormattedNumber } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; import { usePageId } from '../use_page_id'; +import { FormattedDateAndTime } from '../formatted_date_time'; import { selectIsLoading, selectPageIndex, @@ -56,22 +51,6 @@ const TruncateTooltipText = styled(TruncateText)` } `; -const FormattedDateAndTime: React.FC<{ date: Date }> = ({ date }) => { - // If date is greater than or equal to 24h (ago), then show it as a date - // else, show it as relative to "now" - return Date.now() - date.getTime() >= 8.64e7 ? ( - <> - - {' @'} - - - ) : ( - <> - - - ); -}; - const PolicyLink: React.FC<{ name: string; route: string }> = ({ name, route }) => { const history = useHistory(); diff --git a/x-pack/plugins/endpoint/scripts/resolver_generator.ts b/x-pack/plugins/endpoint/scripts/resolver_generator.ts index a3e56497f079..503999daec58 100644 --- a/x-pack/plugins/endpoint/scripts/resolver_generator.ts +++ b/x-pack/plugins/endpoint/scripts/resolver_generator.ts @@ -32,7 +32,7 @@ async function main() { }, metadataIndex: { alias: 'mi', - describe: 'index to store endpoint metadata in', + describe: 'index to store host metadata in', default: 'endpoint-agent-1', type: 'string', }, @@ -76,15 +76,15 @@ async function main() { type: 'number', default: 30, }, - numEndpoints: { + numHosts: { alias: 'ne', - describe: 'number of different endpoints to generate alerts for', + describe: 'number of different hosts to generate alerts for', type: 'number', default: 1, }, - alertsPerEndpoint: { + alertsPerHost: { alias: 'ape', - describe: 'number of resolver trees to make for each endpoint', + describe: 'number of resolver trees to make for each host', type: 'number', default: 1, }, @@ -133,12 +133,12 @@ async function main() { } const generator = new EndpointDocGenerator(argv.seed); - for (let i = 0; i < argv.numEndpoints; i++) { + for (let i = 0; i < argv.numHosts; i++) { await client.index({ index: argv.metadataIndex, - body: generator.generateEndpointMetadata(), + body: generator.generateHostMetadata(), }); - for (let j = 0; j < argv.alertsPerEndpoint; j++) { + for (let j = 0; j < argv.alertsPerHost; j++) { const resolverDocs = generator.generateFullResolverTree( argv.ancestors, argv.generations, diff --git a/x-pack/plugins/endpoint/server/routes/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata.test.ts index ee374bc1b57d..65e07edbcde2 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata.test.ts @@ -18,7 +18,7 @@ import { httpServiceMock, loggingServiceMock, } from '../../../../../src/core/server/mocks'; -import { EndpointMetadata, EndpointResultList } from '../../common/types'; +import { HostMetadata, HostResultList } from '../../common/types'; import { SearchResponse } from 'elasticsearch'; import { registerEndpointRoutes } from './metadata'; import { EndpointConfigSchema } from '../config'; @@ -49,8 +49,8 @@ describe('test endpoint route', () => { it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); - const response: SearchResponse = (data as unknown) as SearchResponse< - EndpointMetadata + const response: SearchResponse = (data as unknown) as SearchResponse< + HostMetadata >; mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => @@ -72,8 +72,8 @@ describe('test endpoint route', () => { expect(mockScopedClient.callAsCurrentUser).toBeCalled(); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); - const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as EndpointResultList; - expect(endpointResultList.endpoints.length).toEqual(2); + const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; + expect(endpointResultList.hosts.length).toEqual(2); expect(endpointResultList.total).toEqual(2); expect(endpointResultList.request_page_index).toEqual(0); expect(endpointResultList.request_page_size).toEqual(10); @@ -93,7 +93,7 @@ describe('test endpoint route', () => { }, }); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve((data as unknown) as SearchResponse) + Promise.resolve((data as unknown) as SearchResponse) ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -117,8 +117,8 @@ describe('test endpoint route', () => { }); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); - const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as EndpointResultList; - expect(endpointResultList.endpoints.length).toEqual(2); + const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; + expect(endpointResultList.hosts.length).toEqual(2); expect(endpointResultList.total).toEqual(2); expect(endpointResultList.request_page_index).toEqual(10); expect(endpointResultList.request_page_size).toEqual(10); @@ -140,7 +140,7 @@ describe('test endpoint route', () => { }, }); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve((data as unknown) as SearchResponse) + Promise.resolve((data as unknown) as SearchResponse) ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -177,8 +177,8 @@ describe('test endpoint route', () => { }); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); - const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as EndpointResultList; - expect(endpointResultList.endpoints.length).toEqual(2); + const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; + expect(endpointResultList.hosts.length).toEqual(2); expect(endpointResultList.total).toEqual(2); expect(endpointResultList.request_page_index).toEqual(10); expect(endpointResultList.request_page_size).toEqual(10); @@ -234,8 +234,8 @@ describe('test endpoint route', () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: (data as any).hits.hits[0]._id }, }); - const response: SearchResponse = (data as unknown) as SearchResponse< - EndpointMetadata + const response: SearchResponse = (data as unknown) as SearchResponse< + HostMetadata >; mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => @@ -257,7 +257,7 @@ describe('test endpoint route', () => { expect(mockScopedClient.callAsCurrentUser).toBeCalled(); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); - const result = mockResponse.ok.mock.calls[0][0]?.body as EndpointMetadata; + const result = mockResponse.ok.mock.calls[0][0]?.body as HostMetadata; expect(result).toHaveProperty('endpoint'); }); }); diff --git a/x-pack/plugins/endpoint/server/routes/metadata.ts b/x-pack/plugins/endpoint/server/routes/metadata.ts index 278cfac020a3..463a071ab0c7 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata.ts @@ -12,11 +12,11 @@ import { kibanaRequestToMetadataListESQuery, kibanaRequestToMetadataGetESQuery, } from '../services/endpoint/metadata_query_builders'; -import { EndpointMetadata, EndpointResultList } from '../../common/types'; +import { HostMetadata, HostResultList } from '../../common/types'; import { EndpointAppContext } from '../types'; interface HitSource { - _source: EndpointMetadata; + _source: HostMetadata; } export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { @@ -57,8 +57,8 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( 'search', queryParams - )) as SearchResponse; - return res.ok({ body: mapToEndpointResultList(queryParams, response) }); + )) as SearchResponse; + return res.ok({ body: mapToHostResultList(queryParams, response) }); } catch (err) { return res.internalError({ body: err }); } @@ -79,7 +79,7 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( 'search', query - )) as SearchResponse; + )) as SearchResponse; if (response.hits.hits.length === 0) { return res.notFound({ body: 'Endpoint Not Found' }); @@ -93,27 +93,27 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp ); } -function mapToEndpointResultList( +function mapToHostResultList( queryParams: Record, - searchResponse: SearchResponse -): EndpointResultList { - const totalNumberOfEndpoints = searchResponse?.aggregations?.total?.value || 0; + searchResponse: SearchResponse +): HostResultList { + const totalNumberOfHosts = searchResponse?.aggregations?.total?.value || 0; if (searchResponse.hits.hits.length > 0) { return { request_page_size: queryParams.size, request_page_index: queryParams.from, - endpoints: searchResponse.hits.hits + hosts: searchResponse.hits.hits .map(response => response.inner_hits.most_recent.hits.hits) .flatMap(data => data as HitSource) .map(entry => entry._source), - total: totalNumberOfEndpoints, + total: totalNumberOfHosts, }; } else { return { request_page_size: queryParams.size, request_page_index: queryParams.from, - total: totalNumberOfEndpoints, - endpoints: [], + total: totalNumberOfHosts, + hosts: [], }; } } diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index 5f18bdd9bea0..49e527fa3e7e 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; /** - * The number of alert documents in the es archive. + * The number of host documents in the es archive. */ -const numberOfEndpointsInFixture = 3; +const numberOfHostsInFixture = 3; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -24,7 +24,7 @@ export default function({ getService }: FtrProviderContext) { .send() .expect(200); expect(body.total).to.eql(0); - expect(body.endpoints.length).to.eql(0); + expect(body.hosts.length).to.eql(0); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); @@ -33,14 +33,14 @@ export default function({ getService }: FtrProviderContext) { describe('POST /api/endpoint/metadata when index is not empty', () => { before(() => esArchiver.load('endpoint/metadata/api_feature')); after(() => esArchiver.unload('endpoint/metadata/api_feature')); - it('metadata api should return one entry for each endpoint with default paging', async () => { + it('metadata api should return one entry for each host with default paging', async () => { const { body } = await supertest .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send() .expect(200); - expect(body.total).to.eql(numberOfEndpointsInFixture); - expect(body.endpoints.length).to.eql(numberOfEndpointsInFixture); + expect(body.total).to.eql(numberOfHostsInFixture); + expect(body.hosts.length).to.eql(numberOfHostsInFixture); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); @@ -60,8 +60,8 @@ export default function({ getService }: FtrProviderContext) { ], }) .expect(200); - expect(body.total).to.eql(numberOfEndpointsInFixture); - expect(body.endpoints.length).to.eql(1); + expect(body.total).to.eql(numberOfHostsInFixture); + expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(1); expect(body.request_page_index).to.eql(1); }); @@ -84,8 +84,8 @@ export default function({ getService }: FtrProviderContext) { ], }) .expect(200); - expect(body.total).to.eql(numberOfEndpointsInFixture); - expect(body.endpoints.length).to.eql(0); + expect(body.total).to.eql(numberOfHostsInFixture); + expect(body.hosts.length).to.eql(0); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(30); }); @@ -115,7 +115,7 @@ export default function({ getService }: FtrProviderContext) { .send({ filter: 'not host.ip:10.100.170.247' }) .expect(200); expect(body.total).to.eql(2); - expect(body.endpoints.length).to.eql(2); + expect(body.hosts.length).to.eql(2); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); @@ -139,7 +139,7 @@ export default function({ getService }: FtrProviderContext) { .expect(200); expect(body.total).to.eql(2); const resultIps: string[] = [].concat( - ...body.endpoints.map((metadata: Record) => metadata.host.ip) + ...body.hosts.map((metadata: Record) => metadata.host.ip) ); expect(resultIps).to.eql([ '10.48.181.222', @@ -150,7 +150,7 @@ export default function({ getService }: FtrProviderContext) { '10.128.235.38', ]); expect(resultIps).not.include.eql(notIncludedIp); - expect(body.endpoints.length).to.eql(2); + expect(body.hosts.length).to.eql(2); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); @@ -166,10 +166,10 @@ export default function({ getService }: FtrProviderContext) { .expect(200); expect(body.total).to.eql(1); const resultOsVariantValue: Set = new Set( - body.endpoints.map((metadata: Record) => metadata.host.os.variant) + body.hosts.map((metadata: Record) => metadata.host.os.variant) ); expect(Array.from(resultOsVariantValue)).to.eql([variantValue]); - expect(body.endpoints.length).to.eql(1); + expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); @@ -184,17 +184,17 @@ export default function({ getService }: FtrProviderContext) { }) .expect(200); expect(body.total).to.eql(1); - const resultIp: string = body.endpoints[0].host.ip.filter( + const resultIp: string = body.hosts[0].host.ip.filter( (ip: string) => ip === targetEndpointIp ); expect(resultIp).to.eql([targetEndpointIp]); - expect(body.endpoints[0].event.created).to.eql(1584044335459); - expect(body.endpoints.length).to.eql(1); + expect(body.hosts[0].event.created).to.eql(1584044335459); + expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); - it('metadata api should return all endpoints when filter is empty string', async () => { + it('metadata api should return all hosts when filter is empty string', async () => { const { body } = await supertest .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') @@ -202,8 +202,8 @@ export default function({ getService }: FtrProviderContext) { filter: '', }) .expect(200); - expect(body.total).to.eql(numberOfEndpointsInFixture); - expect(body.endpoints.length).to.eql(numberOfEndpointsInFixture); + expect(body.total).to.eql(numberOfHostsInFixture); + expect(body.hosts.length).to.eql(numberOfHostsInFixture); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts index 287892903dd2..bf3d642307d8 100644 --- a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts +++ b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts @@ -41,18 +41,13 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.existOrFail('welcomeTitle'); }); - it(`endpoint management shows 'Manage Endpoints'`, async () => { - await pageObjects.common.navigateToUrlWithBrowserHistory( - 'endpoint', - '/management', - undefined, - { - basePath: '/s/custom_space', - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); - await testSubjects.existOrFail('managementViewTitle'); + it(`endpoint management shows 'Hosts'`, async () => { + await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/hosts', undefined, { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('hostListTitle'); }); }); diff --git a/x-pack/test/functional/apps/endpoint/header_nav.ts b/x-pack/test/functional/apps/endpoint/header_nav.ts index 2368ad077cf6..d1fa7311d61e 100644 --- a/x-pack/test/functional/apps/endpoint/header_nav.ts +++ b/x-pack/test/functional/apps/endpoint/header_nav.ts @@ -19,19 +19,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders the tabs when the app loads', async () => { const homeTabText = await testSubjects.getVisibleText('homeEndpointTab'); - const managementTabText = await testSubjects.getVisibleText('managementEndpointTab'); + const hostsTabText = await testSubjects.getVisibleText('hostsEndpointTab'); const alertsTabText = await testSubjects.getVisibleText('alertsEndpointTab'); const policiesTabText = await testSubjects.getVisibleText('policiesEndpointTab'); expect(homeTabText.trim()).to.be('Home'); - expect(managementTabText.trim()).to.be('Management'); + expect(hostsTabText.trim()).to.be('Hosts'); expect(alertsTabText.trim()).to.be('Alerts'); expect(policiesTabText.trim()).to.be('Policies'); }); - it('renders the management page when the Management tab is selected', async () => { - await (await testSubjects.find('managementEndpointTab')).click(); - await testSubjects.existOrFail('managementViewTitle'); + it('renders the hosts page when the Hosts tab is selected', async () => { + await (await testSubjects.find('hostsEndpointTab')).click(); + await testSubjects.existOrFail('hostListTitle'); }); it('renders the alerts page when the Alerts tab is selected', async () => { @@ -45,8 +45,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the home page when Home tab is selected after selecting another tab', async () => { - await (await testSubjects.find('managementEndpointTab')).click(); - await testSubjects.existOrFail('managementViewTitle'); + await (await testSubjects.find('hostsEndpointTab')).click(); + await testSubjects.existOrFail('hostListTitle'); await (await testSubjects.find('homeEndpointTab')).click(); await testSubjects.existOrFail('welcomeTitle'); diff --git a/x-pack/test/functional/apps/endpoint/management.ts b/x-pack/test/functional/apps/endpoint/host_list.ts similarity index 58% rename from x-pack/test/functional/apps/endpoint/management.ts rename to x-pack/test/functional/apps/endpoint/host_list.ts index 640f6264c3a0..baace0f7670e 100644 --- a/x-pack/test/functional/apps/endpoint/management.ts +++ b/x-pack/test/functional/apps/endpoint/host_list.ts @@ -12,15 +12,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); - describe('Endpoint Management List', function() { + describe('host list', function() { this.tags('ciGroup7'); before(async () => { await esArchiver.load('endpoint/metadata/api_feature'); - await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/management'); + await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/hosts'); }); it('finds title', async () => { - const title = await testSubjects.getVisibleText('managementViewTitle'); + const title = await testSubjects.getVisibleText('hostListTitle'); expect(title).to.equal('Hosts'); }); @@ -67,21 +67,70 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'xxxx', ], ]; - const tableData = await pageObjects.endpoint.getEndpointAppTableData('managementListTable'); + const tableData = await pageObjects.endpoint.getEndpointAppTableData('hostListTable'); expect(tableData).to.eql(expectedData); }); - it('displays no items found', async () => { + it('display details flyout when the hostname is clicked on', async () => { + await (await testSubjects.find('hostnameCellLink')).click(); + await testSubjects.existOrFail('hostDetailsUpperList'); + await testSubjects.existOrFail('hostDetailsLowerList'); + }); + + it('displays no items found when empty', async () => { // clear out the data and reload the page await esArchiver.unload('endpoint/metadata/api_feature'); - await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/management'); + await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/hosts'); // get the table data and verify no entries appear - const tableData = await pageObjects.endpoint.getEndpointAppTableData('managementListTable'); + const tableData = await pageObjects.endpoint.getEndpointAppTableData('hostListTable'); expect(tableData[1][0]).to.equal('No items found'); // reload the data so the other tests continue to pass await esArchiver.load('endpoint/metadata/api_feature'); }); + describe('has a url with a host id', () => { + before(async () => { + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'endpoint', + '/hosts', + 'selected_host=cbe80003-6964-4e0f-aba1-f94c32b44e95' + ); + }); + + it('shows a flyout', async () => { + await testSubjects.existOrFail('hostDetailsFlyout'); + }); + + it('displays details row headers', async () => { + const expectedData = [ + 'OS', + 'Last Seen', + 'Alerts', + 'Policy', + 'Policy Status', + 'IP Address', + 'Hostname', + 'Sensor Version', + ]; + const keys = await pageObjects.endpoint.hostFlyoutDescriptionKeys('hostDetailsFlyout'); + expect(keys).to.eql(expectedData); + }); + + it('displays details row descriptions', async () => { + const values = await pageObjects.endpoint.hostFlyoutDescriptionValues('hostDetailsFlyout'); + + expect(values).to.eql([ + 'Windows Server 2012', + '', + '0', + 'C2A9093E-E289-4C0A-AA44-8C32A414FA7A', + 'active', + '10.48.181.22210.116.62.6210.102.83.30', + 'Host-cxz5glsoup', + '6.6.9', + ]); + }); + }); after(async () => { await esArchiver.unload('endpoint/metadata/api_feature'); }); diff --git a/x-pack/test/functional/apps/endpoint/index.ts b/x-pack/test/functional/apps/endpoint/index.ts index 15ce522ce56b..4d55b3af4956 100644 --- a/x-pack/test/functional/apps/endpoint/index.ts +++ b/x-pack/test/functional/apps/endpoint/index.ts @@ -12,7 +12,7 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./landing_page')); loadTestFile(require.resolve('./header_nav')); - loadTestFile(require.resolve('./management')); + loadTestFile(require.resolve('./host_list')); loadTestFile(require.resolve('./policy_list')); loadTestFile(require.resolve('./policy_details')); loadTestFile(require.resolve('./alerts')); diff --git a/x-pack/test/functional/page_objects/endpoint_page.ts b/x-pack/test/functional/page_objects/endpoint_page.ts index 6350f51f707f..4becbf797abc 100644 --- a/x-pack/test/functional/page_objects/endpoint_page.ts +++ b/x-pack/test/functional/page_objects/endpoint_page.ts @@ -63,9 +63,42 @@ export function EndpointPageProvider({ getService }: FtrProviderContext) { async waitForTableToHaveData(dataTestSubj: string) { await retry.waitForWithTimeout('table to have data', 2000, async () => { const tableData = await this.getEndpointAppTableData(dataTestSubj); - if (tableData[1][0] === 'No items found') return false; + if (tableData[1][0] === 'No items found') { + return false; + } return true; }); }, + + async hostFlyoutDescriptionKeys(dataTestSubj: string) { + await testSubjects.exists(dataTestSubj); + const detailsData: WebElementWrapper = await testSubjects.find(dataTestSubj); + const $ = await detailsData.parseDomContent(); + return $('dt') + .toArray() + .map(key => + $(key) + .text() + .replace(/ /g, '') + .trim() + ); + }, + + async hostFlyoutDescriptionValues(dataTestSubj: string) { + await testSubjects.exists(dataTestSubj); + const detailsData: WebElementWrapper = await testSubjects.find(dataTestSubj); + const $ = await detailsData.parseDomContent(); + return $('dd') + .toArray() + .map((value, index) => { + if (index === 1) { + return ''; + } + return $(value) + .text() + .replace(/ /g, '') + .trim(); + }); + }, }; } From fae93176e2a0d3d251d90a35bd80f2493bf7325e Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 18 Mar 2020 08:11:36 +0100 Subject: [PATCH 073/115] [Console] Fix for `_settings` and x-pack autocomplete (#60246) * Add settings completion to index create endpoint and clean up. The cleanup is largely for moving settings data completion to JS and removing the dynamic logic for loading different ES versions. This is unused and unnecessary at this point. * Add new settings JS files and move BOOLEAN to shared file. * Important fix for loading x-pack console extensions. After migrating the x-pack console extensions were being loaded too late and were not being served to the client. * Reorder imports to convention --- src/plugins/console/public/lib/kb/kb.js | 10 +- src/plugins/console/server/lib/index.ts | 2 +- .../lib/spec_definitions/{es_6_0.js => es.js} | 44 +++---- .../server/lib/spec_definitions/index.d.ts | 9 +- .../server/lib/spec_definitions/index.js | 6 +- .../{es_6_0 => js}/aggregations.js | 0 .../{es_6_0 => js}/aliases.js | 0 .../{es_6_0 => js}/document.js | 0 .../spec_definitions/{es_6_0 => js}/filter.js | 0 .../{es_6_0 => js}/globals.js | 0 .../spec_definitions/{es_6_0 => js}/ingest.js | 0 .../{es_6_0 => js}/mappings.js | 4 +- .../{es_6_0 => js}/query/dsl.js | 0 .../{es_6_0 => js}/query/index.js | 0 .../{es_6_0 => js}/query/templates.js | 0 .../{es_6_0 => js}/reindex.js | 0 .../spec_definitions/{es_6_0 => js}/search.js | 0 .../lib/spec_definitions/js/settings.js | 74 ++++++++++++ .../server/lib/spec_definitions/js/shared.js | 22 ++++ .../spec_definitions/{spec => json}/.eslintrc | 0 .../{spec => json}/generated/_common.json | 0 .../{spec => json}/generated/bulk.json | 0 .../{spec => json}/generated/cat.aliases.json | 0 .../generated/cat.allocation.json | 0 .../{spec => json}/generated/cat.count.json | 0 .../generated/cat.fielddata.json | 0 .../{spec => json}/generated/cat.health.json | 0 .../{spec => json}/generated/cat.help.json | 0 .../{spec => json}/generated/cat.indices.json | 0 .../{spec => json}/generated/cat.master.json | 0 .../generated/cat.nodeattrs.json | 0 .../{spec => json}/generated/cat.nodes.json | 0 .../generated/cat.pending_tasks.json | 0 .../{spec => json}/generated/cat.plugins.json | 0 .../generated/cat.recovery.json | 0 .../generated/cat.repositories.json | 0 .../generated/cat.segments.json | 0 .../{spec => json}/generated/cat.shards.json | 0 .../generated/cat.snapshots.json | 0 .../{spec => json}/generated/cat.tasks.json | 0 .../generated/cat.templates.json | 0 .../generated/cat.thread_pool.json | 0 .../generated/clear_scroll.json | 0 .../generated/cluster.allocation_explain.json | 0 .../generated/cluster.get_settings.json | 0 .../generated/cluster.health.json | 0 .../generated/cluster.pending_tasks.json | 0 .../generated/cluster.put_settings.json | 0 .../generated/cluster.remote_info.json | 0 .../generated/cluster.reroute.json | 0 .../generated/cluster.state.json | 0 .../generated/cluster.stats.json | 0 .../{spec => json}/generated/count.json | 0 .../{spec => json}/generated/create.json | 0 .../{spec => json}/generated/delete.json | 0 .../generated/delete_by_query.json | 0 .../generated/delete_by_query_rethrottle.json | 0 .../generated/delete_script.json | 0 .../{spec => json}/generated/exists.json | 0 .../generated/exists_source.json | 0 .../{spec => json}/generated/explain.json | 0 .../{spec => json}/generated/field_caps.json | 0 .../{spec => json}/generated/get.json | 0 .../{spec => json}/generated/get_script.json | 0 .../generated/get_script_context.json | 0 .../generated/get_script_languages.json | 0 .../{spec => json}/generated/get_source.json | 0 .../{spec => json}/generated/index.json | 0 .../generated/indices.analyze.json | 0 .../generated/indices.clear_cache.json | 0 .../generated/indices.clone.json | 0 .../generated/indices.close.json | 0 .../generated/indices.create.json | 0 .../generated/indices.delete.json | 0 .../generated/indices.delete_alias.json | 0 .../generated/indices.delete_template.json | 0 .../generated/indices.exists.json | 0 .../generated/indices.exists_alias.json | 0 .../generated/indices.exists_template.json | 0 .../generated/indices.exists_type.json | 0 .../generated/indices.flush.json | 0 .../generated/indices.flush_synced.json | 0 .../generated/indices.forcemerge.json | 0 .../{spec => json}/generated/indices.get.json | 0 .../generated/indices.get_alias.json | 0 .../generated/indices.get_field_mapping.json | 0 .../generated/indices.get_mapping.json | 0 .../generated/indices.get_settings.json | 0 .../generated/indices.get_template.json | 0 .../generated/indices.get_upgrade.json | 0 .../generated/indices.open.json | 0 .../generated/indices.put_alias.json | 0 .../generated/indices.put_mapping.json | 0 .../generated/indices.put_settings.json | 0 .../generated/indices.put_template.json | 0 .../generated/indices.recovery.json | 0 .../generated/indices.refresh.json | 0 .../generated/indices.rollover.json | 0 .../generated/indices.segments.json | 0 .../generated/indices.shard_stores.json | 0 .../generated/indices.shrink.json | 0 .../generated/indices.split.json | 0 .../generated/indices.stats.json | 0 .../generated/indices.update_aliases.json | 0 .../generated/indices.upgrade.json | 0 .../generated/indices.validate_query.json | 0 .../{spec => json}/generated/info.json | 0 .../generated/ingest.delete_pipeline.json | 0 .../generated/ingest.get_pipeline.json | 0 .../generated/ingest.processor_grok.json | 0 .../generated/ingest.put_pipeline.json | 0 .../generated/ingest.simulate.json | 0 .../{spec => json}/generated/mget.json | 0 .../{spec => json}/generated/msearch.json | 0 .../generated/msearch_template.json | 0 .../generated/mtermvectors.json | 0 .../generated/nodes.hot_threads.json | 0 .../{spec => json}/generated/nodes.info.json | 0 .../nodes.reload_secure_settings.json | 0 .../{spec => json}/generated/nodes.stats.json | 0 .../{spec => json}/generated/nodes.usage.json | 0 .../{spec => json}/generated/ping.json | 0 .../{spec => json}/generated/put_script.json | 0 .../{spec => json}/generated/rank_eval.json | 0 .../{spec => json}/generated/reindex.json | 0 .../generated/reindex_rethrottle.json | 0 .../generated/render_search_template.json | 0 .../generated/scripts_painless_execute.json | 0 .../{spec => json}/generated/scroll.json | 0 .../{spec => json}/generated/search.json | 0 .../generated/search_shards.json | 0 .../generated/search_template.json | 0 .../snapshot.cleanup_repository.json | 0 .../generated/snapshot.create.json | 0 .../generated/snapshot.create_repository.json | 0 .../generated/snapshot.delete.json | 0 .../generated/snapshot.delete_repository.json | 0 .../generated/snapshot.get.json | 0 .../generated/snapshot.get_repository.json | 0 .../generated/snapshot.restore.json | 0 .../generated/snapshot.status.json | 0 .../generated/snapshot.verify_repository.json | 0 .../generated/tasks.cancel.json | 0 .../{spec => json}/generated/tasks.get.json | 0 .../{spec => json}/generated/tasks.list.json | 0 .../{spec => json}/generated/termvectors.json | 0 .../{spec => json}/generated/update.json | 0 .../generated/update_by_query.json | 0 .../generated/update_by_query_rethrottle.json | 0 .../spec_definitions/{spec => json}/index.js | 0 .../overrides/clear_scroll.json | 0 .../overrides/cluster.health.json | 0 .../overrides/cluster.put_settings.json | 0 .../overrides/cluster.reroute.json | 0 .../{spec => json}/overrides/count.json | 0 .../overrides/indices.analyze.json | 0 .../overrides/indices.clone.json | 0 .../overrides/indices.create.json | 0 .../overrides/indices.delete_template.json | 0 .../overrides/indices.exists_template.json | 0 .../overrides/indices.get_field_mapping.json | 0 .../overrides/indices.get_mapping.json | 0 .../overrides/indices.get_template.json | 0 .../overrides/indices.put_alias.json | 0 .../json/overrides/indices.put_settings.json | 7 ++ .../overrides/indices.put_template.json | 0 .../overrides/indices.rollover.json | 0 .../overrides/indices.update_aliases.json | 0 .../overrides/indices.validate_query.json | 0 .../overrides/snapshot.create.json | 0 .../overrides/snapshot.create_repository.json | 0 .../overrides/snapshot.restore.json | 0 .../server/lib/spec_definitions/server.js | 21 +--- .../lib/spec_definitions/server.test.js | 51 --------- .../spec/overrides/indices.put_settings.json | 108 ------------------ src/plugins/console/server/plugin.ts | 11 +- .../api/console/spec_definitions/index.ts | 22 +--- .../generated/ml.estimate_memory_usage.json | 2 +- 178 files changed, 163 insertions(+), 230 deletions(-) rename src/plugins/console/server/lib/spec_definitions/{es_6_0.js => es.js} (54%) rename src/plugins/console/server/lib/spec_definitions/{es_6_0 => js}/aggregations.js (100%) rename src/plugins/console/server/lib/spec_definitions/{es_6_0 => js}/aliases.js (100%) rename src/plugins/console/server/lib/spec_definitions/{es_6_0 => js}/document.js (100%) rename src/plugins/console/server/lib/spec_definitions/{es_6_0 => js}/filter.js (100%) rename src/plugins/console/server/lib/spec_definitions/{es_6_0 => js}/globals.js (100%) rename src/plugins/console/server/lib/spec_definitions/{es_6_0 => js}/ingest.js (100%) rename src/plugins/console/server/lib/spec_definitions/{es_6_0 => js}/mappings.js (99%) rename src/plugins/console/server/lib/spec_definitions/{es_6_0 => js}/query/dsl.js (100%) rename src/plugins/console/server/lib/spec_definitions/{es_6_0 => js}/query/index.js (100%) rename src/plugins/console/server/lib/spec_definitions/{es_6_0 => js}/query/templates.js (100%) rename src/plugins/console/server/lib/spec_definitions/{es_6_0 => js}/reindex.js (100%) rename src/plugins/console/server/lib/spec_definitions/{es_6_0 => js}/search.js (100%) create mode 100644 src/plugins/console/server/lib/spec_definitions/js/settings.js create mode 100644 src/plugins/console/server/lib/spec_definitions/js/shared.js rename src/plugins/console/server/lib/spec_definitions/{spec => json}/.eslintrc (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/_common.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/bulk.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.aliases.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.allocation.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.count.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.fielddata.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.health.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.help.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.indices.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.master.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.nodeattrs.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.nodes.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.pending_tasks.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.plugins.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.recovery.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.repositories.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.segments.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.shards.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.snapshots.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.tasks.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.templates.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cat.thread_pool.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/clear_scroll.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cluster.allocation_explain.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cluster.get_settings.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cluster.health.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cluster.pending_tasks.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cluster.put_settings.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cluster.remote_info.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cluster.reroute.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cluster.state.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/cluster.stats.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/count.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/create.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/delete.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/delete_by_query.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/delete_by_query_rethrottle.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/delete_script.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/exists.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/exists_source.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/explain.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/field_caps.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/get.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/get_script.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/get_script_context.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/get_script_languages.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/get_source.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/index.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.analyze.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.clear_cache.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.clone.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.close.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.create.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.delete.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.delete_alias.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.delete_template.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.exists.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.exists_alias.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.exists_template.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.exists_type.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.flush.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.flush_synced.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.forcemerge.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.get.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.get_alias.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.get_field_mapping.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.get_mapping.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.get_settings.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.get_template.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.get_upgrade.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.open.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.put_alias.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.put_mapping.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.put_settings.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.put_template.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.recovery.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.refresh.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.rollover.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.segments.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.shard_stores.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.shrink.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.split.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.stats.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.update_aliases.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.upgrade.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/indices.validate_query.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/info.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/ingest.delete_pipeline.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/ingest.get_pipeline.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/ingest.processor_grok.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/ingest.put_pipeline.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/ingest.simulate.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/mget.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/msearch.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/msearch_template.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/mtermvectors.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/nodes.hot_threads.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/nodes.info.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/nodes.reload_secure_settings.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/nodes.stats.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/nodes.usage.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/ping.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/put_script.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/rank_eval.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/reindex.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/reindex_rethrottle.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/render_search_template.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/scripts_painless_execute.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/scroll.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/search.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/search_shards.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/search_template.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/snapshot.cleanup_repository.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/snapshot.create.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/snapshot.create_repository.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/snapshot.delete.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/snapshot.delete_repository.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/snapshot.get.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/snapshot.get_repository.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/snapshot.restore.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/snapshot.status.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/snapshot.verify_repository.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/tasks.cancel.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/tasks.get.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/tasks.list.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/termvectors.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/update.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/update_by_query.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/generated/update_by_query_rethrottle.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/index.js (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/clear_scroll.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/cluster.health.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/cluster.put_settings.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/cluster.reroute.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/count.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.analyze.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.clone.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.create.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.delete_template.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.exists_template.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.get_field_mapping.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.get_mapping.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.get_template.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.put_alias.json (100%) create mode 100644 src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_settings.json rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.put_template.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.rollover.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.update_aliases.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/indices.validate_query.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/snapshot.create.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/snapshot.create_repository.json (100%) rename src/plugins/console/server/lib/spec_definitions/{spec => json}/overrides/snapshot.restore.json (100%) delete mode 100644 src/plugins/console/server/lib/spec_definitions/server.test.js delete mode 100644 src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json diff --git a/src/plugins/console/public/lib/kb/kb.js b/src/plugins/console/public/lib/kb/kb.js index 95896bed0298..053b82bd81d0 100644 --- a/src/plugins/console/public/lib/kb/kb.js +++ b/src/plugins/console/public/lib/kb/kb.js @@ -147,13 +147,9 @@ function loadApisFromJson( } export function setActiveApi(api) { - if (_.isString(api)) { + if (!api) { $.ajax({ - url: - '../api/console/api_server?sense_version=' + - encodeURIComponent('@@SENSE_VERSION') + - '&apis=' + - encodeURIComponent(api), + url: '../api/console/api_server', dataType: 'json', // disable automatic guessing }).then( function(data) { @@ -169,7 +165,7 @@ export function setActiveApi(api) { ACTIVE_API = api; } -setActiveApi('es_6_0'); +setActiveApi(); export const _test = { loadApisFromJson: loadApisFromJson, diff --git a/src/plugins/console/server/lib/index.ts b/src/plugins/console/server/lib/index.ts index 98004768f880..2347084b73a6 100644 --- a/src/plugins/console/server/lib/index.ts +++ b/src/plugins/console/server/lib/index.ts @@ -22,4 +22,4 @@ export { ProxyConfigCollection } from './proxy_config_collection'; export { proxyRequest } from './proxy_request'; export { getElasticsearchProxyConfig } from './elasticsearch_proxy_config'; export { setHeaders } from './set_headers'; -export { addProcessorDefinition, addExtensionSpecFilePath } from './spec_definitions'; +export { addProcessorDefinition, addExtensionSpecFilePath, loadSpec } from './spec_definitions'; diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0.js b/src/plugins/console/server/lib/spec_definitions/es.js similarity index 54% rename from src/plugins/console/server/lib/spec_definitions/es_6_0.js rename to src/plugins/console/server/lib/spec_definitions/es.js index 171d23240795..fc24a64f8a6f 100644 --- a/src/plugins/console/server/lib/spec_definitions/es_6_0.js +++ b/src/plugins/console/server/lib/spec_definitions/es.js @@ -18,26 +18,30 @@ */ import Api from './api'; -import { getSpec } from './spec'; -import { register } from './es_6_0/ingest'; -const ES_6_0 = new Api('es_6_0'); -const spec = getSpec(); +import { getSpec } from './json'; +import { register } from './js/ingest'; +const ES = new Api('es'); -// adding generated specs -Object.keys(spec).forEach(endpoint => { - ES_6_0.addEndpointDescription(endpoint, spec[endpoint]); -}); +export const loadSpec = () => { + const spec = getSpec(); -//adding globals and custom API definitions -require('./es_6_0/aliases')(ES_6_0); -require('./es_6_0/aggregations')(ES_6_0); -require('./es_6_0/document')(ES_6_0); -require('./es_6_0/filter')(ES_6_0); -require('./es_6_0/globals')(ES_6_0); -register(ES_6_0); -require('./es_6_0/mappings')(ES_6_0); -require('./es_6_0/query')(ES_6_0); -require('./es_6_0/reindex')(ES_6_0); -require('./es_6_0/search')(ES_6_0); + // adding generated specs + Object.keys(spec).forEach(endpoint => { + ES.addEndpointDescription(endpoint, spec[endpoint]); + }); -export default ES_6_0; + // adding globals and custom API definitions + require('./js/aliases')(ES); + require('./js/aggregations')(ES); + require('./js/document')(ES); + require('./js/filter')(ES); + require('./js/globals')(ES); + register(ES); + require('./js/mappings')(ES); + require('./js/settings')(ES); + require('./js/query')(ES); + require('./js/reindex')(ES); + require('./js/search')(ES); +}; + +export default ES; diff --git a/src/plugins/console/server/lib/spec_definitions/index.d.ts b/src/plugins/console/server/lib/spec_definitions/index.d.ts index 0a79d3fb386f..da0125a186c1 100644 --- a/src/plugins/console/server/lib/spec_definitions/index.d.ts +++ b/src/plugins/console/server/lib/spec_definitions/index.d.ts @@ -19,6 +19,13 @@ export declare function addProcessorDefinition(...args: any[]): any; -export declare function resolveApi(senseVersion: string, apis: string[]): object; +export declare function resolveApi(): object; export declare function addExtensionSpecFilePath(...args: any[]): any; + +/** + * A function that synchronously reads files JSON from disk and builds + * the autocomplete structures served to the client. This must be called + * after any extensions have been loaded. + */ +export declare function loadSpec(): any; diff --git a/src/plugins/console/server/lib/spec_definitions/index.js b/src/plugins/console/server/lib/spec_definitions/index.js index 3fe1913d5a19..abf55639fbee 100644 --- a/src/plugins/console/server/lib/spec_definitions/index.js +++ b/src/plugins/console/server/lib/spec_definitions/index.js @@ -17,8 +17,10 @@ * under the License. */ -export { addProcessorDefinition } from './es_6_0/ingest'; +export { addProcessorDefinition } from './js/ingest'; -export { addExtensionSpecFilePath } from './spec'; +export { addExtensionSpecFilePath } from './json'; + +export { loadSpec } from './es'; export { resolveApi } from './server'; diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/aggregations.js b/src/plugins/console/server/lib/spec_definitions/js/aggregations.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/aggregations.js rename to src/plugins/console/server/lib/spec_definitions/js/aggregations.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/aliases.js b/src/plugins/console/server/lib/spec_definitions/js/aliases.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/aliases.js rename to src/plugins/console/server/lib/spec_definitions/js/aliases.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/document.js b/src/plugins/console/server/lib/spec_definitions/js/document.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/document.js rename to src/plugins/console/server/lib/spec_definitions/js/document.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/filter.js b/src/plugins/console/server/lib/spec_definitions/js/filter.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/filter.js rename to src/plugins/console/server/lib/spec_definitions/js/filter.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/globals.js b/src/plugins/console/server/lib/spec_definitions/js/globals.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/globals.js rename to src/plugins/console/server/lib/spec_definitions/js/globals.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/ingest.js b/src/plugins/console/server/lib/spec_definitions/js/ingest.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/ingest.js rename to src/plugins/console/server/lib/spec_definitions/js/ingest.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js b/src/plugins/console/server/lib/spec_definitions/js/mappings.js similarity index 99% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js rename to src/plugins/console/server/lib/spec_definitions/js/mappings.js index 8c31e5bc6fbb..5884d14d4dc8 100644 --- a/src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js +++ b/src/plugins/console/server/lib/spec_definitions/js/mappings.js @@ -19,9 +19,7 @@ const _ = require('lodash'); -const BOOLEAN = { - __one_of: [true, false], -}; +import { BOOLEAN } from './shared'; export default function(api) { api.addEndpointDescription('put_mapping', { diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js b/src/plugins/console/server/lib/spec_definitions/js/query/dsl.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js rename to src/plugins/console/server/lib/spec_definitions/js/query/dsl.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/index.js b/src/plugins/console/server/lib/spec_definitions/js/query/index.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/query/index.js rename to src/plugins/console/server/lib/spec_definitions/js/query/index.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/templates.js b/src/plugins/console/server/lib/spec_definitions/js/query/templates.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/query/templates.js rename to src/plugins/console/server/lib/spec_definitions/js/query/templates.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/reindex.js b/src/plugins/console/server/lib/spec_definitions/js/reindex.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/reindex.js rename to src/plugins/console/server/lib/spec_definitions/js/reindex.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/search.js b/src/plugins/console/server/lib/spec_definitions/js/search.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/search.js rename to src/plugins/console/server/lib/spec_definitions/js/search.js diff --git a/src/plugins/console/server/lib/spec_definitions/js/settings.js b/src/plugins/console/server/lib/spec_definitions/js/settings.js new file mode 100644 index 000000000000..26cd0987c34a --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/js/settings.js @@ -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 { BOOLEAN } from './shared'; + +export default function(api) { + api.addEndpointDescription('put_settings', { + data_autocomplete_rules: { + refresh_interval: '1s', + number_of_shards: 1, + number_of_replicas: 1, + 'blocks.read_only': BOOLEAN, + 'blocks.read': BOOLEAN, + 'blocks.write': BOOLEAN, + 'blocks.metadata': BOOLEAN, + term_index_interval: 32, + term_index_divisor: 1, + 'translog.flush_threshold_ops': 5000, + 'translog.flush_threshold_size': '200mb', + 'translog.flush_threshold_period': '30m', + 'translog.disable_flush': BOOLEAN, + 'cache.filter.max_size': '2gb', + 'cache.filter.expire': '2h', + 'gateway.snapshot_interval': '10s', + routing: { + allocation: { + include: { + tag: '', + }, + exclude: { + tag: '', + }, + require: { + tag: '', + }, + total_shards_per_node: -1, + }, + }, + 'recovery.initial_shards': { + __one_of: ['quorum', 'quorum-1', 'half', 'full', 'full-1'], + }, + 'ttl.disable_purge': BOOLEAN, + analysis: { + analyzer: {}, + tokenizer: {}, + filter: {}, + char_filter: {}, + }, + 'cache.query.enable': BOOLEAN, + shadow_replicas: BOOLEAN, + shared_filesystem: BOOLEAN, + data_path: 'path', + codec: { + __one_of: ['default', 'best_compression', 'lucene_default'], + }, + }, + }); +} diff --git a/src/plugins/console/server/lib/spec_definitions/js/shared.js b/src/plugins/console/server/lib/spec_definitions/js/shared.js new file mode 100644 index 000000000000..ace189e2d091 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/js/shared.js @@ -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. + */ + +export const BOOLEAN = Object.freeze({ + __one_of: [true, false], +}); diff --git a/src/plugins/console/server/lib/spec_definitions/spec/.eslintrc b/src/plugins/console/server/lib/spec_definitions/json/.eslintrc similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/.eslintrc rename to src/plugins/console/server/lib/spec_definitions/json/.eslintrc diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/_common.json b/src/plugins/console/server/lib/spec_definitions/json/generated/_common.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/_common.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/_common.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/bulk.json b/src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/bulk.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.aliases.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.aliases.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.aliases.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.aliases.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.allocation.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.allocation.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.allocation.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.allocation.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.count.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.count.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.count.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.count.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.fielddata.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.fielddata.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.fielddata.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.fielddata.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.health.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.health.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.health.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.health.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.help.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.help.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.help.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.help.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.indices.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.indices.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.master.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.master.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.master.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.master.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.nodeattrs.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodeattrs.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.nodeattrs.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodeattrs.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.nodes.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.nodes.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.pending_tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.pending_tasks.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.pending_tasks.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.pending_tasks.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.plugins.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.plugins.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.recovery.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.recovery.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.recovery.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.recovery.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.repositories.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.repositories.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.repositories.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.repositories.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.segments.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.segments.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.segments.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.segments.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.shards.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.shards.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.snapshots.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.snapshots.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.snapshots.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.snapshots.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.tasks.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.templates.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.templates.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.templates.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.templates.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.thread_pool.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.thread_pool.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/clear_scroll.json b/src/plugins/console/server/lib/spec_definitions/json/generated/clear_scroll.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/clear_scroll.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/clear_scroll.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.allocation_explain.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.allocation_explain.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.allocation_explain.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.allocation_explain.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.get_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.get_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.health.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.health.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.health.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.health.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.pending_tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.pending_tasks.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.pending_tasks.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.pending_tasks.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.put_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.remote_info.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.remote_info.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.remote_info.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.remote_info.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.reroute.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.reroute.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.reroute.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.reroute.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.state.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.state.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.state.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.state.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.stats.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.stats.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/count.json b/src/plugins/console/server/lib/spec_definitions/json/generated/count.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/count.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/count.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/create.json b/src/plugins/console/server/lib/spec_definitions/json/generated/create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/create.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/delete.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/delete.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/delete_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/delete_by_query.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/delete_by_query_rethrottle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query_rethrottle.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/delete_by_query_rethrottle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query_rethrottle.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/delete_script.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_script.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/delete_script.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/delete_script.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/exists.json b/src/plugins/console/server/lib/spec_definitions/json/generated/exists.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/exists.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/exists.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/exists_source.json b/src/plugins/console/server/lib/spec_definitions/json/generated/exists_source.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/exists_source.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/exists_source.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/explain.json b/src/plugins/console/server/lib/spec_definitions/json/generated/explain.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/explain.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/explain.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/field_caps.json b/src/plugins/console/server/lib/spec_definitions/json/generated/field_caps.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/field_caps.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/field_caps.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get_script.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get_script.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get_script.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_context.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_context.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_context.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get_script_context.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_languages.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_languages.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_languages.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get_script_languages.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get_source.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get_source.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/index.json b/src/plugins/console/server/lib/spec_definitions/json/generated/index.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/index.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/index.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.analyze.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.analyze.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.analyze.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.analyze.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.clear_cache.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.clear_cache.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.clear_cache.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.clear_cache.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.clone.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.clone.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.clone.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.clone.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.close.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.close.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.close.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.close.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.create.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.create.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_type.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_type.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_type.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_type.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.flush.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.flush.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.flush.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.flush.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.flush_synced.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.flush_synced.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.flush_synced.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.flush_synced.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.forcemerge.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.forcemerge.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.forcemerge.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.forcemerge.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_field_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_field_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_upgrade.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_upgrade.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_upgrade.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_upgrade.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.open.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.open.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.open.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.open.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.recovery.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.recovery.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.recovery.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.recovery.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.refresh.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.refresh.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.refresh.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.refresh.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.rollover.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.rollover.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.segments.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.segments.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.segments.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.segments.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.shard_stores.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shard_stores.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.shard_stores.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.shard_stores.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.shrink.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.shrink.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.split.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.split.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.update_aliases.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.update_aliases.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.update_aliases.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.update_aliases.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.upgrade.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.upgrade.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.upgrade.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.upgrade.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.validate_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.validate_query.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.validate_query.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.validate_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/info.json b/src/plugins/console/server/lib/spec_definitions/json/generated/info.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/info.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/info.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.delete_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.delete_pipeline.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.delete_pipeline.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.delete_pipeline.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.get_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.get_pipeline.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.processor_grok.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.processor_grok.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.processor_grok.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.processor_grok.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.put_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.put_pipeline.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.put_pipeline.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.put_pipeline.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.simulate.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.simulate.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.simulate.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.simulate.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/mget.json b/src/plugins/console/server/lib/spec_definitions/json/generated/mget.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/mget.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/mget.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/msearch.json b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/msearch.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/msearch_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/msearch_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/mtermvectors.json b/src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/mtermvectors.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.hot_threads.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.hot_threads.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.hot_threads.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.hot_threads.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.info.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.info.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.info.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.info.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.reload_secure_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.reload_secure_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.reload_secure_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.reload_secure_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.usage.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.usage.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.usage.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.usage.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/put_script.json b/src/plugins/console/server/lib/spec_definitions/json/generated/put_script.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/put_script.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/put_script.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/rank_eval.json b/src/plugins/console/server/lib/spec_definitions/json/generated/rank_eval.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/rank_eval.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/rank_eval.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/reindex.json b/src/plugins/console/server/lib/spec_definitions/json/generated/reindex.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/reindex.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/reindex.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/reindex_rethrottle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/reindex_rethrottle.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/reindex_rethrottle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/reindex_rethrottle.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/render_search_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/render_search_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/render_search_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/render_search_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/scripts_painless_execute.json b/src/plugins/console/server/lib/spec_definitions/json/generated/scripts_painless_execute.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/scripts_painless_execute.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/scripts_painless_execute.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/scroll.json b/src/plugins/console/server/lib/spec_definitions/json/generated/scroll.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/scroll.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/scroll.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/search.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/search.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/search.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/search_shards.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search_shards.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/search_shards.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/search_shards.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/search_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/search_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.cleanup_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.cleanup_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.create.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.create.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.create_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.create_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.delete.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.delete_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.delete_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.get_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.get_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.restore.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.restore.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.restore.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.restore.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.status.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.status.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.status.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.status.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.verify_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.verify_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.verify_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.verify_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.cancel.json b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.cancel.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.get.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/tasks.get.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.list.json b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.list.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.list.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/tasks.list.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/termvectors.json b/src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/termvectors.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/update.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/update.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/update.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/update_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/update_by_query.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/update_by_query_rethrottle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query_rethrottle.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/update_by_query_rethrottle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query_rethrottle.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/index.js b/src/plugins/console/server/lib/spec_definitions/json/index.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/index.js rename to src/plugins/console/server/lib/spec_definitions/json/index.js diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/clear_scroll.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/clear_scroll.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/clear_scroll.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/clear_scroll.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.health.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.health.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.health.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.health.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.put_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.reroute.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.reroute.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.reroute.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.reroute.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/count.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/count.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/count.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/count.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.analyze.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.analyze.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.analyze.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.analyze.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.clone.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.clone.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.clone.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.clone.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.create.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.create.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.delete_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.delete_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.delete_template.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.delete_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.exists_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.exists_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.exists_template.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.exists_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_field_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_field_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_field_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_field_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_template.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_alias.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_settings.json new file mode 100644 index 000000000000..2ae8fd82be4d --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_settings.json @@ -0,0 +1,7 @@ +{ + "indices.put_settings": { + "data_autocomplete_rules": { + "__scope_link": "put_settings" + } + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_template.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.rollover.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.rollover.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.rollover.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.rollover.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.update_aliases.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.update_aliases.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.update_aliases.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.update_aliases.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.validate_query.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.validate_query.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.validate_query.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.validate_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.create.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.create.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.create_repository.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.create_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.restore.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.restore.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.restore.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.restore.json diff --git a/src/plugins/console/server/lib/spec_definitions/server.js b/src/plugins/console/server/lib/spec_definitions/server.js index dd700bf01950..cb855958d403 100644 --- a/src/plugins/console/server/lib/spec_definitions/server.js +++ b/src/plugins/console/server/lib/spec_definitions/server.js @@ -17,21 +17,10 @@ * under the License. */ -import _ from 'lodash'; +import es from './es'; -const KNOWN_APIS = ['es_6_0']; - -export function resolveApi(senseVersion, apis) { - const result = {}; - _.each(apis, function(name) { - { - if (KNOWN_APIS.includes(name)) { - // for now we ignore sense_version. might add it in the api name later - const api = require('./' + name); // eslint-disable-line import/no-dynamic-require - result[name] = api.asJson(); - } - } - }); - - return result; +export function resolveApi() { + return { + es: es.asJson(), + }; } diff --git a/src/plugins/console/server/lib/spec_definitions/server.test.js b/src/plugins/console/server/lib/spec_definitions/server.test.js deleted file mode 100644 index 747689237c17..000000000000 --- a/src/plugins/console/server/lib/spec_definitions/server.test.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { resolveApi } from './server'; - -describe('resolveApi', () => { - it('allows known APIs to be resolved', () => { - const mockReply = jest.fn(result => ({ type: () => result })); - const result = resolveApi('Sense Version', ['es_6_0'], { response: mockReply }); - expect(result).toMatchObject({ - es_6_0: { - endpoints: expect.any(Object), - globals: expect.any(Object), - name: expect.any(String), - }, - }); - }); - - it('does not resolve APIs that are not known', () => { - const mockReply = jest.fn(result => ({ type: () => result })); - const result = resolveApi('Sense Version', ['unknown'], { response: mockReply }); - expect(result).toEqual({}); - }); - - it('handles request for apis that are known and unknown', () => { - const mockReply = jest.fn(result => ({ type: () => result })); - const result = resolveApi('Sense Version', ['es_6_0'], { response: mockReply }); - expect(result).toMatchObject({ - es_6_0: { - endpoints: expect.any(Object), - globals: expect.any(Object), - name: expect.any(String), - }, - }); - }); -}); diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json b/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json deleted file mode 100644 index 2e1e3024665a..000000000000 --- a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "indices.put_settings": { - "data_autocomplete_rules": { - "refresh_interval": "1s", - "number_of_shards": 1, - "number_of_replicas": 1, - "blocks.read_only": { - "__one_of": [ - false, - true - ] - }, - "blocks.read": { - "__one_of": [ - true, - false - ] - }, - "blocks.write": { - "__one_of": [ - true, - false - ] - }, - "blocks.metadata": { - "__one_of": [ - true, - false - ] - }, - "term_index_interval": 32, - "term_index_divisor": 1, - "translog.flush_threshold_ops": 5000, - "translog.flush_threshold_size": "200mb", - "translog.flush_threshold_period": "30m", - "translog.disable_flush": { - "__one_of": [ - true, - false - ] - }, - "cache.filter.max_size": "2gb", - "cache.filter.expire": "2h", - "gateway.snapshot_interval": "10s", - "routing": { - "allocation": { - "include": { - "tag": "" - }, - "exclude": { - "tag": "" - }, - "require": { - "tag": "" - }, - "total_shards_per_node": -1 - } - }, - "recovery.initial_shards": { - "__one_of": [ - "quorum", - "quorum-1", - "half", - "full", - "full-1" - ] - }, - "ttl.disable_purge": { - "__one_of": [ - true, - false - ] - }, - "analysis": { - "analyzer": {}, - "tokenizer": {}, - "filter": {}, - "char_filter": {} - }, - "cache.query.enable": { - "__one_of": [ - true, - false - ] - }, - "shadow_replicas": { - "__one_of": [ - true, - false - ] - }, - "shared_filesystem": { - "__one_of": [ - true, - false - ] - }, - "data_path": "path", - "codec": { - "__one_of": [ - "default", - "best_compression", - "lucene_default" - ] - } - } - } -} diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index 65647bd5acb7..1954918f4d74 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -21,7 +21,12 @@ import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/serv import { readLegacyEsConfig } from '../../../legacy/core_plugins/console_legacy'; -import { ProxyConfigCollection, addExtensionSpecFilePath, addProcessorDefinition } from './lib'; +import { + ProxyConfigCollection, + addExtensionSpecFilePath, + addProcessorDefinition, + loadSpec, +} from './lib'; import { ConfigType } from './config'; import { registerProxyRoute } from './routes/api/console/proxy'; import { registerSpecDefinitionsRoute } from './routes/api/console/spec_definitions'; @@ -75,5 +80,7 @@ export class ConsoleServerPlugin implements Plugin { }; } - start() {} + start() { + loadSpec(); + } } diff --git a/src/plugins/console/server/routes/api/console/spec_definitions/index.ts b/src/plugins/console/server/routes/api/console/spec_definitions/index.ts index e2ece37f407a..88bc250bbfce 100644 --- a/src/plugins/console/server/routes/api/console/spec_definitions/index.ts +++ b/src/plugins/console/server/routes/api/console/spec_definitions/index.ts @@ -16,33 +16,19 @@ * specific language governing permissions and limitations * under the License. */ -import { schema, TypeOf } from '@kbn/config-schema'; import { IRouter, RequestHandler } from 'kibana/server'; import { resolveApi } from '../../../../lib/spec_definitions'; export const registerSpecDefinitionsRoute = ({ router }: { router: IRouter }) => { - const handler: RequestHandler> = async ( - ctx, - request, - response - ) => { - const { sense_version: version, apis } = request.query; - + const handler: RequestHandler = async (ctx, request, response) => { return response.ok({ - body: resolveApi(version, apis.split(',')), + body: resolveApi(), headers: { 'Content-Type': 'application/json', }, }); }; - const validate = { - query: schema.object({ - sense_version: schema.string({ defaultValue: '' }), - apis: schema.string(), - }), - }; - - router.get({ path: '/api/console/api_server', validate }, handler); - router.post({ path: '/api/console/api_server', validate }, handler); + router.get({ path: '/api/console/api_server', validate: false }, handler); + router.post({ path: '/api/console/api_server', validate: false }, handler); }; diff --git a/x-pack/plugins/console_extensions/server/spec/generated/ml.estimate_memory_usage.json b/x-pack/plugins/console_extensions/server/spec/generated/ml.estimate_memory_usage.json index a6ec31465392..2195b74640c7 100644 --- a/x-pack/plugins/console_extensions/server/spec/generated/ml.estimate_memory_usage.json +++ b/x-pack/plugins/console_extensions/server/spec/generated/ml.estimate_memory_usage.json @@ -1,7 +1,7 @@ { "ml.estimate_memory_usage": { "methods": [ - "POST" + "PUT" ], "patterns": [ "_ml/data_frame/analytics/_estimate_memory_usage" From 2fbf38b57ade3585cf092359e51208178a72a461 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Wed, 18 Mar 2020 10:25:30 +0300 Subject: [PATCH 074/115] [NP] Use local helper shortenDottedString for discover (#60271) * Move shortenDottedString into kibana_utils * Move helper back to data utils * Use local helper for discover * Clean up --- .../kibana/public/discover/kibana_services.ts | 2 -- .../angular/directives/field_name/field_name.tsx | 2 +- .../components/table_header/helpers.tsx | 3 ++- .../discover/np_ready/helpers/index.ts} | 16 +--------------- .../np_ready/helpers/shorten_dotted_string.ts} | 7 +------ 5 files changed, 5 insertions(+), 25 deletions(-) rename src/legacy/core_plugins/kibana/{common/utils/__tests__/shorten_dotted_string.js => public/discover/np_ready/helpers/index.ts} (60%) rename src/legacy/core_plugins/kibana/{common/utils/shorten_dotted_string.js => public/discover/np_ready/helpers/shorten_dotted_string.ts} (81%) diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 8202ba13b30c..5f3dbb65fd8f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -53,8 +53,6 @@ export { wrapInI18nContext } from 'ui/i18n'; import { search } from '../../../../../plugins/data/public'; export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; // @ts-ignore -export { shortenDottedString } from '../../common/utils/shorten_dotted_string'; -// @ts-ignore export { intervalOptions } from 'ui/agg_types'; export { subscribeWithScope } from '../../../../../plugins/kibana_legacy/public'; // @ts-ignore diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx index 26d8a5abb247..1b3b16332fa4 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx @@ -21,7 +21,7 @@ import classNames from 'classnames'; import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { FieldIcon, FieldIconProps } from '../../../../../../../../../plugins/kibana_react/public'; -import { shortenDottedString } from '../../../../kibana_services'; +import { shortenDottedString } from '../../../helpers'; import { getFieldTypeName } from './field_type_name'; // property field is provided at discover's field chooser diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx index a2ad18d59d93..bd48b1e08387 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { IndexPattern, shortenDottedString } from '../../../../../kibana_services'; +import { IndexPattern } from '../../../../../kibana_services'; +import { shortenDottedString } from '../../../../helpers'; export type SortOrder = [string, string]; export interface ColumnProps { diff --git a/src/legacy/core_plugins/kibana/common/utils/__tests__/shorten_dotted_string.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/index.ts similarity index 60% rename from src/legacy/core_plugins/kibana/common/utils/__tests__/shorten_dotted_string.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/index.ts index 267ca74c7c42..7196c96989e9 100644 --- a/src/legacy/core_plugins/kibana/common/utils/__tests__/shorten_dotted_string.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/index.ts @@ -17,18 +17,4 @@ * under the License. */ -import expect from '@kbn/expect'; -import { shortenDottedString } from '../shorten_dotted_string'; - -describe('shortenDottedString', () => { - it('Convert a dot.notated.string into a short string', () => { - expect(shortenDottedString('dot.notated.string')).to.equal('d.n.string'); - }); - - it('Ignores non-string values', () => { - expect(shortenDottedString(true)).to.equal(true); - expect(shortenDottedString(123)).to.equal(123); - const obj = { key: 'val' }; - expect(shortenDottedString(obj)).to.equal(obj); - }); -}); +export { shortenDottedString } from './shorten_dotted_string'; diff --git a/src/legacy/core_plugins/kibana/common/utils/shorten_dotted_string.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/shorten_dotted_string.ts similarity index 81% rename from src/legacy/core_plugins/kibana/common/utils/shorten_dotted_string.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/shorten_dotted_string.ts index ca76a2a53774..9d78a9678433 100644 --- a/src/legacy/core_plugins/kibana/common/utils/shorten_dotted_string.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/shorten_dotted_string.ts @@ -22,10 +22,5 @@ const DOT_PREFIX_RE = /(.).+?\./g; /** * Convert a dot.notated.string into a short * version (d.n.string) - * - * @param {string} str - the long string to convert - * @return {string} */ -export function shortenDottedString(input) { - return typeof input !== 'string' ? input : input.replace(DOT_PREFIX_RE, '$1.'); -} +export const shortenDottedString = (input: string) => input.replace(DOT_PREFIX_RE, '$1.'); From fd16c461289700eca91e0929851a5051f22c4523 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 18 Mar 2020 08:33:53 +0000 Subject: [PATCH 075/115] [ML] Re-enabling file upload telemetry (#60418) * [ML] Re-enabling file upload telemetry * small refactor * removing exported function * removing commented out code * removing commented out include * cleaning up types --- x-pack/plugins/ml/mappings.json | 13 -- x-pack/plugins/ml/public/plugin.ts | 3 + .../ml/server/lib/ml_telemetry/index.ts | 15 -- .../ml_telemetry/make_ml_usage_collector.ts | 41 ------ .../lib/ml_telemetry/ml_telemetry.test.ts | 128 ------------------ .../server/lib/ml_telemetry/ml_telemetry.ts | 72 ---------- .../plugins/ml/server/lib/telemetry/index.ts | 8 ++ .../lib/telemetry/internal_repository.ts | 15 ++ .../ml/server/lib/telemetry/mappings.ts | 25 ++++ .../lib/telemetry/ml_usage_collector.ts | 32 +++++ .../ml/server/lib/telemetry/telemetry.test.ts | 49 +++++++ .../ml/server/lib/telemetry/telemetry.ts | 81 +++++++++++ x-pack/plugins/ml/server/plugin.ts | 6 +- .../ml/server/routes/file_data_visualizer.ts | 4 +- 14 files changed, 217 insertions(+), 275 deletions(-) delete mode 100644 x-pack/plugins/ml/mappings.json delete mode 100644 x-pack/plugins/ml/server/lib/ml_telemetry/index.ts delete mode 100644 x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts delete mode 100644 x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts delete mode 100644 x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts create mode 100644 x-pack/plugins/ml/server/lib/telemetry/index.ts create mode 100644 x-pack/plugins/ml/server/lib/telemetry/internal_repository.ts create mode 100644 x-pack/plugins/ml/server/lib/telemetry/mappings.ts create mode 100644 x-pack/plugins/ml/server/lib/telemetry/ml_usage_collector.ts create mode 100644 x-pack/plugins/ml/server/lib/telemetry/telemetry.test.ts create mode 100644 x-pack/plugins/ml/server/lib/telemetry/telemetry.ts diff --git a/x-pack/plugins/ml/mappings.json b/x-pack/plugins/ml/mappings.json deleted file mode 100644 index 041b85dbea4a..000000000000 --- a/x-pack/plugins/ml/mappings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type" : "long" - } - } - } - } - } -} diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 624e877bda49..79aebece85af 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { Plugin, CoreStart, CoreSetup, AppMountParameters } from 'kibana/public'; import { ManagementSetup } from 'src/plugins/management/public'; import { SharePluginStart } from 'src/plugins/share/public'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { SecurityPluginSetup } from '../../security/public'; @@ -24,6 +25,7 @@ export interface MlSetupDependencies { security: SecurityPluginSetup; licensing: LicensingPluginSetup; management: ManagementSetup; + usageCollection: UsageCollectionSetup; } export class MlPlugin implements Plugin { @@ -47,6 +49,7 @@ export class MlPlugin implements Plugin { security: pluginsSetup.security, licensing: pluginsSetup.licensing, management: pluginsSetup.management, + usageCollection: pluginsSetup.usageCollection, }, { element: params.element, diff --git a/x-pack/plugins/ml/server/lib/ml_telemetry/index.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/index.ts deleted file mode 100644 index dffd95f50e0d..000000000000 --- a/x-pack/plugins/ml/server/lib/ml_telemetry/index.ts +++ /dev/null @@ -1,15 +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. - */ - -export { - createMlTelemetry, - incrementFileDataVisualizerIndexCreationCount, - storeMlTelemetry, - MlTelemetry, - MlTelemetrySavedObject, - ML_TELEMETRY_DOC_ID, -} from './ml_telemetry'; -export { makeMlUsageCollector } from './make_ml_usage_collector'; diff --git a/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts deleted file mode 100644 index 15a430a08eac..000000000000 --- a/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts +++ /dev/null @@ -1,41 +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 { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { SavedObjectsServiceStart } from 'kibana/server'; -import { - createMlTelemetry, - ML_TELEMETRY_DOC_ID, - MlTelemetry, - MlTelemetrySavedObject, -} from './ml_telemetry'; - -export function makeMlUsageCollector( - usageCollection: UsageCollectionSetup | undefined, - savedObjects: SavedObjectsServiceStart -): void { - if (!usageCollection) { - return; - } - - const mlUsageCollector = usageCollection.makeUsageCollector({ - type: 'ml', - isReady: () => true, - fetch: async (): Promise => { - try { - const mlTelemetrySavedObject: MlTelemetrySavedObject = await savedObjects - .createInternalRepository() - .get('ml-telemetry', ML_TELEMETRY_DOC_ID); - - return mlTelemetrySavedObject.attributes; - } catch (err) { - return createMlTelemetry(); - } - }, - }); - - usageCollection.registerCollector(mlUsageCollector); -} diff --git a/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts deleted file mode 100644 index cda160877f7a..000000000000 --- a/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts +++ /dev/null @@ -1,128 +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 { -// createMlTelemetry, -// incrementFileDataVisualizerIndexCreationCount, -// ML_TELEMETRY_DOC_ID, -// MlTelemetry, -// storeMlTelemetry, -// } from './ml_telemetry'; - -describe('ml_telemetry', () => { - describe('createMlTelemetry', () => { - it('should create a MlTelemetry object', () => { - // const mlTelemetry = createMlTelemetry(1); - // expect(mlTelemetry.file_data_visualizer.index_creation_count).toBe(1); - }); - it('should ignore undefined or unknown values', () => { - // const mlTelemetry = createMlTelemetry(undefined); - // expect(mlTelemetry.file_data_visualizer.index_creation_count).toBe(0); - }); - }); - - describe('storeMlTelemetry', () => { - // let mlTelemetry: MlTelemetry; - // let internalRepository: any; - - // beforeEach(() => { - // internalRepository = { create: jest.fn(), get: jest.fn() }; - // mlTelemetry = { - // file_data_visualizer: { - // index_creation_count: 1, - // }, - // }; - // }); - - it('should call internalRepository create with the given MlTelemetry object', () => { - // storeMlTelemetry(internalRepository, mlTelemetry); - // expect(internalRepository.create.mock.calls[0][1]).toBe(mlTelemetry); - }); - - it('should call internalRepository create with the ml-telemetry document type and ID', () => { - // storeMlTelemetry(internalRepository, mlTelemetry); - // expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry'); - // expect(internalRepository.create.mock.calls[0][2].id).toBe(ML_TELEMETRY_DOC_ID); - }); - - it('should call internalRepository create with overwrite: true', () => { - // storeMlTelemetry(internalRepository, mlTelemetry); - // expect(internalRepository.create.mock.calls[0][2].overwrite).toBe(true); - }); - }); - - describe('incrementFileDataVisualizerIndexCreationCount', () => { - // let savedObjectsClient: any; - - // function createSavedObjectsClientInstance( - // telemetryEnabled?: boolean, - // indexCreationCount?: number - // ) { - // return { - // create: jest.fn(), - // get: jest.fn(obj => { - // switch (obj) { - // case 'telemetry': - // if (telemetryEnabled === undefined) { - // throw Error; - // } - // return { - // attributes: { - // enabled: telemetryEnabled, - // }, - // }; - // case 'ml-telemetry': - // // emulate that a non-existing saved object will throw an error - // if (indexCreationCount === undefined) { - // throw Error; - // } - // return { - // attributes: { - // file_data_visualizer: { - // index_creation_count: indexCreationCount, - // }, - // }, - // }; - // } - // }), - // }; - // } - - // function mockInit(telemetryEnabled?: boolean, indexCreationCount?: number): void { - // savedObjectsClient = createSavedObjectsClientInstance(telemetryEnabled, indexCreationCount); - // } - - it('should not increment if telemetry status cannot be determined', async () => { - // mockInit(); - // await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - // expect(savedObjectsClient.create.mock.calls).toHaveLength(0); - }); - - it('should not increment if telemetry status is disabled', async () => { - // mockInit(false); - // await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - // expect(savedObjectsClient.create.mock.calls).toHaveLength(0); - }); - - it('should initialize index_creation_count with 1', async () => { - // mockInit(true); - // await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - // expect(savedObjectsClient.create.mock.calls[0][0]).toBe('ml-telemetry'); - // expect(savedObjectsClient.create.mock.calls[0][1]).toEqual({ - // file_data_visualizer: { index_creation_count: 1 }, - // }); - }); - - it('should increment index_creation_count to 2', async () => { - // mockInit(true, 1); - // await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - // expect(savedObjectsClient.create.mock.calls[0][0]).toBe('ml-telemetry'); - // expect(savedObjectsClient.create.mock.calls[0][1]).toEqual({ - // file_data_visualizer: { index_creation_count: 2 }, - // }); - }); - }); -}); diff --git a/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts deleted file mode 100644 index 1ca155582db1..000000000000 --- a/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts +++ /dev/null @@ -1,72 +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 { SavedObjectAttributes, SavedObjectsClientContract } from 'kibana/server'; - -export interface MlTelemetry extends SavedObjectAttributes { - file_data_visualizer: { - index_creation_count: number; - }; -} - -export interface MlTelemetrySavedObject { - attributes: MlTelemetry; -} - -export const ML_TELEMETRY_DOC_ID = 'ml-telemetry'; - -export function createMlTelemetry(count: number = 0): MlTelemetry { - return { - file_data_visualizer: { - index_creation_count: count, - }, - }; -} -// savedObjects -export function storeMlTelemetry( - savedObjectsClient: SavedObjectsClientContract, - mlTelemetry: MlTelemetry -): void { - savedObjectsClient.create('ml-telemetry', mlTelemetry, { - id: ML_TELEMETRY_DOC_ID, - overwrite: true, - }); -} - -export async function incrementFileDataVisualizerIndexCreationCount( - savedObjectsClient: SavedObjectsClientContract -): Promise { - return; - try { - const { attributes } = await savedObjectsClient.get<{ enabled: boolean }>( - 'telemetry', - 'telemetry' - ); - - if (attributes.enabled === false) { - return; - } - } catch (error) { - // if we aren't allowed to get the telemetry document, - // we assume we couldn't opt in to telemetry and won't increment the index count. - return; - } - - let indicesCount = 1; - - try { - const { attributes } = (await savedObjectsClient.get( - 'ml-telemetry', - ML_TELEMETRY_DOC_ID - )) as MlTelemetrySavedObject; - indicesCount = attributes.file_data_visualizer.index_creation_count + 1; - } catch (e) { - /* silently fail, this will happen if the saved object doesn't exist yet. */ - } - - const mlTelemetry = createMlTelemetry(indicesCount); - storeMlTelemetry(savedObjectsClient, mlTelemetry); -} diff --git a/x-pack/plugins/ml/server/lib/telemetry/index.ts b/x-pack/plugins/ml/server/lib/telemetry/index.ts new file mode 100644 index 000000000000..b5ec80daf178 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/telemetry/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 { initMlTelemetry } from './ml_usage_collector'; +export { updateTelemetry } from './telemetry'; diff --git a/x-pack/plugins/ml/server/lib/telemetry/internal_repository.ts b/x-pack/plugins/ml/server/lib/telemetry/internal_repository.ts new file mode 100644 index 000000000000..a273ea4baadf --- /dev/null +++ b/x-pack/plugins/ml/server/lib/telemetry/internal_repository.ts @@ -0,0 +1,15 @@ +/* + * 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 { SavedObjectsServiceStart, ISavedObjectsRepository } from 'kibana/server'; + +let internalRepository: ISavedObjectsRepository | null = null; +export const setInternalRepository = ( + createInternalRepository: SavedObjectsServiceStart['createInternalRepository'] +) => { + internalRepository = createInternalRepository(); +}; +export const getInternalRepository = () => internalRepository; diff --git a/x-pack/plugins/ml/server/lib/telemetry/mappings.ts b/x-pack/plugins/ml/server/lib/telemetry/mappings.ts new file mode 100644 index 000000000000..87e224332842 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/telemetry/mappings.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 { SavedObjectsType } from 'src/core/server'; +import { TELEMETRY_DOC_ID } from './telemetry'; + +export const mlTelemetryMappingsType: SavedObjectsType = { + name: TELEMETRY_DOC_ID, + hidden: false, + namespaceAgnostic: true, + mappings: { + properties: { + file_data_visualizer: { + properties: { + index_creation_count: { + type: 'long', + }, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/ml/server/lib/telemetry/ml_usage_collector.ts b/x-pack/plugins/ml/server/lib/telemetry/ml_usage_collector.ts new file mode 100644 index 000000000000..21e5dce8e470 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/telemetry/ml_usage_collector.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'kibana/server'; + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { getTelemetry, initTelemetry } from './telemetry'; +import { mlTelemetryMappingsType } from './mappings'; +import { setInternalRepository } from './internal_repository'; + +const TELEMETRY_TYPE = 'mlTelemetry'; + +export function initMlTelemetry(coreSetup: CoreSetup, usageCollection: UsageCollectionSetup) { + coreSetup.savedObjects.registerType(mlTelemetryMappingsType); + registerMlUsageCollector(usageCollection); + coreSetup.getStartServices().then(([core]) => { + setInternalRepository(core.savedObjects.createInternalRepository); + }); +} + +function registerMlUsageCollector(usageCollection: UsageCollectionSetup): void { + const mlUsageCollector = usageCollection.makeUsageCollector({ + type: TELEMETRY_TYPE, + isReady: () => true, + fetch: async () => (await getTelemetry()) || initTelemetry(), + }); + + usageCollection.registerCollector(mlUsageCollector); +} diff --git a/x-pack/plugins/ml/server/lib/telemetry/telemetry.test.ts b/x-pack/plugins/ml/server/lib/telemetry/telemetry.test.ts new file mode 100644 index 000000000000..f41c4fda93a5 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/telemetry/telemetry.test.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. + */ + +import { getTelemetry, updateTelemetry } from './telemetry'; + +const internalRepository = () => ({ + get: jest.fn(() => null), + create: jest.fn(() => ({ attributes: 'test' })), + update: jest.fn(() => ({ attributes: 'test' })), +}); + +function mockInit(getVal: any = { attributes: {} }): any { + return { + ...internalRepository(), + get: jest.fn(() => getVal), + }; +} + +describe('ml plugin telemetry', () => { + describe('getTelemetry', () => { + it('should get existing telemetry', async () => { + const internalRepo = mockInit(); + await getTelemetry(internalRepo); + expect(internalRepo.update.mock.calls.length).toBe(0); + expect(internalRepo.get.mock.calls.length).toBe(1); + expect(internalRepo.create.mock.calls.length).toBe(0); + }); + }); + + describe('updateTelemetry', () => { + it('should update existing telemetry', async () => { + const internalRepo = mockInit({ + attributes: { + file_data_visualizer: { + index_creation_count: 2, + }, + }, + }); + + await updateTelemetry(internalRepo); + expect(internalRepo.update.mock.calls.length).toBe(1); + expect(internalRepo.get.mock.calls.length).toBe(1); + expect(internalRepo.create.mock.calls.length).toBe(0); + }); + }); +}); diff --git a/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts b/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts new file mode 100644 index 000000000000..bc56e8b2a437 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { ISavedObjectsRepository } from 'kibana/server'; + +import { getInternalRepository } from './internal_repository'; + +export const TELEMETRY_DOC_ID = 'ml-telemetry'; + +interface Telemetry { + file_data_visualizer: { + index_creation_count: number; + }; +} + +export interface TelemetrySavedObject { + attributes: Telemetry; +} + +export function initTelemetry(): Telemetry { + return { + file_data_visualizer: { + index_creation_count: 0, + }, + }; +} + +export async function getTelemetry( + internalRepository?: ISavedObjectsRepository +): Promise { + if (internalRepository === undefined) { + return null; + } + + let telemetrySavedObject; + + try { + telemetrySavedObject = await internalRepository.get( + TELEMETRY_DOC_ID, + TELEMETRY_DOC_ID + ); + } catch (e) { + // Fail silently + } + + return telemetrySavedObject ? telemetrySavedObject.attributes : null; +} + +export async function updateTelemetry(internalRepo?: ISavedObjectsRepository) { + const internalRepository = internalRepo || getInternalRepository(); + if (internalRepository === null) { + return; + } + + let telemetry = await getTelemetry(internalRepository); + // Create if doesn't exist + if (telemetry === null || _.isEmpty(telemetry)) { + const newTelemetrySavedObject = await internalRepository.create( + TELEMETRY_DOC_ID, + initTelemetry(), + { id: TELEMETRY_DOC_ID } + ); + telemetry = newTelemetrySavedObject.attributes; + } + + if (telemetry !== null) { + await internalRepository.update(TELEMETRY_DOC_ID, TELEMETRY_DOC_ID, incrementCounts(telemetry)); + } +} + +function incrementCounts(telemetry: Telemetry) { + return { + file_data_visualizer: { + index_creation_count: telemetry.file_data_visualizer.index_creation_count + 1, + }, + }; +} diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 01d0bcc86701..8948d232b9e5 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -16,7 +16,7 @@ import { PluginsSetup, RouteInitialization } from './types'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; import { elasticsearchJsPlugin } from './client/elasticsearch_ml'; -import { makeMlUsageCollector } from './lib/ml_telemetry'; +import { initMlTelemetry } from './lib/telemetry'; import { initMlServerLog } from './client/log'; import { initSampleDataSets } from './lib/sample_data_sets'; @@ -130,9 +130,7 @@ export class MlServerPlugin implements Plugin { - makeMlUsageCollector(plugins.usageCollection, core.savedObjects); - }); + initMlTelemetry(coreSetup, plugins.usageCollection); return createSharedServices(this.mlLicense, plugins.spaces, plugins.cloud); } diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index a14d51ae61b0..fcfd6e121c9f 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -19,7 +19,7 @@ import { } from '../models/file_data_visualizer'; import { RouteInitialization } from '../types'; -import { incrementFileDataVisualizerIndexCreationCount } from '../lib/ml_telemetry'; +import { updateTelemetry } from '../lib/telemetry'; function analyzeFiles(context: RequestHandlerContext, data: InputData, overrides: InputOverrides) { const { analyzeFile } = fileDataVisualizerProvider(context.ml!.mlClient.callAsCurrentUser); @@ -132,7 +132,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat // follow-up import calls to just add additional data will include the `id` of the created // index, we'll ignore those and don't increment the counter. if (id === undefined) { - await incrementFileDataVisualizerIndexCreationCount(context.core.savedObjects.client); + await updateTelemetry(); } const result = await importData( From 45f59f7d9e2592894a30d7e9482c4a4ce0504173 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Wed, 18 Mar 2020 12:19:50 +0100 Subject: [PATCH 076/115] Enforce `required` presence for value/key validation of `recordOf` and `mapOf`. (#60406) --- .../kbn-config-schema/src/internals/index.ts | 12 ++++++++---- .../src/types/map_of_type.test.ts | 18 ++++++++++++++++++ .../kbn-config-schema/src/types/map_type.ts | 5 ++++- .../src/types/record_of_type.test.ts | 18 ++++++++++++++++++ .../kbn-config-schema/src/types/record_type.ts | 5 ++++- .../roles/model/put_payload.test.ts | 2 +- 6 files changed, 53 insertions(+), 7 deletions(-) diff --git a/packages/kbn-config-schema/src/internals/index.ts b/packages/kbn-config-schema/src/internals/index.ts index 8f5d09e5b8b4..f84e14d2f741 100644 --- a/packages/kbn-config-schema/src/internals/index.ts +++ b/packages/kbn-config-schema/src/internals/index.ts @@ -314,7 +314,8 @@ export const internals = Joi.extend([ for (const [entryKey, entryValue] of value) { const { value: validatedEntryKey, error: keyError } = Joi.validate( entryKey, - params.key + params.key, + { presence: 'required' } ); if (keyError) { @@ -323,7 +324,8 @@ export const internals = Joi.extend([ const { value: validatedEntryValue, error: valueError } = Joi.validate( entryValue, - params.value + params.value, + { presence: 'required' } ); if (valueError) { @@ -374,7 +376,8 @@ export const internals = Joi.extend([ for (const [entryKey, entryValue] of Object.entries(value)) { const { value: validatedEntryKey, error: keyError } = Joi.validate( entryKey, - params.key + params.key, + { presence: 'required' } ); if (keyError) { @@ -383,7 +386,8 @@ export const internals = Joi.extend([ const { value: validatedEntryValue, error: valueError } = Joi.validate( entryValue, - params.value + params.value, + { presence: 'required' } ); if (valueError) { diff --git a/packages/kbn-config-schema/src/types/map_of_type.test.ts b/packages/kbn-config-schema/src/types/map_of_type.test.ts index b015f51bdc8a..1c5a227ef0fa 100644 --- a/packages/kbn-config-schema/src/types/map_of_type.test.ts +++ b/packages/kbn-config-schema/src/types/map_of_type.test.ts @@ -159,6 +159,24 @@ test('object within mapOf', () => { expect(type.validate(value)).toEqual(expected); }); +test('enforces required object fields within mapOf', () => { + const type = schema.mapOf( + schema.string(), + schema.object({ + bar: schema.object({ + baz: schema.number(), + }), + }) + ); + const value = { + foo: {}, + }; + + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[foo.bar.baz]: expected value of type [number] but got [undefined]"` + ); +}); + test('error preserves full path', () => { const type = schema.object({ grandParentKey: schema.object({ diff --git a/packages/kbn-config-schema/src/types/map_type.ts b/packages/kbn-config-schema/src/types/map_type.ts index 231c3726ae9d..6da664bf9561 100644 --- a/packages/kbn-config-schema/src/types/map_type.ts +++ b/packages/kbn-config-schema/src/types/map_type.ts @@ -57,7 +57,10 @@ export class MapOfType extends Type> { path.length, 0, // If `key` validation failed, let's stress that to make error more obvious. - type === 'map.key' ? `key("${entryKey}")` : entryKey.toString() + type === 'map.key' ? `key("${entryKey}")` : entryKey.toString(), + // Error could have happened deep inside value/key schema and error message should + // include full path. + ...(reason instanceof SchemaTypeError ? reason.path : []) ); return reason instanceof SchemaTypesError diff --git a/packages/kbn-config-schema/src/types/record_of_type.test.ts b/packages/kbn-config-schema/src/types/record_of_type.test.ts index ef15e7b0f6ad..aee7dde71c3e 100644 --- a/packages/kbn-config-schema/src/types/record_of_type.test.ts +++ b/packages/kbn-config-schema/src/types/record_of_type.test.ts @@ -159,6 +159,24 @@ test('object within recordOf', () => { expect(type.validate(value)).toEqual({ foo: { bar: 123 } }); }); +test('enforces required object fields within recordOf', () => { + const type = schema.recordOf( + schema.string(), + schema.object({ + bar: schema.object({ + baz: schema.number(), + }), + }) + ); + const value = { + foo: {}, + }; + + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[foo.bar.baz]: expected value of type [number] but got [undefined]"` + ); +}); + test('error preserves full path', () => { const type = schema.object({ grandParentKey: schema.object({ diff --git a/packages/kbn-config-schema/src/types/record_type.ts b/packages/kbn-config-schema/src/types/record_type.ts index c6d4b4d71b4f..ef9e70cbabc0 100644 --- a/packages/kbn-config-schema/src/types/record_type.ts +++ b/packages/kbn-config-schema/src/types/record_type.ts @@ -49,7 +49,10 @@ export class RecordOfType extends Type> { path.length, 0, // If `key` validation failed, let's stress that to make error more obvious. - type === 'record.key' ? `key("${entryKey}")` : entryKey.toString() + type === 'record.key' ? `key("${entryKey}")` : entryKey.toString(), + // Error could have happened deep inside value/key schema and error message should + // include full path. + ...(reason instanceof SchemaTypeError ? reason.path : []) ); return reason instanceof SchemaTypesError diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts index acde73dcd819..eedd63e22852 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts @@ -28,7 +28,7 @@ describe('Put payload schema', () => { kibana: [{ feature: { foo: ['!foo'] } }], }) ).toThrowErrorMatchingInlineSnapshot( - `"[kibana.0.feature.foo]: only a-z, A-Z, 0-9, '_', and '-' are allowed"` + `"[kibana.0.feature.foo.0]: only a-z, A-Z, 0-9, '_', and '-' are allowed"` ); }); From d466cc9cca836790cdeb198d60a8d54b839c609d Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 18 Mar 2020 12:28:46 +0100 Subject: [PATCH 077/115] add data-test-subj where possible on SO management table (#60226) * add data-test-subj where possible * add per-action dts * update snapshots --- .../table/__jest__/__snapshots__/table.test.js.snap | 10 ++++++++++ .../components/objects_table/components/table/table.js | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap index 805131042f38..a4dcfb9c3818 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap @@ -126,6 +126,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` Array [ Object { "align": "center", + "data-test-subj": "savedObjectsTableRowType", "description": "Type of the saved object", "field": "type", "name": "Type", @@ -134,6 +135,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` "width": "50px", }, Object { + "data-test-subj": "savedObjectsTableRowTitle", "dataType": "string", "description": "Title of the saved object", "field": "meta.title", @@ -145,6 +147,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` "actions": Array [ Object { "available": [Function], + "data-test-subj": "savedObjectsTableAction-inspect", "description": "Inspect this saved object", "icon": "inspect", "name": "Inspect", @@ -152,6 +155,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` "type": "icon", }, Object { + "data-test-subj": "savedObjectsTableAction-relationships", "description": "View the relationships this saved object has to other saved objects", "icon": "kqlSelector", "name": "Relationships", @@ -198,6 +202,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` } } responsive={true} + rowProps={[Function]} selection={ Object { "onSelectionChange": [Function], @@ -334,6 +339,7 @@ exports[`Table should render normally 1`] = ` Array [ Object { "align": "center", + "data-test-subj": "savedObjectsTableRowType", "description": "Type of the saved object", "field": "type", "name": "Type", @@ -342,6 +348,7 @@ exports[`Table should render normally 1`] = ` "width": "50px", }, Object { + "data-test-subj": "savedObjectsTableRowTitle", "dataType": "string", "description": "Title of the saved object", "field": "meta.title", @@ -353,6 +360,7 @@ exports[`Table should render normally 1`] = ` "actions": Array [ Object { "available": [Function], + "data-test-subj": "savedObjectsTableAction-inspect", "description": "Inspect this saved object", "icon": "inspect", "name": "Inspect", @@ -360,6 +368,7 @@ exports[`Table should render normally 1`] = ` "type": "icon", }, Object { + "data-test-subj": "savedObjectsTableAction-relationships", "description": "View the relationships this saved object has to other saved objects", "icon": "kqlSelector", "name": "Relationships", @@ -406,6 +415,7 @@ exports[`Table should render normally 1`] = ` } } responsive={true} + rowProps={[Function]} selection={ Object { "onSelectionChange": [Function], diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js index a119817fdc0c..386b35399b75 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js @@ -178,6 +178,7 @@ export class Table extends PureComponent { { defaultMessage: 'Type of the saved object' } ), sortable: false, + 'data-test-subj': 'savedObjectsTableRowType', render: (type, object) => { return ( @@ -201,6 +202,7 @@ export class Table extends PureComponent { ), dataType: 'string', sortable: false, + 'data-test-subj': 'savedObjectsTableRowTitle', render: (title, object) => { const { path } = object.meta.inAppUrl || {}; const canGoInApp = this.props.canGoInApp(object); @@ -230,6 +232,7 @@ export class Table extends PureComponent { icon: 'inspect', onClick: object => goInspectObject(object), available: object => !!object.meta.editUrl, + 'data-test-subj': 'savedObjectsTableAction-inspect', }, { name: i18n.translate( @@ -246,10 +249,12 @@ export class Table extends PureComponent { type: 'icon', icon: 'kqlSelector', onClick: object => onShowRelationships(object), + 'data-test-subj': 'savedObjectsTableAction-relationships', }, ...this.extraActions.map(action => { return { ...action.euiAction, + 'data-test-subj': `savedObjectsTableAction-${action.id}`, onClick: object => { this.setState({ activeAction: action, @@ -372,6 +377,9 @@ export class Table extends PureComponent { pagination={pagination} selection={selection} onChange={onTableChange} + rowProps={item => ({ + 'data-test-subj': `savedObjectsTableRow row-${item.id}`, + })} />

    x(v`!BtEr^b;>wW+JJQRxHA>Gb&L3 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%0Ag#cwAESE}orA{Qbktd!mU#NsPv!!@O0B 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

    From 70c1b69eb08bc8af29d89194b8f356d70fd9e8a8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 18 Mar 2020 14:04:25 +0200 Subject: [PATCH 078/115] [SIEM][Case] Update connector through flyout (#60307) * Move add flyout to parent * Disable mapping * Show edit flyout * Do not update connectors throught cases API * Fix uncontrolled input error * Disable edit button * Add comments * Change undefined to null Co-authored-by: Elastic Machine --- .../public/containers/case/configure/api.ts | 19 +---- .../public/containers/case/configure/types.ts | 8 +- .../case/configure/use_connectors.tsx | 55 +------------ .../siem/public/lib/connectors/servicenow.tsx | 10 ++- .../components/configure_cases/connectors.tsx | 43 +--------- .../case/components/configure_cases/index.tsx | 82 +++++++++++++++---- .../components/configure_cases/mapping.tsx | 56 ++++++++++--- .../configure_cases/translations.ts | 4 + .../api/cases/configure/patch_connector.ts | 68 --------------- .../plugins/case/server/routes/api/index.ts | 2 - 10 files changed, 131 insertions(+), 216 deletions(-) delete mode 100644 x-pack/plugins/case/server/routes/api/cases/configure/patch_connector.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts index a6db36d8f64e..ed47cdc62a1b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts @@ -16,7 +16,7 @@ import { KibanaServices } from '../../../lib/kibana'; import { CASES_CONFIGURE_URL } from '../constants'; import { ApiProps } from '../types'; import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; -import { CaseConfigure, PatchConnectorProps } from './types'; +import { CaseConfigure } from './types'; export const fetchConnectors = async ({ signal }: ApiProps): Promise => { const response = await KibanaServices.get().http.fetch( @@ -79,20 +79,3 @@ export const patchCaseConfigure = async ( decodeCaseConfigureResponse(response) ); }; - -export const patchConfigConnector = async ({ - connectorId, - config, - signal, -}: PatchConnectorProps): Promise => { - const response = await KibanaServices.get().http.fetch( - `${CASES_CONFIGURE_URL}/connectors/${connectorId}`, - { - method: 'PATCH', - body: JSON.stringify(config), - signal, - } - ); - - return response; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts index 840828307163..fc7aaa3643d7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticUser, ApiProps } from '../types'; +import { ElasticUser } from '../types'; import { ActionType, - CasesConnectorConfiguration, CasesConfigurationMaps, CaseField, ClosureType, @@ -33,11 +32,6 @@ export interface CaseConfigure { version: string; } -export interface PatchConnectorProps extends ApiProps { - connectorId: string; - config: CasesConnectorConfiguration; -} - export interface CCMapsCombinedActionAttributes extends CasesConfigurationMaps { actionType?: ActionType; } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx index f905ebe756d7..d31dcdbee2a1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx @@ -8,14 +8,13 @@ import { useState, useEffect, useCallback } from 'react'; import { useStateToaster, errorToToaster } from '../../../components/toasters'; import * as i18n from '../translations'; -import { fetchConnectors, patchConfigConnector } from './api'; -import { CasesConfigurationMapping, Connector } from './types'; +import { fetchConnectors } from './api'; +import { Connector } from './types'; export interface ReturnConnectors { loading: boolean; connectors: Connector[]; refetchConnectors: () => void; - updateConnector: (connectorId: string, mappings: CasesConfigurationMapping[]) => unknown; } export const useConnectors = (): ReturnConnectors => { @@ -53,55 +52,6 @@ export const useConnectors = (): ReturnConnectors => { }; }, []); - const updateConnector = useCallback( - (connectorId: string, mappings: CasesConfigurationMapping[]) => { - if (connectorId === 'none') { - return; - } - - let didCancel = false; - const abortCtrl = new AbortController(); - const update = async () => { - try { - setLoading(true); - await patchConfigConnector({ - connectorId, - config: { - cases_configuration: { - mapping: mappings.map(m => ({ - source: m.source, - target: m.target, - action_type: m.actionType, - })), - }, - }, - signal: abortCtrl.signal, - }); - if (!didCancel) { - setLoading(false); - refetchConnectors(); - } - } catch (error) { - if (!didCancel) { - setLoading(false); - refetchConnectors(); - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); - } - } - }; - update(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; - }, - [] - ); - useEffect(() => { refetchConnectors(); }, []); @@ -110,6 +60,5 @@ export const useConnectors = (): ReturnConnectors => { loading, connectors, refetchConnectors, - updateConnector, }; }; diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx index 877757df30fb..8e947fbc0f9b 100644 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx @@ -87,6 +87,10 @@ export function getActionType(): ActionTypeModel { const ServiceNowConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { + /* We do not provide defaults values to the fields (like empty string for apiUrl) intentionally. + * If we do, errors will be shown the first time the flyout is open even though the user did not + * interact with the form. Also, we would like to show errors for empty fields provided by the user. + /*/ const { apiUrl, casesConfiguration: { mapping = [] } = {} } = action.config; const { username, password } = action.secrets; @@ -153,7 +157,7 @@ const ServiceNowConnectorFields: React.FunctionComponent void; - refetchConnectors: () => void; selectedConnector: string; + handleShowAddFlyout: () => void; } -const actionTypes = [ - { - id: '.servicenow', - name: 'ServiceNow', - enabled: true, - }, -]; - const ConnectorsComponent: React.FC = ({ connectors, disabled, isLoading, onChangeConnector, - refetchConnectors, selectedConnector, + handleShowAddFlyout, }) => { - const { http, triggers_actions_ui, notifications, application } = useKibana().services; - const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); - - const handleShowFlyout = useCallback(() => setAddFlyoutVisibility(true), []); - const dropDownLabel = ( {i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL} - {i18n.ADD_NEW_CONNECTOR} + {i18n.ADD_NEW_CONNECTOR} ); - const reloadConnectors = useCallback(async () => refetchConnectors(), []); - return ( <> = ({ /> - - - ); }; 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 da715fb66953..b3c424bef6a7 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 @@ -9,8 +9,18 @@ import styled, { css } from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { noop, 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'; +import { + ActionsConnectorsContextProvider, + ConnectorAddFlyout, + ConnectorEditFlyout, +} from '../../../../../../../../plugins/triggers_actions_ui/public'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionConnectorTableItem } from '../../../../../../../../plugins/triggers_actions_ui/public/types'; + import { ClosureType, CasesConfigurationMapping, @@ -40,8 +50,25 @@ const initialState: State = { mapping: null, }; +const actionTypes = [ + { + id: '.servicenow', + name: 'ServiceNow', + enabled: true, + }, +]; + const ConfigureCasesComponent: React.FC = () => { + const { http, triggers_actions_ui, notifications, application } = useKibana().services; + const [connectorIsValid, setConnectorIsValid] = useState(true); + const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); + const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); + const [editedConnectorItem, setEditedConnectorItem] = useState( + null + ); + + const handleShowAddFlyout = useCallback(() => setAddFlyoutVisibility(true), []); const [{ connectorId, closureType, mapping }, dispatch] = useReducer( configureCasesReducer(), @@ -73,20 +100,18 @@ const ConfigureCasesComponent: React.FC = () => { setConnectorId, setClosureType, }); - const { - loading: isLoadingConnectors, - connectors, - refetchConnectors, - updateConnector, - } = useConnectors(); + const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); + // ActionsConnectorsContextProvider reloadConnectors prop expects a Promise. + // TODO: Fix it if reloadConnectors type change. + const reloadConnectors = useCallback(async () => refetchConnectors(), []); const isLoadingAny = isLoadingConnectors || persistLoading || loadingCaseConfigure; + const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connectorId === 'none'; const handleSubmit = useCallback( // TO DO give a warning/error to user when field are not mapped so they have chance to do it () => { persistCaseConfigure({ connectorId, closureType }); - updateConnector(connectorId, mapping ?? []); }, [connectorId, closureType, mapping] ); @@ -124,6 +149,14 @@ const ConfigureCasesComponent: React.FC = () => { } }, [connectors, connectorId]); + useEffect(() => { + if (!isLoadingConnectors && connectorId !== 'none') { + setEditedConnectorItem( + connectors.find(c => c.id === connectorId) as ActionConnectorTableItem + ); + } + }, [connectors, connectorId]); + return ( {!connectorIsValid && ( @@ -139,7 +172,7 @@ const ConfigureCasesComponent: React.FC = () => { disabled={persistLoading || isLoadingConnectors} isLoading={isLoadingConnectors} onChangeConnector={setConnectorId} - refetchConnectors={refetchConnectors} + handleShowAddFlyout={handleShowAddFlyout} selectedConnector={connectorId} /> @@ -152,15 +185,11 @@ const ConfigureCasesComponent: React.FC = () => { @@ -194,6 +223,29 @@ const ConfigureCasesComponent: React.FC = () => { + + + {editedConnectorItem && ( + + )} + ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx index 10c8f6b93802..2600a9f4e13a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx @@ -4,8 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiDescribedFormGroup } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; + +import { + EuiDescribedFormGroup, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiButtonEmpty, +} from '@elastic/eui'; import * as i18n from './translations'; @@ -14,18 +22,44 @@ import { CasesConfigurationMapping } from '../../../../containers/case/configure interface MappingProps { disabled: boolean; + updateConnectorDisabled: boolean; mapping: CasesConfigurationMapping[] | null; onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; + setEditFlyoutVisibility: React.Dispatch>; } -const MappingComponent: React.FC = ({ disabled, mapping, onChangeMapping }) => ( - {i18n.FIELD_MAPPING_TITLE}} - description={i18n.FIELD_MAPPING_DESC} - > - - -); +const EuiButtonEmptyExtended = styled(EuiButtonEmpty)` + font-size: 12px; + height: 24px; +`; + +const MappingComponent: React.FC = ({ + disabled, + updateConnectorDisabled, + mapping, + onChangeMapping, + setEditFlyoutVisibility, +}) => { + const onClick = useCallback(() => setEditFlyoutVisibility(true), []); + + return ( + {i18n.FIELD_MAPPING_TITLE}} + description={i18n.FIELD_MAPPING_DESC} + > + + + + + {i18n.UPDATE_CONNECTOR} + + + + + + + ); +}; export const Mapping = React.memo(MappingComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts index d24921a63608..dd9bf82fb0b0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts @@ -186,3 +186,7 @@ export const FIELD_MAPPING_FIELD_COMMENTS = i18n.translate( defaultMessage: 'Comments', } ); + +export const UPDATE_CONNECTOR = i18n.translate('xpack.siem.case.configureCases.updateConnector', { + defaultMessage: 'Update connector', +}); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_connector.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_connector.ts deleted file mode 100644 index a9fbe0ef4f72..000000000000 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_connector.ts +++ /dev/null @@ -1,68 +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 { schema } from '@kbn/config-schema'; -import Boom from 'boom'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; - -import { ActionResult } from '../../../../../../actions/common'; -import { CasesConnectorConfigurationRT, throwErrors } from '../../../../../common/api'; -import { RouteDeps } from '../../types'; -import { wrapError, escapeHatch } from '../../utils'; - -export function initCaseConfigurePatchActionConnector({ caseService, router }: RouteDeps) { - router.patch( - { - path: '/api/cases/configure/connectors/{connector_id}', - validate: { - params: schema.object({ - connector_id: schema.string(), - }), - body: escapeHatch, - }, - }, - async (context, request, response) => { - try { - const query = pipe( - CasesConnectorConfigurationRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const client = context.core.savedObjects.client; - const { connector_id: connectorId } = request.params; - const { cases_configuration: casesConfiguration } = query; - - const normalizedMapping = casesConfiguration.mapping.map(m => ({ - source: m.source, - target: m.target, - actionType: m.action_type, - })); - - const action = await client.get('action', connectorId); - - const { config } = action.attributes; - const res = await client.update('action', connectorId, { - config: { - ...config, - casesConfiguration: { ...casesConfiguration, mapping: normalizedMapping }, - }, - }); - - return response.ok({ - body: CasesConnectorConfigurationRT.encode({ - cases_configuration: - res.attributes.config?.casesConfiguration ?? - action.attributes.config.casesConfiguration, - }), - }); - } catch (error) { - return response.customError(wrapError(error)); - } - } - ); -} diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts index 956f410c9c10..60ee57a0efea 100644 --- a/x-pack/plugins/case/server/routes/api/index.ts +++ b/x-pack/plugins/case/server/routes/api/index.ts @@ -26,7 +26,6 @@ import { initGetTagsApi } from './cases/tags/get_tags'; import { RouteDeps } from './types'; import { initCaseConfigureGetActionConnector } from './cases/configure/get_connectors'; -import { initCaseConfigurePatchActionConnector } from './cases/configure/patch_connector'; import { initGetCaseConfigure } from './cases/configure/get_configure'; import { initPatchCaseConfigure } from './cases/configure/patch_configure'; import { initPostCaseConfigure } from './cases/configure/post_configure'; @@ -48,7 +47,6 @@ export function initCaseApi(deps: RouteDeps) { initPostCommentApi(deps); // Cases Configure initCaseConfigureGetActionConnector(deps); - initCaseConfigurePatchActionConnector(deps); initGetCaseConfigure(deps); initPatchCaseConfigure(deps); initPostCaseConfigure(deps); From a97ecaae69d4ba4ba5e7a8e21b2d49671eb2596c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Wed, 18 Mar 2020 08:31:03 -0400 Subject: [PATCH 079/115] Fix create alert button from not showing in alerts list (#60444) --- .../plugins/triggers_actions_ui/index.ts | 7 ----- x-pack/plugins/triggers_actions_ui/README.md | 6 ---- .../public/application/app.tsx | 1 - .../actions_connectors_list.test.tsx | 4 --- .../sections/alert_form/alert_add.test.tsx | 1 - .../components/alerts_list.test.tsx | 28 ------------------- .../alerts_list/components/alerts_list.tsx | 4 +-- .../triggers_actions_ui/public/plugin.ts | 1 - x-pack/test/functional_with_es_ssl/config.ts | 2 -- 9 files changed, 1 insertion(+), 53 deletions(-) diff --git a/x-pack/legacy/plugins/triggers_actions_ui/index.ts b/x-pack/legacy/plugins/triggers_actions_ui/index.ts index e871573b266a..eb74290c8468 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/index.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/index.ts @@ -24,18 +24,11 @@ export function triggersActionsUI(kibana: any) { return Joi.object() .keys({ enabled: Joi.boolean().default(true), - createAlertUiEnabled: Joi.boolean().default(false), }) .default(); }, uiExports: { styleSheetPaths: resolve(__dirname, 'public/index.scss'), - injectDefaultVars(server: Legacy.Server) { - const serverConfig = server.config(); - return { - createAlertUiEnabled: serverConfig.get('xpack.triggers_actions_ui.createAlertUiEnabled'), - }; - }, }, }); } diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index 0d667f477f93..e6af63ecd435 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -7,12 +7,6 @@ As a developer you can reuse and extend built-in alerts and actions UI functiona - Create and register a new Action Type. - Embed the Create Alert flyout within any Kibana plugin. -To enable Alerts and Actions UIs, the following configuration settings are needed: -``` -xpack.triggers_actions_ui.enabled: true -xpack.triggers_actions_ui.createAlertUiEnabled: true -``` - ----- 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 51ed3c1ebafa..70945350c3cf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -30,7 +30,6 @@ export interface AppDeps { chrome: ChromeStart; docLinks: DocLinksStart; toastNotifications: ToastsSetup; - injectedMetadata: any; http: HttpSetup; uiSettings: IUiSettingsClient; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; 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 509bd7131394..f94efc0d0672 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 @@ -58,7 +58,6 @@ describe('actions_connectors_list component empty', () => { dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, - injectedMetadata: mockes.injectedMetadata, http: mockes.http, uiSettings: mockes.uiSettings, capabilities: { @@ -155,7 +154,6 @@ describe('actions_connectors_list component with items', () => { dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, - injectedMetadata: mockes.injectedMetadata, http: mockes.http, uiSettings: mockes.uiSettings, capabilities: { @@ -239,7 +237,6 @@ describe('actions_connectors_list component empty with show only capability', () dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, - injectedMetadata: mockes.injectedMetadata, http: mockes.http, uiSettings: mockes.uiSettings, capabilities: { @@ -328,7 +325,6 @@ describe('actions_connectors_list with show only capability', () => { dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, - injectedMetadata: mockes.injectedMetadata, http: mockes.http, uiSettings: mockes.uiSettings, capabilities: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 1177b41788bd..fc524debe744 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -43,7 +43,6 @@ describe('alert_add', () => { const mockes = coreMock.createSetup(); deps = { toastNotifications: mockes.notifications.toasts, - injectedMetadata: mockes.injectedMetadata, http: mockes.http, uiSettings: mockes.uiSettings, dataPlugin: dataPluginMock.createStartContract(), 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 f8f0c278c81e..a80daf544f34 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 @@ -92,13 +92,6 @@ describe('alerts_list component empty', () => { dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, - injectedMetadata: { - getInjectedVar(name: string) { - if (name === 'createAlertUiEnabled') { - return true; - } - }, - } as any, http: mockes.http, uiSettings: mockes.uiSettings, capabilities: { @@ -220,13 +213,6 @@ describe('alerts_list component with items', () => { dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, - injectedMetadata: { - getInjectedVar(name: string) { - if (name === 'createAlertUiEnabled') { - return true; - } - }, - } as any, http: mockes.http, uiSettings: mockes.uiSettings, capabilities: { @@ -315,13 +301,6 @@ describe('alerts_list component empty with show only capability', () => { dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, - injectedMetadata: { - getInjectedVar(name: string) { - if (name === 'createAlertUiEnabled') { - return true; - } - }, - } as any, http: mockes.http, uiSettings: mockes.uiSettings, capabilities: { @@ -439,13 +418,6 @@ describe('alerts_list with show only capability', () => { dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, - injectedMetadata: { - getInjectedVar(name: string) { - if (name === 'createAlertUiEnabled') { - return true; - } - }, - } as any, http: mockes.http, uiSettings: mockes.uiSettings, capabilities: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 8d8fc177b57a..c409dead7c85 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -52,7 +52,6 @@ export const AlertsList: React.FunctionComponent = () => { const history = useHistory(); const { http, - injectedMetadata, toastNotifications, capabilities, alertTypeRegistry, @@ -63,7 +62,6 @@ export const AlertsList: React.FunctionComponent = () => { } = useAppDependencies(); const canDelete = hasDeleteAlertsCapability(capabilities); const canSave = hasSaveAlertsCapability(capabilities); - const createAlertUiEnabled = injectedMetadata.getInjectedVar('createAlertUiEnabled'); const [actionTypes, setActionTypes] = useState([]); const [selectedIds, setSelectedIds] = useState([]); @@ -273,7 +271,7 @@ export const AlertsList: React.FunctionComponent = () => { />, ]; - if (canSave && createAlertUiEnabled) { + if (canSave) { toolsRight.push( Date: Wed, 18 Mar 2020 13:36:20 +0100 Subject: [PATCH 080/115] [License Management] NP migration (#60250) --- x-pack/.i18nrc.json | 2 +- x-pack/index.js | 2 - .../upload_license.test.tsx.snap | 2826 ---------------- .../plugins/license_management/index.ts | 49 - .../public/management_section.ts | 20 - .../public/np_ready/application/boot.tsx | 79 - .../np_ready/application/breadcrumbs.ts | 37 - .../store/actions/set_breadcrumb.ts | 22 - .../public/np_ready/plugin.ts | 52 - .../public/register_route.ts | 87 - .../server/np_ready/lib/start_trial.ts | 47 - .../server/np_ready/plugin.ts | 35 - .../api/license/register_permissions_route.ts | 29 - .../api/license/register_start_basic_route.ts | 27 - .../license/register_start_trial_routes.ts | 34 - .../server/np_ready/types.ts | 24 - .../xpack_main/public/components/index.js | 6 +- .../__snapshots__/add_license.test.js.snap | 0 .../__snapshots__/license_status.test.js.snap | 0 .../request_trial_extension.test.js.snap | 0 .../revert_to_basic.test.js.snap | 0 .../__snapshots__/start_trial.test.js.snap | 0 .../upload_license.test.tsx.snap | 2891 +++++++++++++++++ .../__jest__/add_license.test.js | 2 +- .../__jest__/api_responses/index.js | 0 .../__jest__/api_responses/upload_license.js | 0 .../__jest__/license_status.test.js | 2 +- .../__jest__/request_trial_extension.test.js | 2 +- .../__jest__/revert_to_basic.test.js | 2 +- .../__jest__/start_trial.test.js | 4 +- .../__jest__/upload_license.test.tsx | 72 +- .../license_management/__jest__/util/index.js | 0 .../license_management/__jest__/util/util.js | 24 +- .../__mocks__/focus-trap-react.js | 0 .../common/constants/base_path.ts | 2 + .../common/constants/external_links.ts | 0 .../common/constants/index.ts | 2 +- .../common/constants/permissions.ts | 0 .../common/constants/plugin.ts | 4 +- x-pack/plugins/license_management/kibana.json | 9 + .../application/_license_management.scss | 0 .../public}/application/app.container.js | 0 .../public}/application/app.js | 2 +- .../public/application/app_context.tsx | 53 + .../public/application/app_providers.tsx | 55 + .../public/application/breadcrumbs.ts | 72 + .../components/telemetry_opt_in/index.ts | 0 .../telemetry_opt_in/telemetry_opt_in.tsx | 0 .../public}/application/index.scss | 5 +- .../public/application/index.tsx | 35 + .../public}/application/lib/es.ts | 13 +- .../public}/application/lib/telemetry.ts | 6 +- .../public}/application/sections/index.js | 0 .../add_license/add_license.js | 2 +- .../license_dashboard/add_license/index.js | 0 .../sections/license_dashboard/index.js | 0 .../license_dashboard.container.js | 0 .../license_dashboard/license_dashboard.js | 0 .../license_dashboard/license_status/index.js | 0 .../license_status.container.js | 0 .../license_status/license_status.js | 0 .../request_trial_extension/index.js | 0 .../request_trial_extension.container.js | 0 .../request_trial_extension.js | 2 +- .../revert_to_basic/index.js | 0 .../revert_to_basic.container.js | 0 .../revert_to_basic/revert_to_basic.js | 2 +- .../license_dashboard/start_trial/index.ts | 0 .../start_trial/start_trial.container.js | 0 .../start_trial/start_trial.tsx | 36 +- .../sections/upload_license/index.js | 0 .../upload_license.container.js | 0 .../sections/upload_license/upload_license.js | 2 +- .../store/actions/add_error_message.js | 0 .../application/store/actions/add_license.js | 0 .../application/store/actions/index.js | 0 .../application/store/actions/permissions.js | 0 .../store/actions/set_breadcrumb.ts | 17 + .../application/store/actions/start_basic.js | 4 +- .../application/store/actions/start_trial.js | 8 +- .../store/actions/upload_license.js | 6 +- .../public}/application/store/index.js | 0 .../application/store/reducers/index.js | 0 .../application/store/reducers/license.js | 0 .../store/reducers/license_management.js | 0 .../application/store/reducers/permissions.js | 0 .../reducers/start_basic_license_status.js | 0 .../store/reducers/trial_status.js | 0 .../store/reducers/upload_error_message.js | 0 .../store/reducers/upload_status.js | 0 .../public}/application/store/store.js | 0 .../license_management/public}/index.ts | 4 +- .../license_management/public/plugin.ts | 83 + .../license_management/public/types.ts} | 5 +- .../license_management/server/config.ts | 16 + .../license_management/server}/index.ts | 11 +- .../server/lib/is_es_error.ts} | 10 +- .../license_management/server}/lib/license.ts | 36 +- .../server}/lib/permissions.ts | 20 +- .../server}/lib/start_basic.ts | 25 +- .../server/lib/start_trial.ts | 45 + .../license_management/server/plugin.ts | 35 + .../server}/routes/api/license/index.ts | 0 .../api/license/register_license_route.ts | 23 +- .../api/license/register_permissions_route.ts | 26 + .../api/license/register_start_basic_route.ts | 33 + .../license/register_start_trial_routes.ts | 31 + .../server/routes/helpers.ts} | 4 +- .../license_management/server/routes/index.ts | 23 + .../license_management/server/types.ts | 32 + 110 files changed, 3652 insertions(+), 3524 deletions(-) delete mode 100644 x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap delete mode 100644 x-pack/legacy/plugins/license_management/index.ts delete mode 100644 x-pack/legacy/plugins/license_management/public/management_section.ts delete mode 100644 x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx delete mode 100644 x-pack/legacy/plugins/license_management/public/np_ready/application/breadcrumbs.ts delete mode 100644 x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/set_breadcrumb.ts delete mode 100644 x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts delete mode 100644 x-pack/legacy/plugins/license_management/public/register_route.ts delete mode 100644 x-pack/legacy/plugins/license_management/server/np_ready/lib/start_trial.ts delete mode 100644 x-pack/legacy/plugins/license_management/server/np_ready/plugin.ts delete mode 100644 x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_permissions_route.ts delete mode 100644 x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_basic_route.ts delete mode 100644 x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_trial_routes.ts delete mode 100644 x-pack/legacy/plugins/license_management/server/np_ready/types.ts rename x-pack/{legacy => }/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap (100%) rename x-pack/{legacy => }/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap (100%) rename x-pack/{legacy => }/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap (100%) rename x-pack/{legacy => }/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap (100%) rename x-pack/{legacy => }/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap (100%) create mode 100644 x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap rename x-pack/{legacy => }/plugins/license_management/__jest__/add_license.test.js (90%) rename x-pack/{legacy => }/plugins/license_management/__jest__/api_responses/index.js (100%) rename x-pack/{legacy => }/plugins/license_management/__jest__/api_responses/upload_license.js (100%) rename x-pack/{legacy => }/plugins/license_management/__jest__/license_status.test.js (88%) rename x-pack/{legacy => }/plugins/license_management/__jest__/request_trial_extension.test.js (96%) rename x-pack/{legacy => }/plugins/license_management/__jest__/revert_to_basic.test.js (94%) rename x-pack/{legacy => }/plugins/license_management/__jest__/start_trial.test.js (96%) rename x-pack/{legacy => }/plugins/license_management/__jest__/upload_license.test.tsx (55%) rename x-pack/{legacy => }/plugins/license_management/__jest__/util/index.js (100%) rename x-pack/{legacy => }/plugins/license_management/__jest__/util/util.js (60%) rename x-pack/{legacy => }/plugins/license_management/__mocks__/focus-trap-react.js (100%) rename x-pack/{legacy => }/plugins/license_management/common/constants/base_path.ts (87%) rename x-pack/{legacy => }/plugins/license_management/common/constants/external_links.ts (100%) rename x-pack/{legacy => }/plugins/license_management/common/constants/index.ts (87%) rename x-pack/{legacy => }/plugins/license_management/common/constants/permissions.ts (100%) rename x-pack/{legacy => }/plugins/license_management/common/constants/plugin.ts (79%) create mode 100644 x-pack/plugins/license_management/kibana.json rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/_license_management.scss (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/app.container.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/app.js (97%) create mode 100644 x-pack/plugins/license_management/public/application/app_context.tsx create mode 100644 x-pack/plugins/license_management/public/application/app_providers.tsx create mode 100644 x-pack/plugins/license_management/public/application/breadcrumbs.ts rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/components/telemetry_opt_in/index.ts (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/components/telemetry_opt_in/telemetry_opt_in.tsx (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/index.scss (63%) create mode 100644 x-pack/plugins/license_management/public/application/index.tsx rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/lib/es.ts (79%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/lib/telemetry.ts (69%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/index.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/add_license/add_license.js (94%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/add_license/index.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/index.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/license_dashboard.container.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/license_dashboard.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/license_status/index.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/license_status/license_status.container.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/license_status/license_status.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/request_trial_extension/index.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/request_trial_extension/request_trial_extension.container.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js (96%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/revert_to_basic/index.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/revert_to_basic/revert_to_basic.container.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js (98%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/start_trial/index.ts (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/start_trial/start_trial.container.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/license_dashboard/start_trial/start_trial.tsx (92%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/upload_license/index.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/upload_license/upload_license.container.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/sections/upload_license/upload_license.js (99%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/actions/add_error_message.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/actions/add_license.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/actions/index.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/actions/permissions.js (100%) create mode 100644 x-pack/plugins/license_management/public/application/store/actions/set_breadcrumb.ts rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/actions/start_basic.js (95%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/actions/start_trial.js (85%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/actions/upload_license.js (96%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/index.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/reducers/index.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/reducers/license.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/reducers/license_management.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/reducers/permissions.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/reducers/start_basic_license_status.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/reducers/trial_status.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/reducers/upload_error_message.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/reducers/upload_status.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/application/store/store.js (100%) rename x-pack/{legacy/plugins/license_management/public/np_ready => plugins/license_management/public}/index.ts (86%) create mode 100644 x-pack/plugins/license_management/public/plugin.ts rename x-pack/{legacy/plugins/license_management/public/legacy.ts => plugins/license_management/public/types.ts} (78%) create mode 100644 x-pack/plugins/license_management/server/config.ts rename x-pack/{legacy/plugins/license_management/server/np_ready => plugins/license_management/server}/index.ts (57%) rename x-pack/{legacy/plugins/license_management/public/np_ready/application/lib/docs_links.ts => plugins/license_management/server/lib/is_es_error.ts} (55%) rename x-pack/{legacy/plugins/license_management/server/np_ready => plugins/license_management/server}/lib/license.ts (50%) rename x-pack/{legacy/plugins/license_management/server/np_ready => plugins/license_management/server}/lib/permissions.ts (53%) rename x-pack/{legacy/plugins/license_management/server/np_ready => plugins/license_management/server}/lib/start_basic.ts (50%) create mode 100644 x-pack/plugins/license_management/server/lib/start_trial.ts create mode 100644 x-pack/plugins/license_management/server/plugin.ts rename x-pack/{legacy/plugins/license_management/server/np_ready => plugins/license_management/server}/routes/api/license/index.ts (100%) rename x-pack/{legacy/plugins/license_management/server/np_ready => plugins/license_management/server}/routes/api/license/register_license_route.ts (50%) create mode 100644 x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts create mode 100644 x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts create mode 100644 x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts rename x-pack/{legacy/plugins/license_management/public/np_ready/application/index.ts => plugins/license_management/server/routes/helpers.ts} (65%) create mode 100644 x-pack/plugins/license_management/server/routes/index.ts create mode 100644 x-pack/plugins/license_management/server/types.ts diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 60a8d1fcbf22..1564eb94a690 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -22,7 +22,7 @@ "xpack.infra": "plugins/infra", "xpack.ingestManager": "plugins/ingest_manager", "xpack.lens": "legacy/plugins/lens", - "xpack.licenseMgmt": "legacy/plugins/license_management", + "xpack.licenseMgmt": "plugins/license_management", "xpack.licensing": "plugins/licensing", "xpack.logstash": "legacy/plugins/logstash", "xpack.main": "legacy/plugins/xpack_main", diff --git a/x-pack/index.js b/x-pack/index.js index ab31d40c5d71..fb14b3dc10a4 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -16,7 +16,6 @@ import { logstash } from './legacy/plugins/logstash'; import { beats } from './legacy/plugins/beats_management'; import { apm } from './legacy/plugins/apm'; import { maps } from './legacy/plugins/maps'; -import { licenseManagement } from './legacy/plugins/license_management'; import { indexManagement } from './legacy/plugins/index_management'; import { indexLifecycleManagement } from './legacy/plugins/index_lifecycle_management'; import { spaces } from './legacy/plugins/spaces'; @@ -52,7 +51,6 @@ module.exports = function(kibana) { apm(kibana), maps(kibana), canvas(kibana), - licenseManagement(kibana), indexManagement(kibana), indexLifecycleManagement(kibana), infra(kibana), diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap deleted file mode 100644 index e19958568b3b..000000000000 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ /dev/null @@ -1,2826 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UploadLicense should display a modal when license requires acknowledgement 1`] = ` - - - - - -
    - -
    - -

    - - Upload your license - -

    -
    - -
    - - - -
    -
    -
    -
    - -
    -
    -
    - Confirm License Upload -
    -
    -
    -
    -
    -
    -
    - Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. -
    -
    -
      -
    • - Watcher will be disabled -
    • -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    -
    - } - > - - } - confirmButtonText={ - - } - onCancel={[Function]} - onConfirm={[Function]} - title={ - - } - > - - - -
    -
    -
    - -
    - -
    -
    -
    - Confirm License Upload -
    -
    -
    -
    -
    -
    -
    - Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. -
    -
    -
      -
    • - Watcher will be disabled -
    • -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - > - -
    - -
    -
    -
    - Confirm License Upload -
    -
    -
    -
    -
    -
    -
    - Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. -
    -
    -
      -
    • - Watcher will be disabled -
    • -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - /> - -
    - - - - - -
    - -
    - -
    - - Confirm License Upload - -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    - Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. -
    -
    - -
    -
      -
    • - Watcher will be disabled -
    • -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    - - - - - - -
    -
    -
    -
    -
    -
    - - - - - - - -
    -

    - - Your license key is a JSON file with a signature attached. - -

    -

    - - - , - } - } - > - Uploading a license will replace your current - - license. - -

    -
    -
    - -
    - - -
    - -
    - -
    - -
    - - } - onChange={[Function]} - > - -
    -
    - - - -
    -
    -
    - - -
    - -
    - -
    - - -
    - - -
    - - -
    - - - - -
    - - - -
    -
    -
    -
    -
    - -
    - -
    - - - - - -`; - -exports[`UploadLicense should display an error when ES says license is expired 1`] = ` - - - - - -
    - -
    - -

    - - Upload your license - -

    -
    - -
    - - -
    -

    - - Your license key is a JSON file with a signature attached. - -

    -

    - - - , - } - } - > - Uploading a license will replace your current - - license. - -

    -
    - - -
    - - -
    - - -
    -
    - - Please address the errors in your form. - -
    - -
    -
      -
    • - The supplied license has expired. -
    • -
    -
    -
    -
    -
    -
    - -
    - -
    - -
    - - } - onChange={[Function]} - > - -
    -
    - - - -
    -
    -
    - - -
    - -
    - -
    - - -
    - - -
    - - -
    - - - - -
    - - - -
    -
    -
    -
    -
    - -
    - -
    - - - - - -`; - -exports[`UploadLicense should display an error when ES says license is invalid 1`] = ` - - - - - -
    - -
    - -

    - - Upload your license - -

    -
    - -
    - - -
    -

    - - Your license key is a JSON file with a signature attached. - -

    -

    - - - , - } - } - > - Uploading a license will replace your current - - license. - -

    -
    - - -
    - - -
    - - -
    -
    - - Please address the errors in your form. - -
    - -
    -
      -
    • - The supplied license is not valid for this product. -
    • -
    -
    -
    -
    -
    -
    - -
    - -
    - -
    - - } - onChange={[Function]} - > - -
    -
    - - - -
    -
    -
    - - -
    - -
    - -
    - - -
    - - -
    - - -
    - - - - -
    - - - -
    -
    -
    -
    -
    - -
    - -
    - - - - - -`; - -exports[`UploadLicense should display an error when submitting invalid JSON 1`] = ` - - - - - -
    - -
    - -

    - - Upload your license - -

    -
    - -
    - - -
    -

    - - Your license key is a JSON file with a signature attached. - -

    -

    - - - , - } - } - > - Uploading a license will replace your current - - license. - -

    -
    - - -
    - - -
    - - -
    -
    - - Please address the errors in your form. - -
    - -
    -
      -
    • - Error encountered uploading license: Check your license file. -
    • -
    -
    -
    -
    -
    -
    - -
    - -
    - -
    - - } - onChange={[Function]} - > - -
    -
    - - - -
    -
    -
    - - -
    - -
    - -
    - - -
    - - -
    - - -
    - - - - -
    - - - -
    -
    -
    -
    -
    - -
    - -
    - - - - - -`; - -exports[`UploadLicense should display error when ES returns error 1`] = ` - - - - - -
    - -
    - -

    - - Upload your license - -

    -
    - -
    - - -
    -

    - - Your license key is a JSON file with a signature attached. - -

    -

    - - - , - } - } - > - Uploading a license will replace your current - - license. - -

    -
    - - -
    - - -
    - - -
    -
    - - Please address the errors in your form. - -
    - -
    -
      -
    • - Error encountered uploading license: Can not upgrade to a production license unless TLS is configured or security is disabled -
    • -
    -
    -
    -
    -
    -
    - -
    - -
    - -
    - - } - onChange={[Function]} - > - -
    -
    - - - -
    -
    -
    - - -
    - -
    - -
    - - -
    - - -
    - - -
    - - - - -
    - - - -
    -
    -
    -
    -
    - -
    - -
    - - - - - -`; diff --git a/x-pack/legacy/plugins/license_management/index.ts b/x-pack/legacy/plugins/license_management/index.ts deleted file mode 100644 index e9fbb56e9d6a..000000000000 --- a/x-pack/legacy/plugins/license_management/index.ts +++ /dev/null @@ -1,49 +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 { Legacy } from 'kibana'; -import { resolve } from 'path'; -import { PLUGIN } from './common/constants'; -import { plugin } from './server/np_ready'; - -export function licenseManagement(kibana: any) { - return new kibana.Plugin({ - id: PLUGIN.ID, - configPrefix: 'xpack.license_management', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/np_ready/application/index.scss'), - managementSections: ['plugins/license_management/legacy'], - injectDefaultVars(server: Legacy.Server) { - const config = server.config(); - return { - licenseManagementUiEnabled: config.get('xpack.license_management.ui.enabled'), - }; - }, - }, - config(Joi: any) { - return Joi.object({ - // display menu item - ui: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - - // enable plugin - enabled: Joi.boolean().default(true), - }).default(); - }, - init: (server: Legacy.Server) => { - plugin({} as any).setup(server.newPlatform.setup.core, { - ...server.newPlatform.setup.plugins, - __LEGACY: { - xpackMain: server.plugins.xpack_main, - elasticsearch: server.plugins.elasticsearch, - }, - }); - }, - }); -} diff --git a/x-pack/legacy/plugins/license_management/public/management_section.ts b/x-pack/legacy/plugins/license_management/public/management_section.ts deleted file mode 100644 index c7232649857e..000000000000 --- a/x-pack/legacy/plugins/license_management/public/management_section.ts +++ /dev/null @@ -1,20 +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 { management } from 'ui/management'; -import chrome from 'ui/chrome'; -import { BASE_PATH, PLUGIN } from '../common/constants'; - -const licenseManagementUiEnabled = chrome.getInjected('licenseManagementUiEnabled'); - -if (licenseManagementUiEnabled) { - management.getSection('elasticsearch').register('license_management', { - visible: true, - display: PLUGIN.TITLE, - order: 99, - url: `#${BASE_PATH}home`, - }); -} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx b/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx deleted file mode 100644 index 49bb4ce984e4..000000000000 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx +++ /dev/null @@ -1,79 +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 { Provider } from 'react-redux'; -import { HashRouter } from 'react-router-dom'; -import { render, unmountComponentAtNode } from 'react-dom'; -import * as history from 'history'; -import { DocLinksStart, HttpSetup, ToastsSetup, ChromeStart } from 'src/core/public'; - -import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; -// @ts-ignore -import { App } from './app.container'; -// @ts-ignore -import { licenseManagementStore } from './store'; - -import { setDocLinks } from './lib/docs_links'; -import { BASE_PATH } from '../../../common/constants'; -import { Breadcrumb } from './breadcrumbs'; - -interface AppDependencies { - element: HTMLElement; - chrome: ChromeStart; - - I18nContext: any; - legacy: { - xpackInfo: any; - refreshXpack: () => void; - MANAGEMENT_BREADCRUMB: Breadcrumb; - }; - - toasts: ToastsSetup; - docLinks: DocLinksStart; - http: HttpSetup; - telemetry?: TelemetryPluginSetup; -} - -export const boot = (deps: AppDependencies) => { - const { I18nContext, element, legacy, toasts, docLinks, http, chrome, telemetry } = deps; - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; - const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; - const securityDocumentationLink = `${esBase}/security-settings.html`; - - const initialState = { license: legacy.xpackInfo.get('license') }; - - setDocLinks({ securityDocumentationLink }); - - const services = { - legacy: { - refreshXpack: legacy.refreshXpack, - xPackInfo: legacy.xpackInfo, - }, - // So we can imperatively control the hash route - history: history.createHashHistory({ basename: BASE_PATH }), - toasts, - http, - chrome, - telemetry, - MANAGEMENT_BREADCRUMB: legacy.MANAGEMENT_BREADCRUMB, - }; - - const store = licenseManagementStore(initialState, services); - - render( - - - - - - - , - element - ); - - return () => unmountComponentAtNode(element); -}; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/breadcrumbs.ts b/x-pack/legacy/plugins/license_management/public/np_ready/application/breadcrumbs.ts deleted file mode 100644 index 2da04b22c038..000000000000 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/breadcrumbs.ts +++ /dev/null @@ -1,37 +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 { i18n } from '@kbn/i18n'; - -import { BASE_PATH } from '../../../common/constants'; - -export interface Breadcrumb { - text: string; - href: string; -} - -export function getDashboardBreadcrumbs(root: Breadcrumb) { - return [ - root, - { - text: i18n.translate('xpack.licenseMgmt.dashboard.breadcrumb', { - defaultMessage: 'License management', - }), - href: `#${BASE_PATH}home`, - }, - ]; -} - -export function getUploadBreadcrumbs(root: Breadcrumb) { - return [ - ...getDashboardBreadcrumbs(root), - { - text: i18n.translate('xpack.licenseMgmt.upload.breadcrumb', { - defaultMessage: 'Upload', - }), - }, - ]; -} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/set_breadcrumb.ts b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/set_breadcrumb.ts deleted file mode 100644 index bcb4a907bdf8..000000000000 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/set_breadcrumb.ts +++ /dev/null @@ -1,22 +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 { ThunkAction } from 'redux-thunk'; -import { ChromeStart } from 'src/core/public'; -import { getDashboardBreadcrumbs, getUploadBreadcrumbs, Breadcrumb } from '../../breadcrumbs'; - -export const setBreadcrumb = ( - section: 'dashboard' | 'upload' -): ThunkAction => ( - dispatch, - getState, - { chrome, MANAGEMENT_BREADCRUMB } -) => { - if (section === 'upload') { - chrome.setBreadcrumbs(getUploadBreadcrumbs(MANAGEMENT_BREADCRUMB)); - } else { - chrome.setBreadcrumbs(getDashboardBreadcrumbs(MANAGEMENT_BREADCRUMB)); - } -}; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts deleted file mode 100644 index 60876c9b638d..000000000000 --- a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts +++ /dev/null @@ -1,52 +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 { CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; -import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; -import { PLUGIN } from '../../common/constants'; -import { Breadcrumb } from './application/breadcrumbs'; -export interface Plugins { - telemetry: TelemetryPluginSetup; - __LEGACY: { - xpackInfo: XPackMainPlugin; - refreshXpack: () => void; - MANAGEMENT_BREADCRUMB: Breadcrumb; - }; -} - -export class LicenseManagementUIPlugin implements Plugin { - setup({ application, notifications, http }: CoreSetup, { __LEGACY, telemetry }: Plugins) { - application.register({ - id: PLUGIN.ID, - title: PLUGIN.TITLE, - async mount( - { - core: { - docLinks, - i18n: { Context: I18nContext }, - chrome, - }, - }, - { element } - ) { - const { boot } = await import('./application'); - return boot({ - legacy: { ...__LEGACY }, - I18nContext, - toasts: notifications.toasts, - docLinks, - http, - element, - chrome, - telemetry, - }); - }, - }); - } - start(core: CoreStart, plugins: any) {} - stop() {} -} diff --git a/x-pack/legacy/plugins/license_management/public/register_route.ts b/x-pack/legacy/plugins/license_management/public/register_route.ts deleted file mode 100644 index f9258f68c555..000000000000 --- a/x-pack/legacy/plugins/license_management/public/register_route.ts +++ /dev/null @@ -1,87 +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 { App } from 'src/core/public'; - -/* Legacy Imports */ -import { npSetup, npStart } from 'ui/new_platform'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; -import chrome from 'ui/chrome'; -import routes from 'ui/routes'; -// @ts-ignore -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -import { plugin } from './np_ready'; -import { BASE_PATH } from '../common/constants'; - -const licenseManagementUiEnabled = chrome.getInjected('licenseManagementUiEnabled'); - -if (licenseManagementUiEnabled) { - /* - This method handles the cleanup needed when route is scope is destroyed. It also prevents Angular - from destroying scope when route changes and both old route and new route are this same route. - */ - const manageAngularLifecycle = ($scope: any, $route: any, unmount: () => void) => { - const lastRoute = $route.current; - const deregister = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - // if templates are the same we are on the same route - if (lastRoute.$$route.template === currentRoute.$$route.template) { - // this prevents angular from destroying scope - $route.current = lastRoute; - } - }); - $scope.$on('$destroy', () => { - if (deregister) { - deregister(); - } - unmount(); - }); - }; - - const template = ` -
    -
    `; - - routes.when(`${BASE_PATH}:view?`, { - template, - controllerAs: 'licenseManagement', - controller: class LicenseManagementController { - constructor($injector: any, $rootScope: any, $scope: any, $route: any) { - $scope.$$postDigest(() => { - const element = document.getElementById('licenseReactRoot')!; - - const refreshXpack = async () => { - await xpackInfo.refresh($injector); - }; - - plugin({} as any).setup( - { - ...npSetup.core, - application: { - ...npSetup.core.application, - async register(app: App) { - const unmountApp = await app.mount({ ...npStart } as any, { - element, - appBasePath: '', - onAppLeave: () => undefined, - // TODO: adapt to use Core's ScopedHistory - history: {} as any, - }); - manageAngularLifecycle($scope, $route, unmountApp as any); - }, - }, - }, - { - telemetry: (npSetup.plugins as any).telemetry, - __LEGACY: { xpackInfo, refreshXpack, MANAGEMENT_BREADCRUMB }, - } - ); - }); - } - } as any, - } as any); -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_trial.ts b/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_trial.ts deleted file mode 100644 index 3569085d413c..000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_trial.ts +++ /dev/null @@ -1,47 +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 { KibanaRequest } from 'src/core/server'; -import { ElasticsearchPlugin } from '../../../../../../../src/legacy/core_plugins/elasticsearch'; - -export async function canStartTrial( - req: KibanaRequest, - elasticsearch: ElasticsearchPlugin -) { - const { callWithRequest } = elasticsearch.getCluster('admin'); - const options = { - method: 'GET', - path: '/_license/trial_status', - }; - try { - const response = await callWithRequest(req as any, 'transport.request', options); - return response.eligible_to_start_trial; - } catch (error) { - return error.body; - } -} - -export async function startTrial( - req: KibanaRequest, - elasticsearch: ElasticsearchPlugin, - xpackInfo: any -) { - const { callWithRequest } = elasticsearch.getCluster('admin'); - const options = { - method: 'POST', - path: '/_license/start_trial?acknowledge=true', - }; - try { - const response = await callWithRequest(req as any, 'transport.request', options); - const { trial_was_started: trialWasStarted } = response; - if (trialWasStarted) { - await xpackInfo.refreshNow(); - } - return response; - } catch (error) { - return error.body; - } -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/plugin.ts b/x-pack/legacy/plugins/license_management/server/np_ready/plugin.ts deleted file mode 100644 index 9f065cf98d71..000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/plugin.ts +++ /dev/null @@ -1,35 +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 { Plugin, CoreSetup } from 'src/core/server'; -import { Dependencies, Server } from './types'; - -import { - registerLicenseRoute, - registerStartTrialRoutes, - registerStartBasicRoute, - registerPermissionsRoute, -} from './routes/api/license'; - -export class LicenseManagementServerPlugin implements Plugin { - setup({ http }: CoreSetup, { __LEGACY }: Dependencies) { - const xpackInfo = __LEGACY.xpackMain.info; - const router = http.createRouter(); - - const server: Server = { - router, - }; - - const legacy = { plugins: __LEGACY }; - - registerLicenseRoute(server, legacy, xpackInfo); - registerStartTrialRoutes(server, legacy, xpackInfo); - registerStartBasicRoute(server, legacy, xpackInfo); - registerPermissionsRoute(server, legacy, xpackInfo); - } - start() {} - stop() {} -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_permissions_route.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_permissions_route.ts deleted file mode 100644 index 0f6c343d04fc..000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_permissions_route.ts +++ /dev/null @@ -1,29 +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 { getPermissions } from '../../../lib/permissions'; -import { Legacy, Server } from '../../../types'; - -export function registerPermissionsRoute(server: Server, legacy: Legacy, xpackInfo: any) { - server.router.post( - { path: '/api/license/permissions', validate: false }, - async (ctx, request, response) => { - if (!xpackInfo) { - // xpackInfo is updated via poll, so it may not be available until polling has begun. - // In this rare situation, tell the client the service is temporarily unavailable. - return response.customError({ statusCode: 503, body: 'Security info unavailable' }); - } - - try { - return response.ok({ - body: await getPermissions(request, legacy.plugins.elasticsearch, xpackInfo), - }); - } catch (e) { - return response.internalError({ body: e }); - } - } - ); -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_basic_route.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_basic_route.ts deleted file mode 100644 index ee7ac8602104..000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_basic_route.ts +++ /dev/null @@ -1,27 +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 { schema } from '@kbn/config-schema'; -import { startBasic } from '../../../lib/start_basic'; -import { Legacy, Server } from '../../../types'; - -export function registerStartBasicRoute(server: Server, legacy: Legacy, xpackInfo: any) { - server.router.post( - { - path: '/api/license/start_basic', - validate: { query: schema.object({ acknowledge: schema.string() }) }, - }, - async (ctx, request, response) => { - try { - return response.ok({ - body: await startBasic(request, legacy.plugins.elasticsearch, xpackInfo), - }); - } catch (e) { - return response.internalError({ body: e }); - } - } - ); -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_trial_routes.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_trial_routes.ts deleted file mode 100644 index d93f13eba363..000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_trial_routes.ts +++ /dev/null @@ -1,34 +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 { canStartTrial, startTrial } from '../../../lib/start_trial'; -import { Legacy, Server } from '../../../types'; - -export function registerStartTrialRoutes(server: Server, legacy: Legacy, xpackInfo: any) { - server.router.get( - { path: '/api/license/start_trial', validate: false }, - async (ctx, request, response) => { - try { - return response.ok({ body: await canStartTrial(request, legacy.plugins.elasticsearch) }); - } catch (e) { - return response.internalError({ body: e }); - } - } - ); - - server.router.post( - { path: '/api/license/start_trial', validate: false }, - async (ctx, request, response) => { - try { - return response.ok({ - body: await startTrial(request, legacy.plugins.elasticsearch, xpackInfo), - }); - } catch (e) { - return response.internalError({ body: e }); - } - } - ); -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/types.ts b/x-pack/legacy/plugins/license_management/server/np_ready/types.ts deleted file mode 100644 index 0e66946ec1cc..000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/types.ts +++ /dev/null @@ -1,24 +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 { IRouter } from 'src/core/server'; -import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; -import { ElasticsearchPlugin } from '../../../../../../src/legacy/core_plugins/elasticsearch'; - -export interface Dependencies { - __LEGACY: { - xpackMain: XPackMainPlugin; - elasticsearch: ElasticsearchPlugin; - }; -} - -export interface Server { - router: IRouter; -} - -export interface Legacy { - plugins: Dependencies['__LEGACY']; -} diff --git a/x-pack/legacy/plugins/xpack_main/public/components/index.js b/x-pack/legacy/plugins/xpack_main/public/components/index.js index e57bd6af189f..871d86e642de 100644 --- a/x-pack/legacy/plugins/xpack_main/public/components/index.js +++ b/x-pack/legacy/plugins/xpack_main/public/components/index.js @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export { LicenseStatus } from '../../../license_management/public/np_ready/application/sections/license_dashboard/license_status/license_status'; +export { LicenseStatus } from '../../../../../plugins/license_management/public/application/sections/license_dashboard/license_status/license_status'; -export { AddLicense } from '../../../license_management/public/np_ready/application/sections/license_dashboard/add_license/add_license'; +export { AddLicense } from '../../../../../plugins/license_management/public/application/sections/license_dashboard/add_license/add_license'; /* * For to link to management */ -export { BASE_PATH as MANAGEMENT_BASE_PATH } from '../../../license_management/common/constants'; +export { BASE_PATH as MANAGEMENT_BASE_PATH } from '../../../../../plugins/license_management/common/constants'; diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap rename to x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap rename to x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap rename to x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap rename to x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap rename to x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap new file mode 100644 index 000000000000..5a7d13618080 --- /dev/null +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -0,0 +1,2891 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UploadLicense should display a modal when license requires acknowledgement 1`] = ` + + + + + + +
    + +
    + +

    + + Upload your license + +

    +
    + +
    + + + +
    +
    +
    +
    + +
    +
    +
    + Confirm License Upload +
    +
    +
    +
    +
    +
    +
    + Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. +
    +
    +
      +
    • + Watcher will be disabled +
    • +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + } + > + + } + confirmButtonText={ + + } + onCancel={[Function]} + onConfirm={[Function]} + title={ + + } + > + + + +
    +
    +
    + +
    + +
    +
    +
    + Confirm License Upload +
    +
    +
    +
    +
    +
    +
    + Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. +
    +
    +
      +
    • + Watcher will be disabled +
    • +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + > + +
    + +
    +
    +
    + Confirm License Upload +
    +
    +
    +
    +
    +
    +
    + Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. +
    +
    +
      +
    • + Watcher will be disabled +
    • +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + /> + +
    + + + + + +
    + +
    + +
    + + Confirm License Upload + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    + Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. +
    +
    + +
    +
      +
    • + Watcher will be disabled +
    • +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + + + + +
    +
    +
    +
    +
    +
    + + + + + + + +
    +

    + + Your license key is a JSON file with a signature attached. + +

    +

    + + + , + } + } + > + Uploading a license will replace your current + + license. + +

    +
    +
    + +
    + + +
    + +
    + +
    + +
    + + } + onChange={[Function]} + > + +
    +
    + + + +
    +
    +
    + + +
    + +
    + +
    + + +
    + + +
    + + +
    + + + + +
    + + + +
    +
    +
    +
    +
    + +
    + +
    + + + + + + +`; + +exports[`UploadLicense should display an error when ES says license is expired 1`] = ` + + + + + + +
    + +
    + +

    + + Upload your license + +

    +
    + +
    + + +
    +

    + + Your license key is a JSON file with a signature attached. + +

    +

    + + + , + } + } + > + Uploading a license will replace your current + + license. + +

    +
    + + +
    + + +
    + + +
    +
    + + Please address the errors in your form. + +
    + +
    +
      +
    • + The supplied license has expired. +
    • +
    +
    +
    +
    +
    +
    + +
    + +
    + +
    + + } + onChange={[Function]} + > + +
    +
    + + + +
    +
    +
    + + +
    + +
    + +
    + + +
    + + +
    + + +
    + + + + +
    + + + +
    +
    +
    +
    +
    + +
    + +
    + + + + + + +`; + +exports[`UploadLicense should display an error when ES says license is invalid 1`] = ` + + + + + + +
    + +
    + +

    + + Upload your license + +

    +
    + +
    + + +
    +

    + + Your license key is a JSON file with a signature attached. + +

    +

    + + + , + } + } + > + Uploading a license will replace your current + + license. + +

    +
    + + +
    + + +
    + + +
    +
    + + Please address the errors in your form. + +
    + +
    +
      +
    • + The supplied license is not valid for this product. +
    • +
    +
    +
    +
    +
    +
    + +
    + +
    + +
    + + } + onChange={[Function]} + > + +
    +
    + + + +
    +
    +
    + + +
    + +
    + +
    + + +
    + + +
    + + +
    + + + + +
    + + + +
    +
    +
    +
    +
    + +
    + +
    + + + + + + +`; + +exports[`UploadLicense should display an error when submitting invalid JSON 1`] = ` + + + + + + +
    + +
    + +

    + + Upload your license + +

    +
    + +
    + + +
    +

    + + Your license key is a JSON file with a signature attached. + +

    +

    + + + , + } + } + > + Uploading a license will replace your current + + license. + +

    +
    + + +
    + + +
    + + +
    +
    + + Please address the errors in your form. + +
    + +
    +
      +
    • + Error encountered uploading license: Check your license file. +
    • +
    +
    +
    +
    +
    +
    + +
    + +
    + +
    + + } + onChange={[Function]} + > + +
    +
    + + + +
    +
    +
    + + +
    + +
    + +
    + + +
    + + +
    + + +
    + + + + +
    + + + +
    +
    +
    +
    +
    + +
    + +
    + + + + + + +`; + +exports[`UploadLicense should display error when ES returns error 1`] = ` + + + + + + +
    + +
    + +

    + + Upload your license + +

    +
    + +
    + + +
    +

    + + Your license key is a JSON file with a signature attached. + +

    +

    + + + , + } + } + > + Uploading a license will replace your current + + license. + +

    +
    + + +
    + + +
    + + +
    +
    + + Please address the errors in your form. + +
    + +
    +
      +
    • + Error encountered uploading license: Can not upgrade to a production license unless TLS is configured or security is disabled +
    • +
    +
    +
    +
    +
    +
    + +
    + +
    + +
    + + } + onChange={[Function]} + > + +
    +
    + + + +
    +
    +
    + + +
    + +
    + +
    + + +
    + + +
    + + +
    + + + + +
    + + + +
    +
    +
    +
    +
    + +
    + +
    + + + + + + +`; diff --git a/x-pack/legacy/plugins/license_management/__jest__/add_license.test.js b/x-pack/plugins/license_management/__jest__/add_license.test.js similarity index 90% rename from x-pack/legacy/plugins/license_management/__jest__/add_license.test.js rename to x-pack/plugins/license_management/__jest__/add_license.test.js index 6ffb43025ff5..070d4df98a90 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/add_license.test.js +++ b/x-pack/plugins/license_management/__jest__/add_license.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AddLicense } from '../public/np_ready/application/sections/license_dashboard/add_license'; +import { AddLicense } from '../public/application/sections/license_dashboard/add_license'; import { createMockLicense, getComponent } from './util'; jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); diff --git a/x-pack/legacy/plugins/license_management/__jest__/api_responses/index.js b/x-pack/plugins/license_management/__jest__/api_responses/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/__jest__/api_responses/index.js rename to x-pack/plugins/license_management/__jest__/api_responses/index.js diff --git a/x-pack/legacy/plugins/license_management/__jest__/api_responses/upload_license.js b/x-pack/plugins/license_management/__jest__/api_responses/upload_license.js similarity index 100% rename from x-pack/legacy/plugins/license_management/__jest__/api_responses/upload_license.js rename to x-pack/plugins/license_management/__jest__/api_responses/upload_license.js diff --git a/x-pack/legacy/plugins/license_management/__jest__/license_status.test.js b/x-pack/plugins/license_management/__jest__/license_status.test.js similarity index 88% rename from x-pack/legacy/plugins/license_management/__jest__/license_status.test.js rename to x-pack/plugins/license_management/__jest__/license_status.test.js index f44d5c1f138b..dc7dc7d00f49 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/license_status.test.js +++ b/x-pack/plugins/license_management/__jest__/license_status.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LicenseStatus } from '../public/np_ready/application/sections/license_dashboard/license_status'; +import { LicenseStatus } from '../public/application/sections/license_dashboard/license_status'; import { createMockLicense, getComponent } from './util'; describe('LicenseStatus component', () => { diff --git a/x-pack/legacy/plugins/license_management/__jest__/request_trial_extension.test.js b/x-pack/plugins/license_management/__jest__/request_trial_extension.test.js similarity index 96% rename from x-pack/legacy/plugins/license_management/__jest__/request_trial_extension.test.js rename to x-pack/plugins/license_management/__jest__/request_trial_extension.test.js index a74a7b16185c..6d5a9fdd3fb3 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/request_trial_extension.test.js +++ b/x-pack/plugins/license_management/__jest__/request_trial_extension.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestTrialExtension } from '../public/np_ready/application/sections/license_dashboard/request_trial_extension'; +import { RequestTrialExtension } from '../public/application/sections/license_dashboard/request_trial_extension'; import { createMockLicense, getComponent } from './util'; jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); diff --git a/x-pack/legacy/plugins/license_management/__jest__/revert_to_basic.test.js b/x-pack/plugins/license_management/__jest__/revert_to_basic.test.js similarity index 94% rename from x-pack/legacy/plugins/license_management/__jest__/revert_to_basic.test.js rename to x-pack/plugins/license_management/__jest__/revert_to_basic.test.js index 488279d87ece..c223c39a8f12 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/revert_to_basic.test.js +++ b/x-pack/plugins/license_management/__jest__/revert_to_basic.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RevertToBasic } from '../public/np_ready/application/sections/license_dashboard/revert_to_basic'; +import { RevertToBasic } from '../public/application/sections/license_dashboard/revert_to_basic'; import { createMockLicense, getComponent } from './util'; jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); diff --git a/x-pack/legacy/plugins/license_management/__jest__/start_trial.test.js b/x-pack/plugins/license_management/__jest__/start_trial.test.js similarity index 96% rename from x-pack/legacy/plugins/license_management/__jest__/start_trial.test.js rename to x-pack/plugins/license_management/__jest__/start_trial.test.js index 5436a51a2632..5bd005bc1adb 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/start_trial.test.js +++ b/x-pack/plugins/license_management/__jest__/start_trial.test.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { StartTrial } from '../public/np_ready/application/sections/license_dashboard/start_trial'; +import { StartTrial } from '../public/application/sections/license_dashboard/start_trial'; import { createMockLicense, getComponent } from './util'; -jest.mock('ui/new_platform'); + jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); describe('StartTrial component when trial is allowed', () => { diff --git a/x-pack/legacy/plugins/license_management/__jest__/upload_license.test.tsx b/x-pack/plugins/license_management/__jest__/upload_license.test.tsx similarity index 55% rename from x-pack/legacy/plugins/license_management/__jest__/upload_license.test.tsx rename to x-pack/plugins/license_management/__jest__/upload_license.test.tsx index ca9b5b0db9ca..ad2fbd288e9f 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/upload_license.test.tsx +++ b/x-pack/plugins/license_management/__jest__/upload_license.test.tsx @@ -4,21 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServiceMock, chromeServiceMock } from '../../../../../src/core/public/mocks'; -import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; import React from 'react'; import { Provider } from 'react-redux'; - -jest.mock('ui/new_platform'); +import { httpServiceMock } from '../../../../src/core/public/mocks'; +import { mountWithIntl } from '../../../test_utils/enzyme_helpers'; // @ts-ignore -import { uploadLicense } from '../public/np_ready/application/store/actions/upload_license'; +import { uploadLicense } from '../public/application/store/actions/upload_license'; // @ts-ignore -import { licenseManagementStore } from '../public/np_ready/application/store/store'; +import { licenseManagementStore } from '../public/application/store/store'; // @ts-ignore -import { UploadLicense } from '../public/np_ready/application/sections/upload_license'; +import { UploadLicense } from '../public/application/sections/upload_license'; +import { AppContextProvider } from '../public/application/app_context'; import { UPLOAD_LICENSE_EXPIRED, @@ -33,36 +32,43 @@ window.location.reload = () => {}; let store: any = null; let component: any = null; -const services = { - legacy: { - xPackInfo: { + +const appDependencies = { + plugins: { + licensing: { refresh: jest.fn(), - get: () => { - return { license: { type: 'basic' } }; - }, }, - refreshXpack: jest.fn(), }, + docLinks: {}, +}; + +const thunkServices = { http: httpServiceMock.createSetupContract(), - chrome: chromeServiceMock.createStartContract(), history: { replace: jest.fn(), }, + breadcrumbService: { + setBreadcrumbs() {}, + }, + licensing: appDependencies.plugins.licensing, }; describe('UploadLicense', () => { beforeEach(() => { - store = licenseManagementStore({}, services); + store = licenseManagementStore({}, thunkServices); component = ( - - - + + + + + ); + appDependencies.plugins.licensing.refresh.mockResolvedValue({}); }); afterEach(() => { - services.legacy.xPackInfo.refresh.mockReset(); - services.history.replace.mockReset(); + appDependencies.plugins.licensing.refresh.mockReset(); + thunkServices.history.replace.mockReset(); jest.clearAllMocks(); }); @@ -74,46 +80,46 @@ describe('UploadLicense', () => { }); it('should display an error when ES says license is invalid', async () => { - services.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_INVALID[2])); + thunkServices.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_INVALID[2])); const rendered = mountWithIntl(component); const invalidLicense = JSON.stringify({ license: { type: 'basic' } }); - await uploadLicense(invalidLicense)(store.dispatch, null, services); + await uploadLicense(invalidLicense)(store.dispatch, null, thunkServices); rendered.update(); expect(rendered).toMatchSnapshot(); }); it('should display an error when ES says license is expired', async () => { - services.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_EXPIRED[2])); + thunkServices.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_EXPIRED[2])); const rendered = mountWithIntl(component); const invalidLicense = JSON.stringify({ license: { type: 'basic' } }); - await uploadLicense(invalidLicense)(store.dispatch, null, services); + await uploadLicense(invalidLicense)(store.dispatch, null, thunkServices); rendered.update(); expect(rendered).toMatchSnapshot(); }); it('should display a modal when license requires acknowledgement', async () => { - services.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_REQUIRES_ACK[2])); + thunkServices.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_REQUIRES_ACK[2])); const unacknowledgedLicense = JSON.stringify({ license: { type: 'basic' }, }); - await uploadLicense(unacknowledgedLicense, 'trial')(store.dispatch, null, services); + await uploadLicense(unacknowledgedLicense, 'trial')(store.dispatch, null, thunkServices); const rendered = mountWithIntl(component); expect(rendered).toMatchSnapshot(); }); it('should refresh xpack info and navigate to BASE_PATH when ES accepts new license', async () => { - services.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_SUCCESS[2])); + thunkServices.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_SUCCESS[2])); const validLicense = JSON.stringify({ license: { type: 'basic' } }); - await uploadLicense(validLicense)(store.dispatch, null, services); - expect(services.legacy.refreshXpack).toHaveBeenCalled(); - expect(services.history.replace).toHaveBeenCalled(); + await uploadLicense(validLicense)(store.dispatch, null, thunkServices); + expect(appDependencies.plugins.licensing.refresh).toHaveBeenCalled(); + expect(thunkServices.history.replace).toHaveBeenCalled(); }); it('should display error when ES returns error', async () => { - services.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_TLS_NOT_ENABLED[2])); + thunkServices.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_TLS_NOT_ENABLED[2])); const rendered = mountWithIntl(component); const license = JSON.stringify({ license: { type: 'basic' } }); - await uploadLicense(license)(store.dispatch, null, services); + await uploadLicense(license)(store.dispatch, null, thunkServices); rendered.update(); expect(rendered).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/license_management/__jest__/util/index.js b/x-pack/plugins/license_management/__jest__/util/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/__jest__/util/index.js rename to x-pack/plugins/license_management/__jest__/util/index.js diff --git a/x-pack/legacy/plugins/license_management/__jest__/util/util.js b/x-pack/plugins/license_management/__jest__/util/util.js similarity index 60% rename from x-pack/legacy/plugins/license_management/__jest__/util/util.js rename to x-pack/plugins/license_management/__jest__/util/util.js index 93b97c51b24d..5a7e49c8c331 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/util/util.js +++ b/x-pack/plugins/license_management/__jest__/util/util.js @@ -3,15 +3,22 @@ * 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 @kbn/eslint/no-restricted-paths */ -import { Provider } from 'react-redux'; -import { licenseManagementStore } from '../../public/np_ready/application/store/store'; import React from 'react'; -import { mountWithIntl } from '../../../../../test_utils/enzyme_helpers'; -import { httpServiceMock } from '../../../../../../src/core/public/mocks'; +import { Provider } from 'react-redux'; + +import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; +import { httpServiceMock } from '../../../../../src/core/public/mocks'; +import { licenseManagementStore } from '../../public/application/store/store'; +import { AppContextProvider } from '../../public/application/app_context'; const highExpirationMillis = new Date('October 13, 2099 00:00:00Z').getTime(); +const appDependencies = { + docLinks: {}, +}; + export const createMockLicense = (type, expiryDateInMillis = highExpirationMillis) => { return { type, @@ -19,14 +26,17 @@ export const createMockLicense = (type, expiryDateInMillis = highExpirationMilli isActive: new Date().getTime() < expiryDateInMillis, }; }; + export const getComponent = (initialState, Component) => { const services = { http: httpServiceMock.createSetupContract(), }; const store = licenseManagementStore(initialState, services); return mountWithIntl( - - - + + + + + ); }; diff --git a/x-pack/legacy/plugins/license_management/__mocks__/focus-trap-react.js b/x-pack/plugins/license_management/__mocks__/focus-trap-react.js similarity index 100% rename from x-pack/legacy/plugins/license_management/__mocks__/focus-trap-react.js rename to x-pack/plugins/license_management/__mocks__/focus-trap-react.js diff --git a/x-pack/legacy/plugins/license_management/common/constants/base_path.ts b/x-pack/plugins/license_management/common/constants/base_path.ts similarity index 87% rename from x-pack/legacy/plugins/license_management/common/constants/base_path.ts rename to x-pack/plugins/license_management/common/constants/base_path.ts index 9b24ab561dba..7b981ec8727e 100644 --- a/x-pack/legacy/plugins/license_management/common/constants/base_path.ts +++ b/x-pack/plugins/license_management/common/constants/base_path.ts @@ -5,3 +5,5 @@ */ export const BASE_PATH = '/management/elasticsearch/license_management/'; + +export const API_BASE_PATH = '/api/license'; diff --git a/x-pack/legacy/plugins/license_management/common/constants/external_links.ts b/x-pack/plugins/license_management/common/constants/external_links.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/common/constants/external_links.ts rename to x-pack/plugins/license_management/common/constants/external_links.ts diff --git a/x-pack/legacy/plugins/license_management/common/constants/index.ts b/x-pack/plugins/license_management/common/constants/index.ts similarity index 87% rename from x-pack/legacy/plugins/license_management/common/constants/index.ts rename to x-pack/plugins/license_management/common/constants/index.ts index c115fb7b69c0..ec411fea4b7a 100644 --- a/x-pack/legacy/plugins/license_management/common/constants/index.ts +++ b/x-pack/plugins/license_management/common/constants/index.ts @@ -5,6 +5,6 @@ */ export { PLUGIN } from './plugin'; -export { BASE_PATH } from './base_path'; +export { BASE_PATH, API_BASE_PATH } from './base_path'; export { EXTERNAL_LINKS } from './external_links'; export { APP_PERMISSION } from './permissions'; diff --git a/x-pack/legacy/plugins/license_management/common/constants/permissions.ts b/x-pack/plugins/license_management/common/constants/permissions.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/common/constants/permissions.ts rename to x-pack/plugins/license_management/common/constants/permissions.ts diff --git a/x-pack/legacy/plugins/license_management/common/constants/plugin.ts b/x-pack/plugins/license_management/common/constants/plugin.ts similarity index 79% rename from x-pack/legacy/plugins/license_management/common/constants/plugin.ts rename to x-pack/plugins/license_management/common/constants/plugin.ts index 14b591e3834e..406ac867a77b 100644 --- a/x-pack/legacy/plugins/license_management/common/constants/plugin.ts +++ b/x-pack/plugins/license_management/common/constants/plugin.ts @@ -6,8 +6,8 @@ import { i18n } from '@kbn/i18n'; export const PLUGIN = { - TITLE: i18n.translate('xpack.licenseMgmt.managementSectionDisplayName', { + title: i18n.translate('xpack.licenseMgmt.managementSectionDisplayName', { defaultMessage: 'License Management', }), - ID: 'license_management', + id: 'license_management', }; diff --git a/x-pack/plugins/license_management/kibana.json b/x-pack/plugins/license_management/kibana.json new file mode 100644 index 000000000000..be28c8e978d8 --- /dev/null +++ b/x-pack/plugins/license_management/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "licenseManagement", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["home", "licensing", "management"], + "optionalPlugins": ["telemetry"], + "configPath": ["xpack", "license_management"] +} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/_license_management.scss b/x-pack/plugins/license_management/public/application/_license_management.scss similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/_license_management.scss rename to x-pack/plugins/license_management/public/application/_license_management.scss diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/app.container.js b/x-pack/plugins/license_management/public/application/app.container.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/app.container.js rename to x-pack/plugins/license_management/public/application/app.container.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js b/x-pack/plugins/license_management/public/application/app.js similarity index 97% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/app.js rename to x-pack/plugins/license_management/public/application/app.js index 6a6c38fa6abb..1bc8e9cd563e 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js +++ b/x-pack/plugins/license_management/public/application/app.js @@ -8,7 +8,7 @@ import React, { Component } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { LicenseDashboard, UploadLicense } from './sections'; import { Switch, Route } from 'react-router-dom'; -import { APP_PERMISSION, BASE_PATH } from '../../../common/constants'; +import { APP_PERMISSION, BASE_PATH } from '../../common/constants'; import { EuiPageBody, EuiEmptyPrompt, EuiText, EuiLoadingSpinner, EuiCallOut } from '@elastic/eui'; export class App extends Component { diff --git a/x-pack/plugins/license_management/public/application/app_context.tsx b/x-pack/plugins/license_management/public/application/app_context.tsx new file mode 100644 index 000000000000..1e90f4c907b8 --- /dev/null +++ b/x-pack/plugins/license_management/public/application/app_context.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { createContext, useContext } from 'react'; + +import { CoreStart } from '../../../../../src/core/public'; +import { LicensingPluginSetup, ILicense } from '../../../licensing/public'; +import { TelemetryPluginSetup } from '../../../../../src/plugins/telemetry/public'; +import { ClientConfigType } from '../types'; +import { BreadcrumbService } from './breadcrumbs'; + +const AppContext = createContext(undefined); + +export interface AppDependencies { + core: CoreStart; + services: { + breadcrumbService: BreadcrumbService; + }; + plugins: { + licensing: LicensingPluginSetup; + telemetry?: TelemetryPluginSetup; + }; + docLinks: { + security: string; + }; + store: { + initialLicense: ILicense; + }; + config: ClientConfigType; +} + +export const AppContextProvider = ({ + children, + value, +}: { + value: AppDependencies; + children: React.ReactNode; +}) => { + return {children}; +}; + +export const AppContextConsumer = AppContext.Consumer; + +export const useAppContext = () => { + const ctx = useContext(AppContext); + if (!ctx) { + throw new Error('"useAppContext" can only be called inside of AppContext.Provider!'); + } + return ctx; +}; diff --git a/x-pack/plugins/license_management/public/application/app_providers.tsx b/x-pack/plugins/license_management/public/application/app_providers.tsx new file mode 100644 index 000000000000..9f9fd2a8275d --- /dev/null +++ b/x-pack/plugins/license_management/public/application/app_providers.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import * as history from 'history'; +import { Provider } from 'react-redux'; + +import { BASE_PATH } from '../../common/constants'; +import { AppContextProvider, AppDependencies } from './app_context'; +// @ts-ignore +import { licenseManagementStore } from './store'; + +interface Props { + appDependencies: AppDependencies; + children: React.ReactNode; +} + +export const AppProviders = ({ appDependencies, children }: Props) => { + const { + core, + plugins, + services, + store: { initialLicense }, + } = appDependencies; + + const { + http, + notifications: { toasts }, + i18n: { Context: I18nContext }, + } = core; + + // Setup Redux store + const thunkServices = { + // So we can imperatively control the hash route + history: history.createHashHistory({ basename: BASE_PATH }), + toasts, + http, + telemetry: plugins.telemetry, + licensing: plugins.licensing, + breadcrumbService: services.breadcrumbService, + }; + const initialState = { license: initialLicense }; + + const store = licenseManagementStore(initialState, thunkServices); + + return ( + + + {children} + + + ); +}; diff --git a/x-pack/plugins/license_management/public/application/breadcrumbs.ts b/x-pack/plugins/license_management/public/application/breadcrumbs.ts new file mode 100644 index 000000000000..b1773a10f01b --- /dev/null +++ b/x-pack/plugins/license_management/public/application/breadcrumbs.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { ManagementAppMountParams } from '../../../../../src/plugins/management/public'; +import { BASE_PATH } from '../../common/constants'; + +type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; + +export class BreadcrumbService { + private breadcrumbs: { + [key: string]: Array<{ + text: string; + href?: string; + }>; + } = { + dashboard: [], + upload: [], + }; + private setBreadcrumbsHandler?: SetBreadcrumbs; + + public setup(setBreadcrumbsHandler: SetBreadcrumbs): void { + this.setBreadcrumbsHandler = setBreadcrumbsHandler; + + // Home and sections + this.breadcrumbs.dashboard = [ + { + text: i18n.translate('xpack.licenseMgmt.dashboard.breadcrumb', { + defaultMessage: 'License management', + }), + href: `#${BASE_PATH}home`, + }, + ]; + + this.breadcrumbs.upload = [ + ...this.breadcrumbs.dashboard, + { + text: i18n.translate('xpack.licenseMgmt.upload.breadcrumb', { + defaultMessage: 'Upload', + }), + }, + ]; + } + + public setBreadcrumbs(type: 'dashboard' | 'upload'): void { + if (!this.setBreadcrumbsHandler) { + throw new Error(`BreadcrumbService#setup() must be called first!`); + } + + const newBreadcrumbs = this.breadcrumbs[type] + ? [...this.breadcrumbs[type]] + : [...this.breadcrumbs.home]; + + // Pop off last breadcrumb + const lastBreadcrumb = newBreadcrumbs.pop() as { + text: string; + href?: string; + }; + + // Put last breadcrumb back without href + newBreadcrumbs.push({ + ...lastBreadcrumb, + href: undefined, + }); + + this.setBreadcrumbsHandler(newBreadcrumbs); + } +} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.ts b/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/index.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.ts rename to x-pack/plugins/license_management/public/application/components/telemetry_opt_in/index.ts diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx b/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/telemetry_opt_in.tsx similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx rename to x-pack/plugins/license_management/public/application/components/telemetry_opt_in/telemetry_opt_in.tsx diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/index.scss b/x-pack/plugins/license_management/public/application/index.scss similarity index 63% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/index.scss rename to x-pack/plugins/license_management/public/application/index.scss index 4fb8aafcca93..92150eea4021 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/index.scss +++ b/x-pack/plugins/license_management/public/application/index.scss @@ -1,7 +1,4 @@ -// EUI globals -@import 'src/legacy/ui/public/styles/styling_constants'; - -// License amnagement plugin styles +// License management plugin styles // Prefix all styles with "lic" to avoid conflicts. // Examples diff --git a/x-pack/plugins/license_management/public/application/index.tsx b/x-pack/plugins/license_management/public/application/index.tsx new file mode 100644 index 000000000000..75f2f98f51e6 --- /dev/null +++ b/x-pack/plugins/license_management/public/application/index.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter } from 'react-router-dom'; + +import { AppDependencies } from './app_context'; +import { AppProviders } from './app_providers'; +// @ts-ignore +import { App } from './app.container'; + +const AppWithRouter = (props: { [key: string]: any }) => ( + + + +); + +export const renderApp = (element: Element, dependencies: AppDependencies) => { + render( + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; +}; + +export { AppDependencies }; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/es.ts b/x-pack/plugins/license_management/public/application/lib/es.ts similarity index 79% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/lib/es.ts rename to x-pack/plugins/license_management/public/application/lib/es.ts index 3924de2202d5..52df5c250922 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/es.ts +++ b/x-pack/plugins/license_management/public/application/lib/es.ts @@ -5,11 +5,10 @@ */ import { HttpSetup } from 'src/core/public'; - -const BASE_PATH = '/api/license'; +import { API_BASE_PATH } from '../../../common/constants'; export function putLicense(http: HttpSetup, license: string, acknowledge: boolean) { - return http.put(BASE_PATH, { + return http.put(API_BASE_PATH, { query: { acknowledge: acknowledge ? 'true' : '', }, @@ -22,7 +21,7 @@ export function putLicense(http: HttpSetup, license: string, acknowledge: boolea } export function startBasic(http: HttpSetup, acknowledge: boolean) { - return http.post(`${BASE_PATH}/start_basic`, { + return http.post(`${API_BASE_PATH}/start_basic`, { query: { acknowledge: acknowledge ? 'true' : '', }, @@ -35,7 +34,7 @@ export function startBasic(http: HttpSetup, acknowledge: boolean) { } export function startTrial(http: HttpSetup) { - return http.post(`${BASE_PATH}/start_trial`, { + return http.post(`${API_BASE_PATH}/start_trial`, { headers: { contentType: 'application/json', }, @@ -44,7 +43,7 @@ export function startTrial(http: HttpSetup) { } export function canStartTrial(http: HttpSetup) { - return http.get(`${BASE_PATH}/start_trial`, { + return http.get(`${API_BASE_PATH}/start_trial`, { headers: { contentType: 'application/json', }, @@ -53,7 +52,7 @@ export function canStartTrial(http: HttpSetup) { } export function getPermissions(http: HttpSetup) { - return http.post(`${BASE_PATH}/permissions`, { + return http.post(`${API_BASE_PATH}/permissions`, { headers: { contentType: 'application/json', }, diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts b/x-pack/plugins/license_management/public/application/lib/telemetry.ts similarity index 69% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts rename to x-pack/plugins/license_management/public/application/lib/telemetry.ts index 9cc4ec5978fd..1d90fce6f6b9 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts +++ b/x-pack/plugins/license_management/public/application/lib/telemetry.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TelemetryPluginSetup } from '../../../../../../../../src/plugins/telemetry/public'; +import { TelemetryPluginSetup } from '../../../../../../src/plugins/telemetry/public'; -export { OptInExampleFlyout } from '../../../../../../../../src/plugins/telemetry/public/components'; -export { PRIVACY_STATEMENT_URL } from '../../../../../../../../src/plugins/telemetry/common/constants'; +export { OptInExampleFlyout } from '../../../../../../src/plugins/telemetry/public/components'; +export { PRIVACY_STATEMENT_URL } from '../../../../../../src/plugins/telemetry/common/constants'; export { TelemetryPluginSetup, shouldShowTelemetryOptIn }; function shouldShowTelemetryOptIn( diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/index.js b/x-pack/plugins/license_management/public/application/sections/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/index.js rename to x-pack/plugins/license_management/public/application/sections/index.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/add_license/add_license.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js similarity index 94% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/add_license/add_license.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js index d2f44bfc701f..158702e1286a 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/add_license/add_license.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { BASE_PATH } from '../../../../../../common/constants'; +import { BASE_PATH } from '../../../../../common/constants'; import { EuiCard, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/add_license/index.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/add_license/index.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/index.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/index.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/index.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/index.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.container.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_dashboard.container.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.container.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/license_dashboard.container.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_dashboard.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/license_dashboard.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/index.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/index.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/license_status.container.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/license_status.container.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/license_status.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/license_status.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/index.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/index.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/index.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/request_trial_extension.container.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.container.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/request_trial_extension.container.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.container.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js similarity index 96% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js index fae454cbaac5..fb1ea026abaa 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js @@ -8,7 +8,7 @@ import React from 'react'; import { EuiFlexItem, EuiCard, EuiLink, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EXTERNAL_LINKS } from '../../../../../../common/constants'; +import { EXTERNAL_LINKS } from '../../../../../common/constants'; export const RequestTrialExtension = ({ shouldShowRequestTrialExtension }) => { if (!shouldShowRequestTrialExtension) { diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/index.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/index.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/index.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/revert_to_basic.container.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.container.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/revert_to_basic.container.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.container.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js similarity index 98% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js index 9115e82833ee..2424e336fe6e 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js @@ -16,7 +16,7 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EXTERNAL_LINKS } from '../../../../../../common/constants'; +import { EXTERNAL_LINKS } from '../../../../../common/constants'; export class RevertToBasic extends React.PureComponent { cancel = () => { diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/index.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/index.ts diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.container.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.container.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.container.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.container.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx similarity index 92% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx index e0f8ade8e45d..25cbfb724223 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx @@ -24,8 +24,8 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { TelemetryOptIn } from '../../../components/telemetry_opt_in'; -import { EXTERNAL_LINKS } from '../../../../../../common/constants'; -import { getDocLinks } from '../../../lib/docs_links'; +import { EXTERNAL_LINKS } from '../../../../../common/constants'; +import { AppContextConsumer, AppDependencies } from '../../../app_context'; import { TelemetryPluginSetup, shouldShowTelemetryOptIn } from '../../../lib/telemetry'; interface Props { @@ -68,7 +68,7 @@ export class StartTrial extends Component { cancel = () => { this.setState({ showConfirmation: false }); }; - acknowledgeModal() { + acknowledgeModal(docLinks: AppDependencies['docLinks']) { const { showConfirmation, isOptingInToTelemetry } = this.state; const { telemetry } = this.props; @@ -148,7 +148,7 @@ export class StartTrial extends Component { values={{ authenticationTypeList: 'AD/LDAP, SAML, PKI, SAML/SSO', securityDocumentationLinkText: ( - + { ); return ( - - {this.acknowledgeModal()} - + {dependencies => ( + + {this.acknowledgeModal(dependencies!.docLinks)} + + } + description={description} + footer={footer} /> - } - description={description} - footer={footer} - /> - + + )} + ); } } diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/upload_license/index.js b/x-pack/plugins/license_management/public/application/sections/upload_license/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/upload_license/index.js rename to x-pack/plugins/license_management/public/application/sections/upload_license/index.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/upload_license/upload_license.container.js b/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.container.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/upload_license/upload_license.container.js rename to x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.container.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/upload_license/upload_license.js b/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js similarity index 99% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/upload_license/upload_license.js rename to x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js index e8dd9495a8c2..49f2474f8391 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/upload_license/upload_license.js +++ b/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js @@ -5,7 +5,7 @@ */ import React, { Fragment } from 'react'; -import { BASE_PATH } from '../../../../../common/constants'; +import { BASE_PATH } from '../../../../common/constants'; import { EuiButton, EuiButtonEmpty, diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/add_error_message.js b/x-pack/plugins/license_management/public/application/store/actions/add_error_message.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/add_error_message.js rename to x-pack/plugins/license_management/public/application/store/actions/add_error_message.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/add_license.js b/x-pack/plugins/license_management/public/application/store/actions/add_license.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/add_license.js rename to x-pack/plugins/license_management/public/application/store/actions/add_license.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/index.js b/x-pack/plugins/license_management/public/application/store/actions/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/index.js rename to x-pack/plugins/license_management/public/application/store/actions/index.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/permissions.js b/x-pack/plugins/license_management/public/application/store/actions/permissions.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/permissions.js rename to x-pack/plugins/license_management/public/application/store/actions/permissions.js diff --git a/x-pack/plugins/license_management/public/application/store/actions/set_breadcrumb.ts b/x-pack/plugins/license_management/public/application/store/actions/set_breadcrumb.ts new file mode 100644 index 000000000000..2c6a726203bc --- /dev/null +++ b/x-pack/plugins/license_management/public/application/store/actions/set_breadcrumb.ts @@ -0,0 +1,17 @@ +/* + * 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 { ThunkAction } from 'redux-thunk'; +import { BreadcrumbService } from '../../breadcrumbs'; + +export const setBreadcrumb = ( + section: 'dashboard' | 'upload' +): ThunkAction => ( + dispatch, + getState, + { breadcrumbService } +) => { + breadcrumbService.setBreadcrumbs(section); +}; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/start_basic.js b/x-pack/plugins/license_management/public/application/store/actions/start_basic.js similarity index 95% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/start_basic.js rename to x-pack/plugins/license_management/public/application/store/actions/start_basic.js index 5bc9e8fad07b..93c722c1f896 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/start_basic.js +++ b/x-pack/plugins/license_management/public/application/store/actions/start_basic.js @@ -19,7 +19,7 @@ export const cancelStartBasicLicense = createAction( export const startBasicLicense = (currentLicenseType, ack) => async ( dispatch, getState, - { legacy: { refreshXpack }, toasts, http } + { licensing, toasts, http } ) => { /*eslint camelcase: 0*/ const { acknowledged, basic_was_started, error_message, acknowledge } = await startBasic( @@ -28,7 +28,7 @@ export const startBasicLicense = (currentLicenseType, ack) => async ( ); if (acknowledged) { if (basic_was_started) { - await refreshXpack(); + await licensing.refresh(); // reload necessary to get left nav to refresh with proper links window.location.reload(); } else { diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/start_trial.js b/x-pack/plugins/license_management/public/application/store/actions/start_trial.js similarity index 85% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/start_trial.js rename to x-pack/plugins/license_management/public/application/store/actions/start_trial.js index c8ec538e846e..3bae271b213c 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/start_trial.js +++ b/x-pack/plugins/license_management/public/application/store/actions/start_trial.js @@ -14,15 +14,11 @@ export const loadTrialStatus = () => async (dispatch, getState, { http }) => { dispatch(trialStatusLoaded(trialOK)); }; -export const startLicenseTrial = () => async ( - dispatch, - getState, - { legacy: { refreshXpack }, toasts, http } -) => { +export const startLicenseTrial = () => async (dispatch, getState, { licensing, toasts, http }) => { /*eslint camelcase: 0*/ const { trial_was_started, error_message } = await startTrial(http); if (trial_was_started) { - await refreshXpack(); + await licensing.refresh(); // reload necessary to get left nav to refresh with proper links window.location.reload(); } else { diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/upload_license.js b/x-pack/plugins/license_management/public/application/store/actions/upload_license.js similarity index 96% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/upload_license.js rename to x-pack/plugins/license_management/public/application/store/actions/upload_license.js index 51b3af2b6308..376a22d3d1ef 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/upload_license.js +++ b/x-pack/plugins/license_management/public/application/store/actions/upload_license.js @@ -24,7 +24,7 @@ const dispatchFromResponse = async ( dispatch, currentLicenseType, newLicenseType, - { history, legacy: { xPackInfo, refreshXpack } } + { history, licensing } ) => { const { error, acknowledged, license_status: licenseStatus, acknowledge } = response; if (error) { @@ -50,8 +50,8 @@ const dispatchFromResponse = async ( ) ); } else { - await refreshXpack(); - dispatch(addLicense(xPackInfo.get('license'))); + const updatedLicense = await licensing.refresh(); + dispatch(addLicense(updatedLicense)); dispatch(uploadLicenseStatus({})); history.replace('/home'); // reload necessary to get left nav to refresh with proper links diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/index.js b/x-pack/plugins/license_management/public/application/store/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/index.js rename to x-pack/plugins/license_management/public/application/store/index.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/index.js b/x-pack/plugins/license_management/public/application/store/reducers/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/index.js rename to x-pack/plugins/license_management/public/application/store/reducers/index.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/license.js b/x-pack/plugins/license_management/public/application/store/reducers/license.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/license.js rename to x-pack/plugins/license_management/public/application/store/reducers/license.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/license_management.js b/x-pack/plugins/license_management/public/application/store/reducers/license_management.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/license_management.js rename to x-pack/plugins/license_management/public/application/store/reducers/license_management.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/permissions.js b/x-pack/plugins/license_management/public/application/store/reducers/permissions.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/permissions.js rename to x-pack/plugins/license_management/public/application/store/reducers/permissions.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/start_basic_license_status.js b/x-pack/plugins/license_management/public/application/store/reducers/start_basic_license_status.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/start_basic_license_status.js rename to x-pack/plugins/license_management/public/application/store/reducers/start_basic_license_status.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/trial_status.js b/x-pack/plugins/license_management/public/application/store/reducers/trial_status.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/trial_status.js rename to x-pack/plugins/license_management/public/application/store/reducers/trial_status.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/upload_error_message.js b/x-pack/plugins/license_management/public/application/store/reducers/upload_error_message.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/upload_error_message.js rename to x-pack/plugins/license_management/public/application/store/reducers/upload_error_message.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/upload_status.js b/x-pack/plugins/license_management/public/application/store/reducers/upload_status.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/upload_status.js rename to x-pack/plugins/license_management/public/application/store/reducers/upload_status.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/store.js b/x-pack/plugins/license_management/public/application/store/store.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/store/store.js rename to x-pack/plugins/license_management/public/application/store/store.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/index.ts b/x-pack/plugins/license_management/public/index.ts similarity index 86% rename from x-pack/legacy/plugins/license_management/public/np_ready/index.ts rename to x-pack/plugins/license_management/public/index.ts index 59e2f02d8cb5..3c76549ebdc1 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/index.ts +++ b/x-pack/plugins/license_management/public/index.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { PluginInitializerContext } from 'src/core/public'; + import { LicenseManagementUIPlugin } from './plugin'; +import './application/index.scss'; -export const plugin = (ctx: PluginInitializerContext) => new LicenseManagementUIPlugin(); +export const plugin = (ctx: PluginInitializerContext) => new LicenseManagementUIPlugin(ctx); diff --git a/x-pack/plugins/license_management/public/plugin.ts b/x-pack/plugins/license_management/public/plugin.ts new file mode 100644 index 000000000000..00d353bc97e0 --- /dev/null +++ b/x-pack/plugins/license_management/public/plugin.ts @@ -0,0 +1,83 @@ +/* + * 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 { first } from 'rxjs/operators'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; + +import { TelemetryPluginSetup } from '../../../../src/plugins/telemetry/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; +import { LicensingPluginSetup } from '../../../plugins/licensing/public'; +import { PLUGIN } from '../common/constants'; +import { ClientConfigType } from './types'; +import { AppDependencies } from './application'; +import { BreadcrumbService } from './application/breadcrumbs'; + +interface PluginsDependencies { + management: ManagementSetup; + licensing: LicensingPluginSetup; + telemetry?: TelemetryPluginSetup; +} + +export class LicenseManagementUIPlugin implements Plugin { + private breadcrumbService = new BreadcrumbService(); + + constructor(private readonly initializerContext: PluginInitializerContext) {} + + setup(coreSetup: CoreSetup, plugins: PluginsDependencies) { + const config = this.initializerContext.config.get(); + + if (!config.ui.enabled) { + // No need to go any further + return; + } + + const { getStartServices } = coreSetup; + const { management, telemetry, licensing } = plugins; + + management.sections.getSection('elasticsearch')!.registerApp({ + id: PLUGIN.id, + title: PLUGIN.title, + order: 99, + mount: async ({ element, setBreadcrumbs }) => { + const [core] = await getStartServices(); + const initialLicense = await plugins.licensing.license$.pipe(first()).toPromise(); + + // Setup documentation links + const { docLinks } = core; + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; + const appDocLinks = { + security: `${esBase}/security-settings.html`, + }; + + // Setup services + this.breadcrumbService.setup(setBreadcrumbs); + + const appDependencies: AppDependencies = { + core, + config, + plugins: { + licensing, + telemetry, + }, + services: { + breadcrumbService: this.breadcrumbService, + }, + store: { + initialLicense, + }, + docLinks: appDocLinks, + }; + + const { renderApp } = await import('./application'); + + return renderApp(element, appDependencies); + }, + }); + } + + start() {} + stop() {} +} diff --git a/x-pack/legacy/plugins/license_management/public/legacy.ts b/x-pack/plugins/license_management/public/types.ts similarity index 78% rename from x-pack/legacy/plugins/license_management/public/legacy.ts rename to x-pack/plugins/license_management/public/types.ts index 0e7c3ae60c77..4213203bf42c 100644 --- a/x-pack/legacy/plugins/license_management/public/legacy.ts +++ b/x-pack/plugins/license_management/public/types.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './management_section'; -import './register_route'; +export interface ClientConfigType { + ui: { enabled: boolean }; +} diff --git a/x-pack/plugins/license_management/server/config.ts b/x-pack/plugins/license_management/server/config.ts new file mode 100644 index 000000000000..9bc39204a7c3 --- /dev/null +++ b/x-pack/plugins/license_management/server/config.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), +}); + +export type LicenseManagementConfig = TypeOf; diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/index.ts b/x-pack/plugins/license_management/server/index.ts similarity index 57% rename from x-pack/legacy/plugins/license_management/server/np_ready/index.ts rename to x-pack/plugins/license_management/server/index.ts index 2ad4143a9473..b378fffbce7e 100644 --- a/x-pack/legacy/plugins/license_management/server/np_ready/index.ts +++ b/x-pack/plugins/license_management/server/index.ts @@ -4,7 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; + import { LicenseManagementServerPlugin } from './plugin'; +import { configSchema, LicenseManagementConfig } from './config'; export const plugin = (ctx: PluginInitializerContext) => new LicenseManagementServerPlugin(); + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + ui: true, + }, +}; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/docs_links.ts b/x-pack/plugins/license_management/server/lib/is_es_error.ts similarity index 55% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/lib/docs_links.ts rename to x-pack/plugins/license_management/server/lib/is_es_error.ts index 761fcd2674df..4137293cf39c 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/docs_links.ts +++ b/x-pack/plugins/license_management/server/lib/is_es_error.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -let docLinks: Record = {}; +import * as legacyElasticsearch from 'elasticsearch'; -export const setDocLinks = (links: Record) => { - docLinks = links; -}; +const esErrorsParent = legacyElasticsearch.errors._Abstract; -export const getDocLinks = () => docLinks; +export function isEsError(err: Error) { + return err instanceof esErrorsParent; +} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/lib/license.ts b/x-pack/plugins/license_management/server/lib/license.ts similarity index 50% rename from x-pack/legacy/plugins/license_management/server/np_ready/lib/license.ts rename to x-pack/plugins/license_management/server/lib/license.ts index b52c9d50170b..d36365eb62a7 100644 --- a/x-pack/legacy/plugins/license_management/server/np_ready/lib/license.ts +++ b/x-pack/plugins/license_management/server/lib/license.ts @@ -3,29 +3,39 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'src/core/server'; -import { ElasticsearchPlugin } from '../../../../../../../src/legacy/core_plugins/elasticsearch'; +import { LicensingPluginSetup } from '../../../licensing/server'; +import { CallAsCurrentUser } from '../types'; + const getLicensePath = (acknowledge: boolean) => `/_license${acknowledge ? '?acknowledge=true' : ''}`; -export async function putLicense( - req: KibanaRequest, - elasticsearch: ElasticsearchPlugin, - xpackInfo: any -) { - const { acknowledge } = req.query; - const { callWithRequest } = elasticsearch.getCluster('admin'); +interface PutLicenseArg { + acknowledge: boolean; + callAsCurrentUser: CallAsCurrentUser; + licensing: LicensingPluginSetup; + license: { [key: string]: any }; +} + +export async function putLicense({ + acknowledge, + callAsCurrentUser, + licensing, + license, +}: PutLicenseArg) { const options = { method: 'POST', - path: getLicensePath(Boolean(acknowledge)), - body: req.body, + path: getLicensePath(acknowledge), + body: license, }; + try { - const response = await callWithRequest(req as any, 'transport.request', options); + const response = await callAsCurrentUser('transport.request', options); const { acknowledged, license_status: licenseStatus } = response; + if (acknowledged && licenseStatus === 'valid') { - await xpackInfo.refreshNow(); + await licensing.refresh(); } + return response; } catch (error) { return error.body; diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/lib/permissions.ts b/x-pack/plugins/license_management/server/lib/permissions.ts similarity index 53% rename from x-pack/legacy/plugins/license_management/server/np_ready/lib/permissions.ts rename to x-pack/plugins/license_management/server/lib/permissions.ts index 84cd92821797..a1ecc2e7b403 100644 --- a/x-pack/legacy/plugins/license_management/server/np_ready/lib/permissions.ts +++ b/x-pack/plugins/license_management/server/lib/permissions.ts @@ -4,23 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'src/core/server'; -import { ElasticsearchPlugin } from '../../../../../../../src/legacy/core_plugins/elasticsearch'; +import { CallAsCurrentUser } from '../types'; -export async function getPermissions( - req: KibanaRequest, - elasticsearch: ElasticsearchPlugin, - xpackInfo: any -) { - const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'); - if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) { +interface GetPermissionsArg { + isSecurityEnabled: boolean; + callAsCurrentUser: CallAsCurrentUser; +} + +export async function getPermissions({ isSecurityEnabled, callAsCurrentUser }: GetPermissionsArg) { + if (!isSecurityEnabled) { // If security isn't enabled, let the user use license management return { hasPermission: true, }; } - const { callWithRequest } = elasticsearch.getCluster('admin'); const options = { method: 'POST', path: '/_security/user/_has_privileges', @@ -30,7 +28,7 @@ export async function getPermissions( }; try { - const response = await callWithRequest(req as any, 'transport.request', options); + const response = await callAsCurrentUser('transport.request', options); return { hasPermission: response.cluster.manage, }; diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_basic.ts b/x-pack/plugins/license_management/server/lib/start_basic.ts similarity index 50% rename from x-pack/legacy/plugins/license_management/server/np_ready/lib/start_basic.ts rename to x-pack/plugins/license_management/server/lib/start_basic.ts index ba042be132d6..d48192c6ca32 100644 --- a/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_basic.ts +++ b/x-pack/plugins/license_management/server/lib/start_basic.ts @@ -3,29 +3,28 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { KibanaRequest } from 'kibana/server'; -import { ElasticsearchPlugin } from '../../../../../../../src/legacy/core_plugins/elasticsearch'; +import { LicensingPluginSetup } from '../../../licensing/server'; +import { CallAsCurrentUser } from '../types'; const getStartBasicPath = (acknowledge: boolean) => `/_license/start_basic${acknowledge ? '?acknowledge=true' : ''}`; -export async function startBasic( - req: KibanaRequest, - elasticsearch: ElasticsearchPlugin, - xpackInfo: any -) { - const { acknowledge } = req.query; - const { callWithRequest } = elasticsearch.getCluster('admin'); +interface StartBasicArg { + acknowledge: boolean; + callAsCurrentUser: CallAsCurrentUser; + licensing: LicensingPluginSetup; +} + +export async function startBasic({ acknowledge, callAsCurrentUser, licensing }: StartBasicArg) { const options = { method: 'POST', - path: getStartBasicPath(Boolean(acknowledge)), + path: getStartBasicPath(acknowledge), }; try { - const response = await callWithRequest(req as any, 'transport.request', options); + const response = await callAsCurrentUser('transport.request', options); const { basic_was_started: basicWasStarted } = response; if (basicWasStarted) { - await xpackInfo.refreshNow(); + await licensing.refresh(); } return response; } catch (error) { diff --git a/x-pack/plugins/license_management/server/lib/start_trial.ts b/x-pack/plugins/license_management/server/lib/start_trial.ts new file mode 100644 index 000000000000..d3e2ba37ec20 --- /dev/null +++ b/x-pack/plugins/license_management/server/lib/start_trial.ts @@ -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 { LicensingPluginSetup } from '../../../licensing/server'; +import { CallAsCurrentUser } from '../types'; + +export async function canStartTrial(callAsCurrentUser: CallAsCurrentUser) { + const options = { + method: 'GET', + path: '/_license/trial_status', + }; + try { + const response = await callAsCurrentUser('transport.request', options); + return response.eligible_to_start_trial; + } catch (error) { + return error.body; + } +} + +interface StartTrialArg { + callAsCurrentUser: CallAsCurrentUser; + licensing: LicensingPluginSetup; +} + +export async function startTrial({ callAsCurrentUser, licensing }: StartTrialArg) { + const options = { + method: 'POST', + path: '/_license/start_trial?acknowledge=true', + }; + try { + const response = await callAsCurrentUser('transport.request', options); + const { trial_was_started: trialWasStarted } = response; + + if (trialWasStarted) { + await licensing.refresh(); + } + + return response; + } catch (error) { + return error.body; + } +} diff --git a/x-pack/plugins/license_management/server/plugin.ts b/x-pack/plugins/license_management/server/plugin.ts new file mode 100644 index 000000000000..9546f5b1ef88 --- /dev/null +++ b/x-pack/plugins/license_management/server/plugin.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup } from 'kibana/server'; + +import { ApiRoutes } from './routes'; +import { isEsError } from './lib/is_es_error'; +import { Dependencies } from './types'; + +export class LicenseManagementServerPlugin implements Plugin { + private readonly apiRoutes = new ApiRoutes(); + + setup({ http }: CoreSetup, { licensing, security }: Dependencies) { + const router = http.createRouter(); + + this.apiRoutes.setup({ + router, + plugins: { + licensing, + }, + lib: { + isEsError, + }, + config: { + isSecurityEnabled: security !== undefined, + }, + }); + } + + start() {} + stop() {} +} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/index.ts b/x-pack/plugins/license_management/server/routes/api/license/index.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/index.ts rename to x-pack/plugins/license_management/server/routes/api/license/index.ts diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts b/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts similarity index 50% rename from x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts rename to x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts index 03ec583a3416..0f426764f68e 100644 --- a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts @@ -6,12 +6,13 @@ import { schema } from '@kbn/config-schema'; import { putLicense } from '../../../lib/license'; -import { Legacy, Server } from '../../../types'; +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../helpers'; -export function registerLicenseRoute(server: Server, legacy: Legacy, xpackInfo: any) { - server.router.put( +export function registerLicenseRoute({ router, plugins: { licensing } }: RouteDependencies) { + router.put( { - path: '/api/license', + path: addBasePath(''), validate: { query: schema.object({ acknowledge: schema.string() }), body: schema.object({ @@ -19,13 +20,19 @@ export function registerLicenseRoute(server: Server, legacy: Legacy, xpackInfo: }), }, }, - async (ctx, request, response) => { + async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.adminClient; try { - return response.ok({ - body: await putLicense(request, legacy.plugins.elasticsearch, xpackInfo), + return res.ok({ + body: await putLicense({ + acknowledge: Boolean(req.query.acknowledge), + callAsCurrentUser, + licensing, + license: req.body, + }), }); } catch (e) { - return response.internalError({ body: e }); + return res.internalError({ body: e }); } } ); diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts b/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts new file mode 100644 index 000000000000..7aa3c4733acf --- /dev/null +++ b/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts @@ -0,0 +1,26 @@ +/* + * 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 { getPermissions } from '../../../lib/permissions'; +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../helpers'; + +export function registerPermissionsRoute({ + router, + config: { isSecurityEnabled }, +}: RouteDependencies) { + router.post({ path: addBasePath('/permissions'), validate: false }, async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.adminClient; + + try { + return res.ok({ + body: await getPermissions({ callAsCurrentUser, isSecurityEnabled }), + }); + } catch (e) { + return res.internalError({ body: e }); + } + }); +} diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts b/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts new file mode 100644 index 000000000000..ebfa283872e6 --- /dev/null +++ b/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { startBasic } from '../../../lib/start_basic'; +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../helpers'; + +export function registerStartBasicRoute({ router, plugins: { licensing } }: RouteDependencies) { + router.post( + { + path: addBasePath('/start_basic'), + validate: { query: schema.object({ acknowledge: schema.string() }) }, + }, + async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.adminClient; + try { + return res.ok({ + body: await startBasic({ + acknowledge: Boolean(req.query.acknowledge), + callAsCurrentUser, + licensing, + }), + }); + } catch (e) { + return res.internalError({ body: e }); + } + } + ); +} diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts b/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts new file mode 100644 index 000000000000..e418c390aaab --- /dev/null +++ b/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.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 { canStartTrial, startTrial } from '../../../lib/start_trial'; +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../helpers'; + +export function registerStartTrialRoutes({ router, plugins: { licensing } }: RouteDependencies) { + router.get({ path: addBasePath('/start_trial'), validate: false }, async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.adminClient; + try { + return res.ok({ body: await canStartTrial(callAsCurrentUser) }); + } catch (e) { + return res.internalError({ body: e }); + } + }); + + router.post({ path: addBasePath('/start_trial'), validate: false }, async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.adminClient; + try { + return res.ok({ + body: await startTrial({ callAsCurrentUser, licensing }), + }); + } catch (e) { + return res.internalError({ body: e }); + } + }); +} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/index.ts b/x-pack/plugins/license_management/server/routes/helpers.ts similarity index 65% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/index.ts rename to x-pack/plugins/license_management/server/routes/helpers.ts index 1f963d7f8fcc..f1bbfd5fd449 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/index.ts +++ b/x-pack/plugins/license_management/server/routes/helpers.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './boot'; +import { API_BASE_PATH } from '../../common/constants'; + +export const addBasePath = (uri: string): string => API_BASE_PATH + uri; diff --git a/x-pack/plugins/license_management/server/routes/index.ts b/x-pack/plugins/license_management/server/routes/index.ts new file mode 100644 index 000000000000..9d196b6673e5 --- /dev/null +++ b/x-pack/plugins/license_management/server/routes/index.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 { RouteDependencies } from '../types'; + +import { + registerLicenseRoute, + registerStartTrialRoutes, + registerStartBasicRoute, + registerPermissionsRoute, +} from './api/license'; + +export class ApiRoutes { + setup(dependencies: RouteDependencies) { + registerLicenseRoute(dependencies); + registerStartTrialRoutes(dependencies); + registerStartBasicRoute(dependencies); + registerPermissionsRoute(dependencies); + } +} diff --git a/x-pack/plugins/license_management/server/types.ts b/x-pack/plugins/license_management/server/types.ts new file mode 100644 index 000000000000..37f4781ba1e0 --- /dev/null +++ b/x-pack/plugins/license_management/server/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ScopedClusterClient, IRouter } from 'kibana/server'; + +import { LicensingPluginSetup } from '../../licensing/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { isEsError } from './lib/is_es_error'; + +export interface Dependencies { + licensing: LicensingPluginSetup; + security?: SecurityPluginSetup; +} + +export interface RouteDependencies { + router: IRouter; + plugins: { + licensing: LicensingPluginSetup; + }; + lib: { + isEsError: typeof isEsError; + }; + config: { + isSecurityEnabled: boolean; + }; +} + +export type CallAsCurrentUser = ScopedClusterClient['callAsCurrentUser']; + +export type CallAsInternalUser = ScopedClusterClient['callAsInternalUser']; From 59a522b4ef862bda5d515da3d9e47c3d82995425 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 18 Mar 2020 14:27:56 +0100 Subject: [PATCH 081/115] Upgrade @types/node to match Node.js runtime (#60368) Kibana uses Node.js v10.19.0. The closest version of @types/node to this version is currently v10.17.17. This commit updates the resolutions field in package.json to ensure that the latest version less than 10.20.0 is always used. --- package.json | 4 ++-- packages/kbn-dev-utils/src/run/run.ts | 8 +++++++- packages/kbn-pm/dist/index.js | 5 ++++- packages/kbn-pm/package.json | 2 +- packages/kbn-test/src/functional_test_runner/cli.ts | 7 ++++++- x-pack/package.json | 4 ++-- yarn.lock | 8 ++++---- 7 files changed, 26 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 583e99158da7..aa9c8f6c4016 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "url": "https://github.com/elastic/kibana.git" }, "resolutions": { - "**/@types/node": "10.12.27", + "**/@types/node": ">=10.17.17 <10.20.0", "**/@types/react": "^16.9.19", "**/@types/react-router": "^5.1.3", "**/@types/hapi": "^17.0.18", @@ -350,7 +350,7 @@ "@types/mocha": "^5.2.7", "@types/moment-timezone": "^0.5.12", "@types/mustache": "^0.8.31", - "@types/node": "^10.12.27", + "@types/node": ">=10.17.17 <10.20.0", "@types/node-forge": "^0.9.0", "@types/normalize-path": "^3.0.0", "@types/numeral": "^0.0.26", diff --git a/packages/kbn-dev-utils/src/run/run.ts b/packages/kbn-dev-utils/src/run/run.ts index e185f86cc3bf..35477e988d83 100644 --- a/packages/kbn-dev-utils/src/run/run.ts +++ b/packages/kbn-dev-utils/src/run/run.ts @@ -17,6 +17,8 @@ * under the License. */ +import { inspect } from 'util'; + // @ts-ignore @types are outdated and module is super simple import exitHook from 'exit-hook'; @@ -62,7 +64,11 @@ export async function run(fn: RunFn, options: Options = {}) { process.on('unhandledRejection', error => { log.error('UNHANDLED PROMISE REJECTION'); - log.error(error); + log.error( + error instanceof Error + ? error + : new Error(`non-Error type rejection value: ${inspect(error)}`) + ); process.exit(1); }); diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 16fc0d891185..9fab74ea47a8 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -39806,6 +39806,7 @@ exports.isFailError = fail_1.isFailError; */ Object.defineProperty(exports, "__esModule", { value: true }); 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); @@ -39825,7 +39826,9 @@ async function run(fn, options = {}) { }); process.on('unhandledRejection', error => { log.error('UNHANDLED PROMISE REJECTION'); - log.error(error); + log.error(error instanceof Error + ? error + : new Error(`non-Error type rejection value: ${util_1.inspect(error)}`)); process.exit(1); }); const handleErrorWithoutExit = (error) => { diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 278fdbd2bc9a..a05e1634226e 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -26,7 +26,7 @@ "@types/lodash.clonedeepwith": "^4.5.3", "@types/log-symbols": "^2.0.0", "@types/ncp": "^2.0.1", - "@types/node": "^10.12.27", + "@types/node": ">=10.17.17 <10.20.0", "@types/ora": "^1.3.5", "@types/read-pkg": "^4.0.0", "@types/strip-ansi": "^3.0.0", diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index 3aaaa47ead5b..276a51c3a6a9 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -18,6 +18,7 @@ */ import { resolve } from 'path'; +import { inspect } from 'util'; import { run, createFlagError, Flags } from '@kbn/dev-utils'; import { FunctionalTestRunner } from './functional_test_runner'; @@ -86,7 +87,11 @@ export function runFtrCli() { } }; - process.on('unhandledRejection', err => teardown(err)); + process.on('unhandledRejection', err => + teardown( + err instanceof Error ? err : new Error(`non-Error type rejection value: ${inspect(err)}`) + ) + ); process.on('SIGTERM', () => teardown()); process.on('SIGINT', () => teardown()); diff --git a/x-pack/package.json b/x-pack/package.json index 6f15b46e28f5..192ecd25b582 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -23,7 +23,7 @@ } }, "resolutions": { - "**/@types/node": "10.12.27" + "**/@types/node": ">=10.17.17 <10.20.0" }, "devDependencies": { "@cypress/webpack-preprocessor": "^4.1.0", @@ -80,7 +80,7 @@ "@types/mime": "^2.0.1", "@types/mocha": "^5.2.7", "@types/nock": "^10.0.3", - "@types/node": "^10.12.27", + "@types/node": ">=10.17.17 <10.20.0", "@types/node-fetch": "^2.5.0", "@types/nodemailer": "^6.2.1", "@types/object-hash": "^1.3.0", diff --git a/yarn.lock b/yarn.lock index eaee706101a7..b4945cc3f410 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4894,10 +4894,10 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@10.12.27", "@types/node@8.10.54", "@types/node@>=8.9.0", "@types/node@^10.12.27", "@types/node@^12.0.2": - version "10.12.27" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.27.tgz#eb3843f15d0ba0986cc7e4d734d2ee8b50709ef8" - integrity sha512-e9wgeY6gaY21on3ve0xAjgBVjGDWq/xUteK0ujsE53bUoxycMkqfnkUgMt6ffZtykZ5X12Mg3T7Pw4TRCObDKg== +"@types/node@*", "@types/node@8.10.54", "@types/node@>=10.17.17 <10.20.0", "@types/node@>=8.9.0", "@types/node@^12.0.2": + version "10.17.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.17.tgz#7a183163a9e6ff720d86502db23ba4aade5999b8" + integrity sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q== "@types/nodemailer@^6.2.1": version "6.2.1" From 95a42ed2c9d5c5726410eb7f0a0fe602a7d37e3a Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 18 Mar 2020 14:43:30 +0100 Subject: [PATCH 082/115] [Uptime] replace fetch with kibana http (#59881) * use kibana http * unused import * fix type * update type * refactor * fix types * fix type * fix type --- .../uptime/common/constants/rest_api.ts | 12 +- .../connected/charts/snapshot_container.tsx | 16 +-- .../monitor/list_drawer_container.tsx | 10 +- .../monitor/status_bar_container.tsx | 10 +- .../monitor/status_details_container.tsx | 10 +- .../parameterize_values.test.ts.snap | 5 - .../lib/helper/__tests__/get_api_path.test.ts | 24 ---- .../__tests__/parameterize_values.test.ts | 30 ----- .../uptime/public/lib/helper/get_api_path.ts | 8 -- .../plugins/uptime/public/lib/helper/index.ts | 2 - .../public/lib/helper/parameterize_values.ts | 16 --- .../plugins/uptime/public/pages/monitor.tsx | 4 +- .../uptime/public/state/actions/monitor.ts | 113 +++--------------- .../public/state/actions/monitor_status.ts | 13 +- .../uptime/public/state/actions/snapshot.ts | 52 +------- .../__snapshots__/snapshot.test.ts.snap | 6 +- .../state/api/__tests__/snapshot.test.ts | 72 ++++++----- .../uptime/public/state/api/index_pattern.ts | 17 +-- .../uptime/public/state/api/index_status.ts | 26 +--- .../uptime/public/state/api/monitor.ts | 47 ++------ .../public/state/api/monitor_duration.ts | 23 +--- .../uptime/public/state/api/monitor_status.ts | 33 ++--- .../public/state/api/overview_filters.ts | 44 ++----- .../plugins/uptime/public/state/api/ping.ts | 17 +-- .../uptime/public/state/api/snapshot.ts | 28 ++--- .../plugins/uptime/public/state/api/types.ts | 3 +- .../plugins/uptime/public/state/api/utils.ts | 80 +++++++++++++ .../public/state/effects/fetch_effect.ts | 21 ++-- .../uptime/public/state/effects/monitor.ts | 64 ++++------ .../public/state/effects/monitor_status.ts | 68 ++++------- .../uptime/public/state/effects/snapshot.ts | 14 ++- .../uptime/public/state/kibana_service.ts | 34 ++++++ .../state/reducers/__tests__/snapshot.test.ts | 54 ++++----- .../uptime/public/state/reducers/monitor.ts | 28 ++--- .../public/state/reducers/monitor_status.ts | 24 ++-- .../public/state/reducers/overview_filters.ts | 1 + .../uptime/public/state/reducers/snapshot.ts | 16 +-- .../uptime/public/state/selectors/index.ts | 4 +- .../plugins/uptime/public/uptime_app.tsx | 3 + .../lib/requests/get_snapshot_counts.ts | 14 +-- .../rest_api/index_state/get_index_pattern.ts | 3 +- .../rest_api/index_state/get_index_status.ts | 4 +- .../rest_api/monitors/monitor_locations.ts | 3 +- .../rest_api/monitors/monitors_details.ts | 3 +- .../rest_api/monitors/monitors_durations.ts | 4 +- .../uptime/server/rest_api/monitors/status.ts | 7 +- .../overview_filters/get_overview_filters.ts | 3 +- .../uptime/server/rest_api/pings/get_all.ts | 3 +- .../rest_api/pings/get_ping_histogram.ts | 3 +- .../uptime/server/rest_api/pings/get_pings.ts | 3 +- .../rest_api/snapshot/get_snapshot_count.ts | 3 +- .../apis/uptime/feature_controls.ts | 4 +- .../apis/uptime/rest/doc_count.ts | 4 +- 53 files changed, 443 insertions(+), 670 deletions(-) delete mode 100644 x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/parameterize_values.test.ts.snap delete mode 100644 x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/get_api_path.test.ts delete mode 100644 x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/parameterize_values.test.ts delete mode 100644 x-pack/legacy/plugins/uptime/public/lib/helper/get_api_path.ts delete mode 100644 x-pack/legacy/plugins/uptime/public/lib/helper/parameterize_values.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/api/utils.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/kibana_service.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts b/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts index f09c79597783..61197d6dc373 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts @@ -4,6 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -export enum REST_API_URLS { +export enum API_URLS { + INDEX_PATTERN = `/api/uptime/index_pattern`, INDEX_STATUS = '/api/uptime/index_status', + MONITOR_LOCATIONS = `/api/uptime/monitor/locations`, + MONITOR_DURATION = `/api/uptime/monitor/duration`, + MONITOR_DETAILS = `/api/uptime/monitor/details`, + MONITOR_SELECTED = `/api/uptime/monitor/selected`, + MONITOR_STATUS = `/api/uptime/monitor/status`, + PINGS = '/api/uptime/pings', + PING_HISTOGRAM = `/api/uptime/ping/histogram`, + SNAPSHOT_COUNT = `/api/uptime/snapshot/count`, + FILTERS = `/api/uptime/filters`, } diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx index 08421cb56d14..ac8ff13d1edc 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx @@ -8,9 +8,10 @@ import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { useUrlParams } from '../../../hooks'; import { AppState } from '../../../state'; -import { fetchSnapshotCount } from '../../../state/actions'; +import { getSnapshotCountAction } from '../../../state/actions'; import { SnapshotComponent } from '../../functional/snapshot'; import { Snapshot as SnapshotType } from '../../../../common/runtime_types'; +import { SnapShotQueryParams } from '../../../state/api'; /** * Props expected from parent components. @@ -37,7 +38,7 @@ interface StoreProps { * for this component's life cycle */ interface DispatchProps { - loadSnapshotCount: typeof fetchSnapshotCount; + loadSnapshotCount: typeof getSnapshotCountAction; } /** @@ -57,7 +58,7 @@ export const Container: React.FC = ({ const { dateRangeStart, dateRangeEnd, statusFilter } = getUrlParams(); useEffect(() => { - loadSnapshotCount(dateRangeStart, dateRangeEnd, esKuery, statusFilter); + loadSnapshotCount({ dateRangeStart, dateRangeEnd, filters: esKuery, statusFilter }); }, [dateRangeStart, dateRangeEnd, esKuery, lastRefresh, loadSnapshotCount, statusFilter]); return ; }; @@ -81,13 +82,8 @@ const mapStateToProps = ({ * @param dispatch redux-provided action dispatcher */ const mapDispatchToProps = (dispatch: any) => ({ - loadSnapshotCount: ( - dateRangeStart: string, - dateRangeEnd: string, - filters?: string, - statusFilter?: string - ): DispatchProps => { - return dispatch(fetchSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter)); + loadSnapshotCount: (params: SnapShotQueryParams): DispatchProps => { + return dispatch(getSnapshotCountAction(params)); }, }); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.tsx index 8c670b485cc5..ceeaa7026059 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.tsx @@ -7,9 +7,9 @@ import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { AppState } from '../../../state'; -import { getMonitorDetails } from '../../../state/selectors'; +import { monitorDetailsSelector } from '../../../state/selectors'; import { MonitorDetailsActionPayload } from '../../../state/actions/types'; -import { fetchMonitorDetails } from '../../../state/actions/monitor'; +import { getMonitorDetailsAction } from '../../../state/actions/monitor'; import { MonitorListDrawerComponent } from '../../functional/monitor_list/monitor_list_drawer/monitor_list_drawer'; import { useUrlParams } from '../../../hooks'; import { MonitorSummary } from '../../../../common/graphql/types'; @@ -18,7 +18,7 @@ import { MonitorDetails } from '../../../../common/runtime_types/monitor'; interface ContainerProps { summary: MonitorSummary; monitorDetails: MonitorDetails; - loadMonitorDetails: typeof fetchMonitorDetails; + loadMonitorDetails: typeof getMonitorDetailsAction; } const Container: React.FC = ({ summary, loadMonitorDetails, monitorDetails }) => { @@ -38,12 +38,12 @@ const Container: React.FC = ({ summary, loadMonitorDetails, moni }; const mapStateToProps = (state: AppState, { summary }: any) => ({ - monitorDetails: getMonitorDetails(state, summary), + monitorDetails: monitorDetailsSelector(state, summary), }); const mapDispatchToProps = (dispatch: any) => ({ loadMonitorDetails: (actionPayload: MonitorDetailsActionPayload) => - dispatch(fetchMonitorDetails(actionPayload)), + dispatch(getMonitorDetailsAction(actionPayload)), }); export const MonitorListDrawer = connect(mapStateToProps, mapDispatchToProps)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx index b2b555d32a3c..456fa2b30bca 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx @@ -8,9 +8,9 @@ import React, { useContext, useEffect } from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { AppState } from '../../../state'; -import { selectMonitorLocations, selectMonitorStatus } from '../../../state/selectors'; +import { monitorLocationsSelector, selectMonitorStatus } from '../../../state/selectors'; import { MonitorStatusBarComponent } from '../../functional/monitor_status_details/monitor_status_bar'; -import { getMonitorStatus, getSelectedMonitor } from '../../../state/actions'; +import { getMonitorStatusAction, getSelectedMonitorAction } from '../../../state/actions'; import { useUrlParams } from '../../../hooks'; import { Ping } from '../../../../common/graphql/types'; import { MonitorLocations } from '../../../../common/runtime_types/monitor'; @@ -57,20 +57,20 @@ const Container: React.FC = ({ const mapStateToProps = (state: AppState, ownProps: OwnProps) => ({ monitorStatus: selectMonitorStatus(state), - monitorLocations: selectMonitorLocations(state, ownProps.monitorId), + monitorLocations: monitorLocationsSelector(state, ownProps.monitorId), }); const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => { dispatch( - getMonitorStatus({ + getMonitorStatusAction({ monitorId, dateStart, dateEnd, }) ); dispatch( - getSelectedMonitor({ + getSelectedMonitorAction({ monitorId, }) ); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx index 6929e3bd64c4..3ced251dfab8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx @@ -9,8 +9,8 @@ import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { useUrlParams } from '../../../hooks'; import { AppState } from '../../../state'; -import { selectMonitorLocations } from '../../../state/selectors'; -import { fetchMonitorLocations, MonitorLocationsPayload } from '../../../state/actions/monitor'; +import { monitorLocationsSelector } from '../../../state/selectors'; +import { getMonitorLocationsAction, MonitorLocationsPayload } from '../../../state/actions/monitor'; import { MonitorStatusDetailsComponent } from '../../functional/monitor_status_details'; import { MonitorLocations } from '../../../../common/runtime_types'; import { UptimeRefreshContext } from '../../../contexts'; @@ -24,7 +24,7 @@ interface StoreProps { } interface DispatchProps { - loadMonitorLocations: typeof fetchMonitorLocations; + loadMonitorLocations: typeof getMonitorLocationsAction; } type Props = OwnProps & StoreProps & DispatchProps; @@ -48,12 +48,12 @@ export const Container: React.FC = ({ ); }; const mapStateToProps = (state: AppState, { monitorId }: OwnProps) => ({ - monitorLocations: selectMonitorLocations(state, monitorId), + monitorLocations: monitorLocationsSelector(state, monitorId), }); const mapDispatchToProps = (dispatch: Dispatch) => ({ loadMonitorLocations: (params: MonitorLocationsPayload) => { - dispatch(fetchMonitorLocations(params)); + dispatch(getMonitorLocationsAction(params)); }, }); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/parameterize_values.test.ts.snap b/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/parameterize_values.test.ts.snap deleted file mode 100644 index 39c28a87f5e7..000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/parameterize_values.test.ts.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`parameterizeValues parameterizes provided values for multiple fields 1`] = `"foo=bar&foo=baz&bar=foo&bar=baz"`; - -exports[`parameterizeValues parameterizes the provided values for a given field name 1`] = `"foo=bar&foo=baz"`; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/get_api_path.test.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/get_api_path.test.ts deleted file mode 100644 index c111008fdc3d..000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/get_api_path.test.ts +++ /dev/null @@ -1,24 +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 { getApiPath } from '../get_api_path'; - -describe('getApiPath', () => { - it('returns a path with basePath when provided', () => { - const result = getApiPath('/api/foo/bar', '/somebasepath'); - expect(result).toEqual('/somebasepath/api/foo/bar'); - }); - - it('returns a valid path when no basePath present', () => { - const result = getApiPath('/api/foo/bar'); - expect(result).toEqual('/api/foo/bar'); - }); - - it('returns a valid path when an empty string is supplied as basePath', () => { - const result = getApiPath('/api/foo/bar', ''); - expect(result).toEqual('/api/foo/bar'); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/parameterize_values.test.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/parameterize_values.test.ts deleted file mode 100644 index e550a1a6397e..000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/parameterize_values.test.ts +++ /dev/null @@ -1,30 +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 { parameterizeValues } from '../parameterize_values'; - -describe('parameterizeValues', () => { - let params: URLSearchParams; - - beforeEach(() => { - params = new URLSearchParams(); - }); - - it('parameterizes the provided values for a given field name', () => { - parameterizeValues(params, { foo: ['bar', 'baz'] }); - expect(params.toString()).toMatchSnapshot(); - }); - - it('parameterizes provided values for multiple fields', () => { - parameterizeValues(params, { foo: ['bar', 'baz'], bar: ['foo', 'baz'] }); - expect(params.toString()).toMatchSnapshot(); - }); - - it('returns an empty string when there are no values provided', () => { - parameterizeValues(params, { foo: [] }); - expect(params.toString()).toBe(''); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/get_api_path.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/get_api_path.ts deleted file mode 100644 index 398d58f8460b..000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/get_api_path.ts +++ /dev/null @@ -1,8 +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. - */ - -export const getApiPath = (path: string, basePath?: string) => - basePath ? `${basePath}${path}` : path; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/index.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/index.ts index ef191ce32e53..e2aa4a2b3d42 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/index.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/index.ts @@ -7,9 +7,7 @@ export { combineFiltersAndUserSearch } from './combine_filters_and_user_search'; export { convertMicrosecondsToMilliseconds } from './convert_measurements'; export * from './observability_integration'; -export { getApiPath } from './get_api_path'; export { getChartDateLabel } from './charts'; -export { parameterizeValues } from './parameterize_values'; export { seriesHasDownValues } from './series_has_down_values'; export { stringifyKueries } from './stringify_kueries'; export { UptimeUrlParams, getSupportedUrlParams } from './url_params'; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/parameterize_values.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/parameterize_values.ts deleted file mode 100644 index 4c9fa6838c2e..000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/parameterize_values.ts +++ /dev/null @@ -1,16 +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. - */ - -export const parameterizeValues = ( - params: URLSearchParams, - obj: Record -): void => { - Object.keys(obj).forEach(key => { - obj[key].forEach(val => { - params.append(key, val); - }); - }); -}; diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index 18c4927af079..b9d29ed017a0 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -17,7 +17,7 @@ import { MonitorStatusDetails } from '../components/connected'; import { Ping } from '../../common/graphql/types'; import { AppState } from '../state'; import { selectSelectedMonitor } from '../state/selectors'; -import { getSelectedMonitor } from '../state/actions'; +import { getSelectedMonitorAction } from '../state/actions'; import { PageHeader } from './page_header'; interface StateProps { @@ -102,7 +102,7 @@ const mapDispatchToProps: MapDispatchToPropsFunction = (dispa return { dispatchGetMonitorStatus: (monitorId: string) => { dispatch( - getSelectedMonitor({ + getSelectedMonitorAction({ monitorId, }) ); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts index cf4525a08e43..30ea8e71265e 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts @@ -4,108 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createAction } from 'redux-actions'; import { MonitorDetailsActionPayload } from './types'; import { MonitorError } from '../../../common/runtime_types'; import { MonitorLocations } from '../../../common/runtime_types'; import { QueryParams } from './types'; -export const FETCH_MONITOR_DETAILS = 'FETCH_MONITOR_DETAILS'; -export const FETCH_MONITOR_DETAILS_SUCCESS = 'FETCH_MONITOR_DETAILS_SUCCESS'; -export const FETCH_MONITOR_DETAILS_FAIL = 'FETCH_MONITOR_DETAILS_FAIL'; - -export const FETCH_MONITOR_LOCATIONS = 'FETCH_MONITOR_LOCATIONS'; -export const FETCH_MONITOR_LOCATIONS_SUCCESS = 'FETCH_MONITOR_LOCATIONS_SUCCESS'; -export const FETCH_MONITOR_LOCATIONS_FAIL = 'FETCH_MONITOR_LOCATIONS_FAIL'; - -export interface MonitorDetailsState { - monitorId: string; - error: MonitorError; -} - -interface GetMonitorDetailsAction { - type: typeof FETCH_MONITOR_DETAILS; - payload: MonitorDetailsActionPayload; -} - -interface GetMonitorDetailsSuccessAction { - type: typeof FETCH_MONITOR_DETAILS_SUCCESS; - payload: MonitorDetailsState; -} - -interface GetMonitorDetailsFailAction { - type: typeof FETCH_MONITOR_DETAILS_FAIL; - payload: any; -} - export interface MonitorLocationsPayload extends QueryParams { monitorId: string; } -interface GetMonitorLocationsAction { - type: typeof FETCH_MONITOR_LOCATIONS; - payload: MonitorLocationsPayload; -} - -interface GetMonitorLocationsSuccessAction { - type: typeof FETCH_MONITOR_LOCATIONS_SUCCESS; - payload: MonitorLocations; -} - -interface GetMonitorLocationsFailAction { - type: typeof FETCH_MONITOR_LOCATIONS_FAIL; - payload: any; -} - -export function fetchMonitorDetails(payload: MonitorDetailsActionPayload): GetMonitorDetailsAction { - return { - type: FETCH_MONITOR_DETAILS, - payload, - }; -} - -export function fetchMonitorDetailsSuccess( - monitorDetailsState: MonitorDetailsState -): GetMonitorDetailsSuccessAction { - return { - type: FETCH_MONITOR_DETAILS_SUCCESS, - payload: monitorDetailsState, - }; -} - -export function fetchMonitorDetailsFail(error: any): GetMonitorDetailsFailAction { - return { - type: FETCH_MONITOR_DETAILS_FAIL, - payload: error, - }; -} - -export function fetchMonitorLocations(payload: MonitorLocationsPayload): GetMonitorLocationsAction { - return { - type: FETCH_MONITOR_LOCATIONS, - payload, - }; -} - -export function fetchMonitorLocationsSuccess( - monitorLocationsState: MonitorLocations -): GetMonitorLocationsSuccessAction { - return { - type: FETCH_MONITOR_LOCATIONS_SUCCESS, - payload: monitorLocationsState, - }; -} - -export function fetchMonitorLocationsFail(error: any): GetMonitorLocationsFailAction { - return { - type: FETCH_MONITOR_LOCATIONS_FAIL, - payload: error, - }; +export interface MonitorDetailsState { + monitorId: string; + error: MonitorError; } -export type MonitorActionTypes = - | GetMonitorDetailsAction - | GetMonitorDetailsSuccessAction - | GetMonitorDetailsFailAction - | GetMonitorLocationsAction - | GetMonitorLocationsSuccessAction - | GetMonitorLocationsFailAction; +export const getMonitorDetailsAction = createAction( + 'GET_MONITOR_DETAILS' +); +export const getMonitorDetailsActionSuccess = createAction( + 'GET_MONITOR_DETAILS_SUCCESS' +); +export const getMonitorDetailsActionFail = createAction('GET_MONITOR_DETAILS_FAIL'); + +export const getMonitorLocationsAction = createAction( + 'GET_MONITOR_LOCATIONS' +); +export const getMonitorLocationsActionSuccess = createAction( + 'GET_MONITOR_LOCATIONS_SUCCESS' +); +export const getMonitorLocationsActionFail = createAction('GET_MONITOR_LOCATIONS_FAIL'); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts index db103f6cb780..7917628abf7d 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts @@ -5,11 +5,12 @@ */ import { createAction } from 'redux-actions'; import { QueryParams } from './types'; +import { Ping } from '../../../common/graphql/types'; -export const getSelectedMonitor = createAction<{ monitorId: string }>('GET_SELECTED_MONITOR'); -export const getSelectedMonitorSuccess = createAction('GET_SELECTED_MONITOR_SUCCESS'); -export const getSelectedMonitorFail = createAction('GET_SELECTED_MONITOR_FAIL'); +export const getSelectedMonitorAction = createAction<{ monitorId: string }>('GET_SELECTED_MONITOR'); +export const getSelectedMonitorActionSuccess = createAction('GET_SELECTED_MONITOR_SUCCESS'); +export const getSelectedMonitorActionFail = createAction('GET_SELECTED_MONITOR_FAIL'); -export const getMonitorStatus = createAction('GET_MONITOR_STATUS'); -export const getMonitorStatusSuccess = createAction('GET_MONITOR_STATUS_SUCCESS'); -export const getMonitorStatusFail = createAction('GET_MONITOR_STATUS_FAIL'); +export const getMonitorStatusAction = createAction('GET_MONITOR_STATUS'); +export const getMonitorStatusActionSuccess = createAction('GET_MONITOR_STATUS_SUCCESS'); +export const getMonitorStatusActionFail = createAction('GET_MONITOR_STATUS_FAIL'); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts index 57d2b4ce3820..e819a553e61f 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createAction } from 'redux-actions'; import { Snapshot } from '../../../common/runtime_types'; -export const FETCH_SNAPSHOT_COUNT = 'FETCH_SNAPSHOT_COUNT'; -export const FETCH_SNAPSHOT_COUNT_FAIL = 'FETCH_SNAPSHOT_COUNT_FAIL'; -export const FETCH_SNAPSHOT_COUNT_SUCCESS = 'FETCH_SNAPSHOT_COUNT_SUCCESS'; - export interface GetSnapshotPayload { dateRangeStart: string; dateRangeEnd: string; @@ -17,47 +14,6 @@ export interface GetSnapshotPayload { statusFilter?: string; } -interface GetSnapshotCountFetchAction { - type: typeof FETCH_SNAPSHOT_COUNT; - payload: GetSnapshotPayload; -} - -interface GetSnapshotCountSuccessAction { - type: typeof FETCH_SNAPSHOT_COUNT_SUCCESS; - payload: Snapshot; -} - -interface GetSnapshotCountFailAction { - type: typeof FETCH_SNAPSHOT_COUNT_FAIL; - payload: Error; -} - -export type SnapshotActionTypes = - | GetSnapshotCountFetchAction - | GetSnapshotCountSuccessAction - | GetSnapshotCountFailAction; - -export const fetchSnapshotCount = ( - dateRangeStart: string, - dateRangeEnd: string, - filters?: string, - statusFilter?: string -): GetSnapshotCountFetchAction => ({ - type: FETCH_SNAPSHOT_COUNT, - payload: { - dateRangeStart, - dateRangeEnd, - filters, - statusFilter, - }, -}); - -export const fetchSnapshotCountFail = (error: Error): GetSnapshotCountFailAction => ({ - type: FETCH_SNAPSHOT_COUNT_FAIL, - payload: error, -}); - -export const fetchSnapshotCountSuccess = (snapshot: Snapshot) => ({ - type: FETCH_SNAPSHOT_COUNT_SUCCESS, - payload: snapshot, -}); +export const getSnapshotCountAction = createAction('GET_SNAPSHOT_COUNT'); +export const getSnapshotCountActionSuccess = createAction('GET_SNAPSHOT_COUNT_SUCCESS'); +export const getSnapshotCountActionFail = createAction('GET_SNAPSHOT_COUNT_FAIL'); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap index 0d2392390c7e..1cd2aae44651 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`snapshot API throws when server response doesn't correspond to expected type 1`] = ` -[Error: Invalid value undefined supplied to : { down: number, total: number, up: number }/down: number -Invalid value undefined supplied to : { down: number, total: number, up: number }/total: number -Invalid value undefined supplied to : { down: number, total: number, up: number }/up: number] +Object { + "foo": "bar", +} `; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts index e9b1391a23e3..66b376c3ac36 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts @@ -5,17 +5,19 @@ */ import { fetchSnapshotCount } from '../snapshot'; +import { apiService } from '../utils'; +import { HttpFetchError } from '../../../../../../../../src/core/public/http/http_fetch_error'; describe('snapshot API', () => { - let fetchMock: jest.SpyInstance>>; - let mockResponse: Partial; + let fetchMock: jest.SpyInstance>; + let mockResponse: Partial; beforeEach(() => { - fetchMock = jest.spyOn(window, 'fetch'); - mockResponse = { - ok: true, - json: () => new Promise(r => r({ up: 3, down: 12, total: 15 })), - }; + apiService.http = { + get: jest.fn(), + } as any; + fetchMock = jest.spyOn(apiService.http, 'get'); + mockResponse = { up: 3, down: 12, total: 15 }; }); afterEach(() => { @@ -25,49 +27,43 @@ describe('snapshot API', () => { it('calls url with expected params and returns response body on 200', async () => { fetchMock.mockReturnValue(new Promise(r => r(mockResponse))); const resp = await fetchSnapshotCount({ - basePath: '', dateRangeStart: 'now-15m', dateRangeEnd: 'now', filters: 'monitor.id:"auto-http-0X21EE76EAC459873F"', statusFilter: 'up', }); - expect(fetchMock).toHaveBeenCalledWith( - '/api/uptime/snapshot/count?dateRangeStart=now-15m&dateRangeEnd=now&filters=monitor.id%3A%22auto-http-0X21EE76EAC459873F%22&statusFilter=up' - ); + expect(fetchMock).toHaveBeenCalledWith('/api/uptime/snapshot/count', { + query: { + dateRangeEnd: 'now', + dateRangeStart: 'now-15m', + filters: 'monitor.id:"auto-http-0X21EE76EAC459873F"', + statusFilter: 'up', + }, + }); expect(resp).toEqual({ up: 3, down: 12, total: 15 }); }); it(`throws when server response doesn't correspond to expected type`, async () => { - mockResponse = { ok: true, json: () => new Promise(r => r({ foo: 'bar' })) }; + mockResponse = { foo: 'bar' }; fetchMock.mockReturnValue(new Promise(r => r(mockResponse))); - let error: Error | undefined; - try { - await fetchSnapshotCount({ - basePath: '', - dateRangeStart: 'now-15m', - dateRangeEnd: 'now', - filters: 'monitor.id: baz', - statusFilter: 'up', - }); - } catch (e) { - error = e; - } - expect(error).toMatchSnapshot(); + const result = await fetchSnapshotCount({ + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + filters: 'monitor.id: baz', + statusFilter: 'up', + }); + + expect(result).toMatchSnapshot(); }); it('throws an error when response is not ok', async () => { - mockResponse = { ok: false, statusText: 'There was an error fetching your data.' }; - fetchMock.mockReturnValue(new Promise(r => r(mockResponse))); - let error: Error | undefined; - try { - await fetchSnapshotCount({ - basePath: '', - dateRangeStart: 'now-15m', - dateRangeEnd: 'now', - }); - } catch (e) { - error = e; - } - expect(error).toEqual(new Error('There was an error fetching your data.')); + mockResponse = new HttpFetchError('There was an error fetching your data.', 'error', {} as any); + fetchMock.mockReturnValue(mockResponse); + const result = await fetchSnapshotCount({ + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + }); + + expect(result).toEqual(new Error('There was an error fetching your data.')); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index_pattern.ts b/x-pack/legacy/plugins/uptime/public/state/api/index_pattern.ts index 2669376d728a..1eecbc75c5bf 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/index_pattern.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/index_pattern.ts @@ -4,18 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getApiPath } from '../../lib/helper'; +import { API_URLS } from '../../../common/constants'; +import { apiService } from './utils'; -interface APIParams { - basePath: string; -} - -export const fetchIndexPattern = async ({ basePath }: APIParams) => { - const url = getApiPath(`/api/uptime/index_pattern`, basePath); - - const response = await fetch(url); - if (!response.ok) { - throw new Error(response.statusText); - } - return await response.json(); +export const fetchIndexPattern = async () => { + return await apiService.get(API_URLS.INDEX_PATTERN); }; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index_status.ts b/x-pack/legacy/plugins/uptime/public/state/api/index_status.ts index 9c531b3406a7..0e33ab617777 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/index_status.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/index_status.ts @@ -4,28 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { isRight } from 'fp-ts/lib/Either'; -import { getApiPath } from '../../lib/helper'; -import { REST_API_URLS } from '../../../common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; import { StatesIndexStatus, StatesIndexStatusType } from '../../../common/runtime_types'; +import { apiService } from './utils'; -interface ApiRequest { - basePath: string; -} - -export const fetchIndexStatus = async ({ basePath }: ApiRequest): Promise => { - const url = getApiPath(REST_API_URLS.INDEX_STATUS, basePath); - - const response = await fetch(url); - if (!response.ok) { - throw new Error(response.statusText); - } - const responseData = await response.json(); - const decoded = StatesIndexStatusType.decode(responseData); - PathReporter.report(decoded); - if (isRight(decoded)) { - return decoded.right; - } - throw PathReporter.report(decoded); +export const fetchIndexStatus = async (): Promise => { + return await apiService.get(API_URLS.INDEX_STATUS, undefined, StatesIndexStatusType); }; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts index 80fd311c3ec7..b36eccca98da 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts @@ -4,71 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { getApiPath } from '../../lib/helper'; import { BaseParams } from './types'; -import { - MonitorDetailsType, - MonitorDetails, - MonitorLocations, - MonitorLocationsType, -} from '../../../common/runtime_types'; +import { MonitorDetailsType, MonitorLocationsType } from '../../../common/runtime_types'; import { QueryParams } from '../actions/types'; +import { apiService } from './utils'; +import { API_URLS } from '../../../common/constants/rest_api'; interface ApiRequest { monitorId: string; - basePath: string; } export type MonitorQueryParams = BaseParams & ApiRequest; export const fetchMonitorDetails = async ({ monitorId, - basePath, dateStart, dateEnd, -}: MonitorQueryParams): Promise => { - const url = getApiPath(`/api/uptime/monitor/details`, basePath); +}: MonitorQueryParams) => { const params = { monitorId, dateStart, dateEnd, }; - const urlParams = new URLSearchParams(params).toString(); - const response = await fetch(`${url}?${urlParams}`); - - if (!response.ok) { - throw new Error(response.statusText); - } - return response.json().then(data => { - PathReporter.report(MonitorDetailsType.decode(data)); - return data; - }); + return await apiService.get(API_URLS.MONITOR_DETAILS, params, MonitorDetailsType); }; type ApiParams = QueryParams & ApiRequest; -export const fetchMonitorLocations = async ({ - monitorId, - basePath, - dateStart, - dateEnd, -}: ApiParams): Promise => { - const url = getApiPath(`/api/uptime/monitor/locations`, basePath); - +export const fetchMonitorLocations = async ({ monitorId, dateStart, dateEnd }: ApiParams) => { const params = { dateStart, dateEnd, monitorId, }; - const urlParams = new URLSearchParams(params).toString(); - const response = await fetch(`${url}?${urlParams}`); - - if (!response.ok) { - throw new Error(response.statusText); - } - return response.json().then(data => { - PathReporter.report(MonitorLocationsType.decode(data)); - return data; - }); + return await apiService.get(API_URLS.MONITOR_LOCATIONS, params, MonitorLocationsType); }; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts b/x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts index 44e797457e5f..daf725119fcf 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts @@ -4,29 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { stringify } from 'query-string'; - -import { getApiPath } from '../../lib/helper'; import { BaseParams } from './types'; +import { apiService } from './utils'; +import { API_URLS } from '../../../common/constants/rest_api'; -export const fetchMonitorDuration = async ({ - basePath, - monitorId, - dateStart, - dateEnd, -}: BaseParams) => { - const url = getApiPath(`/api/uptime/monitor/duration`, basePath); - - const params = { +export const fetchMonitorDuration = async ({ monitorId, dateStart, dateEnd }: BaseParams) => { + const queryParams = { monitorId, dateStart, dateEnd, }; - const urlParams = stringify(params); - const response = await fetch(`${url}?${urlParams}`); - if (!response.ok) { - throw new Error(response.statusText); - } - return await response.json(); + return await apiService.get(API_URLS.MONITOR_DURATION, queryParams); }; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts index 936e864b7561..0f7608ba57ea 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts @@ -4,46 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getApiPath } from '../../lib/helper'; import { QueryParams } from '../actions/types'; import { Ping } from '../../../common/graphql/types'; +import { apiService } from './utils'; +import { API_URLS } from '../../../common/constants/rest_api'; export interface APIParams { - basePath: string; monitorId: string; } -export const fetchSelectedMonitor = async ({ basePath, monitorId }: APIParams): Promise => { - const url = getApiPath(`/api/uptime/monitor/selected`, basePath); - const params = { +export const fetchSelectedMonitor = async ({ monitorId }: APIParams): Promise => { + const queryParams = { monitorId, }; - const urlParams = new URLSearchParams(params).toString(); - const response = await fetch(`${url}?${urlParams}`); - if (!response.ok) { - throw new Error(response.statusText); - } - const responseData = await response.json(); - return responseData; + + return await apiService.get(API_URLS.MONITOR_SELECTED, queryParams); }; export const fetchMonitorStatus = async ({ - basePath, monitorId, dateStart, dateEnd, -}: QueryParams & APIParams): Promise => { - const url = getApiPath(`/api/uptime/monitor/status`, basePath); - const params = { +}: QueryParams): Promise => { + const queryParams = { monitorId, dateStart, dateEnd, }; - const urlParams = new URLSearchParams(params).toString(); - const response = await fetch(`${url}?${urlParams}`); - if (!response.ok) { - throw new Error(response.statusText); - } - const responseData = await response.json(); - return responseData; + + return await apiService.get(API_URLS.MONITOR_STATUS, queryParams); }; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts b/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts index c3ef62fa88dc..9943bc27f11f 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts @@ -4,18 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; -import { isRight } from 'fp-ts/lib/Either'; import { GetOverviewFiltersPayload } from '../actions/overview_filters'; -import { getApiPath, parameterizeValues } from '../../lib/helper'; import { OverviewFiltersType } from '../../../common/runtime_types'; - -type ApiRequest = GetOverviewFiltersPayload & { - basePath: string; -}; +import { apiService } from './utils'; +import { API_URLS } from '../../../common/constants/rest_api'; export const fetchOverviewFilters = async ({ - basePath, dateRangeStart, dateRangeEnd, search, @@ -23,30 +17,16 @@ export const fetchOverviewFilters = async ({ locations, ports, tags, -}: ApiRequest) => { - const url = getApiPath(`/api/uptime/filters`, basePath); - - const params = new URLSearchParams({ +}: GetOverviewFiltersPayload) => { + const queryParams = { dateRangeStart, dateRangeEnd, - }); - - if (search) { - params.append('search', search); - } - - parameterizeValues(params, { schemes, locations, ports, tags }); - - const response = await fetch(`${url}?${params.toString()}`); - if (!response.ok) { - throw new Error(response.statusText); - } - const responseData = await response.json(); - const decoded = OverviewFiltersType.decode(responseData); - - ThrowReporter.report(decoded); - if (isRight(decoded)) { - return decoded.right; - } - throw new Error('`getOverviewFilters` response did not correspond to expected type'); + schemes, + locations, + ports, + tags, + search, + }; + + return await apiService.get(API_URLS.FILTERS, queryParams, OverviewFiltersType); }; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ping.ts b/x-pack/legacy/plugins/uptime/public/state/api/ping.ts index c61bf42c8c90..df71cc8d67bd 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/ping.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/ping.ts @@ -4,32 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { stringify } from 'query-string'; -import { getApiPath } from '../../lib/helper'; import { APIFn } from './types'; import { GetPingHistogramParams, HistogramResult } from '../../../common/types'; +import { apiService } from './utils'; +import { API_URLS } from '../../../common/constants/rest_api'; export const fetchPingHistogram: APIFn = async ({ - basePath, monitorId, dateStart, dateEnd, statusFilter, filters, }) => { - const url = getApiPath(`/api/uptime/ping/histogram`, basePath); - const params = { + const queryParams = { dateStart, dateEnd, ...(monitorId && { monitorId }), ...(statusFilter && { statusFilter }), ...(filters && { filters }), }; - const urlParams = stringify(params, { sort: false }); - const response = await fetch(`${url}?${urlParams}`); - if (!response.ok) { - throw new Error(response.statusText); - } - const responseData = await response.json(); - return responseData; + + return await apiService.get(API_URLS.PING_HISTOGRAM, queryParams); }; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts index cbfe00a4a874..e663d0241d68 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; -import { isRight } from 'fp-ts/lib/Either'; -import { getApiPath } from '../../lib/helper'; import { SnapshotType, Snapshot } from '../../../common/runtime_types'; +import { apiService } from './utils'; +import { API_URLS } from '../../../common/constants/rest_api'; -interface ApiRequest { - basePath: string; +export interface SnapShotQueryParams { dateRangeStart: string; dateRangeEnd: string; filters?: string; @@ -18,29 +16,17 @@ interface ApiRequest { } export const fetchSnapshotCount = async ({ - basePath, dateRangeStart, dateRangeEnd, filters, statusFilter, -}: ApiRequest): Promise => { - const url = getApiPath(`/api/uptime/snapshot/count`, basePath); - const params = { +}: SnapShotQueryParams): Promise => { + const queryParams = { dateRangeStart, dateRangeEnd, ...(filters && { filters }), ...(statusFilter && { statusFilter }), }; - const urlParams = new URLSearchParams(params).toString(); - const response = await fetch(`${url}?${urlParams}`); - if (!response.ok) { - throw new Error(response.statusText); - } - const responseData = await response.json(); - const decoded = SnapshotType.decode(responseData); - ThrowReporter.report(decoded); - if (isRight(decoded)) { - return decoded.right; - } - throw new Error('`getSnapshotCount` response did not correspond to expected type'); + + return await apiService.get(API_URLS.SNAPSHOT_COUNT, queryParams, SnapshotType); }; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/types.ts b/x-pack/legacy/plugins/uptime/public/state/api/types.ts index a148f1c7d7ae..4232751cbc03 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/types.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/types.ts @@ -5,7 +5,6 @@ */ export interface BaseParams { - basePath: string; dateStart: string; dateEnd: string; filters?: string; @@ -14,4 +13,4 @@ export interface BaseParams { monitorId?: string; } -export type APIFn = (params: { basePath: string } & P) => Promise; +export type APIFn = (params: P) => Promise; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/utils.ts b/x-pack/legacy/plugins/uptime/public/state/api/utils.ts new file mode 100644 index 000000000000..e67efa8570c1 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/utils.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 { PathReporter } from 'io-ts/lib/PathReporter'; +import { isRight } from 'fp-ts/lib/Either'; +import { HttpFetchQuery, HttpSetup } from '../../../../../../../target/types/core/public'; + +class ApiService { + private static instance: ApiService; + private _http!: HttpSetup; + + public get http() { + return this._http; + } + + public set http(httpSetup: HttpSetup) { + this._http = httpSetup; + } + + private constructor() {} + + static getInstance(): ApiService { + if (!ApiService.instance) { + ApiService.instance = new ApiService(); + } + + return ApiService.instance; + } + + public async get(apiUrl: string, params?: HttpFetchQuery, decodeType?: any) { + const response = await this._http!.get(apiUrl, { query: params }); + + if (decodeType) { + const decoded = decodeType.decode(response); + if (isRight(decoded)) { + return decoded.right; + } else { + // eslint-disable-next-line no-console + console.error( + `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}` + ); + } + } + + return response; + } + + public async post(apiUrl: string, data?: any, decodeType?: any) { + const response = await this._http!.post(apiUrl, { + method: 'POST', + body: JSON.stringify(data), + }); + + if (decodeType) { + const decoded = decodeType.decode(response); + if (isRight(decoded)) { + return decoded.right; + } else { + // eslint-disable-next-line no-console + console.warn( + `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}` + ); + } + } + return response; + } + + public async delete(apiUrl: string) { + const response = await this._http!.delete(apiUrl); + if (response instanceof Error) { + throw response; + } + return response; + } +} + +export const apiService = ApiService.getInstance(); diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts b/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts index ea389ff0a674..d1d7626b2eab 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { call, put, select } from 'redux-saga/effects'; +import { call, put } from 'redux-saga/effects'; import { Action } from 'redux-actions'; -import { getBasePath } from '../selectors'; /** * Factory function for a fetch effect. It expects three action creators, @@ -25,15 +24,17 @@ export function fetchEffectFactory( fail: (error: Error) => Action ) { return function*(action: Action) { - try { - const { - payload: { ...params }, - } = action; - const basePath = yield select(getBasePath); - const response = yield call(fetch, { ...params, basePath }); + const { + payload: { ...params }, + } = action; + const response = yield call(fetch, params); + if (response instanceof Error) { + // eslint-disable-next-line no-console + console.error(response); + + yield put(fail(response)); + } else { yield put(success(response)); - } catch (error) { - yield put(fail(error)); } }; } diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts index 1cac7424b4e5..ed21f315476d 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts @@ -4,48 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import { call, put, takeLatest, select } from 'redux-saga/effects'; -import { Action } from 'redux-actions'; +import { takeLatest } from 'redux-saga/effects'; import { - FETCH_MONITOR_DETAILS, - FETCH_MONITOR_DETAILS_SUCCESS, - FETCH_MONITOR_DETAILS_FAIL, - FETCH_MONITOR_LOCATIONS, - FETCH_MONITOR_LOCATIONS_SUCCESS, - FETCH_MONITOR_LOCATIONS_FAIL, + getMonitorDetailsAction, + getMonitorDetailsActionSuccess, + getMonitorDetailsActionFail, + getMonitorLocationsAction, + getMonitorLocationsActionSuccess, + getMonitorLocationsActionFail, } from '../actions/monitor'; import { fetchMonitorDetails, fetchMonitorLocations } from '../api'; -import { getBasePath } from '../selectors'; -import { MonitorDetailsActionPayload } from '../actions/types'; - -function* monitorDetailsEffect(action: Action) { - const { monitorId, dateStart, dateEnd }: MonitorDetailsActionPayload = action.payload; - try { - const basePath = yield select(getBasePath); - const response = yield call(fetchMonitorDetails, { - monitorId, - basePath, - dateStart, - dateEnd, - }); - yield put({ type: FETCH_MONITOR_DETAILS_SUCCESS, payload: response }); - } catch (error) { - yield put({ type: FETCH_MONITOR_DETAILS_FAIL, payload: error.message }); - } -} - -function* monitorLocationsEffect(action: Action) { - const payload = action.payload; - try { - const basePath = yield select(getBasePath); - const response = yield call(fetchMonitorLocations, { basePath, ...payload }); - yield put({ type: FETCH_MONITOR_LOCATIONS_SUCCESS, payload: response }); - } catch (error) { - yield put({ type: FETCH_MONITOR_LOCATIONS_FAIL, payload: error.message }); - } -} +import { fetchEffectFactory } from './fetch_effect'; export function* fetchMonitorDetailsEffect() { - yield takeLatest(FETCH_MONITOR_DETAILS, monitorDetailsEffect); - yield takeLatest(FETCH_MONITOR_LOCATIONS, monitorLocationsEffect); + yield takeLatest( + getMonitorDetailsAction, + fetchEffectFactory( + fetchMonitorDetails, + getMonitorDetailsActionSuccess, + getMonitorDetailsActionFail + ) + ); + + yield takeLatest( + getMonitorLocationsAction, + fetchEffectFactory( + fetchMonitorLocations, + getMonitorLocationsActionSuccess, + getMonitorLocationsActionFail + ) + ); } diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts index cab32092a14c..1207ab20bc71 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts @@ -4,50 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import { call, put, takeLatest, select } from 'redux-saga/effects'; -import { Action } from 'redux-actions'; +import { takeLatest } from 'redux-saga/effects'; import { - getSelectedMonitor, - getSelectedMonitorSuccess, - getSelectedMonitorFail, - getMonitorStatus, - getMonitorStatusSuccess, - getMonitorStatusFail, -} from '../actions/monitor_status'; + getSelectedMonitorAction, + getSelectedMonitorActionSuccess, + getSelectedMonitorActionFail, + getMonitorStatusAction, + getMonitorStatusActionSuccess, + getMonitorStatusActionFail, +} from '../actions'; import { fetchSelectedMonitor, fetchMonitorStatus } from '../api'; -import { getBasePath } from '../selectors'; - -function* selectedMonitorEffect(action: Action) { - const { monitorId } = action.payload; - try { - const basePath = yield select(getBasePath); - const response = yield call(fetchSelectedMonitor, { - monitorId, - basePath, - }); - yield put({ type: getSelectedMonitorSuccess, payload: response }); - } catch (error) { - yield put({ type: getSelectedMonitorFail, payload: error.message }); - } -} - -function* monitorStatusEffect(action: Action) { - const { monitorId, dateStart, dateEnd } = action.payload; - try { - const basePath = yield select(getBasePath); - const response = yield call(fetchMonitorStatus, { - monitorId, - basePath, - dateStart, - dateEnd, - }); - yield put({ type: getMonitorStatusSuccess, payload: response }); - } catch (error) { - yield put({ type: getMonitorStatusFail, payload: error.message }); - } -} +import { fetchEffectFactory } from './fetch_effect'; export function* fetchMonitorStatusEffect() { - yield takeLatest(getMonitorStatus, monitorStatusEffect); - yield takeLatest(getSelectedMonitor, selectedMonitorEffect); + yield takeLatest( + getMonitorStatusAction, + fetchEffectFactory( + fetchMonitorStatus, + getMonitorStatusActionSuccess, + getMonitorStatusActionFail + ) + ); + + yield takeLatest( + getSelectedMonitorAction, + fetchEffectFactory( + fetchSelectedMonitor, + getSelectedMonitorActionSuccess, + getSelectedMonitorActionFail + ) + ); } diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts index 91df43dd9e82..10010004d47a 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts @@ -6,16 +6,20 @@ import { takeLatest } from 'redux-saga/effects'; import { - FETCH_SNAPSHOT_COUNT, - fetchSnapshotCountFail, - fetchSnapshotCountSuccess, + getSnapshotCountAction, + getSnapshotCountActionFail, + getSnapshotCountActionSuccess, } from '../actions'; import { fetchSnapshotCount } from '../api'; import { fetchEffectFactory } from './fetch_effect'; export function* fetchSnapshotCountEffect() { yield takeLatest( - FETCH_SNAPSHOT_COUNT, - fetchEffectFactory(fetchSnapshotCount, fetchSnapshotCountSuccess, fetchSnapshotCountFail) + getSnapshotCountAction, + fetchEffectFactory( + fetchSnapshotCount, + getSnapshotCountActionSuccess, + getSnapshotCountActionFail + ) ); } diff --git a/x-pack/legacy/plugins/uptime/public/state/kibana_service.ts b/x-pack/legacy/plugins/uptime/public/state/kibana_service.ts new file mode 100644 index 000000000000..4fd2d446daa1 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/kibana_service.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart } from 'kibana/public'; +import { apiService } from './api/utils'; + +class KibanaService { + private static instance: KibanaService; + private _core!: CoreStart; + + public get core() { + return this._core; + } + + public set core(coreStart: CoreStart) { + this._core = coreStart; + apiService.http = this._core.http; + } + + private constructor() {} + + static getInstance(): KibanaService { + if (!KibanaService.instance) { + KibanaService.instance = new KibanaService(); + } + + return KibanaService.instance; + } +} + +export const kibanaService = KibanaService.getInstance(); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts index 95c576e0fd72..3650422571ce 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts @@ -5,19 +5,20 @@ */ import { snapshotReducer } from '../snapshot'; -import { SnapshotActionTypes } from '../../actions'; +import { + getSnapshotCountAction, + getSnapshotCountActionSuccess, + getSnapshotCountActionFail, +} from '../../actions'; describe('snapshot reducer', () => { it('updates existing state', () => { - const action: SnapshotActionTypes = { - type: 'FETCH_SNAPSHOT_COUNT', - payload: { - dateRangeStart: 'now-15m', - dateRangeEnd: 'now', - filters: 'foo: bar', - statusFilter: 'up', - }, - }; + const action = getSnapshotCountAction({ + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + filters: 'foo: bar', + statusFilter: 'up', + }); expect( snapshotReducer( { @@ -31,33 +32,28 @@ describe('snapshot reducer', () => { }); it(`sets the state's status to loading during a fetch`, () => { - const action: SnapshotActionTypes = { - type: 'FETCH_SNAPSHOT_COUNT', - payload: { - dateRangeStart: 'now-15m', - dateRangeEnd: 'now', - }, - }; + const action = getSnapshotCountAction({ + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + }); expect(snapshotReducer(undefined, action)).toMatchSnapshot(); }); it('changes the count when a snapshot fetch succeeds', () => { - const action: SnapshotActionTypes = { - type: 'FETCH_SNAPSHOT_COUNT_SUCCESS', - payload: { - up: 10, - down: 15, - total: 25, - }, - }; + const action = getSnapshotCountActionSuccess({ + up: 10, + down: 15, + total: 25, + }); + expect(snapshotReducer(undefined, action)).toMatchSnapshot(); }); it('appends a current error to existing errors list', () => { - const action: SnapshotActionTypes = { - type: 'FETCH_SNAPSHOT_COUNT_FAIL', - payload: new Error(`I couldn't get your data because the server denied the request`), - }; + const action = getSnapshotCountActionFail( + new Error(`I couldn't get your data because the server denied the request`) + ); + expect(snapshotReducer(undefined, action)).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts index aac8a90598d0..632f3a270e1a 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Action } from 'redux-actions'; import { - MonitorActionTypes, MonitorDetailsState, - FETCH_MONITOR_DETAILS, - FETCH_MONITOR_DETAILS_SUCCESS, - FETCH_MONITOR_DETAILS_FAIL, - FETCH_MONITOR_LOCATIONS, - FETCH_MONITOR_LOCATIONS_SUCCESS, - FETCH_MONITOR_LOCATIONS_FAIL, + getMonitorDetailsAction, + getMonitorLocationsAction, + getMonitorDetailsActionSuccess, + getMonitorDetailsActionFail, + getMonitorLocationsActionSuccess, + getMonitorLocationsActionFail, } from '../actions/monitor'; import { MonitorLocations } from '../../../common/runtime_types'; @@ -32,14 +32,14 @@ const initialState: MonitorState = { errors: [], }; -export function monitorReducer(state = initialState, action: MonitorActionTypes): MonitorState { +export function monitorReducer(state = initialState, action: Action): MonitorState { switch (action.type) { - case FETCH_MONITOR_DETAILS: + case String(getMonitorDetailsAction): return { ...state, loading: true, }; - case FETCH_MONITOR_DETAILS_SUCCESS: + case String(getMonitorDetailsActionSuccess): const { monitorId } = action.payload; return { ...state, @@ -49,17 +49,17 @@ export function monitorReducer(state = initialState, action: MonitorActionTypes) }, loading: false, }; - case FETCH_MONITOR_DETAILS_FAIL: + case String(getMonitorDetailsActionFail): return { ...state, errors: [...state.errors, action.payload], }; - case FETCH_MONITOR_LOCATIONS: + case String(getMonitorLocationsAction): return { ...state, loading: true, }; - case FETCH_MONITOR_LOCATIONS_SUCCESS: + case String(getMonitorLocationsActionSuccess): const monLocations = state.monitorLocationsList; monLocations.set(action.payload.monitorId, action.payload); return { @@ -67,7 +67,7 @@ export function monitorReducer(state = initialState, action: MonitorActionTypes) monitorLocationsList: monLocations, loading: false, }; - case FETCH_MONITOR_LOCATIONS_FAIL: + case String(getMonitorLocationsActionFail): return { ...state, errors: [...state.errors, action.payload], diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts index 2688a0946dd6..c2dfbd7f90ff 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts @@ -5,12 +5,12 @@ */ import { handleActions, Action } from 'redux-actions'; import { - getSelectedMonitor, - getSelectedMonitorSuccess, - getSelectedMonitorFail, - getMonitorStatus, - getMonitorStatusSuccess, - getMonitorStatusFail, + getSelectedMonitorAction, + getSelectedMonitorActionSuccess, + getSelectedMonitorActionFail, + getMonitorStatusAction, + getMonitorStatusActionSuccess, + getMonitorStatusActionFail, } from '../actions'; import { Ping } from '../../../common/graphql/types'; import { QueryParams } from '../actions/types'; @@ -31,34 +31,34 @@ type MonitorStatusPayload = QueryParams & Ping; export const monitorStatusReducer = handleActions( { - [String(getSelectedMonitor)]: (state, action: Action) => ({ + [String(getSelectedMonitorAction)]: (state, action: Action) => ({ ...state, loading: true, }), - [String(getSelectedMonitorSuccess)]: (state, action: Action) => ({ + [String(getSelectedMonitorActionSuccess)]: (state, action: Action) => ({ ...state, loading: false, monitor: { ...action.payload } as Ping, }), - [String(getSelectedMonitorFail)]: (state, action: Action) => ({ + [String(getSelectedMonitorActionFail)]: (state, action: Action) => ({ ...state, loading: false, }), - [String(getMonitorStatus)]: (state, action: Action) => ({ + [String(getMonitorStatusAction)]: (state, action: Action) => ({ ...state, loading: true, }), - [String(getMonitorStatusSuccess)]: (state, action: Action) => ({ + [String(getMonitorStatusActionSuccess)]: (state, action: Action) => ({ ...state, loading: false, status: { ...action.payload } as Ping, }), - [String(getMonitorStatusFail)]: (state, action: Action) => ({ + [String(getMonitorStatusActionFail)]: (state, action: Action) => ({ ...state, loading: false, }), diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts index b219421f4f4d..0b67d8b0e768 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts @@ -49,6 +49,7 @@ export function overviewFiltersReducer( return { ...state, errors: [...state.errors, action.payload], + loading: false, }; default: return state; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts index 2155d0e3a74e..3ba1ef84d41a 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Action } from 'redux-actions'; import { Snapshot } from '../../../common/runtime_types'; import { - FETCH_SNAPSHOT_COUNT, - FETCH_SNAPSHOT_COUNT_FAIL, - FETCH_SNAPSHOT_COUNT_SUCCESS, - SnapshotActionTypes, + getSnapshotCountAction, + getSnapshotCountActionSuccess, + getSnapshotCountActionFail, } from '../actions'; export interface SnapshotState { @@ -28,20 +28,20 @@ const initialState: SnapshotState = { loading: false, }; -export function snapshotReducer(state = initialState, action: SnapshotActionTypes): SnapshotState { +export function snapshotReducer(state = initialState, action: Action): SnapshotState { switch (action.type) { - case FETCH_SNAPSHOT_COUNT: + case String(getSnapshotCountAction): return { ...state, loading: true, }; - case FETCH_SNAPSHOT_COUNT_SUCCESS: + case String(getSnapshotCountActionSuccess): return { ...state, count: action.payload, loading: false, }; - case FETCH_SNAPSHOT_COUNT_FAIL: + case String(getSnapshotCountActionFail): return { ...state, errors: [...state.errors, action.payload], diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts index adba288b8b14..4767c25e8f52 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts @@ -13,11 +13,11 @@ export const isIntegrationsPopupOpen = ({ ui: { integrationsPopoverOpen } }: App integrationsPopoverOpen; // Monitor Selectors -export const getMonitorDetails = (state: AppState, summary: any) => { +export const monitorDetailsSelector = (state: AppState, summary: any) => { return state.monitor.monitorDetailsList[summary.monitor_id]; }; -export const selectMonitorLocations = (state: AppState, monitorId: string) => { +export const monitorLocationsSelector = (state: AppState, monitorId: string) => { return state.monitor.monitorLocationsList?.get(monitorId); }; diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index 427870797a20..09156db9ca7d 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -23,6 +23,7 @@ import { CommonlyUsedRange } from './components/functional/uptime_date_picker'; import { store } from './state'; import { setBasePath } from './state/actions'; import { PageRouter } from './routes'; +import { kibanaService } from './state/kibana_service'; export interface UptimeAppColors { danger: string; @@ -83,6 +84,8 @@ const Application = (props: UptimeAppProps) => { ); }, [canSave, renderGlobalHelpControls, setBadge]); + kibanaService.core = core; + // @ts-ignore store.dispatch(setBasePath(basePath)); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index ca9da51cc7ba..1783c6e91df3 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -89,7 +89,7 @@ const statusCountBody = (filters: any): any => { String id = doc["monitor.id"][0]; String idLenDelim = Integer.toHexString(id.length()) + ":" + id; String idLoc = loc == null ? idLenDelim : idLenDelim + loc; - + String status = doc["summary.down"][0] > 0 ? "d" : "u"; String timeAndStatus = doc["@timestamp"][0].toInstant().toEpochMilli().toString() + status; state.locStatus[idLoc] = timeAndStatus; @@ -111,7 +111,7 @@ const statusCountBody = (filters: any): any => { locStatus.merge(entry.getKey(), entry.getValue(), (a,b) -> a.compareTo(b) > 0 ? a : b) } } - + HashMap locTotals = new HashMap(); int total = 0; int down = 0; @@ -130,7 +130,7 @@ const statusCountBody = (filters: any): any => { String id = idLoc.substring(colonIndex + 1, idEnd); String loc = idLoc.substring(idEnd, idLoc.length()); String status = timeStatus.substring(timeStatus.length() - 1); - + // Here we increment counters for the up/down key per location // We also create a new hashmap in locTotals if we've never seen this location // before. @@ -141,7 +141,7 @@ const statusCountBody = (filters: any): any => { res.put('up', 0); res.put('down', 0); } - + if (status == 'u') { res.up++; } else { @@ -150,8 +150,8 @@ const statusCountBody = (filters: any): any => { return res; }); - - + + // We've encountered a new ID if (curId != id) { total++; @@ -171,7 +171,7 @@ const statusCountBody = (filters: any): any => { } } } - + Map result = new HashMap(); result.total = total; result.location_totals = locTotals; diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts index cc65749153c1..806d6e789a89 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts @@ -6,10 +6,11 @@ import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; export const createGetIndexPatternRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', - path: '/api/uptime/index_pattern', + path: API_URLS.INDEX_PATTERN, validate: false, options: { tags: ['access:uptime'], diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts index 44799aa19c14..d4d76c86870e 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts @@ -6,11 +6,11 @@ import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { REST_API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; export const createGetIndexStatusRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', - path: REST_API_URLS.INDEX_STATUS, + path: API_URLS.INDEX_STATUS, validate: false, options: { tags: ['access:uptime'], diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts index f8c7666f53f7..131b3cbe2ab4 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts @@ -7,10 +7,11 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; export const createGetMonitorLocationsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', - path: '/api/uptime/monitor/locations', + path: API_URLS.MONITOR_LOCATIONS, validate: { query: schema.object({ monitorId: schema.string(), diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts index ca88dd965c1a..66e952813eb3 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -7,10 +7,11 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', - path: '/api/uptime/monitor/details', + path: API_URLS.MONITOR_DETAILS, validate: { query: schema.object({ monitorId: schema.string(), diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts index 63e74175609a..f4a4cadc9997 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts @@ -7,10 +7,12 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; export const createGetMonitorDurationRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', - path: '/api/uptime/monitor/duration', + path: API_URLS.MONITOR_DURATION, + validate: { query: schema.object({ monitorId: schema.string(), diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/status.ts b/x-pack/plugins/uptime/server/rest_api/monitors/status.ts index 8dac50c9f590..08cbc2d70e51 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/status.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/status.ts @@ -7,10 +7,12 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; export const createGetMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', - path: '/api/uptime/monitor/selected', + path: API_URLS.MONITOR_SELECTED, + validate: { query: schema.object({ monitorId: schema.string(), @@ -32,7 +34,8 @@ export const createGetMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', - path: '/api/uptime/monitor/status', + path: API_URLS.MONITOR_STATUS, + validate: { query: schema.object({ monitorId: schema.string(), diff --git a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts index 02e54cb44183..5525771539c6 100644 --- a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts +++ b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; import { objectValuesToArrays } from '../../lib/helper'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; const arrayOrStringType = schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) @@ -15,7 +16,7 @@ const arrayOrStringType = schema.maybe( export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', - path: '/api/uptime/filters', + path: API_URLS.FILTERS, validate: { query: schema.object({ dateRangeStart: schema.string(), diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts index 21168edfc974..e301a2cbf9af 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts @@ -7,10 +7,11 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; export const createGetAllRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', - path: '/api/uptime/pings', + path: API_URLS.PINGS, validate: { query: schema.object({ dateRangeStart: schema.string(), diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts index 93ba4490fa31..dfaabcdf93a0 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts @@ -7,10 +7,11 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', - path: '/api/uptime/ping/histogram', + path: API_URLS.PING_HISTOGRAM, validate: { query: schema.object({ dateStart: schema.string(), diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts index e57951c98b6f..458107dd87a7 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts @@ -7,10 +7,11 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', - path: '/api/uptime/pings', + path: API_URLS.PINGS, validate: { query: schema.object({ dateRangeStart: schema.string(), diff --git a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts index c51806e32330..697c49dc8300 100644 --- a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts +++ b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -7,10 +7,11 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; export const createGetSnapshotCount: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', - path: '/api/uptime/snapshot/count', + path: API_URLS.SNAPSHOT_COUNT, validate: { query: schema.object({ dateRangeStart: schema.string(), diff --git a/x-pack/test/api_integration/apis/uptime/feature_controls.ts b/x-pack/test/api_integration/apis/uptime/feature_controls.ts index 15666acab233..91ea1bedb061 100644 --- a/x-pack/test/api_integration/apis/uptime/feature_controls.ts +++ b/x-pack/test/api_integration/apis/uptime/feature_controls.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { PINGS_DATE_RANGE_END, PINGS_DATE_RANGE_START } from './constants'; -import { REST_API_URLS } from '../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../../legacy/plugins/uptime/common/constants'; export default function featureControlsTests({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); @@ -30,7 +30,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const basePath = spaceId ? `/s/${spaceId}` : ''; return await supertest - .get(basePath + REST_API_URLS.INDEX_STATUS) + .get(basePath + API_URLS.INDEX_STATUS) .auth(username, password) .set('kbn-xsrf', 'foo') .then((response: any) => ({ error: undefined, response })) diff --git a/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts b/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts index 1f5322f581b3..3f42511dd165 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts @@ -5,14 +5,14 @@ */ import { FtrProviderContext } from '../../../ftr_provider_context'; import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; -import { REST_API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; export default function({ getService }: FtrProviderContext) { describe('docCount query', () => { const supertest = getService('supertest'); it(`will fetch the index's count`, async () => { - const apiResponse = await supertest.get(REST_API_URLS.INDEX_STATUS); + const apiResponse = await supertest.get(API_URLS.INDEX_STATUS); const data = apiResponse.body; expectFixtureEql(data, 'doc_count'); }); From c8b2b058978bcc6146b7d4dda9e52999aea6be44 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 18 Mar 2020 09:23:03 -0500 Subject: [PATCH 083/115] Fixes to service map single node banner (#60072) * Fixes to service map single node banner * Make the banner 95% width so it takes up the full width * Check the actual count of cytoscape nodes to determine whether or not to show the banner * Make the Cytoscape component able to take a function as children so we can access the cytoscape instance directly * Update the .NET icon * rework * Update x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx Co-Authored-By: Oliver Gupte Co-authored-by: Oliver Gupte --- .../app/ServiceMap/EmptyBanner.test.tsx | 62 +++++++++ .../components/app/ServiceMap/EmptyBanner.tsx | 71 +++++++--- .../app/ServiceMap/icons/dot-net.svg | 126 +----------------- .../components/app/ServiceMap/index.tsx | 4 +- 4 files changed, 116 insertions(+), 147 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx new file mode 100644 index 000000000000..d61dea80666a --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx @@ -0,0 +1,62 @@ +/* + * 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 { act, render, wait } from '@testing-library/react'; +import cytoscape from 'cytoscape'; +import React, { FunctionComponent } from 'react'; +import { MockApmPluginContextWrapper } from '../../../utils/testHelpers'; +import { CytoscapeContext } from './Cytoscape'; +import { EmptyBanner } from './EmptyBanner'; + +const cy = cytoscape({}); + +const wrapper: FunctionComponent = ({ children }) => ( + + {children} + +); + +describe('EmptyBanner', () => { + describe('when cy is undefined', () => { + it('renders null', () => { + const noCytoscapeWrapper: FunctionComponent = ({ children }) => ( + + + {children} + + + ); + const component = render(, { + wrapper: noCytoscapeWrapper + }); + + expect(component.container.children).toHaveLength(0); + }); + }); + + describe('with no nodes', () => { + it('renders null', () => { + const component = render(, { + wrapper + }); + + expect(component.container.children).toHaveLength(0); + }); + }); + + describe('with one node', () => { + it('does not render null', async () => { + const component = render(, { wrapper }); + + await act(async () => { + cy.add({ data: { id: 'test id' } }); + await wait(() => { + expect(component.container.children.length).toBeGreaterThan(0); + }); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx index 418430e37b21..464bf166eb80 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx @@ -7,37 +7,70 @@ import { EuiCallOut } from '@elastic/eui'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; +import { CytoscapeContext } from './Cytoscape'; -const EmptyBannerCallOut = styled(EuiCallOut)` +const EmptyBannerContainer = styled.div` margin: ${lightTheme.gutterTypes.gutterSmall}; /* Add some extra margin so it displays to the right of the controls. */ - margin-left: calc( - ${lightTheme.gutterTypes.gutterLarge} + - ${lightTheme.gutterTypes.gutterExtraLarge} + left: calc( + ${lightTheme.gutterTypes.gutterExtraLarge} + + ${lightTheme.gutterTypes.gutterSmall} ); position: absolute; z-index: 1; `; export function EmptyBanner() { + const cy = useContext(CytoscapeContext); + const [nodeCount, setNodeCount] = useState(0); + + useEffect(() => { + const handler: cytoscape.EventHandler = event => + setNodeCount(event.cy.nodes().length); + + if (cy) { + cy.on('add remove', 'node', handler); + } + + return () => { + if (cy) { + cy.removeListener('add remove', 'node', handler); + } + }; + }, [cy]); + + // Only show if there's a single node. + if (!cy || nodeCount !== 1) { + return null; + } + + // Since we're absolutely positioned, we need to get the full width and + // subtract the space for controls and margins. + const width = + cy.width() - + parseInt(lightTheme.gutterTypes.gutterExtraLarge, 10) - + parseInt(lightTheme.gutterTypes.gutterLarge, 10); + return ( - - {i18n.translate('xpack.apm.serviceMap.emptyBanner.message', { - defaultMessage: - "We will map out connected services and external requests if we can detect them. Please make sure you're running the latest version of the APM agent." - })}{' '} - - {i18n.translate('xpack.apm.serviceMap.emptyBanner.docsLink', { - defaultMessage: 'Learn more in the docs' + + - + > + {i18n.translate('xpack.apm.serviceMap.emptyBanner.message', { + defaultMessage: + "We will map out connected services and external requests if we can detect them. Please make sure you're running the latest version of the APM agent." + })}{' '} + + {i18n.translate('xpack.apm.serviceMap.emptyBanner.docsLink', { + defaultMessage: 'Learn more in the docs' + })} + + + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg index 9f7427f0e100..da7f1a8fde45 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg @@ -1,127 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 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 7bbb77a49c84..1b0486d7d6de 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 @@ -182,9 +182,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { style={cytoscapeDivStyle} > - {serviceName && renderedElements.current.length === 1 && ( - - )} + {serviceName && }
    From fb8175816f1671bd08210091e444a01ab3a225fa Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Wed, 18 Mar 2020 15:31:14 +0100 Subject: [PATCH 084/115] [ML] Disable functional transform tests --- x-pack/test/functional/apps/transform/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 60b72f122f11..5dcfd876f5b5 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -8,7 +8,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { const transform = getService('transform'); - describe('transform', function() { + // prevent test failures with current ES snapshot, see https://github.com/elastic/kibana/issues/60516 + describe.skip('transform', function() { this.tags(['ciGroup9', 'transform']); before(async () => { From a708d69f50acc667e081d81466b5b9e6bb1a4aac Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 18 Mar 2020 18:06:59 +0300 Subject: [PATCH 085/115] [Visualize] Duplicated query filters in es request (#60106) * [Visualize] Duplicated query filters in es request Closes: #59630 * Fix CI * fix CI * move uniq_filters to common * fix scripts/check_published_api_changes Co-authored-by: Elastic Machine --- ...na-plugin-plugins-data-public.esfilters.md | 4 +- .../filter_manager}/compare_filters.test.ts | 2 +- .../query/filter_manager}/compare_filters.ts | 2 +- .../filter_manager}/dedup_filters.test.ts | 10 +---- .../query/filter_manager}/dedup_filters.ts | 2 +- .../data/common/query/filter_manager/index.ts | 22 ++++++++++ .../filter_manager}/uniq_filters.test.ts | 2 +- .../query/filter_manager}/uniq_filters.ts | 2 +- src/plugins/data/common/query/index.ts | 1 + src/plugins/data/public/index.ts | 4 +- src/plugins/data/public/public.api.md | 8 ++-- .../query/filter_manager/filter_manager.ts | 12 ++++-- .../data/public/query/filter_manager/index.ts | 2 - .../query/filter_manager/lib/only_disabled.ts | 3 +- .../state_sync/connect_to_query_state.ts | 3 +- .../create_global_query_observable.ts | 4 +- .../specs/kibana_context.ts | 43 ++++++++++--------- 17 files changed, 73 insertions(+), 53 deletions(-) rename src/plugins/data/{public/query/filter_manager/lib => common/query/filter_manager}/compare_filters.test.ts (99%) rename src/plugins/data/{public/query/filter_manager/lib => common/query/filter_manager}/compare_filters.ts (98%) rename src/plugins/data/{public/query/filter_manager/lib => common/query/filter_manager}/dedup_filters.test.ts (95%) rename src/plugins/data/{public/query/filter_manager/lib => common/query/filter_manager}/dedup_filters.ts (97%) create mode 100644 src/plugins/data/common/query/filter_manager/index.ts rename src/plugins/data/{public/query/filter_manager/lib => common/query/filter_manager}/uniq_filters.test.ts (99%) rename src/plugins/data/{public/query/filter_manager/lib => common/query/filter_manager}/uniq_filters.ts (96%) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md index e03072f9a41c..7fd65e5db35f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -44,8 +44,8 @@ esFilters: { getPhraseFilterField: (filter: import("../common").PhraseFilter) => string; getPhraseFilterValue: (filter: import("../common").PhraseFilter) => string | number | boolean; getDisplayValueFromFilter: typeof getDisplayValueFromFilter; - compareFilters: (first: import("../common").Filter | import("../common").Filter[], second: import("../common").Filter | import("../common").Filter[], comparatorOptions?: import("./query/filter_manager/lib/compare_filters").FilterCompareOptions) => boolean; - COMPARE_ALL_OPTIONS: import("./query/filter_manager/lib/compare_filters").FilterCompareOptions; + compareFilters: (first: import("../common").Filter | import("../common").Filter[], second: import("../common").Filter | import("../common").Filter[], comparatorOptions?: import("../common").FilterCompareOptions) => boolean; + COMPARE_ALL_OPTIONS: import("../common").FilterCompareOptions; generateFilters: typeof generateFilters; onlyDisabledFiltersChanged: (newFilters?: import("../common").Filter[] | undefined, oldFilters?: import("../common").Filter[] | undefined) => boolean; changeTimeFilter: typeof changeTimeFilter; diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts similarity index 99% rename from src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts rename to src/plugins/data/common/query/filter_manager/compare_filters.test.ts index da8f5b356494..b0bb2f754d6c 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts @@ -18,7 +18,7 @@ */ import { compareFilters, COMPARE_ALL_OPTIONS } from './compare_filters'; -import { buildEmptyFilter, buildQueryFilter, FilterStateStore } from '../../../../common'; +import { buildEmptyFilter, buildQueryFilter, FilterStateStore } from '../../es_query'; describe('filter manager utilities', () => { describe('compare filters', () => { diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts b/src/plugins/data/common/query/filter_manager/compare_filters.ts similarity index 98% rename from src/plugins/data/public/query/filter_manager/lib/compare_filters.ts rename to src/plugins/data/common/query/filter_manager/compare_filters.ts index a2105fdc1d3e..e047d5e0665d 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.ts @@ -18,7 +18,7 @@ */ import { defaults, isEqual, omit, map } from 'lodash'; -import { FilterMeta, Filter } from '../../../../common'; +import { FilterMeta, Filter } from '../../es_query'; export interface FilterCompareOptions { disabled?: boolean; diff --git a/src/plugins/data/public/query/filter_manager/lib/dedup_filters.test.ts b/src/plugins/data/common/query/filter_manager/dedup_filters.test.ts similarity index 95% rename from src/plugins/data/public/query/filter_manager/lib/dedup_filters.test.ts rename to src/plugins/data/common/query/filter_manager/dedup_filters.test.ts index ecc0ec94e07c..228489de37da 100644 --- a/src/plugins/data/public/query/filter_manager/lib/dedup_filters.test.ts +++ b/src/plugins/data/common/query/filter_manager/dedup_filters.test.ts @@ -18,14 +18,8 @@ */ import { dedupFilters } from './dedup_filters'; -import { - Filter, - IIndexPattern, - IFieldType, - buildRangeFilter, - buildQueryFilter, - FilterStateStore, -} from '../../../../common'; +import { Filter, buildRangeFilter, buildQueryFilter, FilterStateStore } from '../../es_query'; +import { IIndexPattern, IFieldType } from '../../index_patterns'; describe('filter manager utilities', () => { let indexPattern: IIndexPattern; diff --git a/src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts b/src/plugins/data/common/query/filter_manager/dedup_filters.ts similarity index 97% rename from src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts rename to src/plugins/data/common/query/filter_manager/dedup_filters.ts index d5d0e70504b4..7d1b00ac10c0 100644 --- a/src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts +++ b/src/plugins/data/common/query/filter_manager/dedup_filters.ts @@ -19,7 +19,7 @@ import { filter, find } from 'lodash'; import { compareFilters, FilterCompareOptions } from './compare_filters'; -import { Filter } from '../../../../common'; +import { Filter } from '../../es_query'; /** * Combine 2 filter collections, removing duplicates diff --git a/src/plugins/data/common/query/filter_manager/index.ts b/src/plugins/data/common/query/filter_manager/index.ts new file mode 100644 index 000000000000..315c124f083a --- /dev/null +++ b/src/plugins/data/common/query/filter_manager/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. + */ + +export { dedupFilters } from './dedup_filters'; +export { uniqFilters } from './uniq_filters'; +export { compareFilters, COMPARE_ALL_OPTIONS, FilterCompareOptions } from './compare_filters'; diff --git a/src/plugins/data/public/query/filter_manager/lib/uniq_filters.test.ts b/src/plugins/data/common/query/filter_manager/uniq_filters.test.ts similarity index 99% rename from src/plugins/data/public/query/filter_manager/lib/uniq_filters.test.ts rename to src/plugins/data/common/query/filter_manager/uniq_filters.test.ts index 8b525a3d2a2e..5a35e85c95ea 100644 --- a/src/plugins/data/public/query/filter_manager/lib/uniq_filters.test.ts +++ b/src/plugins/data/common/query/filter_manager/uniq_filters.test.ts @@ -18,7 +18,7 @@ */ import { uniqFilters } from './uniq_filters'; -import { buildQueryFilter, Filter, FilterStateStore } from '../../../../common'; +import { buildQueryFilter, Filter, FilterStateStore } from '../../es_query'; describe('filter manager utilities', () => { describe('niqFilter', () => { diff --git a/src/plugins/data/public/query/filter_manager/lib/uniq_filters.ts b/src/plugins/data/common/query/filter_manager/uniq_filters.ts similarity index 96% rename from src/plugins/data/public/query/filter_manager/lib/uniq_filters.ts rename to src/plugins/data/common/query/filter_manager/uniq_filters.ts index 44c102d7ab15..683cbf7c78a8 100644 --- a/src/plugins/data/public/query/filter_manager/lib/uniq_filters.ts +++ b/src/plugins/data/common/query/filter_manager/uniq_filters.ts @@ -17,8 +17,8 @@ * under the License. */ import { each, union } from 'lodash'; +import { Filter } from '../../es_query'; import { dedupFilters } from './dedup_filters'; -import { Filter } from '../../../../common'; /** * Remove duplicate filters from an array of filters diff --git a/src/plugins/data/common/query/index.ts b/src/plugins/data/common/query/index.ts index d8f7b5091eb8..421cc4f63e4e 100644 --- a/src/plugins/data/common/query/index.ts +++ b/src/plugins/data/common/query/index.ts @@ -17,4 +17,5 @@ * under the License. */ +export * from './filter_manager'; export * from './types'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 58bd9a5ab05d..339a5fea91c5 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -47,13 +47,13 @@ import { isQueryStringFilter, isRangeFilter, toggleFilterNegated, + compareFilters, + COMPARE_ALL_OPTIONS, } from '../common'; import { FilterLabel } from './ui/filter_bar'; import { - compareFilters, - COMPARE_ALL_OPTIONS, generateFilters, onlyDisabledFiltersChanged, changeTimeFilter, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 783411bbf27e..07d8d302bc18 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -362,8 +362,8 @@ export const esFilters: { getPhraseFilterField: (filter: import("../common").PhraseFilter) => string; getPhraseFilterValue: (filter: import("../common").PhraseFilter) => string | number | boolean; getDisplayValueFromFilter: typeof getDisplayValueFromFilter; - compareFilters: (first: import("../common").Filter | import("../common").Filter[], second: import("../common").Filter | import("../common").Filter[], comparatorOptions?: import("./query/filter_manager/lib/compare_filters").FilterCompareOptions) => boolean; - COMPARE_ALL_OPTIONS: import("./query/filter_manager/lib/compare_filters").FilterCompareOptions; + compareFilters: (first: import("../common").Filter | import("../common").Filter[], second: import("../common").Filter | import("../common").Filter[], comparatorOptions?: import("../common").FilterCompareOptions) => boolean; + COMPARE_ALL_OPTIONS: import("../common").FilterCompareOptions; generateFilters: typeof generateFilters; onlyDisabledFiltersChanged: (newFilters?: import("../common").Filter[] | undefined, oldFilters?: import("../common").Filter[] | undefined) => boolean; changeTimeFilter: typeof changeTimeFilter; @@ -1843,8 +1843,8 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:38:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromEvent" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:60:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index c951953b2655..fba1866ebd61 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -22,13 +22,19 @@ import { Subject } from 'rxjs'; import { IUiSettingsClient } from 'src/core/public'; -import { COMPARE_ALL_OPTIONS, compareFilters } from './lib/compare_filters'; import { sortFilters } from './lib/sort_filters'; import { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; -import { uniqFilters } from './lib/uniq_filters'; import { onlyDisabledFiltersChanged } from './lib/only_disabled'; import { PartitionedFilters } from './types'; -import { FilterStateStore, Filter, isFilterPinned } from '../../../common'; + +import { + FilterStateStore, + Filter, + uniqFilters, + isFilterPinned, + compareFilters, + COMPARE_ALL_OPTIONS, +} from '../../../common'; export class FilterManager { private filters: Filter[] = []; diff --git a/src/plugins/data/public/query/filter_manager/index.ts b/src/plugins/data/public/query/filter_manager/index.ts index 09990adacde4..be512c503d53 100644 --- a/src/plugins/data/public/query/filter_manager/index.ts +++ b/src/plugins/data/public/query/filter_manager/index.ts @@ -19,8 +19,6 @@ export { FilterManager } from './filter_manager'; -export { uniqFilters } from './lib/uniq_filters'; export { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; export { onlyDisabledFiltersChanged } from './lib/only_disabled'; export { generateFilters } from './lib/generate_filters'; -export { compareFilters, COMPARE_ALL_OPTIONS } from './lib/compare_filters'; diff --git a/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts b/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts index 34e1ac38ae95..18c51ebeabe5 100644 --- a/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts +++ b/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts @@ -18,8 +18,7 @@ */ import { filter } from 'lodash'; -import { Filter } from '../../../../common'; -import { compareFilters, COMPARE_ALL_OPTIONS } from './compare_filters'; +import { Filter, compareFilters, COMPARE_ALL_OPTIONS } from '../../../../common'; const isEnabled = (f: Filter) => f && f.meta && !f.meta.disabled; diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts index a22e66860c76..331d8969f248 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts @@ -21,10 +21,9 @@ import { Subscription } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import _ from 'lodash'; import { BaseStateContainer } from '../../../../kibana_utils/public'; -import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; import { QuerySetup, QueryStart } from '../query_service'; import { QueryState, QueryStateChange } from './types'; -import { FilterStateStore } from '../../../common/es_query/filters'; +import { FilterStateStore, COMPARE_ALL_OPTIONS, compareFilters } from '../../../common'; /** * Helper to setup two-way syncing of global data and a state container diff --git a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts index d0d97bfaaeb3..dd075f9be7d9 100644 --- a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts +++ b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts @@ -20,10 +20,10 @@ import { Observable, Subscription } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { TimefilterSetup } from '../timefilter'; -import { COMPARE_ALL_OPTIONS, compareFilters, FilterManager } from '../filter_manager'; +import { FilterManager } from '../filter_manager'; import { QueryState, QueryStateChange } from './index'; import { createStateContainer } from '../../../../kibana_utils/public'; -import { isFilterPinned } from '../../../common/es_query/filters'; +import { isFilterPinned, compareFilters, COMPARE_ALL_OPTIONS } from '../../../common'; export function createQueryStateObservable({ timefilter: { timefilter }, diff --git a/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts b/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts index 4092dfbba00d..b8be273d7bbd 100644 --- a/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts +++ b/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - +import { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../../expression_functions'; import { KibanaContext } from '../../expression_types'; +import { Query, uniqFilters } from '../../../../data/common'; interface Arguments { q?: string | null; @@ -35,6 +36,15 @@ export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition< Promise >; +const getParsedValue = (data: any, defaultValue: any) => + typeof data === 'string' && data.length ? JSON.parse(data) || defaultValue : defaultValue; + +const mergeQueries = (first: Query | Query[] = [], second: Query | Query[]) => + uniq( + [...(Array.isArray(first) ? first : [first]), ...(Array.isArray(second) ? second : [second])], + (n: any) => JSON.stringify(n.query) + ); + export const kibanaContextFunction: ExpressionFunctionKibanaContext = { name: 'kibana_context', type: 'kibana_context', @@ -75,9 +85,9 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { }, async fn(input, args, { getSavedObject }) { - const queryArg = args.q ? JSON.parse(args.q) : []; - let queries = Array.isArray(queryArg) ? queryArg : [queryArg]; - let filters = args.filters ? JSON.parse(args.filters) : []; + const timeRange = getParsedValue(args.timeRange, input?.timeRange); + let queries = mergeQueries(input?.query, getParsedValue(args?.q, [])); + let filters = [...(input?.filters || []), ...getParsedValue(args?.filters, [])]; if (args.savedSearchId) { if (typeof getSavedObject !== 'function') { @@ -89,29 +99,20 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { } const obj = await getSavedObject('search', args.savedSearchId); const search = obj.attributes.kibanaSavedObjectMeta as { searchSourceJSON: string }; - const data = JSON.parse(search.searchSourceJSON) as { query: string; filter: any[] }; - queries = queries.concat(data.query); - filters = filters.concat(data.filter); - } + const { query, filter } = getParsedValue(search.searchSourceJSON, {}); - if (input && input.query) { - queries = queries.concat(input.query); - } - - if (input && input.filters) { - filters = filters.concat(input.filters).filter((f: any) => !f.meta.disabled); + if (query) { + queries = mergeQueries(queries, query); + } + if (filter) { + filters = [...filters, ...(Array.isArray(filter) ? filter : [filter])]; + } } - const timeRange = args.timeRange - ? JSON.parse(args.timeRange) - : input - ? input.timeRange - : undefined; - return { type: 'kibana_context', query: queries, - filters, + filters: uniqFilters(filters).filter((f: any) => !f.meta?.disabled), timeRange, }; }, From 6abb9d7d18d32ee944181841ed9919f3444d1365 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 18 Mar 2020 08:19:50 -0700 Subject: [PATCH 086/115] Closes #60265. Adds Beta badge to service map (#60482) --- .../app/ServiceMap/PlatinumLicensePrompt.tsx | 56 ++++++++++++------- .../components/app/ServiceMap/index.tsx | 23 +++++++- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.tsx index 9213349a1492..77f0b64ba0fb 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.tsx @@ -6,10 +6,12 @@ import { EuiButton, - EuiEmptyPrompt, + EuiPanel, EuiFlexGroup, EuiFlexItem, - EuiPanel + EuiTitle, + EuiText, + EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; @@ -18,7 +20,8 @@ import { useKibanaUrl } from '../../../hooks/useKibanaUrl'; export function PlatinumLicensePrompt() { // Set the height to give it some top margin - const style = { height: '60vh' }; + const flexGroupStyle = { height: '60vh' }; + const flexItemStyle = { width: 600, textAlign: 'center' as const }; const licensePageUrl = useKibanaUrl( '/app/kibana', @@ -29,30 +32,41 @@ export function PlatinumLicensePrompt() { - - - - {i18n.translate( - 'xpack.apm.serviceMap.licensePromptButtonText', - { - defaultMessage: 'Start 30-day Platinum trial' - } - )} - - ]} - body={

    {invalidLicenseMessage}

    } - title={ + + + + +

    {i18n.translate('xpack.apm.serviceMap.licensePromptTitle', { defaultMessage: 'Service maps is available in Platinum.' })}

    - } - /> +
    + + +

    {invalidLicenseMessage}

    +
    + + + {i18n.translate('xpack.apm.serviceMap.licensePromptButtonText', { + defaultMessage: 'Start 30-day Platinum trial' + })} + +
    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 1b0486d7d6de..93aa3d406028 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 @@ -15,6 +15,8 @@ import React, { useRef, useState } from 'react'; +import { EuiBetaBadge } from '@elastic/eui'; +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'; @@ -56,7 +58,12 @@ ${theme.euiColorLightShade}`, margin: `-${theme.gutterTypes.gutterLarge}`, marginTop: 0 }; - +const BetaBadgeContainer = styled.div` + right: ${theme.gutterTypes.gutterMedium}; + position: absolute; + 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) { @@ -184,6 +191,20 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { {serviceName && } + + +
    ) : ( From 965679a5b1d61bc1ad6a38d350cca92243cfbca6 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Wed, 18 Mar 2020 18:28:22 +0300 Subject: [PATCH 087/115] [NP] Cutover ensureDefaultIndexPattern to kibana_utils (#59895) * Cutover ensure_default_index_pattern to kibana_utils * Fix conflicts * Proper name for argument Co-authored-by: Elastic Machine --- .../kibana/public/dashboard/legacy_imports.ts | 1 - .../public/dashboard/np_ready/legacy_app.js | 25 ++++----- .../kibana/public/discover/kibana_services.ts | 7 ++- .../discover/np_ready/angular/discover.js | 4 +- .../kibana/public/visualize/legacy_imports.ts | 1 - .../public/visualize/np_ready/legacy_app.js | 16 +++--- src/legacy/ui/public/legacy_compat/index.ts | 5 +- .../kibana_legacy/public/angular/index.ts | 1 - .../history}/ensure_default_index_pattern.tsx | 53 ++++++++----------- .../kibana_utils/public/history/index.ts | 1 + src/plugins/kibana_utils/public/index.ts | 2 +- 11 files changed, 50 insertions(+), 66 deletions(-) rename src/plugins/{kibana_legacy/public/angular => kibana_utils/public/history}/ensure_default_index_pattern.tsx (67%) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index b497f73f3df2..3f81bfe5aadf 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -33,7 +33,6 @@ export { IInjector } from 'ui/chrome'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { configureAppAngularModule, - ensureDefaultIndexPattern, IPrivate, migrateLegacyQuery, PrivateProvider, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index f7baba663da7..64abbdfb87d5 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -23,11 +23,11 @@ import dashboardTemplate from './dashboard_app.html'; import dashboardListingTemplate from './listing/dashboard_listing_ng_wrapper.html'; import { createHashHistory } from 'history'; -import { ensureDefaultIndexPattern } from '../legacy_imports'; import { initDashboardAppDirective } from './dashboard_app'; import { createDashboardEditUrl, DashboardConstants } from './dashboard_constants'; import { createKbnUrlStateStorage, + ensureDefaultIndexPattern, redirectWhenMissing, InvalidJSONProperty, SavedObjectNotFound, @@ -137,8 +137,8 @@ export function initDashboardApp(app, deps) { }); }, resolve: { - dash: function($rootScope, $route, kbnUrl, history) { - return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl).then(() => { + dash: function($route, history) { + return ensureDefaultIndexPattern(deps.core, deps.data, history).then(() => { const savedObjectsClient = deps.savedObjectsClient; const title = $route.current.params.title; if (title) { @@ -172,11 +172,9 @@ export function initDashboardApp(app, deps) { controller: createNewDashboardCtrl, requireUICapability: 'dashboard.createNew', resolve: { - dash: function($rootScope, kbnUrl, history) { - return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl) - .then(() => { - return deps.savedDashboards.get(); - }) + dash: history => + ensureDefaultIndexPattern(deps.core, deps.data, history) + .then(() => deps.savedDashboards.get()) .catch( redirectWhenMissing({ history, @@ -185,8 +183,7 @@ export function initDashboardApp(app, deps) { }, toastNotifications: deps.core.notifications.toasts, }) - ); - }, + ), }, }) .when(createDashboardEditUrl(':id'), { @@ -194,13 +191,11 @@ export function initDashboardApp(app, deps) { template: dashboardTemplate, controller: createNewDashboardCtrl, resolve: { - dash: function($rootScope, $route, kbnUrl, history) { + dash: function($route, kbnUrl, history) { const id = $route.current.params.id; - return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl) - .then(() => { - return deps.savedDashboards.get(id); - }) + return ensureDefaultIndexPattern(deps.core, deps.data, history) + .then(() => deps.savedDashboards.get(id)) .then(savedDashboard => { deps.chrome.recentlyAccessed.add( savedDashboard.getFullPath(), diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 5f3dbb65fd8f..725e94f16e2e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -54,14 +54,17 @@ import { search } from '../../../../../plugins/data/public'; export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; // @ts-ignore export { intervalOptions } from 'ui/agg_types'; -export { subscribeWithScope } from '../../../../../plugins/kibana_legacy/public'; // @ts-ignore export { timezoneProvider } from 'ui/vis/lib/timezone'; -export { unhashUrl, redirectWhenMissing } from '../../../../../plugins/kibana_utils/public'; export { + unhashUrl, + redirectWhenMissing, ensureDefaultIndexPattern, +} from '../../../../../plugins/kibana_utils/public'; +export { formatMsg, formatStack, + subscribeWithScope, } from '../../../../../plugins/kibana_legacy/public'; // EXPORT types diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 6978781fe669..9a383565f4f4 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -115,9 +115,9 @@ app.config($routeProvider => { template: indexTemplate, reloadOnSearch: false, resolve: { - savedObjects: function($route, kbnUrl, Promise, $rootScope) { + savedObjects: function($route, Promise) { const savedSearchId = $route.current.params.id; - return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl).then(() => { + return ensureDefaultIndexPattern(core, data, history).then(() => { const { appStateContainer } = getState({ history }); const { index } = appStateContainer.getState(); return Promise.props({ diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index 69af466a0372..e6b7a29e28d8 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -33,7 +33,6 @@ export { DashboardConstants } from '../dashboard/np_ready/dashboard_constants'; export { VisSavedObject, VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/'; export { configureAppAngularModule, - ensureDefaultIndexPattern, IPrivate, migrateLegacyQuery, PrivateProvider, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js index 1002f401706c..0f1d50b149cd 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js @@ -24,6 +24,7 @@ import { createHashHistory } from 'history'; import { createKbnUrlStateStorage, redirectWhenMissing, + ensureDefaultIndexPattern, } from '../../../../../../plugins/kibana_utils/public'; import editorTemplate from './editor/editor.html'; @@ -32,7 +33,6 @@ import visualizeListingTemplate from './listing/visualize_listing.html'; import { initVisualizeAppDirective } from './visualize_app'; import { VisualizeConstants } from './visualize_constants'; import { VisualizeListingController } from './listing/visualize_listing'; -import { ensureDefaultIndexPattern } from '../legacy_imports'; import { getLandingBreadcrumbs, @@ -82,8 +82,7 @@ export function initVisualizeApp(app, deps) { controllerAs: 'listingController', resolve: { createNewVis: () => false, - hasDefaultIndex: ($rootScope, kbnUrl) => - ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl), + hasDefaultIndex: history => ensureDefaultIndexPattern(deps.core, deps.data, history), }, }) .when(VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, { @@ -94,8 +93,7 @@ export function initVisualizeApp(app, deps) { controllerAs: 'listingController', resolve: { createNewVis: () => true, - hasDefaultIndex: ($rootScope, kbnUrl) => - ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl), + hasDefaultIndex: history => ensureDefaultIndexPattern(deps.core, deps.data, history), }, }) .when(VisualizeConstants.CREATE_PATH, { @@ -103,7 +101,7 @@ export function initVisualizeApp(app, deps) { template: editorTemplate, k7Breadcrumbs: getCreateBreadcrumbs, resolve: { - savedVis: function($route, $rootScope, kbnUrl, history) { + savedVis: function($route, history) { const { core, data, savedVisualizations, visualizations, toastNotifications } = deps; const visTypes = visualizations.all(); const visType = find(visTypes, { name: $route.current.params.type }); @@ -121,7 +119,7 @@ export function initVisualizeApp(app, deps) { ); } - return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl) + return ensureDefaultIndexPattern(core, data, history) .then(() => savedVisualizations.get($route.current.params)) .then(savedVis => { if (savedVis.vis.type.setup) { @@ -144,9 +142,9 @@ export function initVisualizeApp(app, deps) { template: editorTemplate, k7Breadcrumbs: getEditBreadcrumbs, resolve: { - savedVis: function($route, $rootScope, kbnUrl, history) { + savedVis: function($route, history) { const { chrome, core, data, savedVisualizations, toastNotifications } = deps; - return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl) + return ensureDefaultIndexPattern(core, data, history) .then(() => savedVisualizations.get($route.current.params.id)) .then(savedVis => { chrome.recentlyAccessed.add(savedVis.getFullPath(), savedVis.title, savedVis.id); diff --git a/src/legacy/ui/public/legacy_compat/index.ts b/src/legacy/ui/public/legacy_compat/index.ts index 3b700c8d5939..2067fa648930 100644 --- a/src/legacy/ui/public/legacy_compat/index.ts +++ b/src/legacy/ui/public/legacy_compat/index.ts @@ -17,7 +17,4 @@ * under the License. */ -export { - configureAppAngularModule, - ensureDefaultIndexPattern, -} from '../../../../plugins/kibana_legacy/public'; +export { configureAppAngularModule } from '../../../../plugins/kibana_legacy/public'; diff --git a/src/plugins/kibana_legacy/public/angular/index.ts b/src/plugins/kibana_legacy/public/angular/index.ts index 5fc37ac39612..16bae6c4cffe 100644 --- a/src/plugins/kibana_legacy/public/angular/index.ts +++ b/src/plugins/kibana_legacy/public/angular/index.ts @@ -21,7 +21,6 @@ export { PromiseServiceCreator } from './promises'; // @ts-ignore export { watchMultiDecorator } from './watch_multi'; export * from './angular_config'; -export { ensureDefaultIndexPattern } from './ensure_default_index_pattern'; // @ts-ignore export { createTopNavDirective, createTopNavHelper, loadKbnTopNavDirectives } from './kbn_top_nav'; export { subscribeWithScope } from './subscribe_with_scope'; diff --git a/src/plugins/kibana_legacy/public/angular/ensure_default_index_pattern.tsx b/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx similarity index 67% rename from src/plugins/kibana_legacy/public/angular/ensure_default_index_pattern.tsx rename to src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx index 1a3bb84ae757..7992f650cb37 100644 --- a/src/plugins/kibana_legacy/public/angular/ensure_default_index_pattern.tsx +++ b/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx @@ -18,14 +18,13 @@ */ import { contains } from 'lodash'; -import { IRootScopeService } from 'angular'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { History } from 'history'; import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; import { EuiCallOut } from '@elastic/eui'; import { CoreStart } from 'kibana/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { toMountPoint } from '../../../kibana_react/public'; let bannerId: string; let timeoutId: NodeJS.Timeout | undefined; @@ -39,18 +38,17 @@ let timeoutId: NodeJS.Timeout | undefined; * resolve to wait for the URL change to happen. */ export async function ensureDefaultIndexPattern( - newPlatform: CoreStart, + core: CoreStart, data: DataPublicPluginStart, - $rootScope: IRootScopeService, - kbnUrl: any + history: History ) { const patterns = await data.indexPatterns.getIds(); - let defaultId = newPlatform.uiSettings.get('defaultIndex'); + let defaultId = core.uiSettings.get('defaultIndex'); let defined = !!defaultId; const exists = contains(patterns, defaultId); if (defined && !exists) { - newPlatform.uiSettings.remove('defaultIndex'); + core.uiSettings.remove('defaultIndex'); defaultId = defined = false; } @@ -61,10 +59,9 @@ export async function ensureDefaultIndexPattern( // If there is any index pattern created, set the first as default if (patterns.length >= 1) { defaultId = patterns[0]; - newPlatform.uiSettings.set('defaultIndex', defaultId); + core.uiSettings.set('defaultIndex', defaultId); } else { - const canManageIndexPatterns = - newPlatform.application.capabilities.management.kibana.index_patterns; + const canManageIndexPatterns = core.application.capabilities.management.kibana.index_patterns; const redirectTarget = canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home'; if (timeoutId) { @@ -73,31 +70,27 @@ export async function ensureDefaultIndexPattern( // Avoid being hostile to new users who don't have an index pattern setup yet // give them a friendly info message instead of a terse error message - bannerId = newPlatform.overlays.banners.replace(bannerId, (element: HTMLElement) => { - ReactDOM.render( - - - , - element - ); - return () => ReactDOM.unmountComponentAtNode(element); - }); + bannerId = core.overlays.banners.replace( + bannerId, + toMountPoint( + + ) + ); // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around timeoutId = setTimeout(() => { - newPlatform.overlays.banners.remove(bannerId); + core.overlays.banners.remove(bannerId); timeoutId = undefined; }, 15000); - kbnUrl.change(redirectTarget); - $rootScope.$digest(); + history.push(redirectTarget); // return never-resolving promise to stop resolving and wait for the url change return new Promise(() => {}); diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts index bb13ea09f928..1a73bbb6b04a 100644 --- a/src/plugins/kibana_utils/public/history/index.ts +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -19,3 +19,4 @@ export { removeQueryParam } from './remove_query_param'; export { redirectWhenMissing } from './redirect_when_missing'; +export { ensureDefaultIndexPattern } from './ensure_default_index_pattern'; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 47f90cbe2a62..1876e688c989 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -73,5 +73,5 @@ export { StartSyncStateFnType, StopSyncStateFnType, } from './state_sync'; -export { removeQueryParam, redirectWhenMissing } from './history'; +export { removeQueryParam, redirectWhenMissing, ensureDefaultIndexPattern } from './history'; export { applyDiff } from './state_management/utils/diff_object'; From f93ec7988b13d5e690e6490ff443ebebc1bcd46d Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Wed, 18 Mar 2020 17:14:45 +0100 Subject: [PATCH 088/115] [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 ad4d636164d7..0e239c24dd9c 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 005bb78e458e..de4ba25590c9 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 aa5be59b6a5c..f4e13748641e 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 f075771e9808..71c9acc6c10d 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 76991bde7700..5c402b896093 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 3cdf011d9d0e..929f2518ee74 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 eb515f5652f3..4a1a84baf659 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 86b61245aa3b..5a71c7dee54d 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 000000000000..609914616a68 --- /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 089/115] 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 aa56b565ef32..1fa620c5394a 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 32fb35d6adeb..f5da3918bf10 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 090/115] [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 000000000000..7e6778ca4fb4 --- /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 715d98ed3369..e8c9c2e3cf6c 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 091/115] 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 1dd069bb907d..98ab4c1f15a5 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 092/115] 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 80694347393c..026c23b93b04 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 9ab9f2ad0d6b..71b42e346411 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 27226eb010ba..365f3afdab39 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 39bd66ff71c6..ee97a5acfd3d 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 e810fe0ccdba..a2e951cb5b77 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 5ce508ec1ed5..97fc02ac64e1 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 zcmce;Wmr`08$CLJfFd2zprnL=fOIJ!9g-s5Al+ReT>{b_(%qfXCDIH%bPYod&40uD zzQ1$Mm-FqMxh^gpnZ5V3?^tWy>)B(7qPzqK8VMQ*1j3M#6jK6$khVb}gwv-ez`vBe zMtuYPf#9Gd@c~peO11+6y#z^#y;pHf-CuM`)pBaRhs`i3*X73k_<=!I5o1+7;cVh^b#wRG zc}{LoE^q7BQ^55{n*FGY34Azp?9y8}BMK*sej@mvm!uByj^h`Erhnh1&s|dZ{`>bV z3s{YbXG)#bs&g}gLo)&!M;j{6Kd-rbF)FFo_<7X02>6*{|9pO3RqTJKxueCE68Yb| z_lmwORF5wukTjzR{Q92ccp0NZ+hwscg4;Xl8D8Ydi1_oSK6tt5 z76|_qDk}Ey3f0Xp`NPl0S5Q=x*Vve(-yL{-=m9-}P%e*#KQ|eFfoAL|!e7U&sH!T* zOtA-dD8C$5^@KNb+xPC|8(Rv924b4bR%A73mFYk`EA&O}c+;+HN07@~d-GYEWBM`@ zV=L`8=cBCe5QHxWqKJe%dBFA173Bo&fj9L_(_TJa2V6x(MN+^1YLIqQ9=3!jXgkvu ztCh}ewCS<+XWV;o$HfucT#osy$8Kfx9n{|^Qr=%|>Of@Z@MvMz*bh00UYp4$K4l@) z;qm9czwlUt@l528W!9BN!k6!TPeuiUFi5$M_uDtC+)mUYg}vEzTMkdqiP^u?Zwd5t z1=!dhn3n5zT9i-B&kw4q>vU?Eraf~+QSxbqBB+(?bYE_zRSyZlYZo;Lb$_?0Z*n;A zf9{QGo z9+`rIlJ&nbCU%Fs6B>3rb73mOSo;f%P+EOv$cO3m2^1i1U)~ED}q%A?xy4JM#0*0 zgsbj(#lGYQ7j)h?>wv&)!3sW@9am?h7n}8MLuo!YUT`k;a*gg(UIpb(pE_%2yaS`i zy;RC)>dV`)%4hoU6h>EX;R*H^7qH=U!Pr7&1%pF}v6Cx{=LijuNd9Li{=>=d@O(cr z_xgB4gBGt{H{#TJo#qEhK39eY27Y8|I{J2a&1E&;G6aQq++UL8hR`UCx`p;NA^; zgp~?%%h`sIhbqicOaxj+Z>i<7<)~{&fE&&*UB2A0&^T7D*J<(ngFYzT`%0JD9imDn zpW4CQc1iw%Me9k&&E<3VgM3zS8b90(WZia&x>!HbbV0=oo;Pl1Z5N1g6&N)wYYc6^ z^W?sV``lysx}9vgKu=DiGUm77`Zkc1lSz&R_dNyTcVB68K1Qse3SZ!)a9OPZ35CF1 zeMiIaA|m8UwCmHCZx5qn)7+KHL4s-W4v_lxzOI0);sLa= zN2`SH&6;}C@;!ZeV)y(zrV6X#>$}!luW%m6Co|rMVI?}PfrEo+ng7Bq2Wd*^H zY}osU1hccV--WTamrU4`j7~P+wD#g-)^oTVp2#NJKB>Py?OTkB=rw4*5dvA?!mI?2 z`ypiZ{cKdxPoo6SX?(6G$R8UD-ZCWUvOKlbPW+fx28?PG-Nn5vLS0w;c57&%uK3!L zD@3mCCa&CJO)9_2{KSNjiRmF?6y6t~K4h`EEWk*0JXbZ^&TCn#y>C(CS+tjK7s5z+J?@@k*(WXWp-EZ?kmi4L_M{(0{jl(~HqeeiDpB zJzHak;j%QxTYSA1#)@<_o1*|a8uM8i>%}NeVmG$;Sqk-vwCNdC1!>v!7z`O|ig*2ozUR@MroidSGMq2{{2bG|{gz(feqrL_3Oumy5QGc8ovK4C z3;-i)l=D|%HA?EbV<}-TkRAYDSsBl!ZSL{})~B%J9SJ_z^g}&F)|;=1y@U^qouPI3 zoL7Tqn~ceo^JMAp(M~%5{%ZDOb%DcSyN7$i*Ar?5_^(Q`wf|B5a!~6cfPEUb;7cQg9AsDz%dUV9a(4B zjP7cj8!O^b2NKqq`(Y|QWmcX$^~T0V^J3j2$N#K*$ZEZxvyTn*Z#l7JRDRqqnFQc& z5Rd+bITRMWHodGO0C_?^xM70UyWTgu?O%?NJ%p@G>cwhcfQB&Ku8_wX_LuX_h)*DnZ-2af5!5W9m;MSxf%OsBnVIvPVXvgKlP4 zR`he$)Xo;S6StcXUZB@ZMw*_253e&u8ni$?aqttnI~Pp*BR(tmfI}n>~RNZK#;x&NH~Z5*e=hb3RqwHTMoJnWnzIu z`CO1Ec<)LB95H%L$!xs#`&l?KcOU_iI;>ClCVoJf4sU1sC2oZM?^Q_Sb$UC3UeYuR zm{iDs=TGA>@ylpQCGFdB+?_VdnY!~k+@^C2qK|MZAn`y*vC#qw%a?msgQ85kdn0b$ zn9h0I5Rg!g797Ta>g(}4oEHMFUG}v9j|f_zlMla%?xvNgc2}aPL+Fi z|73{0>?UtFz752<**9(9J|hBOZS#7Nj^#uNc2Y+Pj1s78^(M66&>ptJq0P)wC>Otz z(%x@ryEttw`P_Rf>jQv(v%v}n**Ztv+@#UQ7HV5_$U(aApQxPLw*b zb9Jo_47a%USFRi{b)T_p&Ce9y8CWr ztF^`7-_IUEjf8~ca(}BVC#N-MX8-Wcx}KG#(dfkk&JpTxrsPEOg&GgVL$9Uy=fD>L z0clWp>`Rrk-!&a}nE^_vK})_j{LpgaY@fq{ceg-FmVX0(R1k{Da(`-kv&?#*?{Te7 z@MqDQaOBM!fvbMJYr*ugmB#FxIsdjV-mjPAv|st9#HNw&Dh|ow)OWM2I)mSN@#{= z0a_Ix_4)GK{hRAVCsZkwGyX(1k&%NdgCwT5-x7J`a#~uDyb{laSn-aoTuP`t$7M#Ij&w{#d)(D07 zbks2Q_G>VUcRH~lM^yYczc?fv;=l7Yr8_rlNSsLx`REv zht|8;-qDXO9#~Xag*gb9`^s|v%3c0%oUQ}^fQDgO*a z*WHqEEz>lFPQ8j|MpqwT^g%Bu8NP6auLlt77%X04c%E0QJ8TEzc*3sAtF|M01a6Ly zQawgby=`n1ATl&DHT6L*9=bHRopt21b>f^P2W8>y)#vBVsq!-{GAnRX0V^vjbRxFQ zG4M_lFtbtN0Jvr93AW0@CIE02XTJVfdKx?QAkA<=JmAh-jUtV>T3r?47M;x&&R%pk)5UjwjZK z#~aA~{2dQx$y_6h^~LPLt!S$M#l;b%pkmQu;Ugc0L%MtXTr~?IuuF8W;i!l_$X+R8YXu)=5nr(`Ummk_8=MB+X@G}VSN%%y?!6@&F7*%IX{nD?dqId)(rX(&a zMo4eu&V&P&$(OZ%X7ayS(3+AZLtD(1Fl5(!;{&0(7&`2yG4lCWt@l_$pXIHzz1lPC zg#npg&hojR=|s1C@62iW5RRJ2ZG%8hCY&_Hk|O>Z-vk!p(gf9QStkc1U4k}O7+z$? zaEsgXqiZBD_rp%vf6V|%N-Ol~)v(%w6T7z0z7d^GUP7Y~Sk+NPPfMlQGjJUg#OJ>@ zL)a$8Lw4w`1yJ{pdWpfv{23<~SEYV8-I9lo2K+XK%LW)7ZH{_LWz5XY584EALyitT zWRl>67A2n>13xpPh&(W{F0iVhct+>?hIuv&OiJO%Bo5P4Z%Qs}ll|Fo*J?^${8PsHW;8W=J%$oWYst-@>@(u#KH5Bo~B8c`A^1`48s*a4aUF`Q_0 z6~gbZm{9&1ke?RB!q&_z^|3x9UI;#!4s};k$Tk2^^K2SQj4v#7@k6~q9rI448VyHs zAoT7Pe`BV^lgW0#EZ89^(99dimBK41DcOE!T|PDzyxQIDL&0bC6x69l+Nq~m4hDTh zr;ME0THbQNa;9zW_v!lnthrVD-wf=RiwLwpQO_Wi^a_F@hLOBzzGCd1fe|S@`xVh- zPLpEOWs4m@2m1a_zsPQE40z7M6UAPia(v7lh)LcB%yLrs6P`z%_GOJ%_9&QS{lH4h zES`U=tqlZ7=OsR-!^J9uh~F7KM0HQZUPWHO4h=*=u%YEJwV^UIS?M^37SEzP zz(AkTzY6IyBxD)`7KfJ`wtt(51LHa&zF4b;pX=*<8HBEQRx~_Aj zinQVf@jxK?Rof%950k7Nq}Tp<60hO_jd>`0`0BMU7rOs5iSVu9?mpNw7z*1he$Km< zTFqns_mEy_(b@&xhM-i4Z`H|_%3`|J@Xab$i9vQ^pCbRd8wTBLMCJGoaJ)GC-|Cek zuQ_|~w><&R6LDF-1|1xb11b!0#gPxFDk{~fcPgq=w#NhXhv%2Z#%b@N`*Vaqg-BHY zjh`YWiX!C>v=H_n&?%3KE1o`rx;=MSHGsy*i{92*F>BVvNOBGW3+u@Zk=VCe@JGeS z{nayZTMg~r*h3N`^O>>IE-(Gn)4lQ@>H22DUv*&f-;f#2Cx)(Gdn*c4G@FdLf9vF$ z2lIb!^Q1NWNIC7b94#TQw-NfT23a)xU_5(x)pq6NQj#WyhYS-C$^$?hO48xwPB3h? zU-1Dg`Z8UTZEL5|RiG4|lNt)98;Mgo32IXP*! z9*qaTT<}jb`Ij-SwzvHtvoEaYwwSN5w_Zk4@K|R6`u7Gq-SwCl6v_7$S2>Ri?6thz z{ZXHvnNF4map=a(`YbVuyhnz0B)`m?Kcn$#BGqTf@kPry3YPFO4G3T;qy5D3#JmBZ z-j4@wX12G371cI3QyoST`L2Jct4smJpQxbyPEY5!yFDoP+3Wy_tey0pqt zKq9Duq9`-s4rKKI`+$-jo&2l=vJ-=L(Z8far3Cu_lY+}zw~Tw5xSVUm5QyF_3`q5% zueVGDZn_yMm2mq1M}wtzJncRe@msnUS#57WuqV+a$yS%X9Xu(oKe|S>FhA=#Z;DW1 z93fEG4(gkjhu+RiP9MyiVE!fzi?==4YMbAy>ycSnogedx+u0i3Y`?$ec61c8=gU*d z1!@07@st#v&irQ)9+)5ZNU3iOt-ArP%`3Ae9Wbg|zP{hng_@41*qmLvT~p(R*aKR) z7t7m~G^Jj}X<0{Gc?jO-!=!uB^$xHUGR!T%uHAb(NXYl94A%Vq_w-ROTK0@Jl^rvp zifzIxRX!eJh)p(^)?u^QP?-Yr>{c2BP5yHUBt zdSUp&P^w*T;c$x?3TXLeQ@!zSXn9Sz{Td?>onoOJt|F98?D(h)qlLayvLuf8-5n>i zLylk&p5uZL&ue~e*d6MSU9)x#XMNR5t@vV;$97g9F;8YptUOjr{zcR5DSbG<)7%;O5uZMh?zoSa6m9eQ&w*BwdsuqKP?5=fNG5=KXAemdkF?DNdm zoMgcP>aTU1w3l?hyO*m}czDZ4j%lL9=^srkaY_Q8F8WGG=^_gsi=~qX>4EFWy}W&l z)RW&6KCXEuhdjM?D;^H73u`7xaVAkrl| z)sf40=Y;6w?1{i0fIWbEiR_o30j}}6-MbYynG_8t=MPLG9Ldy|pg!4ZADwBt5+_MX z=GjW+05o2d!){Gs?nK&kD@;>&Lq@mhg)e|jC#R`FfO%|icV3U$2yF|xxSA>70;ITp za<6sYfw{Ty%|+#+aPuO|$0&+`Wy`)#+kD0@ZOMK z;qEiMJxcJbBx1k>~~^xY@j4lzRDxScE@qR0mwm9Hx*ioYqHi%decD-ajII zMSPzQ-=*jibUk|MY-zK9$4jA5;pKsZgrai3sydWzhVoGB(#d_4^wItB*J8OU;UtGf znvgb!Kg|@)tl)ZX+tW`Hh$}Ex;&=8mAYe2fPUZDSE^qHFJ|VR4#?YNVI0?caN9Q6 zh>au$s#xgmVq($;EFYF1*S>tOAkTlcet6B2&g<`U1F@@vnymxQ&|X_>c1vohrbVz= z_RoX_2SB7GovgP9f6SqYm9lQS%x}3~Qkv)IYuMzNsrK%sixj$&%##(06sf@QowAsQ zl{c~?{#i5MIMKZzX}@acZ#WK>aoZj6v4ss2H{*xz?W}1K9$j_yFzdF30!lTA(k*5} zA)^-%cGov|{bm2aA#HKns}GG}L?ksEG`n7@B4%bP)J*^}t317IGpe@@KQ{5$P!cR4 zL<*i8dR~RHf@}v$!Wz!!nVO*~&Vy17+VUpDJcRk(<6hSr8R;Vt%WWaFHm%+5>2BY3 z@1Rx-SCISF>@fZeeuqu;QJz~BOYmm(jLra-_Yq!;kg#YF`sS8!JAp5)%#!|H@$<%g zBcf>`An+4cjjK*Do^<4BVpwt~?5nd%m zv$S0lcJmnZwiLFjhJy1FD|7EpJMHX%t-nQ%_q*;n%N0?sQ!9vM(em(GK|=gaF4 z;IUpvzmJnqU zJ-!U%wFqH!wryv)pt@qh-(QS}KroeJc1frkv?Te2S?^^E#tcIW#L#BC;GQZr zU@!UF;PpU6N1SN?$#xwJlL335wxCxr@6IAVxyN_CLnr#Ys&|&Bc!xr()UnyVIM|+0 z^N=LecMm!mHEX2yfceYrmjAm>Pb_g73$B?!J3jY}-OqH)n)&S>(P^bi_*6;KcoDyj z1@WW(lOL}m%bdOzO-M@oS--No8ctD0)nh3kgP)({vb@AzKae-?OojGA%w6Ninu#Sp zg!@N7Tu}`3>w_JE$sW}F>g_SI;Rv7i>j2yjSo7ut&jG_wWKNet&9tH?_r6yu=$ut>d zXxbvFSu%lp3BG3U9Ei-45Yk6DbP_CvspDo-UFW{|ks{?ppi?i{FmiC7v))=lr7*Q{A`gn>`CjgGSshL!#GJt@ zDwex4br^iKUpx{{w7J_h!j8c;RpYQJ<$uD%4yLbP4lMeHgnkzgq_TNcc(II51$5*94rGGVSL8I*E_K9zHw}f+ZsJl*B-+(eJ z3j-v!1p4Z&F9hlrJ*jQlZV`sm%aB*HBr=Rwe5JkNLz+qMYjz8lGpHw*?7(Js7d0yv zyK5$rM3hXmd{7~=BZJfwnD3m(Gv&)B%U`Q;c3}eJ7rjBx<*torsWD2jSv39|zXWikEx9Y!W(jsAcWHL(hM1N=jhbg?Bm_0i9T++MIG z#1e0LBbKv@h#EQw>4pYNzW3Pw^)b7WhQPR0_?(>Uw*iBvpKJO@x)`?|M^ntJQRFrE zU9HWf`0h_3`01`xBWuP&gG0dS>Mu^yUdFJxVf@i%p(jlD;cCA)(_U~I6V^B$RT@q6 z<^!dxvEoG%&D_pXM-s_|og%=&5>a{RlU6do;;Xky`oN>9npW(vU_$ZP6-8ef~RO~xii^m)8- z_%_!NW!=n(D!_R4y6teyR@-u@12Ti&B-yt9PDHoX1j*Q%d--O53n9^S|I>oq%&nH| z-5ff(KyXp=)nT8S>I{cb|4S4s5=q9m?8>?B0@0vYNjh_YiurO~{Bi&s-gnMGTM%E@ zJ3b1-Kqpz5-BZ#{m@rb7^T3p#jnP&;J4oDILUP>J+HCXmQbcGX`x#&MJVy4H9j^x^ zQ=Rc2ToufRwK#=bXZj(TeLY_}LzeudR06IDK!_s%rP?3kb6FMkZed|Z^j_#)GRg~H zJ|U|c`QF`EKcm?gGWvpZJ_C{fgmndOtUdAV2O6HnNl))rGXNeZnyq1+ZoN!XqzJ&CL5GsdBlrD%=yOvBuXofcg#U>+1Re7nFh!vd#`m5Ld_ zm(Mp$xTcCWa~V5r1AMG`$gvAfv{GdbZ&wDzVsAyK7k}f|=k&bB$096ALIOk7cmrj? zH3UvQk^C85bn)#>SI_Yw9~&F*j=QS_*rU7bps5F(uGt=1`cp6D?RR*)c`tdU=x_#V zTUhL%21LEq{tlp_DreGEq+85U0AgS46?J)x@GmEI>sM9srS{;2tbg<}{4SK6;o{IN zH*Jj(4WC1cCIeu1PHU!Z<}}}tgdh}eAokl+0HA5A)vV3B_2IYFOB-ynOZciRW%#Xc zoDQx_R+eIoKZqyw?8;>^M@r|~0;G|@0PH`Z4zZgl7Qc&bd9Zwy47$jr>gs>nHeXTX z5}~N5&8pah8QB?8&8aUF^~^I!{M5&_sD7q-sw% zfcij6v?gAvi3l5BL+u9xoSE((5-&c|#E^E{ev@PlSR89o+pvzMi!yos>r{}VJ;@J= z?DKUn2I+B~dR zsbrq4YquNjZ~R_@Q%WGKhCW(L`SB*WrfVS1=l77s9nM0FNn95Oomz}*INSn^Z59@L z-^N8X$8m4P3e>E<%QDMKd?WLh+F@;)tp_FW^9PVj9&!}5d-Vpxq?HPcI&KEZgteMq zW#mDN_fym;E6y4@LggWR~C#yg!4u|6czCkR<5L@i|MKvEnZQ zM+PPS>BI}m-hSH?VUZ_B!Jt5gX5%N_zQLa9sly=of)Cu|+!DV2MjjUH}?Fm zRbN_|S|^B~Igez^*v%=;L_$C;igWQV&O({2XG#9tmvSIJED`Cm2)|rrUpR^>{a!0XkCdkIDca?qRhk*~y-gHEWka{+ zk@YNEXNggM5FyeXSEc$WDzn$`WU4swyNw)55gi#Qz$ttuV_2Df2q1v8O=`cy;=+zo zC>F|YOOh{M<#*?*A~Z?L|FeQ(t^mw})-MY3LGNGwd^wpu+CT1txTAgNMo?0rV zkkbSO6CucIMfE*1g8{S*4Lg#C!NHV81YW2(s_R^3vI^C zJZjSi7wFo!Mv^7BhHGtG)k4^Q(*%su~JX?4liSoX6_` zCK3glh8%7WRCkey_M*Fzt3Pw>>ZtaUPdHr|gr6OV&I zmck~y3Y`)uFw1*eDK%L$N2AfqLNl!tRe3J>PTYv_adC=lsU4gVB^wz&zdJ}(GN}Rv zAW^1{ZXSgBqNUTh7d2N94NcVz0K~2wf$MyGyv{nT*N)d7hLhe4nUPAur;8F!kE3l3 zM0nF?w#m&LBYs!+#5ulfp3hQr8JUXrF}+XSZYoP&1n&XJFGqpQsFX;eX^^OaCP|{H z>hV5QM$>}|BKW77=;Ic8Yp_wq<5ecthDL{1N0~P=We_aBMi`i8mHxC!**BXGU|^u| zt9v33{@rgdAIsPn_Rly(Rg=TZ?>8`NIrI|yZ%GI~oedj5?>6R8M-G55R7)acrY|Pa zEr&8c6&H%xe|V?@oUbrrdbT-@8TKn%3HH5f+?^;RcDV+@khB2W);bKU)hC5Ni_~Q7 z00pBV=TY^>U#(qDIpn_}*bK?}Vy@VKadk4FL+*uyecBvV>UyKNS!XsLqr-r^q?c-` zqflj8nuj|;@Yo`Blj$^R@>{u*WWL7VxC=zY(Zc#D`=Wa|vA|qr)9fw`xNnwt)LvX$ zC!-|hy=N;4tEHOz2M%04?X*T0ygc(K(k>&Y0)9inVn>j-cQ}*ZCuEPWg&CKkx)UaJ ztFSsolRfgGk-*(&HB)J4ljJ}9Q$U8|d#7K|37|7iDAKwxkhJMh?U^g&g}7&ht@}QP zI=1+Qg*Nk0?p!gjD487kKr3S?XRkgfXWcGTCsb6E#{)=C?%&-2mYC;{RU<3vK(i-9 z>`^uKTpR5N_m9O7AppD-wRdY~W6XL9!wpR<-OonRb2upL z<=!{pKaFAy1jlWhwfKUKixgAw1#H;TV^vk_j8~pBiVzNPJsy@T7(rHg%G+1Oow&W`0x|Vgh$h3d!m|?-`JwjFj>*#CHApgF7N6vQ~jHk}1<^ zfw4}9O76>9vujW)srE=bR2XlfQ`KyNhy;cjOLdx^^;I4$sFa>F){3HOv8DFU!M~Z4 zSe96i0awISd(4YuHMh${TJQ@b5nw^MvvaI#k>=n0=Fz2BLwl8JaLtFw(#H8M{il5+ zQKKy%HW8G>TKpD9ex4d>{&^w4>Df-je}c@%)UcHeROyiU-)!SUrA*H|o-(o*m`=PH zUpzliy?qdFFCIfiMY3mUW4NKWX=@rd&g+0{rkqK2xdH0~(81-(Ao+ExC|W#gV#nzF zq0V&1AuE8S?VA*${X-s$ZWY#J+k6bKJaA0@GXL5w;h_fqio^ostO)duR(T;i`m7Sb z_t6hfN~J{e#S_-TYy&k&Mp^PT`9s^D+-he(*WZz)W}(z^{E77U4%?jKFOW8k=7ZqC ze-QxXr662s%Q5^%8HVx_H z^+HT+IJbIy-K&`WUu4+Z(YAY9HDdh1$Z?ReDZV@Z1wyv>6mWctF{_miEb*U1p4qU$ zIcNpv;{D3yWENLH6y2(f;}Y0X9We$AOf<&8NW-e4omV1ZxG1u1B|Cy58%@^BQl)d7b zDlwXg$B6p%#uI&{&@6G1LMrtJsN9?Ldd+RmNKX?5T8g=DyiCWUh92 z(c1QD`59HxgX5I@)DS@2K<9FNzs#V7 zLcFn}YWXBqwCrQetNs(9+SZpOTQoDPW425fp-9p(-dL56Yf4%zE2_H%{i`79$ z%c%%-K%cavT#cehlJ2p~ZGoh)yC8^d`C7dPyluvexRCYCb=Vg7@(WQU2MnTFNgM0T=eR&|-oHR*Yy zQlSdkyo{WPzMpU%LhNP(gb8rzy!HG37dX+a#}{Z!M9#B7N5wbFSij?xIR1iN95$hU zJSvU69i!W(M^-E{x@HB|+v6I^4l|PWBb%q9s-l-$>N2aG?qSPR<=Bof9BKRf-Vrce ziEAzlcTHZpZ9MOl+VH@p1PBvjH;5(X?L%77vL+$ z?fK9RVEC$M$?tyN9MuwxhT?9Qj66(T(c!OCZTN9;=EP0)lwM8*KCE6OCB8xC(Df#u zHuOn{K50RArBx({0xjGu{5{gd=PZP6jDc8U*Mzc)>F(Jn{Zww&_;~*OUE>lfCgzWWWm`di zb+%(P)M9+W-nEV>? z6hv0ZkrfZQH0s}p(Wt#9FWk^udEWarysO#Xf~cZW4In(s$I$|DdN;~YGXVZ6Esuls zGYuh$AWrIN*Yj~XJVqwVcqT$^`+{1K1-pnT#nl5lpq3nsUWtk2rU>Krv6QHYW>&{| z#7eaOp}j`5dPas>{nGRgLTX(nn5kx1+djYXotmvSMrQSGrtr^~3(>=Xg&g{5J|j-; zMnUX9BWuu%)}eoi^Jn432F46W{KFmTByesg-mj_h?2s7JfMkD6#lyPUhXFAf=~Kh9 zD`0l>MzKZ~gA&dVKLgp|UBjY%-N78Cv%nJeYfoiF+nyR*8sE~_ny3Q6T5-(aG$?|r zm-k^C^pi6um&0i^-o>zLHL(!zl!;Aib-1anYXI=TSEH#{s*I@@t}n2`<3Z_~S~}%K z#1h54E8H?Da$48II9eevhomQ9#)BJG-xK@wnG zBR8&uXI)Z+{%kN45T}Y}nGI9R)35j?&jAFE3K|4Y^SMQZ^iuTRFWu<^26TAeRtreP zQ2BY(s9U7X;*pzm^X11S&ugUka;@hbqESLa9SCtZhY9uUXTKp0C*@e1C$z!AM&i}n z@=e%G2wwdMoG?Hv86^oPW>z!nuNJ-LF#j6y!f2I+;Z2(eSpo+%AL^m9$quV!SQK3s zSwDMfV`kYqu?yi)*Zx3*yKyYTvT`iL3R*dEN#HQ=a>bE<3YQhvF9iOPnw_VQ}#F!x6l1vI3<5FcS6h9 zE!M~?q3|uM(hg@NMQZ?Aog7~rU7WmrvPn4>eUMeG^jVvdx~ghi^~@`PhO7>reK-J& z!?a6^L7hUvCL_(4skzxDAP z+ugvo&qR!oA$^f3E5Nha8mt`rGEVyMeGm4xe6!61)N7CM(kcKTAuLp|)&+{tFr+4MOnYy?^ z0D?lvtlLvapW)kGmt=eK`30*Rr7wI*S4v!**YIQ1@Rke?&Okw}<_F~V@Mum>H-b!= z^O{=viZ}irgz-VCnEoP~9~$dpR7>x=tN{Mt#30ekzN84093R|6ZvtP z4H$*`Au(MZa`QR-i6Rn?KN%JB2j6XBg8ttu-!9>30-T+ovnl;ROh;hL%R_aOYi_(G zmTe$ige_#^lfW_**aZ0~XG7k7o33JX~nIPvMTp=iSaO9R57ZmXXHGVc*Py+ zXCx>;8{Yu86i!w)W-uV&qiKf^OuR&j;6GU)?W*i4LI81W`+sz`xmtix00&^X8=K(^ z^0?$UTe$YO6P9@6Gqh0;C0iD%UuL3C{A%w#`JgG?TxOP2vLQx6?WGO!#{d&sXiETu zNn2F+>Kj<~LKm|RIqUpMp^83XH6DXXfX~mcU@{{$tFPTVuOp+j97#x2Udvbgo>9{Z z$V;u0@`^?4k#bhCiBe>KRSq^@g%Qlan3P_JLeLd~9XA{^nLi}K1djJ7>6YM3>mUoDX(!T&3IxOET8-b#|ja2MB=#j0fOdJXdOwh@> zE^h3AGF5x>;n_YF9|ed_QP^>y0WkjjLbHM_Exy&(uCnJn}@W-=&A!L49 zJs@=VNr?=}ERa(6VajkqId1=SKuNmUgsJy^H9`FRX3HCO+|I9E+7F&gAmPH z1{D>0Jz&!(v+aG{0&O*7meDvt@ZOqz@+3DSmNt7gWjJ0$e#L=jR4x=9i7ehvxIn`( z`i`-U<2|t6_2<7~^|YaRfCY&O&K=JIDw^3xGj)xhL`W&;gI&z4S`3^)tsa-bL&ro{ zz=g_P4uzTlcGY{*uU+D9h$v}s3;tO=A-Rl=GMg9Qp8XZSqTg^#WW=W@R<5Q-6nAYL za@Y+W3XN5188tK4})F%jR%kd3nUfHxBuM$8ozg zz9TeQk#ZK~_(`@CK<6@n=*+520NRzn7#Ys8!@2(_B+*DQv(z>0_j}XpPCkOrSR|EY z?9KyrhF*(@As=Qz8zSjL0NVi$HUF$m(93+RU>IbjSf2ek<~;zs%uLHUvp#|PfIK(+ zfGKMwii+9H)BY0JFIdh~v!J?FO_gW~mVBns%J3bmu|U}eM6^(qgEoeke4Iw)(t;` zGCr`_3KpF$U+6V{53uWXVL*c`{v}ig?Zer*h7; z9>25+iXa9jP}1xj%keL@5t+K*3;~$p0bq)Fk%!77CaZyDIoH>>ZFW0w=&KdE9WGvq`b&7T4@-4YMNV^l(~L%M}j2E&vM9 zz@k2o!H*~x05S8wKwd>GUPh`8(=G_FSG-z?Rw1yMV+E%eHiWkN769%IKuD544{~@Q z_MPU32CZKwZ!IS6ET{3JnKAWxK$jg>>PNNo*nni>%`L4jV^VNIL|DFZNK2@+1 zwX0X(8#rJ$R|RG&A*KjwU9sGLz@ZAyX1%Xw8WM|J*zgrIWQZwr93LZdu6*NP>a^;K zB^oNL>a~MU=s1f)(T?aC-NqPQE)0Bg`|k^RgM>l+Re9%{*CGkGTUfnzagwG;prCFe z&FaW!I~hd>(J>nOaq|9v78buMacn~XEMiGEptp#-(#h(iT8q?%5RQUkW@%%5Fw<&@ zV+J_ed`+-_*-(w+x9PPQAv!*6<^TfQOZ8iR5EWHG`5UcdRVyoSrUt!%JqXdtF8pt> zOjDyafVf`j&D0-^lj5NIM&VXToNlMBFaN4Pfamc`Z0Q)5R(wax=YT#vz^RRkxrNxs z#Opbs_aXeJK^>&Q39_VxX>pAz1KQ^qeWcyYl?;wS zRu#2nj-lRC&Q^ubA+NK5Ekc$>RYR(|hgpLL>&D7Xw%Sp?rL~j7^@?Pq4WmS6@p<&`{{$ja1W?UO$@?ca{`IJg$CNM z5}t?NRN1eTEPk7go@gjZ?{73>=ZyZefHiYF%4Lq9t~7YJvj(7OwV8=NMR+GfyAATi zmng-lePdECLY2L$XydME=>BbKcisB*n%zutip094sUf33mw-&&HB=tU*MJ(h^CMC2b_*h9R5aSwzy={W*iH871fuzYl83L z>O9VWI3Y=Hx#oy*eB1@W^PQly;DWxd7=&*l-XG#3&N8tPW$6vRBUI|G2C1terx=YR z9Z+giTK;%i!(~Mvo;mn3Z9D5R7@uobyP{0d?Gy4CW|vIqUC=fQEJZ`iokQ}T{XuAR~8(4c*rN=a9xts=T-T+D&wdD<8rU8ROGtM?{3Piu532?wR20kCq;6 zAm3~>vbedF@7wwYS0pr4BwBq0s9$9#h!hH>iy72vJMl!n>Nalno-bi%s|F3>>jMAb zMK+Rl81%1}bVv@n@Vyuo-f}(&XWX}Fa&a~fmL4y~#Pj;7=KH+5MK8Aq>KBH#{dM`R zx3XA)JMWdRy=ceB@%_9azfilsM%fk<6I8S4Z4cxB!}nI;b#HTCOc%rrH-4j!tA`wf zA6NZn$MYJQlsbt>Vhf}aFN9M|dt~v4O?GlOg4?szRt0V2TX67NnZ9c!<_GzwuPQwH zL22{KWxSI4hTevfDBF&0x71AMNvg?=ikn**+gXV1&xEg7h5g#^t0^0|EZR>(e1bBr z8t*YS?BCTqr=|*}o{6>9qLdznU}sLfw%+=&6F!-4KN9A2`vxQe>OoUp*okmHdGT^h za7k443sj`rpjs>&b$tF73y$^tg8u$mI7mP7$cyrRV)e21&4&dRiWMADuHDOi^%vcN({We3O%L*L<OM=3Pyj?LXAm>PLdwZ-C!^m;Q|OrF{&jD+!bcW>>x9K5yB682A=5mBR;pfAP|Wh(i#8A8hVbZAx}n105)v-mYQl; zBlqvg!iwcQr!vonJa*?ZqeR|E1m4H}?0m84TU~$y6fB&Qe@*Z6VD+O^Kr2Mw z(rht-0Eq>c_zN-9aPGYq11I@abnN6$kx3wn08R(S<`sFa9&2;u;mt@+Z`qlhqTbxb z#B$11aW)i|-qDS9uqkA&4UQP7>==s8%r=AVSN#dYw^Z9|Lu=@O0)huYrKVW86!ra9 z0{QvxLB8$JUwx1wP#ectLqq9D2f665oPy~%Rmn3TlN9^k<9!X>lajyt_?Eru)Yx^5 zUTR4TMYIF`kVhuQx0*^N?fWpNow6(7_cQF|X4?b=LJA2QXAKUeKQ3}&Wv8PNc`LHB zXyTq^KP}2f!PU^lYNx#+DC)BZ#Ju4HHV!$Lpg zms$NCs_^~S-^TNWKSOuD_dit0qigCEYuJ1OtCcl|CBdNA9EW+vT*KGmN^>hU`Aet@ z!OV@MHh9|Qto#(^`~lmJK9sP(q_Nv}vU0Cb#YcvPg)qa}Dv{gp76n8Bde<8kY4>@h z|C#yep_eu7-a`0V`I+k78)q6CN^s z3%6#{VVFg|q(J1ksE3J`uIaKW3iw&h2pHoQmxyTR4?3(T^7{bG*{WC?Q-Wwmm&2+N zA`fi3&n}b|^c|^*_It6gdZlXDXv?rLLGY1lU&F4nj>Ic^#MsTZ;(i$hR_62Q&2d zha4T;S!5`^nmIa|x!4f8a+5#hmv$6qznJvvD=niFnJiqNEM4-)l#6U&y38m9EwD{jpOW}R|wlnsJs|ATp{o}FS_u| zTTklw6Norg*` zGDJ?u>!&cT=XQh$TJ87QY;Wejet5o}J6Y%Oc|yPqr`a?%>1ur`K?O59ITwMF6!`^3 z9cx(1Z}xO&>r?+(Tb_XzaKYiqPHSuuVvR6p5}(X0?jgZkgx_vSM|5yr zkIkHT6*>b9WD+LuLZUv~%nGZ~&xZ~-b2dKMWcNo3%a3>2(b`6IE|gjK1>D;7$Z0O9 zHS*)p#k%dR0f@sRUN{`cM~AF|t4tU0&}AN+CGukl?oxdoo3+6Y_I*s)g0ic0-P^rR z5A`eedWJ$gsGv{z%C2{Xbf<7D*E*Sc#8`^jDn!;VNZY3i|S9KMU6%lfP~VyY5` zbVQ%@vU+eXdIrg$VI!oCA)u?dfwWyxzxv{w_=dYVnumalo0B!S(n(CNLYps?S=`fv zq()s|CuKDyD-UhAnPEV6&a$S+ql}8YsDhP1u27<^loMA_#`7a;{s6qcDFoh~)AN}1 ziOPHTqYq-n({Ka`G<$j4E&SwgleY5c5~Ka{e(^=}hN%W=<4%6q{Ed?&tShD>a<;x@ zfK#EsT~IFwSN)J;=R-Ok78Bk?=9@6H5+>30zU1@X$Zm)0Z{(4iw9|f=AZz!H_E`s0 zHV9B=xzd06)#GxgDq(@ohXq=kdP3evpX5x#(1fdk!aDEP4W4*}>X#C$0r*>1hM9Fi%TY#-8l6a~NRVcrua5IrKg4G;wZwG! z38adV)W^doPWxlO9m<@)6f+K&5u*K3;ycyHxL|KujVjWzBPU|5H1358Q+{p z9T1g*O7!!e_N3QWkn;CZ{Ixs;MCtf|SADWN@gUIkwnF8xIa}k6FNxbEBN3VnR;V^pXalu6B8e8aTK-l-7$@MAC zrd#iE)dL0;`2nZ>uLW86B~g7Pn{ddv?ym5ZKJS>zRZ8~_!v5%iq4x(b`7tqe$0)_Yc$hEDtMT0Un|ANVuQvzJ z1A2`+2N{p&tm}Wa5wx}SdWSD?(>?B_T232(uDX%7{k{Bh`gGlfv*(C%;_*a6Px7V0 z=f}yUi-^R9{&aAEe4lbu)Efw$YU$qc-oQ8%v;{Gu)FW-_1#r$3;Y)gR@42RldW!*f z;jo+1F`|?Gg1j*ANFmQ5!naHxK2+Uo?<{+c%|?gQSX!5?JYGjea^XgW(|<7s`6lEd zfJ7K`znYuFe(c>?*5Slb1m#x2sc!kBnMP{z8*xB-78bbKQjV+~cqgwQ^w?rC1;ABY z%~E<_IvUxblk42i;KfBQc0TNaCQf#x6#qx6L=Q|D7>n_4HI^@DVLs;{Z#;zW0&H%$ z^rUwe^xAry@PR#c0?%0N7N89rR%DDJ z1ixfdVP+G6G9dfi9w#8SwA`h`=K1*KD|Kpb`}35^YEvl!wEgC2rQ7FeR@gVWP|9*? zvF(1vdTFlj;(|JlmJaX>jK3e%HGg+r3gAXb+w4-dH#WK*XAE?tg}Rq6oI#It9h(^+{axV&nNmAZ}dhkK!dvvF*M9ev-x$=`b>%EH#uJBH_0;kyC8LE8Z^ zB+$52h_1r{U6C`GS=REMNWRg*LF`#$y-?6~`wofv?-4`qw&@@*Z)-%*>F}gYS26H4 zIlD&Kp{lcWg7)tw8Q$1Uc?Rd}ghG#9K#wH!OrY1byZcJ*3;z9;7bceAr|we-#(0cQ z<66h1pun{d+$1W-VB|8wa5yXo8$H0OUh1o+P*jvvj&f@=fq8Ji)<-6zccrwL3av|i zxTHmJ@%t?BCYf}Sh5I#4ndlXU)5m-KYn&9mOAo7;;pF?ZZPto{c1M4go~@wV|AJZI z`M4eq#LZH&CU|mZ{IEIZ2@g8mOf6eKa@n71zwci0i+)%yUsuq*5pdM$275odEXDnm zm*D(_HrYMAssHQM6gq)icXz!fB+OFHaHA-a)khNva(zhnhXEnCYVusuXurJF3hryR zV3}~er*@0IO~qM{1ETI74nnq>6FK*2P76+h;vorsiQn-@C2L+=L9#Y@_gi`1Oh#B9 z7a-lD_l0ALFeY@ddTr*P+%D+dk9(YDf(5{1=P}ZcTLF|j`~exyg=Btan!k;bus=uy z6wf$jf`5Nv=)U?~=;nGSh%P%)bLY%ddgJ-2Kx2hP^@D5E%DqTZ1Ih zbPperjK}(~r=AxroHDa`huiZK@uqBQkzh#CzF_CA8QN%=>G<^ivB>u+J8iOrrhxtx z-Tps=tuRAD#rRD5E)4BT%Z0r;5^lF2vhy!@>NX5E_HX@8QkVBdrgcFLhY_S&eR+!=oTGk?#|3d%s>jLg*eh1sFEniG7T z!(h?&vSqfg?(E;*Kx7nCYb_48taVF?h?Oq0Bl>L`4nNND2>^DlgwWP!HRL^<3=_g5VEnNEi>Vb97qk{I+T`__6X zJ=1bX(Sl#~&6^&BMlDDC=i(#G!F*czjtG)@7qnehZ9%e{ds4Jgxdv4A>-vrYbFj|~ zlg@epunc}1geM`q{Jd3nLWUzhMcOP4o#*>_1n*bJpohP}nZMIb5(~m5O84Fkg=7D> zyM6TX9l3QzbfkGZf}gOzDAOBUr~QvU=(!XW5Z5N+@`rpF>nql%iypD)9OBH&qfwE& zvW)H25PO9i%P3zlYkAsVsF zlXMmO_j${r-MhPS#++yTU5>Heo$$NG)Y+u@bl(aJ!RuWVmrQ zY9h{D^8WPU=@-PJXf@2>`}?xMZ}(owe!5)R#AMB`4^|^cQ40cNe>E*A{dX?g9Oq$o)vGWD-bUJ(DzIEQb_A~JK z@uQ()-fg>oJv{K+mGRW7Mp4%QqMaHe^kpsUIa5NG<-0cb{_DPBf9OxnYf7FH`p$p8 zpR;i4&aF8JuxJ;ym-EN{lcp&>3)!^hSkARGDxH-|$Q54t29^>L4`YSg+S(0&mQ3^% zsq+Z5Ifs{hSnzVE=|^|_7tO-83-`qi&d@FlNBqGAD}Rrin3$Z}4r*K{{7BY}kK9yL zR||U3wWV49*{qOjF+I2b3n4x}>w>yAh6zQjU2l+?v_!&OL7_zTDL$uQ!H;Dt7MgdRKcCr^Sq?I-7;2dU}jp!1K9y;KXSt zQf-kf+|U1FOri*#pmjHDnoSV!5S~`97MzeUsjzt(q%C*H{MX6f{SA}zsqLJ<_6LUE zkeX_eR#6Gj!hOeW$q2$Ds9Va=p#s`p<0UJmgre3|KL7%~PP0Fa0>2(tUV6@)#|o>I z0$-9ECjumF?dS;O|KFX!Z&D*fZf2w}D+_^nLCvtgemPKCrQ}+mNaG;_t9h(^_v}eA zX#QOqTs^_TjgeC=l;Z3oKM_6O$d`zL3an(E3Txx#a-A!m&#YZx@V>Q~juX%HHPba@ z)6iF50yhRAY&{X(8x6j`+tK&pco4apupng;mq-jQqxY{K{|b;5Dnu+7p)%uw3WnZX zExiv9Md%#s>;XG5rfD@w54dmT6+V=3$%3<$t?B7@@|jETY=12qC*~-jd@NNoWEY{M z4F8n7Euxxz|0vi0F?(+#tln8jb6=EG&d{?j(vr66Lgjrd!x4UB}*C=w3q^fzk3i9)E6lq$S_o z?qZy;6-A8{sR?u0M`+w86N8_9Bv2u7{-p#?IWltzh56t;L&hBu^hWkY4OAH+%i`J_ z)YLqb`1xACI)E+7sWJWeCW@-E0o!}LqQbcQ+#^-DxJYF3)P3eC)jeBfZv@S;+mZb} z(bBo2U2n)n&7icq=qt!?+N}7`|8l`+tA@`Alrm>d0^sw5*7rI|DGmk_KNbMTlin#b zAf8TTL7NP~Hwb9RJEDk7h>3S)D*Kf5VYmHAJa-ah@Nfw8ZEg`9mQyJbXz1*RR>V~` zNAqshL^h|3DnRcTDmBw~=D2kKGMim>s|^K_ zT>x+Q-yd3Gc_E^-Z`rxGGOe$!a0PQ2c#TQiOX3+>j8wAO)xy;NU6XiN5v@uf^cGA-VJ?oO96V!}td81qtaMmCU~<6Z@nt`0KIcm7iFcQ@x#1VdFR@ z#yH7XzbE|p*ceB>`872#NfUh2U?3^g13*1`*bqsg!dHCP?${Nwe!U6H>5Q9dB)q!t zFinV@a$xlAdLj(mH2Z144QW%XLX(+?_~BXu7m%tLIYgpSfmWwv*95{Z ze*sv4o9uj56IK^2|F|Z|@PAr>Iw!fc9I!O?i_IzCCX&#@ex`Ko;+7>)KF7e#t%tdL z$O7|ZIj+Qf4?lV&#p71mtOGL}O-@9A(Nd2RB2U(HaOb<%P(_G(^P}fO;UPKB8JgqJ zS!O}&fmG~>G?QM{XETX*(Y(-^U^2x&Y(O?anb++UR8JYShEQyYrUzstD#lAQA!2EBN=$0bCu?>P07D zKjLR5*=|Pz1I$Oq8QfmVSdb+vnNGzXZ!1x`^UPFBKpx3cMyVMrz=LK!w@s00LFIO_ z;$42RF@qZumUH|pQUJ6)Sv@}%4hjUx2pO(7tc!%XA~qe|{w}?6RyHy3>sJ|-e7~;j zDEW#w;SEIj?|-`((V>bhYt1_nRgjXEy~;1PvYrkzLbR6tn%%%VKce-?Y=W6pd!_%u z`~)`k)(NQ&+U)y2_pR(fWSAk?7y_-6u 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 e1cfafd89723..a9fd636776a4 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 eb208e67ccfe..86a6f958cf25 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 f85714a5913a..c580eb533feb 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 093/115] [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 000000000000..650486f3c385 --- /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 e423de56bf81..7e4cf3d700ec 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 68731bb3f307..5e9b08c09c2c 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 82a4bc00a439..0ec5a855c861 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 52ef480bbb93..d18bc59a35f5 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 6892bf11ecff..c9a03f0a4796 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 876168d2ed96..a709d6caf46c 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 ecba0ec404d4..fec2078cc60c 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 f34d7c08ce08..373afa89921d 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 304abbb06880..e8007f82e30c 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 9f06643626f5..f15307a66238 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 900aece60618..23e4a4fe7d7e 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 4c2a1ea5ac21..4380d3ab9899 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 52a0872f269f..eab22f993d0a 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 84c299698bb3..1250c1106b35 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 034780c7ba14..2241df97291a 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 711e4f9a5c53..6e83fc19a922 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 1ce7eb41e669..759574702c0f 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 094/115] 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 1f1113636046..7aecc408e05f 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 095/115] 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 a5f0d15dee0e..16b952fe0fe4 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 096/115] [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 7b655999ace0..9f836bd043c9 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 8754c0404d40..15d6cf7cf731 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 5ff542d20890..82216e88a091 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 dff36a6dac57..5755258b3638 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 3b9af8349437..20712c3c5a81 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 7fe5b6f5f879..01ccf3c510b6 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 097/115] 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 ea84ba1ad283..c58a7d2fbb5c 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 498f05457bba..dd41093f3a1f 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 07e17ad56229..deb8387fee29 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 a01c13371220..fc5dde94fa85 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 098/115] [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 000000000000..3281fb5892ea --- /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 ce0743ba2bae..2f45c525828d 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 000000000000..4fbb1ef4c55e --- /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 000000000000..5823ddd6b42e --- /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 9622f6ba63fa..6a1f5bc93749 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 000000000000..418f2880c107 --- /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 7a1e5e526624..415630d9f730 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 94e855fc6708..60bbaa9825db 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 755d4bb6b323..1b269e388bea 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 000000000000..b35eeedfa44f --- /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 29223d6a67c6..ef78b5afe3a3 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 eebbaac7d4f9..777566298e60 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 5c9532a3841f..d162e342dfd1 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 48e90b6c41d5..3f596cea1ae3 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 3b3e8004ded0..5ad202a02ae6 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 53536b11aaca..8e1145c531f9 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 5d8188f19f4e..0a4e48a195ec 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 7f0e87076051..440b9aa89a94 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 1e10923cea1d..59120e221ca4 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 52702c1f4ecc..b85cca113cf9 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 badfba7665df..e8a845c4b166 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 25c4fae89f02..963a30c7413e 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 1552db277e60..c5bf9a8be75b 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 c12b4befc068..3ce0fb58aba1 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 b5b34efabda0..2ca18e47a4bf 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 7de3fe1823cb..14fc23751ac1 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 0f74dd605c8f..7ff1c735c861 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 8e05cf287efa..acc26e5fce69 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 f74deb17fff7..5b5028f68f08 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 f4c487b28757..25063944b889 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 030d3a2a1ef8..68e06bacfa7b 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 000000000000..ac84a3b6447d --- /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 1c8ff3e205a3..6ad60e15f10e 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 8bc397dd98b5..dd2cf79318d8 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 b35adcad976c..aa2619e96f83 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 065fbd79d978..0ec9385194cc 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 3a6c10a9f07a..e36157419462 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 748b2fd1d782..77e8ab768cd0 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 70bba3d91c72..6b8955454633 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 b09ccdc3af8b..44987fd3e78f 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 e5eaf8870aa7..79d890bc21f1 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 5ec40a57ebc7..ef2e23e51a09 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 b608151d26ae..3b1513f4bb95 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 7e07569b44b8..1e20df89c8fa 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 000000000000..a01f796fe345 --- /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 44a7c4c9a5f8..ae7de986cf86 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 e50ec593cc99..cb3598652a39 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 099/115] [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 d81b998d5a75..596044163552 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 9b0182b86fc8..5c05d5612e20 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 666d97452ad3..d8e9749744ea 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 5c111b8ea9a8..9946135568e3 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 800e0147528e..89e05573da1c 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 100/115] 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 381b44439033..be687e33e220 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 47120c5da096..6439a68813fd 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 96962b41b3c6..ce8c3542ab69 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 99e67c1c43f3..46d4789e0bd5 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 a1df243b0ee7..8ee81c5e7645 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 01e566af17d0..f844bef6441e 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 22be625611e8..17c8bce65140 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 b3d17affb14c..2d1d8975c9ef 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 4a3c5c42fcb4..3c245bf3f688 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 9a150bbede5f..b9608511159b 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 0bb4f5081966..889b57c8e92e 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 000000000000..dc0a03fab8c7 --- /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 8601c5ce772d..3b216a6c3260 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 7442f14fed06..418b78add242 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 3f1a09523893..329262044357 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 63c118966cfa..b735dae2ca5b 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 101/115] 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 53618ec049e7..0fa304c98893 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 2925e5e16458..8ed01b9b61c7 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 72ecc2e4c87d..825975fa161b 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 c1646bd66e36..a6d279a5c2b4 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 5deaa83a807d..00ac1342400a 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 524cac27341a..5e969fa71517 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 ebb731a1b1ac..d4e418a964c8 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 329ef4756133..fc8f9398807e 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 aac042709881..93da20a8ed93 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 18610c87c0a5..5186a02383d3 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 22f7d3be2795..981b3f592975 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 024001d46324..c3996fe3231b 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 637caa2199c4..ac4bacc291ea 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 07155a4b0a60..85fa3022f59e 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 000000000000..6bf74da6db6f --- /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 102/115] [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 c9dfdf6ad1fe..65f952ca0103 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 bb40a24288a2..84bade4d9449 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 bba6cdb48e67..6c00c01dec44 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 0ec9385194cc..8ce38a128ebc 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 103/115] 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 a94d2319d6e4..ecf60e995d1a 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 000000000000..1f24a094d0ec --- /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 000000000000..44be2b6139aa --- /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 a43aa2202671..64be161fc90b 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 104/115] [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 9201cdcb6bba..7b4e4adc4e4f 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 57627fa60fe4..ea048b84afef 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 d312e7aa35cc..e54e59dd24df 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 9785edbff111..6de75a004303 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 3503bbdcd40e..3ad7a15d0c73 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 69d219463844..1128f25818d7 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 94d0a1cc1aab..51687016f6aa 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 105/115] 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 5c7f48de81f7..728418bf3c33 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 44be2b6139aa..3912b2fffae1 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 bd3c7383d4b9..92880bd12450 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 ecbf0aee63e2..d0de7ae77a81 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 106/115] 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 2e1d6074d0c2..f5ed6da19727 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 107/115] 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 4bedc757f0b5..f33b8b4899d1 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 108/115] [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 f962204c6b1b..5466ba220371 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 5627d3381850..011a2614c1af 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 000000000000..18970ff935b8 --- /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 56c9d6da1560..7a3f0105d3d1 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 bc454ecb1134..7b22078c89d1 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 1d58ef801489..43b4a5f781b8 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 ab73c52ae907..bfca6b206844 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 000000000000..627fa21cc2f6 --- /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 5886a76182ee..d232c86c19e6 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 000000000000..b3b35699914f --- /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 000000000000..32b860e8f703 --- /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 2327ac36a590..6b1a9a828d95 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 e202ff030cd9..bcfcd4f4ee09 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 dbc5dd9bbe29..ea6b02924cb3 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 07578e870bf2..1f3379bf681b 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 c9f44ab0048f..67aaabfe70fd 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 5e0e4223e3e2..8618bf950486 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 0c29bc31cdeb..ee43ae5f1d6e 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 1fc8a86a476f..e59ca5e7e14e 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 aa50626a1231..447b5dc6325e 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 6ad9efebce2d..2b31d37ddddd 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 d727bbb953d2..b819bc691927 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 d019668e2a8d..976f371c6b1a 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 fcfcee99f369..42bade1ba085 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 ec4e707f46e5..d92ef316aef0 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 19bcd2e7f059..967fd46f7e3d 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 1658de77e339..0c2ca882a559 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 7a9159ecc852..46639e1fe338 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 777b9f3cc7a9..859935d85112 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 6ef508b81771..a6da8cd56ec1 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 1393de8c725c..a9982a989663 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 70fcbb2c163c..3243ccb14f89 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 ecf669b0106c..abd8dd7e87f0 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 974ddcf35eeb..ec0a8e7871b5 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 c9b380d3c67e..e86963fd4594 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 bd12872c4dc7..92718b7ae71b 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 4d1b73fb69e5..4496a808f686 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 05b85ffab726..dd88bd80d578 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 fc1c019ff97b..1a5ee793a25d 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 09142c8568b2..b5a01e3e5c6d 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 945b5651be06..28b588a86aeb 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 16f6c0fd6b8b..072e3f5beefe 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 2ba9ec7f8325..ad7050e8dd65 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 a72105142d28..f7a53385200d 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 ea87950a59b7..1b4c06fb5d82 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 3b5ef57d3dcb..dc71ae3678f2 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 922651edc408..010f6b2ee98f 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 9baf6a55b7f4..a9ccda2efe99 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 000000000000..d9fb9d4bbabd --- /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 000000000000..1ab34f26d4b7 --- /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 000000000000..b7f752e6ba5e --- /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 9c3e15de7ce9..82a50222dc35 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 bf7a97a29aef..09daae848538 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 1cfd2f812a19..f54ad67af4a4 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 adbb5fa61895..7b0546f56dd1 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 e3ea121a9ebb..7a4dcf68e0ca 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 a5d1f66d3089..1685c6518def 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 a0e7047ad1cd..bb12b5a802f8 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 eaed3f2ead3a..1ee3d4f0eb8e 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 fa43ac1debb9..f77924aafadf 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 000000000000..aa83df15f68d --- /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 d6a238e5b094..91088acb7a51 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 1570124cdb92..8847a2fdb21a 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 109/115] [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 7ad3944096a5..01b3b1983486 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 000000000000..1ac222802e7d --- /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 60fbb9f0d2af..d0072f035599 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 000000000000..bc4d28ba0e31 --- /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 000000000000..089b0631c209 --- /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 000000000000..2f9a11ef7670 --- /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 000000000000..49285707457e --- /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 000000000000..346ccde45f3f --- /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 000000000000..2d8f73e67cf9 --- /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 3c982747e1d2..000000000000 --- 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 51834268ffa5..918b361a60d7 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 df43d8e908e4..9332ce3e0f90 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 6f72977cb333..efb96f645925 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 31c86d0a4cbf..0498e814440c 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 110/115] 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 a2eba2c23c39..db925813b90c 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 111/115] 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 aa9c8f6c4016..e7143826d572 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 bea153d0a672..ee9f349f4905 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 f9d7bffed1e2..8b964d839990 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 ac98a0e675fb..b3b1eff41e4b 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 3b358c03b805..c348aa43789d 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 9fab74ea47a8..285a780ae053 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 a05e1634226e..a236db9eee18 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 73deadba0a61..0b38554f7806 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 192ecd25b582..209e05f28b58 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 b4945cc3f410..29b9b972b6a4 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 112/115] 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 e7143826d572..d4524f07aaaa 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 209e05f28b58..bc00dc21d990 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 29b9b972b6a4..b18bc67413cd 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 113/115] [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 7472e271bd1e..6edd0a551ebd 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 95e84117106a..b56741b48d36 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 114/115] 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 193d99e1c953..faadfd4bb26d 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 eacb7e891b48..282ec17e94c9 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 707f2305e136..44e335dd7b41 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 13aa2a06306a..5bddf1d3f4b6 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 b30e43c1c3c5..b36f1a747f12 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 4cb05b0426fa..099effc697fc 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 bdd190f26c97..5d3a3cd742bb 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 20c7a88ea4f4..caedbfdec5be 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 5b94cb0435b3..1abe56c99dc8 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 2407dcbbce59..52c473ac2dd3 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 8b0061e047f3..285b69f057cd 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 92f9b92cb1f0..153a8a7ef75e 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 91dca7d275f8..000000000000 --- 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 d6d395feade8..2cd7c5efb74e 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 fb23f9459d30..82699eb5b88f 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 0a3faca1a252..f4a3aed28a0a 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 86a6f958cf25..fe0740849a93 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 c580eb533feb..7d67ec4e7428 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 115/115] [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 000000000000..920064f9a1b7 --- /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 000000000000..4e309faa46e1 --- /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 cb358c15e5fa..25945e72ff17 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 d90c8ea49a53..01f5c364ae42 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 f59370ce481b..aa9b05eb379a 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 2b4fb8fa08a6..f53efc8a3234 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 2b31d37ddddd..e2af678c828e 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 b819bc691927..e8b1162b0618 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 976f371c6b1a..1a4e19c2047b 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 42bade1ba085..3a440178344d 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 16f9a9524df5..f2da3ab4be8f 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 0519addb275d..e30f332ecd1c 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 57759844c100..b4591a8141f7 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 9c86b70b8827..89c9f3402712 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 03059ed5ec5c..2bbd4f78afae 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 c224e0f055b8..f6e1cf6e2420 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 d92ef316aef0..920cf97d32a7 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 967fd46f7e3d..4c980c8cc60d 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 0c2ca882a559..b92c18827557 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 7ebac9b785c8..982e1bb47a53 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 46639e1fe338..d530866edaf0 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 859935d85112..deb319492258 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 a6da8cd56ec1..a15f1ca9b044 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 a9982a989663..c47a412c2e9d 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 3243ccb14f89..3a8d068cad38 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 abd8dd7e87f0..fe7618bca0c7 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 ba6c702e9601..1dce602f3fca 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 a002cc932401..171a34f0d059 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 ec0a8e7871b5..4c60a6614125 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 6512bfdc4361..fa007bba6551 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 3bad87dc1a9a..db5097a6f25d 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 e86963fd4594..0aa7317dd8cd 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 621dcd8fa8ed..0e71237f7523 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 339874e19c33..ffbfd193873a 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 9c80ddde9e7b..bcb24268fc6c 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 92718b7ae71b..469b59a8e08a 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 43d1e7ab2aa3..e87c732e8a2f 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 ecdba7ccc009..6fc1a0c3caa9 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 4496a808f686..8bb155d83cf4 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 7ea7fcbd1d86..389c5ff7ea61 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 0f392e399f36..68be4c627780 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 5c293f4825b9..4752d1794ff2 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 dd88bd80d578..46cd1b653b5b 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 1a5ee793a25d..0eda2a7a13d9 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 9708c928870f..11d8b85f2592 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 d01c5e19d432..cae4365d0685 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 937af223b91a..f5c1970ee8c5 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 62ffcd527eea..ce4bbf420a63 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 7f9b296e2d46..46667826416e 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 9d44e09e847a..1c270ff402f7 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 c2f346cacc43..8dc97d727c4d 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 a2594ffa21c4..fb9ff2c28dc4 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 28b588a86aeb..75de97a55534 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 072e3f5beefe..d90cb7b1f082 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 219cd68d3a2a..68a3c8b30382 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 cd223c24792b..c1eb32be4895 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 ad7050e8dd65..007294293f59 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 a6ba9b19a9d7..953532a6e1c2 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 000000000000..14df1c3d8cd5 --- /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 000000000000..0e0944a11b41 --- /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 e866260662ad..d329070eaaa0 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 e37abf3746ae..a0689966a869 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 f7a53385200d..421172cf0b1a 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 b189eac186a7..612d08c09785 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 dcbb7b8e1fe4..8d7b171a8537 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 6768e9534a87..fdb1cd148c7f 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 1b4c06fb5d82..0bf9d17d70fd 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 8705682f61bc..3ed440813883 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 39b596dfed85..532bfbaf469f 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 1406c7c9000b..f27299436c70 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 dc71ae3678f2..bcbe460fb6a6 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 628f4033d566..4fb73235854c 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 3987654589bd..b2a1d2a6307d 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 000000000000..8c86f4c85af1 --- /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 000000000000..f6856eec5996 --- /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 000000000000..6704c9676fa5 --- /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 010f6b2ee98f..31b922e0067c 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 30dac114ac50..c30635c9d149 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 c2900782ed67..499e3e9c88a8 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 a9ccda2efe99..a1bee162c928 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 7b0546f56dd1..58dd53b6447c 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 f77924aafadf..5973a1dbe5f1 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 d9d381498fb5..c505edc79bc7 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 d2bfeeb6433d..89ebd902834b 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 8847a2fdb21a..6e2a391ec14e 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